top of page

一人暮らしの高齢者を見守るセンサーを作ってみた(その2)

  • H.K
  • 2024年8月2日
  • 読了時間: 28分

 ご紹介した「一人暮らしの高齢者を見守るセンサー」について、新しい情報をお届けします。多くの方々から貴重なご意見やご要望をいただき、ありがとうございました。頂いたご意見を参考に、コードを改良しました。

  1. 複数のセンサーの情報を区別するように変更

  2. 急激な温度・湿度変化、熱中症予防のアラートプッシュ通知機能を追加

  3. センサーの電池残量低下アラートプッシュ通知機能を追加

  4. ドアが開いたらBEEP音で通知

 

はじめに

 導入の際は、プライバシーへの配慮が重要です。必ず同意を得ること、そしてデータの取り扱いに十分注意を払うことが大切です。


なにができるの?

1. ドアの開閉モニタリング

  • ドアの開閉状態変化をリアルタイムで検出・通知

  • 最大10個のドアを同時に監視(ハード仕様上は100個) New!

  • ドアが開いたらBEEP通知 New! (無くても良い、別途スピーカーが必要)

2. 室内環境のモニタリング

  • 温度と湿度を定期的に測定・記録

  • 熱中症の危険レベルに応じた通知 New!

  • 急激な温度・湿度変化を通知 New!

  • 電池残量低下を通知 New!

  • 長時間の無活動検知(10時間以上ドアの開閉がない場合にスマートフォンに通知)

  • センサーから収集したデータ(温度、湿度、ドアの状態、電波強度、電池電圧)をAmbientクラウドサービスに送信・可視化


構築に必要な機材とサービス

1

TWELITE ARIA

磁気・温度・湿度を監視するセンサー

2

TWELITE SPOT

無線ゲートウェイ

3

TWELITE R3

TWELITE SPOTのプログラム書き換え用

4

MONOSTICK

TWELITE ARIAの設定・ファームウェア書き換え・通信モニターなど、ひとつあると便利

5

USB TYPE-Cケーブル*2本 (データ対応)

SPOTとR3用

6

CR2032*センサーの数

TWELITE ARIA用の電池

7

Prowl

iOSに対応したプッシュ通知サービス https://www.prowlapp.com/

8

Ambient

IoTデータの可視化サービス https://ambidata.io/

9

iPhoneまたはiPad

通知を受信する端末。Androidも可(その場合は、プッシュ通知サービスとしてProwlの代わりにPushbulletやPushoverを使う)

センサーの計測箇所を増やすには、TWELITE ARIAが追加で必要になります。

TWELITE ARIAにはBLUEとREDの2種類があり、BLUEよりREDの方がより遠くまで電波が届きます。2024年8月8日時点でAmazonではTWELITE BLUE2,274円、TWELITE RED 3,280円 で売られていました。電波の抜けづらいコンクリートの壁を挟むなど、通信環境が良くない場合にはREDを選ぶと良いでしょう。電波の強度については、LQIの値から確認する事ができます。LQIの値はAmbient(データの可視化サービス)から確認する事ができます。




あらためてご紹介

1. TWELITE ARIAとは

  • モノワイヤレス社が作った小さな無線デバイス

  • 搭載センサー

    • 磁気センサー(ドアの開閉検出などに使える)

    • 温度センサー

    • 湿度センサー

  • 電子工作の知識がなくても、すぐに使える

  • 電池(CR2032)の電力消費が少なく、設定や環境次第で数年間動く

2. TWELITA SPOTとは

  • モノワイヤレス社が作った無線デバイスから送られる情報を集める事ができる

  • Wi-Fiを使ってインターネットに繋ぐ事ができる(別途WiFi AP環境が必要です)

  • データの加工や各種クラウドサービスに適した通信を行う事ができる


3. Ambientとは

  • 日本のアンビエントデーター株式会社  (AmbientData Inc.)が提供するデータの可視化サービス

  • Arduino、ESP32、Raspberry Piなど様々なデバイスに対応したライブラリがある

  • 利用にはアカウントの作成とチャンネルの作成などが必要

  • 本システム構成なら、8個のセンサー(TWELITE ARIA)の可視化が無料枠内に収まる

※下記の図は、1つのセンサーから取得した温度や湿度などを可視化した例です。(Ambientのチャンネル1個で可視化可能)


4. Prowlとは

  • iOSデバイス(iPhone、iPad)に対応したプッシュ通知サービス

  • 利用にはアカウントの作成とAPIキーの生成が必要

  • 有料のiOS用ソフトが必要

    ※Androidでプッシュ通知を確認するのであれば、Pushoverなどがあります。


複数のARIA(センサー)をSPOTが見分けるには

 ポイントはTWELITE-ARIA(センサー)にDevice ID(論理デバイスID)を割り当てる事です。今回ご紹介するTWELITE SPOT用のプログラムはDevice IDを1~10まで使うことができますが、Device IDは必要に応じて100まで増やせます。各TWELITE ARIAに、どのDevice ID(論理デバイスID)が割り当てられているかを確認、変更するには設定用アプリTWELITE STAGE APPを使います(下記図)。

TWELITE STAGE APPの使用方法は、クイックマニュアルを一読する事をお勧めします。TWELITE R2/R3を使ったARIAの設定変更もできます。

※上記の設定では5分毎に再送含めデータを2回送信しており、ドア開閉の割り込みイベントが無ければ理論上は約5.88年動作します


BEEP音による通知機能について

  • TWELITE SPOTの蓋を開け、7Pインターフェイス(ESP32と書かれている側)の端子(GNDとIO2)を使います。

  • Device ID 1のドア(磁気センサ)が開いた時に断続的に音が鳴ります。

  • スピーカーをつながなくても、他の機能には影響ありません。

  • 写真では直接ケーブルをGNDとIO2に差し込んでいますが、このままだと抜けやすいです。ピンヘッダーなどを介して固定すると安定します。

  • ピンヘッダーが長すぎて蓋と干渉します。ニッパーで切るかラジオペンチで曲げるなどして調整すると、蓋を閉める事ができます。


TWELITE SPOTのプログラム

 プログラムを書き込み方法については、公式ページを参照してください。

//
// Arduino IDE , ESP32ボードバージョン v3用
// v3以降からtimerおよびSerialなどの定義が異なる為、修正が必要
// https://docs.espressif.com/projects/arduino-esp32/en/latest/api/timer.html
//

#include <Arduino.h>
#include "MWings.h"
#include "Ambient.h"
#include <EspProwl.h>
#include <array>

const int max_aria = 10; // 管理するデバイスの最大数

// デバッグフラグ
bool DEBUG = true; // デバッグメッセージを表示するかどうかを制御
bool CHECK_MEMORY = false; // メモリ使用状況をチェックするかどうかを制御
bool sendToAmbient = true; // Ambientへのデータ送信フラグ

// WiFi設定
// 注意: 本番環境では、これらの値を外部ファイルや環境変数から読み込むべきです
const char* ssid = "**************"; //WiFi SSID
const char* password = "**************"; //WiFi パスワード
char prowlApiKey[] = "*****************************"; //Prowl API キー

//TWELITE ARIAの電池電圧低下警告
const float BATTERY_LOW_THRESHOLD = 2.7; // 2.7V as the threshold

//BEEPが鳴る回数
volatile int beepCount = 6; //BEEPを鳴らす回数に達したかの確認用
const int MIN_BEEP_COUNT = 6; //BEEPを鳴らす回数の上限
//BEEPの最大鳴動時間
const unsigned long BEEP_DURATION = 60000; // 1分間(ミリ秒)
volatile unsigned long doorOpenTime = 0; // 連続パケット送信対策用(設定値(ms)以内に送られてきたパケットは無視する。0は無視しない。)
volatile bool beepingPaused = true;
//BEEP初期化
volatile bool initialBeepDone = true;
volatile unsigned long lastBeepTime = 0;

// DeviceData構造体
struct DeviceData {
    int16_t temp;
    uint16_t humi;
    uint8_t magn;
    uint16_t sequence;
    uint8_t lqi;
    uint16_t volt;
    unsigned long lastTime;
    bool initialized;
    double pastHeatIndexData[12];
    int hiDataIndex;
    int hiDataCount;
    double pastTemperatureData[12];
    double pastHumidityData[12];
    int dataIndex;
    int dataCount;
    unsigned long lastDataTime;
    double minTemp;
    double maxTemp;
    double minHumi;
    double maxHumi;
    bool hasInitialData;
    bool hasInitialTemperature;
    bool hasInitialHumidity;
    unsigned long lastTemperatureTime;
    unsigned long lastHumidityTime;
};
struct DeviceState {
    DeviceData data;
    uint8_t lastDoorState;
    unsigned long lastDoorEventTime;
    unsigned long lastBatteryAlertTime;
    unsigned long lastHeatStressNotificationTime;
};
std::array<DeviceState, max_aria> devices;



// BEEP制御用の定数
const int BEEP_PIN = 2;  // ESP32のIO2(GPIO2)を使用
const unsigned long BEEP_INTERVAL = 500;  // BEEPの間隔(ミリ秒)

// BEEPを制御する論理デバイスIDを指定
const uint8_t BEEP_CONTROL_DEVICE_ID = 1;  // この変数で制御対象のデバイスIDを指定

// ドア状態用の変数
volatile bool isTargetDeviceDoorOpen = false;
volatile bool beepState = false;  // beepState変数を追加

// 関数プロトタイプ宣言
void analyzeTemperatureAndHumidityChanges(uint8_t logicalId, const DeviceData& currentData);
void checkHumidityChange(uint8_t logicalId, DeviceData& device, double currentHumi, unsigned long currentTime, 
                         unsigned long timeWindow, double threshold, const char* periodStr);
void processDeviceData(uint8_t logicalId, const DeviceData& data);
void updateDeviceData(DeviceData& device, const DeviceData& newData, unsigned long currentTime);
void sendDataToAmbient(uint8_t logicalId, const DeviceData& data, double temp, double humi);
void checkLongInactivity(uint8_t logicalId, const DeviceData& device, unsigned long currentTime);
void detectRapidEnvironmentChanges(uint8_t logicalId, const DeviceData& device, double currentTemp, double currentHumi, unsigned long currentTime);
void saveDataToHistory(DeviceData& device, double currentTemp, double currentHumi);


/**
 * 音を鳴らす制御関数
 * 
 * この関数は、ドアの開閉状態に基づいてBEEPの動作を制御します。
 * 主な機能:
 * - ドアが開いている場合、または初期ビープが完了していない場合にビープ音を鳴らす
 * - ビープの回数と間隔を制御
 * - 一定時間経過後にビープを停止
 * - デバッグ情報の出力
 */
void beeper() {
    unsigned long currentTime = millis();

    if (isTargetDeviceDoorOpen || (!initialBeepDone && beepCount < MIN_BEEP_COUNT)) {
        if (currentTime - doorOpenTime < BEEP_DURATION) {
            if (currentTime - lastBeepTime >= BEEP_INTERVAL) {
                tone(BEEP_PIN, 1800, 250);
                beepCount++;
                lastBeepTime = currentTime;
                debugPrint("***BEEP*** Count: " + String(beepCount) + "\n");
            }
        } else {
            beepingPaused = true;
            initialBeepDone = true;
            noTone(BEEP_PIN);
            debugPrint("Beep stopped after 1 minute\n");
        }
    } else {
        noTone(BEEP_PIN);
    }

    if (beepCount >= MIN_BEEP_COUNT) {
        initialBeepDone = true;
    }
}

/**
 * ドアの状態変化を処理する関数
 * 
 * @param logicalId デバイスの論理ID
 * @param isDoorOpen ドアが開いているかどうかを示すブール値(true: 開、false: 閉)
 * 
 * この関数は以下の処理を行います:
 * 1. 指定されたデバイスが BEEP 制御対象デバイスかどうかをチェック
 * 2. BEEP 制御対象デバイスの場合、以下の処理を実行:
 *    a. ドアの状態(開/閉)を更新
 *    b. BEEP カウントをリセット
 * 
 * 注意:
 * - この関数は割り込み処理中に呼び出されるため、クリティカルセクションを使用して
 *   共有変数へのアクセスを保護しています。
 * - BEEP カウントのリセットにより、ドアの状態が変化するたびに
 *   最小回数(MIN_BEEP_COUNT)のBEEPが保証されます。
 */
void handleDoorStateChange(uint8_t logicalId, bool isDoorOpen) {
    if (logicalId == BEEP_CONTROL_DEVICE_ID) {
        isTargetDeviceDoorOpen = isDoorOpen;
        if (isDoorOpen) {
            doorOpenTime = millis();
            beepingPaused = false;
            initialBeepDone = false;
            beepCount = 0;
            // ドアが開いたら即座にBEEPを鳴らす
            tone(BEEP_PIN, 1800, 250);
            beepCount++;
            lastBeepTime = millis();
        }
        // ドアが閉じられても、最小回数のBEEPが完了するまでビープを継続
        if (!isDoorOpen && beepCount < MIN_BEEP_COUNT) {
            beepingPaused = false;
            initialBeepDone = false;
        }
        
        debugPrint(" Beep control updated - Logical ID: " + String(logicalId) + 
                   ", Is Door Open: " + String(isDoorOpen) + 
                   ", BEEP Count: " + String(beepCount) + 
                   ", Beeping Paused: " + String(beepingPaused) + 
                   ", Initial Beep Done: " + String(initialBeepDone) + "\n");
    }
}

// Ambientの設定を論理ID毎に定義
// 注意: 本番環境では、これらの値を外部ファイルや環境変数から読み込んだ方がいい
struct AmbientConfig {
    unsigned int channelId; // AmbientのチャンネルID
    const char* writeKey; // Ambientの書き込みキー
};
AmbientConfig ambientConfigs[max_aria] = {
    {*****, "****************"},  // 論理ID 1のAmbient設定
    {*****, "****************"},  // 論理ID 2のAmbient設定
    {*****, "****************"},  // 論理ID 3のAmbient設定
    {*****, "****************"},  // 論理ID 4のAmbient設定
    {*****, "****************"},  // 論理ID 5のAmbient設定
    {*****, "****************"},  // 論理ID 6のAmbient設定
    {*****, "****************"},  // 論理ID 7のAmbient設定
    {*****, "****************"},  // 論理ID 8のAmbient設定
    {*****, "****************"},  // 論理ID 9のAmbient設定
    {*****, "****************"}   // 論理ID 10のAmbient設定
};

WiFiClient client;
Ambient ambient;

const int RST_PIN = 5; // リセットピンの定義
const int PRG_PIN = 4; // プログラムピンの定義
const int LED_PIN = 18; // LEDピンの定義
// CHANNELとAPP_IDをARIAと必ず合わせる、300s毎にARIAから温度・湿度の情報が送られてくる事を想定
const uint8_t TWE_CHANNEL = 18; // TWELITEのチャンネル
const uint32_t TWE_APP_ID = 0x67720102; // TWELITEのアプリケーションID

/**
 * オーバーフローを考慮した時間差の計算
 * 
 * @param current 現在の時間
 * @param previous 以前の時間
 * @return 二つの時間の差(ミリ秒)
 * 
 * この関数は、unsigned long型のオーバーフローを考慮して時間差を計算します。
 */
unsigned long timeDifference(unsigned long current, unsigned long previous) {
    return (current >= previous) ? current - previous : (ULONG_MAX - previous) + current + 1;
}

/**
 * 指定した時間が経過したかどうかをチェック
 * 
 * @param current 現在の時間
 * @param previous 以前の時間
 * @param interval チェックする時間間隔
 * @return 指定した時間が経過していればtrue、そうでなければfalse
 * 
 * この関数は、二つの時間の差が指定した間隔以上であるかをチェックします。
 * オーバーフローを考慮した時間差の計算を使用します。
 */
bool hasTimeElapsed(unsigned long current, unsigned long previous, unsigned long interval) {
    return timeDifference(current, previous) >= interval;
}

/**
 * デバッグメッセージを出力する関数
 * 
 * @param message 出力するメッセージ
 * 
 * DEBUGフラグがtrueの場合にのみメッセージを出力します。
 */
 void debugPrint(const String &message) {
    if (DEBUG) {
        Serial.print(message);
    }
}

/**
 * シリアルコマンドを処理する関数
 * 
 * @param command 受信したシリアルコマンド
 * 
 * この関数は以下の処理を行います:
 * 1. "SET" コマンドの解析とデバイスデータの処理
 * 2. Ambientデータ送信の有効/無効切り替え
 * 3. Ambientデータ送信状態の確認
 * 4. 不明なコマンドに対するエラーメッセージの表示
 */
 void processSerialCommand(String command) {
    if (command.startsWith("SET ")) {
        // コマンドを解析
        String params = command.substring(4);
        int values[7];
        int paramIndex = 0;
        int lastIndex = 0;
        for (int i = 0; i < 7; i++) {
            int nextIndex = params.indexOf(' ', lastIndex);
            if (nextIndex == -1) {
                nextIndex = params.length();
            }
            values[i] = params.substring(lastIndex, nextIndex).toInt();
            lastIndex = nextIndex + 1;
            paramIndex++;
        }

        if (paramIndex == 7) {
            // 仮想的なデバイスデータを作成
            DeviceData data;
            data.temp = values[1];
            data.humi = values[2];
            data.magn = values[3];
            data.sequence = values[4];
            data.lqi = values[5];
            data.volt = values[6];

            // デバイスデータを処理
            processDeviceData(values[0], data);

            Serial.println(" Data processed for logical ID: " + String(values[0]));
        } else {
            Serial.println("Invalid command format. Use: SET <logical_id> <temp> <humi> <magn> <sequence> <lqi> <volt>");
        }
    } else if (command == "AMBIENT ON") {
        sendToAmbient = true;
        Serial.println("Ambient data sending turned ON");
    } else if (command == "AMBIENT OFF") {
        sendToAmbient = false;
        Serial.println("Ambient data sending turned OFF");
    } else if (command == "AMBIENT STATUS") {
        Serial.println("Ambient data sending is currently " + String(sendToAmbient ? "ON" : "OFF"));
    } else {
        Serial.println("Unknown command. Use: SET <logical_id> <temp> <humi> <magn> <sequence> <lqi> <volt>");
        Serial.println("Or use: AMBIENT ON, AMBIENT OFF, AMBIENT STATUS");
    }
}

/**
 * メモリ使用状況をチェックし、変更があれば出力する関数
 * 
 * CHECK_MEMORYフラグがtrueの場合にのみ実行されます。
 * 前回のチェック時から空きヒープメモリ量が変化した場合、
 * 新しい空きヒープメモリ量をシリアル出力します。
 */
unsigned long previousFreeHeap = 0;
void checkMemory() {
    if (CHECK_MEMORY) {
        unsigned long currentFreeHeap = ESP.getFreeHeap();
        if (currentFreeHeap != previousFreeHeap) {
            Serial.print("Free Heap: ");
            Serial.println(currentFreeHeap);
            previousFreeHeap = currentFreeHeap;
        }
    }
}

/**
 * 熱中症指数(Heat Index)を計算する関数
 * 
 * @param temperatureCelsius 温度(摂氏)
 * @param humidity 相対湿度(%)
 * @return 熱中症指数(摂氏)
 * 
 * この関数はSteadmanの計算式を使用して熱中症指数を計算します。
 * 計算式は華氏で行われ、結果は摂氏に変換されます。
 * 特定の条件下では補正が行われます。
 */
 double calculateHeatIndex(double temperatureCelsius, double humidity) {
    // 摂氏を華氏に変換
    double t = temperatureCelsius * 9.0 / 5.0 + 32.0;
    double rh = humidity; // 湿度(百分率)

    // 熱中症指数の計算
    double hi = -42.379 + 2.04901523 * t + 10.14333127 * rh
                - 0.22475541 * t * rh - 0.00683783 * t * t
                - 0.05481717 * rh * rh + 0.00122874 * t * t * rh
                + 0.00085282 * t * rh * rh - 0.00000199 * t * t * rh * rh;

    // 特定の条件下での補正
    if ((rh < 13) && (t >= 80.0) && (t <= 112.0)) {
        hi -= ((13 - rh) / 4) * sqrt((17 - abs(t - 95)) / 17);
    } else if ((rh > 85) && (t >= 80.0) && (t <= 87.0)) {
        hi += ((rh - 85) / 10) * ((87 - t) / 5);
    }

    // 華氏の結果を摂氏に戻す
    return (hi - 32) * 5.0 / 9.0;
}

/**
 * 熱中症指数に基づいて警告レベルを決定する
 * 
 * @param heatIndex 熱中症指数(摂氏)
 * @return 警告レベルを示す文字列
 * 
 * 警告レベルは以下の基準で決定されます:
 * - 27℃未満: 「安全」
 * - 27℃以上32℃未満: 「注意」
 * - 32℃以上41℃未満: 「警戒」
 * - 41℃以上54℃未満: 「厳重警戒」
 * - 54℃以上: 「危険」
 */
 String getHeatStressLevel(double heatIndex) {
    if (heatIndex < 27) {
        return "安全";
    } else if (heatIndex < 32) {
        return "注意";
    } else if (heatIndex < 41) {
        return "警戒";
    } else if (heatIndex < 54) {
        return "厳重警戒";
    } else {
        return "危険";
    }
}

/**
 * WiFi接続を確立し維持する関数
 * 
 * この関数は以下の処理を行います:
 * 1. 現在のWiFi接続状態の確認
 * 2. 未接続の場合、接続を試行
 * 3. 接続試行が失敗した場合、システムを再起動
 * 4. 接続成功時またはすでに接続されている場合、処理を終了
 */
 void ensureWiFiConnection() {
    if (WiFi.status() == WL_CONNECTED) return;
    
    debugPrint("Connecting to WiFi...");
    WiFi.begin(ssid, password);
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 40) {
        delay(1000);
        Serial.print(".");
        attempts++;
    }
    if (WiFi.status() == WL_CONNECTED) {
        debugPrint("WiFi connected\n");
    } else {
        debugPrint("WiFi connection failed\n");
        ESP.restart();
    }
    checkMemory();
}


int getDeviceIndex(uint8_t logicalId) {
    return logicalId - 1;  // 論理IDは1から始まると仮定
}

/**
 * Prowl通知を送信する関数
 * 
 * @param event イベントの種類
 * @param description イベントの詳細説明
 * @param priority 通知の優先度
 * 
 * この関数はWiFi接続を確認し、Prowlサービスを通じて通知を送信します。
 * 送信に失敗した場合、最大3回まで再試行します。
 * 全ての試行が失敗した場合、システムを再起動します。
 */
 void sendProwlNotification(const char* event, const char* description, int priority) {
    ensureWiFiConnection();
    if (WiFi.status() != WL_CONNECTED) {
        debugPrint("Cannot send Prowl notification: WiFi not connected\n");
        return;
    }
    
    delay(1000);
    debugPrint(" Sending Prowl notification...");
    char* eventStr = const_cast<char*>(event);
    char* descriptionStr = const_cast<char*>(description);
    
    bool pushResult = false;
    int attempts = 0;
    while (!pushResult && attempts < 40) {
        pushResult = EspProwl.push(eventStr, descriptionStr, priority);
        if (pushResult) {
            debugPrint("Prowl notification sent successfully\n");
            return;
        } else {
            debugPrint("Attempt " + String(attempts + 1) + " failed. Retrying...");
            attempts++;
            delay(1000);  // 再試行前に1秒待機
        }
    }
    
    debugPrint("All attempts to send Prowl notification failed. Restarting...\n");
    ESP.restart();      
}


/**
 * 温度と湿度の変化を分析し、急激な変化を検出する
 * 
 * @param logicalId デバイスの論理ID
 * @param currentData 現在のデバイスデータ
 * 
 * この関数は以下の処理を行います:
 * 1. 新しいデータを履歴に保存
 * 2. 15分間と1時間の温度・湿度変化を計算し、閾値を超えた場合に通知
 * 3. 異常な温度・湿度値を検出し、通知
 */
void analyzeTemperatureAndHumidityChanges(uint8_t logicalId, const DeviceData& currentData) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    DeviceState& deviceState = devices[index];
    DeviceData& device = deviceState.data;

    unsigned long currentTime = millis();

    double currentTemp = currentData.temp / 100.0;
    double currentHumi = currentData.humi / 100.0;

    // データを保存
    saveDataToHistory(device, currentTemp, currentHumi);

    // 15分と1時間の経過時間(ミリ秒)
    const unsigned long fifteenMinutes = 15 * 60 * 1000;
    const unsigned long oneHour = 60 * 60 * 1000;

    // 温度変化の検出と通知
    checkTemperatureChange(logicalId, device, currentTemp, currentTime, fifteenMinutes, 3.0, "15 minutes");
    checkTemperatureChange(logicalId, device, currentTemp, currentTime, oneHour, 5.0, "1 hour");

    // 湿度変化の検出と通知
    checkHumidityChange(logicalId, device, currentHumi, currentTime, fifteenMinutes, 15.0, "15 minutes");
    checkHumidityChange(logicalId, device, currentHumi, currentTime, oneHour, 25.0, "1 hour");

    // 異常な温度と湿度の検出
    detectAbnormalConditions(logicalId, currentTemp, currentHumi);

    device.lastDataTime = currentTime;
    checkMemory();
}

void checkTemperatureChange(uint8_t logicalId, DeviceData& device, double currentTemp, unsigned long currentTime, 
                            unsigned long timeWindow, double threshold, const char* periodStr) {
    if (currentTime - device.lastTemperatureTime >= timeWindow && device.dataCount >= 2) {
        int indexDiff = (timeWindow == 15 * 60 * 1000) ? 2 : device.dataCount;
        double tempChange = currentTemp - device.pastTemperatureData[(device.dataIndex - indexDiff + 12) % 12];
        if (abs(tempChange) >= threshold) {
            char message[150];
            snprintf(message, sizeof(message), 
                     "Significant temperature change. ID: %u, From: %.1f°C To: %.1f°C, Change: %.1f°C in %s", 
                     logicalId, 
                     device.pastTemperatureData[(device.dataIndex - indexDiff + 12) % 12],
                     currentTemp,
                     tempChange,
                     periodStr);
            sendProwlNotification("Temperature Alert", message, 1);
        }
        device.lastTemperatureTime = currentTime;
    }
}

void checkHumidityChange(uint8_t logicalId, DeviceData& device, double currentHumi, unsigned long currentTime, 
                         unsigned long timeWindow, double threshold, const char* periodStr) {
    if (currentTime - device.lastHumidityTime >= timeWindow && device.dataCount >= 2) {
        int indexDiff = (timeWindow == 15 * 60 * 1000) ? 2 : device.dataCount;
        double humiChange = currentHumi - device.pastHumidityData[(device.dataIndex - indexDiff + 12) % 12];
        if (abs(humiChange) >= threshold) {
            char message[150];
            snprintf(message, sizeof(message), 
                     "Significant humidity change. ID: %u, From: %.1f%% To: %.1f%%, Change: %.1f%% in %s", 
                     logicalId, 
                     device.pastHumidityData[(device.dataIndex - indexDiff + 12) % 12],
                     currentHumi,
                     humiChange,
                     periodStr);
            sendProwlNotification("Humidity Alert", message, 1);
        }
        device.lastHumidityTime = currentTime;
    }
}

/**
 * 異常な温度・湿度条件を検出し、警告を発する
 * 
 * @param logicalId デバイスの論理ID
 * @param currentTemp 現在の温度(摂氏)
 * @param currentHumi 現在の湿度(%)
 * 
 * この関数は以下の処理を行います:
 * 1. 温度が-10°C未満または40°C超の場合に警告を発する
 * 2. 湿度が5%未満または95%超の場合に警告を発する
 * 3. 異常を検出した場合、Prowl通知を送信する
 */
void detectAbnormalConditions(uint8_t logicalId, double currentTemp, double currentHumi) {
    if (currentTemp < -10.0 || currentTemp > 40.0) {
        char message[100];
        snprintf(message, sizeof(message), "Abnormal temperature detected. Logical ID: %u, Temperature: %.1f°C", 
                logicalId, currentTemp);
        sendProwlNotification("Temperature Warning", message, 2);
    }

    if (currentHumi < 5.0 || currentHumi > 95.0) {
        char message[100];
        snprintf(message, sizeof(message), "Abnormal humidity detected. Logical ID: %u, Humidity: %.1f%%", 
                logicalId, currentHumi);
        sendProwlNotification("Humidity Warning", message, 2);
    }
}

/**
 * 熱中症指数の傾向を分析する関数
 * 
 * @param logicalId デバイスの論理ID
 * 
 * この関数は以下の処理を行います:
 * 1. 過去1時間分の熱中症指数データを前半と後半に分けて平均を計算
 * 2. 後半の平均が前半の平均より1度以上高い場合、上昇傾向と判断
 * 3. 上昇傾向が検出された場合、1時間に1回の頻度で警告通知を送信
 * 
 * 注意:
 * - この関数は12個のデータポイント(1時間分)が蓄積されてから分析を開始します
 * - 通知の頻度は1時間に1回に制限されています
 */
void analyzeHeatIndexTrend(uint8_t logicalId) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    DeviceState& deviceState = devices[index];
    DeviceData& device = deviceState.data;
    if (device.hiDataCount < 12) return;
    
    double firstHalf = 0, secondHalf = 0;
    for (int i = 0; i < 6; i++) {
        firstHalf += device.pastHeatIndexData[i];
        secondHalf += device.pastHeatIndexData[i + 6];
    }
    firstHalf /= 6;
    secondHalf /= 6;
    
    double increase = secondHalf - firstHalf;
    
    if (increase > 1.0) {
        unsigned long currentTime = millis();
        if (currentTime - deviceState.lastHeatStressNotificationTime > 3600000) {
            char message[100];
            snprintf(message, sizeof(message), "Heat Index rising trend detected. Logical ID: %u, Increase: %.1f in last hour", 
                     logicalId, increase);
            sendProwlNotification("Heat Stress Warning", message, 1);
            deviceState.lastHeatStressNotificationTime = currentTime;
        }
    }
}

//
void handleBeepControl(uint8_t logicalId, bool isDoorOpen) {
    if (logicalId == BEEP_CONTROL_DEVICE_ID) {
        isTargetDeviceDoorOpen = isDoorOpen;
        if (isDoorOpen) {
            beepCount = 0;
            doorOpenTime = millis();
            beepingPaused = false;
        } else {
            beepCount = 0;
            beepingPaused = false;
        }
        
        debugPrint("Beep control updated - Logical ID: " + String(logicalId) + 
                   ", Is Door Open: " + String(isDoorOpen) + 
                   ", BEEP Count: " + String(beepCount) + 
                   ", Beeping Paused: " + String(beepingPaused) + "\n");
    }
}

void sendDoorStateProwlNotification(uint8_t logicalId, bool isDoorOpen) {
    const char* doorState = isDoorOpen ? "Open" : "Closed";
    char message[100];
    snprintf(message, sizeof(message), " Door state changed. Logical ID: %u, New state: %s", logicalId, doorState);
    sendProwlNotification("Door Alert", message, 2);
}

/**
 * デバイスから受信したデータを処理する主要な関数
 * 
 * @param logicalId デバイスの論理ID
 * @param data 受信したデバイスデータ
 * 
 * この関数は以下の処理を行います:
 * 1. 論理IDの有効性チェック
 * 2. バッテリー電圧のチェック
 * 3. ドアの状態変化の処理
 * 4. デバイスデータの更新
 * 5. 熱中症指数の計算と処理
 * 6. Ambientへのデータ送信
 * 7. 長時間の不活性チェック
 * 8. 急激な環境変化の検出
 * 9. データ履歴の保存
 */
void processDeviceData(uint8_t logicalId, const DeviceData& data) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    DeviceState& deviceState = devices[index];
    DeviceData& device = deviceState.data;
    unsigned long currentTime = millis();

    // ドアの状態変化を最初に処理
    bool isDoorStateChanged = processDoorState(logicalId, data.magn, currentTime);

    // ドアの状態が変化した場合、即座にBeep制御を行う
    if (isDoorStateChanged) {
        handleDoorStateChange(logicalId, data.magn != 0);
    }
    // その他のデータ処理
    checkBatteryVoltage(logicalId, data.volt);
    updateDeviceData(device, data, currentTime);

    double currentTemp = data.temp / 100.0;
    double currentHumi = data.humi / 100.0;

    saveDataToHistory(device, currentTemp, currentHumi);

    processHeatIndex(logicalId, currentTemp, currentHumi, currentTime);
    
    // Prowl通知とAmbientデータ送信は、Beep制御の後に行う
    if (isDoorStateChanged) {
        sendDoorStateProwlNotification(logicalId, data.magn != 0);
    }
    sendDataToAmbient(logicalId, data, currentTemp, currentHumi);

    checkLongInactivity(logicalId, device, currentTime);
    detectRapidEnvironmentChanges(logicalId, device, currentTemp, currentHumi, currentTime);

    device.lastDataTime = currentTime;

    checkMemory();

    debugPrint(" Data processed for logical ID: " + String(logicalId) + 
               ", Temp: " + String(currentTemp) + "°C, Humi: " + String(currentHumi) + "%\n");
}

/**
 * 論理IDの有効性をチェックする関数
 * 
 * @param logicalId チェックする論理ID
 * @return 論理IDが有効な場合はtrue、それ以外はfalse
 * 
 * 論理IDが1からmax_ariaの範囲内にあるかをチェックします。
 * 無効な場合はデバッグメッセージを出力します。
 */
bool isValidLogicalId(uint8_t logicalId) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) {
        debugPrint("Invalid logical ID: " + String(logicalId) + "\n");
        return false;
    }
    return true;
}

/**
 * デバイスのバッテリー電圧を確認し、低電圧時に警告を発する
 * 
 * @param logicalId デバイスの論理ID
 * @param voltageData 電圧データ(mV単位)
 * 
 * 電圧が BATTERY_LOW_THRESHOLD 以下の場合、Prowlを通じて警告通知を送信します。
 */
void checkBatteryVoltage(uint8_t logicalId, uint16_t voltageData) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    DeviceState& deviceState = devices[index];
    float batteryVoltage = voltageData / 1000.0;
    unsigned long currentTime = millis();

    if (batteryVoltage <= BATTERY_LOW_THRESHOLD) {
        if (hasTimeElapsed(currentTime, deviceState.lastBatteryAlertTime, 3600000)) {
            char message[100];
            snprintf(message, sizeof(message), "Low battery warning. Logical ID: %u, Battery Voltage: %.2fV", 
                     logicalId, batteryVoltage);
            sendProwlNotification("Battery Alert", message, 1);
            
            deviceState.lastBatteryAlertTime = currentTime;
        }
    }
}

/**
 * ドアの状態変化を処理し、必要に応じて通知を送信する
 * 
 * @param logicalId デバイスの論理ID
 * @param magnetState 磁気センサーの状態(0: 閉、その他: 開)
 * @param currentTime 現在の時刻(ミリ秒)
 * 
 * この関数は以下の処理を行います:
 * 1. 初回データ受信時の初期状態の記録と通知
 * 2. 状態変化の検出と、DOOR_EVENT_WINDOW 内での重複通知の防止
 * 3. 状態変化時のProwl通知の送信
 */
bool processDoorState(uint8_t logicalId, uint8_t magnetState, unsigned long currentTime) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return false;

    DeviceState& deviceState = devices[index];
    DeviceData& device = deviceState.data;
    bool isDoorOpen = (magnetState != 0);
    bool stateChanged = false;
    
    if (!device.initialized) {
        deviceState.lastDoorState = magnetState;
        stateChanged = true;
        device.initialized = true;
    } else if (deviceState.lastDoorState != magnetState) {
        deviceState.lastDoorState = magnetState;
        stateChanged = true;
    }

    if (stateChanged) {
        debugPrint(" Door state changed - Logical ID: " + String(logicalId) + 
                   ", Is Door Open: " + String(isDoorOpen) + "\n");
    }

    return stateChanged;
}

/**
 * デバイスデータを更新し、最小・最大値を記録する
 * 
 * @param device 更新対象のDeviceData構造体
 * @param newData 新しく受信したデータ
 * @param currentTime 現在の時刻(ミリ秒)
 * 
 * この関数は以下の処理を行います:
 * 1. 初回データ受信時の初期化処理
 * 2. デバイスデータの更新
 * 3. 温度・湿度の最小・最大値の更新
 */
void updateDeviceData(DeviceData& device, const DeviceData& newData, unsigned long currentTime) {
    double currentTemp = newData.temp / 100.0;
    double currentHumi = newData.humi / 100.0;

    if (!device.hasInitialData) {
        device = newData;
        device.lastTime = currentTime;
        device.initialized = true;
        device.hiDataIndex = 0;
        device.hiDataCount = 0;
        device.dataIndex = 0;
        device.dataCount = 0;
        device.minTemp = device.maxTemp = currentTemp;
        device.minHumi = device.maxHumi = currentHumi;
        device.hasInitialData = true;
        device.hasInitialTemperature = true;
        device.hasInitialHumidity = true;
        
        // 初期値を履歴データにも設定
        for (int i = 0; i < 12; i++) {
            device.pastTemperatureData[i] = currentTemp;
            device.pastHumidityData[i] = currentHumi;
        }
    } else {
        device = newData;
        device.lastTime = currentTime;
        device.minTemp = min(device.minTemp, currentTemp);
        device.maxTemp = max(device.maxTemp, currentTemp);
        device.minHumi = min(device.minHumi, currentHumi);
        device.maxHumi = max(device.maxHumi, currentHumi);
    }
    // 温度・湿度データの更新時刻を記録
    if (newData.temp != device.temp) {
        device.lastTemperatureTime = currentTime;
    }
    if (newData.humi != device.humi) {
        device.lastHumidityTime = currentTime;
    }
}

/**
 * 熱中症指数(HI)を計算し、警告レベルを判定する
 * 
 * @param logicalId デバイスの論理ID
 * @param temp 温度(摂氏)
 * @param humi 湿度(%)
 * @param currentTime 現在の時刻(ミリ秒)
 * 
 * この関数は以下の処理を行います:
 * 1. 熱中症指数の計算
 * 2. 過去1時間分のHIデータの記録
 * 3. HIの傾向分析
 * 4. 危険レベル(54以上)の場合、1時間に1回の頻度で警告通知を送信
 */
void processHeatIndex(uint8_t logicalId, double temp, double humi, unsigned long currentTime) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    DeviceState& deviceState = devices[index];
    DeviceData& device = deviceState.data;

    double heatIndex = calculateHeatIndex(temp, humi);
    String heatStressLevel = getHeatStressLevel(heatIndex);

    device.pastHeatIndexData[device.hiDataIndex] = heatIndex;
    device.hiDataIndex = (device.hiDataIndex + 1) % 12;
    if (device.hiDataCount < 12) device.hiDataCount++;

    analyzeHeatIndexTrend(logicalId);

    if (heatIndex >= 54) {
        if (hasTimeElapsed(currentTime, deviceState.lastHeatStressNotificationTime, 3600000)) {
            char message[100];
            snprintf(message, sizeof(message), "Heat Stress Alert - Logical ID: %u, HI: %.1f, Level: %s", 
                     logicalId, heatIndex, heatStressLevel.c_str());
            sendProwlNotification("Heat Stress Alert", message, 2);
            deviceState.lastHeatStressNotificationTime = currentTime;
        }
    }
}

/**
 * Ambientクラウドサービスにデータを送信する
 * 
 * @param logicalId デバイスの論理ID
 * @param data デバイスから受信したデータ
 * @param temp 温度(摂氏)
 * @param humi 湿度(%)
 * 
 * この関数は以下の処理を行います:
 * 1. 対応するAmbientチャンネルの設定を取得
 * 2. Ambientにデータをセット(温度、湿度、磁気状態、シーケンス番号、LQI、電圧、熱中症指数)
 * 3. データ送信を試行(最大3回)
 * 4. 送信失敗時にWiFi接続を確認し、再試行
 * 5. 全ての試行が失敗した場合、デバッグメッセージを出力
 */
void sendDataToAmbient(uint8_t logicalId, const DeviceData& data, double temp, double humi) {
    if (!sendToAmbient) {
        debugPrint(" Ambient data sending is turned OFF\n");
        return;
    }
    AmbientConfig& config = ambientConfigs[logicalId - 1];
    ambient.begin(config.channelId, config.writeKey, &client);
    
    ambient.set(1, temp);
    ambient.set(2, humi);
    ambient.set(3, data.magn * 10);
    ambient.set(4, data.sequence);
    ambient.set(5, data.lqi);
    ambient.set(6, data.volt / 1000.00);
    ambient.set(7, calculateHeatIndex(temp, humi));

    debugPrint(" Ambient data send...");
    bool sendSuccess = false;
    int retryCount = 0;
    const int maxRetries = 3;

    while (!sendSuccess && retryCount < maxRetries) {
        if (ambient.send()) {
            sendSuccess = true;
            debugPrint("Ambient data sent successfully\n");
        } else {
            retryCount++;
            debugPrint("Ambient data send failed. Retry #" + String(retryCount) + "\n");
            if (retryCount < maxRetries) {
                ensureWiFiConnection();
            }
        }
    }
    if (!sendSuccess) {
        debugPrint("Ambient data send failed after 3 attempts\n");
    }
}

/**
 * デバイスの長時間の不活性状態をチェックする
 * 
 * @param logicalId デバイスの論理ID
 * @param device デバイスデータ
 * @param currentTime 現在の時刻(ミリ秒)
 * 
 * この関数は以下の処理を行います:
 * 1. デバイスが初期化済みで、最後のデータ受信から10時間以上経過しているかチェック
 * 2. 条件を満たす場合、Prowl通知を送信
 * 3. 通知後、最終データ受信時刻を更新して連続通知を防止
 */
void checkLongInactivity(uint8_t logicalId, const DeviceData& device, unsigned long currentTime) {
    int index = getDeviceIndex(logicalId);
    if (index < 0 || index >= max_aria) return;

    if (device.initialized && hasTimeElapsed(currentTime, device.lastTime, 36000000)) {
        char message[100];
        snprintf(message, sizeof(message), "No change in door signal for more than 10 hours. Logical ID: %u", logicalId);
        sendProwlNotification("Alert", message, 2);
        devices[index].data.lastTime = currentTime;
    }
}

/**
 * 急激な温度・湿度の変化を検出し、警告を発する
 * 
 * @param logicalId デバイスの論理ID
 * @param device デバイスデータ(過去の測定値を含む)
 * @param currentTemp 現在の温度(摂氏)
 * @param currentHumi 現在の湿度(%)
 * 
 * この関数は以下の処理を行います:
 * 1. 初期データが設定され、少なくとも1回のデータ更新が行われていることを確認
 * 2. 直前の測定値と現在の測定値を比較し、5分間の温度・湿度変化を計算
 * 3. 温度変化の閾値を、記録された最大温度と最小温度の差の20%として設定
 * 4. 湿度変化の閾値を、記録された最大湿度と最小湿度の差の20%として設定
 * 5. 温度変化が閾値を超えた場合、Prowl通知を送信
 * 6. 湿度変化が閾値を超えた場合、Prowl通知を送信
 * 
 * 注意:
 * - この関数は5分ごとに呼び出されることを想定しています
 * - 閾値は動的に設定され、環境の通常の変動範囲に基づいて調整されます
 * - 初期データがない場合や、データカウントが0の場合は処理をスキップします
 */
void detectRapidEnvironmentChanges(uint8_t logicalId, const DeviceData& device, double currentTemp, double currentHumi, unsigned long currentTime) {
    if (device.hasInitialData && device.dataCount > 1) {
        unsigned long timeSinceLastData = timeDifference(currentTime, device.lastDataTime);
        
        // 30分以上データが更新されていない場合は比較しない
        if (timeSinceLastData > 30 * 60 * 1000) {
            debugPrint("Skipping rapid change detection due to old data for Logical ID: " + String(logicalId) + "\n");
            return;
        }

        int prevIndex = (device.dataIndex - 1 + 12) % 12;
        double tempChange = currentTemp - device.pastTemperatureData[prevIndex];
        double humiChange = currentHumi - device.pastHumidityData[prevIndex];

        // 変化率を1分あたりの変化量に標準化
        double tempChangeRate = (tempChange / timeSinceLastData) * 60000; // 60000ミリ秒 = 1分
        double humiChangeRate = (humiChange / timeSinceLastData) * 60000;

        debugPrint("Device " + String(logicalId) + " - Temp change rate: " + String(tempChangeRate) + "°C/min, Humi change rate: " + String(humiChangeRate) + "%/min\n");

        // 閾値も1分あたりの変化量に調整
        double tempThreshold = max((device.maxTemp - device.minTemp) * 0.04, 0.2);  // 1分あたり0.2°C、または範囲の4%
        double humiThreshold = max((device.maxHumi - device.minHumi) * 0.04, 1.0);  // 1分あたり1%、または範囲の4%

        if (abs(tempChangeRate) >= tempThreshold) {
            char message[150];
            snprintf(message, sizeof(message), 
                     "Rapid temperature change. ID: %u, From: %.1f°C To: %.1f°C, Change rate: %.2f°C/min", 
                     logicalId, device.pastTemperatureData[prevIndex], currentTemp, tempChangeRate);
            sendProwlNotification("Temperature Alert", message, 1);
        }

        if (abs(humiChangeRate) >= humiThreshold) {
            char message[150];
            snprintf(message, sizeof(message), 
                     "Rapid humidity change. ID: %u, From: %.1f%% To: %.1f%%, Change rate: %.2f%%/min", 
                     logicalId, device.pastHumidityData[prevIndex], currentHumi, humiChangeRate);
            sendProwlNotification("Humidity Alert", message, 1);
        }
    }
}

/**
 * 温度と湿度のデータを履歴に保存する
 * 
 * @param device デバイスデータ構造体
 * @param currentTemp 現在の温度
 * @param currentHumi 現在の湿度
 * 
 * この関数は以下の処理を行います:
 * 1. 現在の温度と湿度を履歴配列に保存
 * 2. データインデックスを更新(循環バッファとして使用)
 * 3. 保存されたデータ数をカウント(最大12個まで)
 */
void saveDataToHistory(DeviceData& device, double currentTemp, double currentHumi) {
    // 現在のインデックスにデータを保存
    device.pastTemperatureData[device.dataIndex] = currentTemp;
    device.pastHumidityData[device.dataIndex] = currentHumi;
    
    // インデックスを更新(循環バッファ)
    device.dataIndex = (device.dataIndex + 1) % 12;
    
    // データ数を更新(最大12個まで)
    if (device.dataCount < 12) {
        device.dataCount++;
    }
    
    debugPrint(" Saving data - Temp: " + String(currentTemp) + ", Humi: " + String(currentHumi) + 
               ", Index: " + String(device.dataIndex) + ", Count: " + String(device.dataCount) + "\n");
}

/**
 * デバイスの初期設定を行う
 * 
 * この関数は起動時に一度だけ実行され、以下の処理を行います:
 * 1. シリアル通信の初期化
 * 2. WiFi接続の確立
 * 3. Prowl通知の初期設定
 * 4. TWELITEの初期化
 * 5. デバイスデータ構造体の初期化
 * 6. TWELITEパケット受信時のコールバック設定
 * 7. メモリ使用状況のチェック
 * 8. デバッグ用コマンドの説明出力
 */
void setup() {    
    Serial.begin(115200);
    Serial2.begin(115200, SERIAL_8N1,16,17); // TWELITE SPOTのESP32-TWELITEはrx:16,tx17ピンで行われている

    pinMode(BEEP_PIN, OUTPUT);  // BEEP用ピン(IO2)を出力モードに設定
    digitalWrite(BEEP_PIN, LOW);  // 初期状態ではBEEPをOFFに設定

    ensureWiFiConnection(); // WiFi接続

    EspProwl.begin(); // Prowl通知の初期化
    EspProwl.setApiKey(prowlApiKey); // Prowl APIキーの設定
    EspProwl.setApplicationName("Home"); // Prowlアプリケーション名の設定
    sendProwlNotification("Info", "twelite spot wave up", 0); // 通知を送信

    debugPrint("twelite...");
    Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID); // TWELITEの初期化
    debugPrint("begin\n");

    debugPrint("Initialized...");
    // 全てのデバイスの初期化
    for (int i = 0; i < max_aria; i++) {
        devices[i] = DeviceState();
        devices[i].data.lastTime = millis();
        devices[i].data.initialized = false;
        devices[i].data.hiDataIndex = 0;
        devices[i].data.hiDataCount = 0;
        devices[i].data.dataIndex = 0;
        devices[i].data.dataCount = 0;
        devices[i].data.lastDataTime = 0;
        devices[i].data.hasInitialData = false;
        devices[i].data.hasInitialTemperature = false;
        devices[i].data.hasInitialHumidity = false;
        devices[i].data.lastTemperatureTime = 0;
        devices[i].data.lastHumidityTime = 0;
        devices[i].lastHeatStressNotificationTime = 0;
        devices[i].lastBatteryAlertTime = 0;
        devices[i].lastDoorEventTime = 0;
    }
    debugPrint("OK\n");

    Twelite.on([](const ParsedAppAriaPacket& packet) {
        debugPrint("ARIA Packet received\n"); // デバッグ出力

        DeviceData data;
        uint8_t logicalId = packet.u8SourceLogicalId;

        data.temp = packet.i16Temp100x;
        data.humi = packet.u16Humid100x;
        data.magn = packet.u8MagnetState;
        data.sequence = packet.u16SequenceNumber;
        data.lqi = packet.u8Lqi;
        data.volt = packet.u16SupplyVoltage;

        debugPrint("Logical ID: " + String(logicalId) + ", Temp: " + String(data.temp/100.0) + ", Humi: " + String(data.humi/100.0) + ", Magn: " + String(data.magn) + ", Seq: " + String(data.sequence) + ", LQI: " + String(data.lqi) + ", Volt: " + String(data.volt/1000.0)+"\n");

        debugPrint(" Before processing - isTargetDeviceDoorOpen: " + String(isTargetDeviceDoorOpen) + 
                   ", beepCount: " + String(beepCount) + 
                   ", beepingPaused: " + String(beepingPaused) + 
                   ", initialBeepDone: " + String(initialBeepDone) + "\n");

        processDeviceData(logicalId, data);

        debugPrint(" After processing - isTargetDeviceDoorOpen: " + String(isTargetDeviceDoorOpen) + 
                   ", beepCount: " + String(beepCount) + 
                   ", beepingPaused: " + String(beepingPaused) + 
                   ", initialBeepDone: " + String(initialBeepDone) + "\n");

        beeper();

        debugPrint("Waiting for ARIA packets...");
    });

    checkMemory();
    // 起動時の説明
    debugPrint("SET <logical_id> <temp> <humi> <magn> <sequence> <lqi> <volt>\n");
    debugPrint("ex: ");
    debugPrint("SET 1 2500 6000 1 100 200 3000\n");
    debugPrint("AMBIENT ON: Turn on Ambient data sending\n");
    debugPrint("AMBIENT OFF: Turn off Ambient data sending\n");
    debugPrint("AMBIENT STATUS: Check current Ambient data sending status\n");

    // 初期状態の設定
    debugPrint("Initial state - isTargetDeviceDoorOpen: " + String(isTargetDeviceDoorOpen) + 
               ", beepCount: " + String(beepCount) + 
               ", beepingPaused: " + String(beepingPaused) + 
               ", initialBeepDone: " + String(initialBeepDone) + "\n");
    
    debugPrint("Waiting for ARIA packets...");

}


/**
 * メインループ関数
 * 
 * この関数は継続的に実行され、以下の処理を行います:
 * 1. TWELITEの状態更新
 * 2. WiFi接続の確認と必要に応じた再接続
 * 3. メモリ使用状況のチェック
 * 4. シリアル入力によるデバッグコマンドの処理
 */
void loop() {
    // TWELITEの更新
    Twelite.update();
    beeper();
    // WiFiが切断されていたら再接続、再接続できなかったらESP32をリブート
    ensureWiFiConnection();
    checkMemory();

    // シリアル入力を使ったデバッグ処理
    //if (Serial.available()) {
    //     String input = Serial.readStringUntil('\n');
    //     processSerialCommand(input);
    //}

    // TWELITE ARIA Serial aDEBUG
    //if (Serial2.available()) {
    //    debugPrint("Raw data received: ");
    //    unsigned long startTime = millis();
    //    while (Serial2.available() && (millis() - startTime < 100)) {  // 100ms timeout
    //        byte data = Serial2.read();
    //        debugPrint(String(data, HEX) + " ");
    //    }
    //    debugPrint("\n");
    //}
}

※赤字の部分を必要に応じて変更します。

頻繁にプログラムの書き直しが必要ならば、OTA (Over The Air)を使ってESP32のスケッチをアップデートができるようにしておくと便利です。本来のコードとOTAコードの混在を防ぐ為にESP32 OTAを使うと良いかもしれません。

※TWELITE SPOTのESP32シリアル経由で、TWELITE ARIAの代わってダミーデータを送る機能をコメントアウトしています。


熱中症指数(Heat Index)について

熱中症指数は calculateHeatIndex 関数で計算されています。この関数は Steadman の計算式を使用しています。プログラム内では熱中症指数が54℃以上の場合(1時間に1回)Prowlを使いメッセージを送信しています。Heat Indexの急上昇アラートなどがあると、より安全かもしれませんね。


感想と反省

 安定して、かつ手間をかけることなく動作すること。これは24時間稼働の仕組みを作る上で大きな課題でした。半年以上の長期運用(放置)テストを経て、安定性(フリーズしない)が確認できました。市販の各種見守りサービスも多々ありますが、DIYならではの自由があります。半田付けをすることなくIoTが体験できるので、週末の工作としていかがでしょう。

 

Comments


Commenting has been turned off.

株式会社日本データコントロール

〒108-0074 東京都港区高輪3-25-23(京急第2ビル)

https://www.ndc-net.co.jp/

高い技術とまごころは私たちの創業精神です。

40年以上に渡り、まごころをもって、確かな技術をお届けしています。

© Nihon Data Control Co., Ltd. 2020. All rights reserved.

bottom of page