編集履歴一覧に戻る
uranoのアイコン画像

urano が 2026年01月14日21時26分58秒 に編集

初版

タイトルの変更

+

NEMU~睡眠環境を改善しよう~

タグの変更

+

Wi-Fi

+

SwitchBot

+

センサー

+

IoT

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

セットアップや使用方法

ライセンスの変更

+

(MIT) The MIT License

本文の変更

+

システム概要 ====  本システム「NEMU」は、Spresenseを中心とした非接触型の睡眠環境モニタリングシステムである。  睡眠の質に影響を与える外的環境要因(温度・湿度・CO₂濃度・音・照度など)を複数のセンサで計測し、取得したデータを統合的に処理・記録することを目的としている。  システムの中核にはSpresense Main Boardを使用し、I2Cおよびアナログ入力を用いて複数の環境センサを接続している。温湿度およびCO₂濃度はSCD41、距離検知にはVL53L0X、加速度検知にはMMA8452Qを用い、これらのセンサはI2Cバスで接続されている。また、音センサ(KY-037)や光センサ(SSC-027700)はアナログ入力としてSpresenseに接続し、周囲環境の変化を取得する。  取得したセンサデータはSpresense上で統合・判定処理を行い、Wi-Fi Add-on Boardを介してクラウド(ELTRES)へ送信・蓄積する構成とした。さらに、Spresense Camera Boardで撮影した画像を用い、Neural Network Consoleで学習させた画像認識モデルにより人物の有無や姿勢を判別し、机での伏せ寝など睡眠にとって不適切な状態を検知する。画像認識の結果に基づき、望ましくない環境での睡眠が検知された場合には、SpresenseのGPIO出力を用いてブザーを起動し、利用者を起床させる。望ましい環境での睡眠を検知された際にはSwitchbotと連携し部屋の電気を消灯させる。 部品 ==== | 使用物品 | 備考 | 個数 | |:---:|:---:|:-| | Spresense メインボード | - | 3 | | Spresense 拡張ボード | - | 3 | | Spresense HDRカメラボード | - | 1 | | Spresense Wi-Fi Add-onボード | - | 1 | Spresense ELTRES Add-onボード | - | 1 | | LPWA/GNSS共通アンテナ | - | 2 | | ブザー | - | 1 | | Switchbot HUB2 mini | - | 1 | | Switchbot Bot32 | - | 1 | | 温湿度,CO2センサ | SCD41 | 1 | | 音センサ | KY-037 | 1 | | 照度センサ | SSCI-027700 | 1 | 設計図 ==== ![システム構成図](https://camo.elchika.com/d0d8f5a33887e5da53011c5b6f6e7f53fd231b8c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61303433616334622d346464342d346531632d383432332d6563353838643435373937382f31326635396233302d643631652d343037312d396530632d663336323333633966663435/) ソースコード ==== ```arduino:環境データ収集用 // ELTRES送信機能有効設定:0=無効、1=有効 #define CONFIG_ELTRES (1) #if CONFIG_ELTRES #include <EltresAddonBoard.h> #endif // CONFIG_ELTRES #include <Wire.h> // RPR-0521RS用 #include <RPR-0521RS.h> // SCD41用 #include <SensirionI2cScd4x.h> #define SCD41_I2C_ADDRESS (0x62) // PIN定義:LED(プログラム状態) #define LED_RUN PIN_LED0 // PIN定義:LED(GNSS電波状態) #define LED_GNSS PIN_LED1 // PIN定義:LED(ELTRES状態) #define LED_SND PIN_LED2 // PIN定義:LED(エラー状態) #define LED_ERR PIN_LED3 // プログラム内部状態:初期状態 #define PROGRAM_STS_INIT (0) // プログラム内部状態:起動中 #define PROGRAM_STS_RUNNING (1) // プログラム内部状態:終了 #define PROGRAM_STS_STOPPED (3) // プログラム内部状態 int program_sts = PROGRAM_STS_INIT; #if CONFIG_ELTRES // GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ bool gnss_recevie_timeout = false; // 点滅処理で最後に変更した時間 uint64_t last_change_blink_time = 0; // イベント通知での送信直前通知(5秒前)受信フラグ bool event_send_ready = false; // ペイロードデータ格納場所 uint8_t payload[16]; uint8_t pay[16]; uint8_t paysound[16]; #endif // CONFIG_ELTRES // SCD4x制御用インスタンス SensirionI2cScd4x scd4x; // SCD4x初期化実施済みフラグ bool sensor_initialized = false; // 最新値(温度) float last_temp = 0; // 最新値(湿度) float last_hum = 0; // 最新値(CO2濃度) uint16_t last_co2 = 0; // RPR-0521RS用インスタンス RPR0521RS rpr0521rs; // 最新値(照度) float last_illuminance = 0; //最新値(音) #define SOUND_PIN A0 int sound_ref = 0; //無音基準 int sound_max = 0; //最大振幅 float sound_db = 0.0f; //疑似デシベル // ===== 設定 ===== const int MIC_PIN = A0; const unsigned long SAMPLE_WINDOW_MS = 200; // 短時間評価 const unsigned long SEND_INTERVAL_MS = 1000; // 送信周期 // ホールド設定 const int DECAY_STEP = 5; // 1秒ごとにどれだけ下げるか unsigned long lastSendTime = 0; int holdSoundLevel = 0; volatile int sound_max_eltres = 0; // ===== 音量取得(peak-to-peak)===== int getSoundLevel() { unsigned long startTime = millis(); int signalMax = 0; int signalMin = 1023; while (millis() - startTime < SAMPLE_WINDOW_MS) { int sample = analogRead(MIC_PIN); if (sample > signalMax) signalMax = sample; if (sample < signalMin) signalMin = sample; } return signalMax - signalMin; } // ===== 音状態判定 ===== const char* evaluateSound(int level) { if (level < 8) return "深夜・無音"; else if (level < 13) return "エアコン"; else if (level < 20) return "小さな物音"; else if (level < 40) return "会話"; else return "大きな音"; } //送信データ入れ替え int cnt = 0; /** * @brief setup()関数 */ void setup() { // シリアルモニタ出力設定 Serial.begin(115200); // LED初期設定 pinMode(LED_RUN, OUTPUT); digitalWrite(LED_RUN, HIGH); pinMode(LED_GNSS, OUTPUT); digitalWrite(LED_GNSS, LOW); pinMode(LED_SND, OUTPUT); digitalWrite(LED_SND, LOW); pinMode(LED_ERR, OUTPUT); digitalWrite(LED_ERR, LOW); #if CONFIG_ELTRES // ELTRES起動処理 eltres_board_result ret = EltresAddonBoard.begin(ELTRES_BOARD_SEND_MODE_1MIN,eltres_event_cb, NULL); if (ret != ELTRES_BOARD_RESULT_OK) { // ELTRESエラー発生 digitalWrite(LED_RUN, LOW); digitalWrite(LED_ERR, HIGH); program_sts = PROGRAM_STS_STOPPED; Serial.print("cannot start eltres board ("); Serial.print(ret); Serial.println(")."); return; } #endif // CONFIG_ELTRES // SCD4xドライバ初期設定 Wire.begin(); scd4x.begin(Wire, SCD41_I2C_ADDRESS); uint16_t error_scd4x; // 電源ON時に以前の設定で測定開始する場合があるので、停止 error_scd4x = scd4x.stopPeriodicMeasurement(); if (error_scd4x) { Serial.print("cannnot stop measurement.("); Serial.print(error_scd4x); Serial.println(")"); } // 自動キャリブレーション機能オフに設定 error_scd4x = scd4x.setAutomaticSelfCalibrationTarget(0); if (error_scd4x) { Serial.print("cannnot stop ASC.("); Serial.print(error_scd4x); Serial.println(")"); } // 測定開始 error_scd4x = scd4x.startPeriodicMeasurement(); if (error_scd4x != NoError) { // 測定開始エラー #if CONFIG_ELTRES EltresAddonBoard.end(); #endif // CONFIG_ELTRES digitalWrite(LED_RUN, LOW); digitalWrite(LED_ERR, HIGH); program_sts = PROGRAM_STS_STOPPED; Serial.print("cannnot start measurement.("); Serial.print(error_scd4x); Serial.println(")"); return; } // 照度・近接一体型センサ初期設定 Wire.begin(); byte rc; rc = rpr0521rs.init(); if (rc != 0) { // センサ初期設定エラー EltresAddonBoard.end(); digitalWrite(LED_RUN, LOW); digitalWrite(LED_ERR, HIGH); program_sts = PROGRAM_STS_STOPPED; Serial.print("cannnot start sensor.("); Serial.print(rc); Serial.println(")"); return; } // 正常 program_sts = PROGRAM_STS_RUNNING; sound_ref = analogRead(SOUND_PIN); if (sound_ref == 0) sound_ref = 1; } /** * @brief loop()関数 */ void loop() { switch (program_sts) { case PROGRAM_STS_RUNNING: // プログラム内部状態:起動中 #if CONFIG_ELTRES if (gnss_recevie_timeout) { // GNSS電波受信タイムアウト(GNSS受信エラー)時の点滅処理 uint64_t now_time = millis(); if ((now_time - last_change_blink_time) >= 1000) { last_change_blink_time = now_time; bool set_value = digitalRead(LED_ERR); bool next_value = (set_value == LOW) ? HIGH : LOW; digitalWrite(LED_ERR, next_value); } } else { digitalWrite(LED_ERR, LOW); } if (event_send_ready) { // 送信直前通知時の処理 event_send_ready = false; setup_payload_temp_hum_co2( last_temp, last_hum, (float)last_co2); setup_payload_illuminance(last_illuminance); setup_payload_sound(sound_db); // 送信ペイロードの設定 if (cnt == 0){ EltresAddonBoard.set_payload(pay);//照度 cnt = 1; Serial.println("payload: illuminance"); } else if(cnt == 1){ EltresAddonBoard.set_payload(payload);//CO2 cnt = 2; Serial.println("payload: co2"); } else{ EltresAddonBoard.set_payload(paysound);//音 cnt = 0; sound_max_eltres = 0; Serial.println("payload: sound"); } } #endif // CONFIG_ELTRES // SCD41から値を取得し、最新値を更新 measure_scd41(); // 照度・近接一体型センサから測定値を取得し、最新値を更新 measure_illuminance(); //音センサの処理 measure_sound(); break; case PROGRAM_STS_STOPPED: // プログラム内部状態:終了 break; } // 次のループ処理まで100ミリ秒待機 delay(100); } #if CONFIG_ELTRES /** * @brief イベント通知受信コールバック * @param event イベント種別 */ void eltres_event_cb(eltres_board_event event) { switch (event) { case ELTRES_BOARD_EVT_GNSS_TMOUT: // GNSS電波受信タイムアウト Serial.println("gnss wait timeout error."); gnss_recevie_timeout = true; break; case ELTRES_BOARD_EVT_IDLE: // アイドル状態 Serial.println("waiting sending timings."); digitalWrite(LED_SND, LOW); break; case ELTRES_BOARD_EVT_SEND_READY: // 送信直前通知(5秒前) Serial.println("Shortly before sending, so setup payload if need."); event_send_ready = true; break; case ELTRES_BOARD_EVT_SENDING: // 送信開始 Serial.println("start sending."); digitalWrite(LED_SND, HIGH); break; case ELTRES_BOARD_EVT_GNSS_UNRECEIVE: // GNSS電波未受信 Serial.println("gnss wave has not been received."); digitalWrite(LED_GNSS, LOW); break; case ELTRES_BOARD_EVT_GNSS_RECEIVE: // GNSS電波受信 Serial.println("gnss wave has been received."); digitalWrite(LED_GNSS, HIGH); gnss_recevie_timeout = false; break; case ELTRES_BOARD_EVT_FAULT: // 内部エラー発生 Serial.println("internal error."); break; } } /** * @brief 温度・湿度・CO2 ペイロード設定 * @param temp 温度 * @param hum 湿度 * @param co2 CO2濃度 */ void setup_payload_temp_hum_co2(float temp, float hum, float co2) { // 設定情報をシリアルモニタへ出力 Serial.print("[setup_payload_temp_hum_co2]"); Serial.print("tem:"); Serial.print(temp, 6); Serial.print(",hum:"); Serial.print(hum, 6); Serial.print(",co2:"); Serial.print(co2); Serial.println(); // ペイロード領域初期化 memset(payload, 0x00, sizeof(payload)); // ペイロード種別[温度・湿度・CO2ペイロード]設定 payload[0] = 0x82; // 温度設定 uint32_t raw; raw = *((uint32_t*)&temp); payload[1] = (uint8_t)((raw >> 24) & 0xff); payload[2] = (uint8_t)((raw >> 16) & 0xff); payload[3] = (uint8_t)((raw >> 8) & 0xff); payload[4] = (uint8_t)((raw >> 0) & 0xff); // 湿度設定 raw = *((uint32_t*)&hum); payload[5] = (uint8_t)((raw >> 24) & 0xff); payload[6] = (uint8_t)((raw >> 16) & 0xff); payload[7] = (uint8_t)((raw >> 8) & 0xff); payload[8] = (uint8_t)((raw >> 0) & 0xff); // CO2設定 raw = *((uint32_t*)&co2); payload[9] = (uint8_t)((raw >> 24) & 0xff); payload[10] = (uint8_t)((raw >> 16) & 0xff); payload[11] = (uint8_t)((raw >> 8) & 0xff); payload[12] = (uint8_t)((raw >> 0) & 0xff); } /** * @breif 気圧圧力照度距離ペイロード設定(照度のみ利用) */ void setup_payload_illuminance(float illuminance) { // 設定情報をシリアルモニタへ出力 Serial.print("[setup_payload_illuminance]"); Serial.print("illuminance:"); Serial.print(illuminance, 6); Serial.print(" lux"); Serial.println(); // ペイロード領域初期化 memset(pay, 0x00, sizeof(pay)); // ペイロード種別[気圧圧力照度距離ペイロード]設定 pay[0] = 0x85; // 照度 uint32_t raw; raw = *((uint32_t*)&illuminance); pay[9] = (uint8_t)((raw >> 24) & 0xff); pay[10] = (uint8_t)((raw >> 16) & 0xff); pay[11] = (uint8_t)((raw >> 8) & 0xff); pay[12] = (uint8_t)((raw >> 0) & 0xff); } //音センサ関連 void setup_payload_sound(float sound_db) { // ペイロード領域初期化 memset(paysound, 0x00, sizeof(paysound)); // ペイロード種別[気圧圧力照度距離ペイロード]設定(音用に流用) paysound[0] = 0x85; // 音 uint32_t raw; raw = *((uint32_t*)&sound_db); paysound[5] = (uint8_t)((raw >> 24) & 0xff); paysound[6] = (uint8_t)((raw >> 16) & 0xff); paysound[7] = (uint8_t)((raw >> 8) & 0xff); paysound[8] = (uint8_t)((raw >> 0) & 0xff); } #endif // CONFIG_ELTRES /** * @brief SCD41から温度、湿度、CO2濃度を取得し、最新値を更新 */ void measure_scd41() { uint16_t error_scd4x; bool data_ready_flag; uint16_t co2; float temp; float hum; error_scd4x = scd4x.getDataReadyStatus(data_ready_flag); if (error_scd4x != NoError) { Serial.print("cannot get data ready status ("); Serial.print(error_scd4x); Serial.println(")"); return; } if (data_ready_flag == false) { // センサの測定待ち return; } error_scd4x = scd4x.readMeasurement(co2, temp, hum); if (error_scd4x != NoError) { Serial.print("cannot read measurement ("); Serial.print(error_scd4x); Serial.println(")"); return; } // 最新値の更新 last_co2 = co2; last_temp = temp; last_hum = hum; // 最新値をシリアルモニタへ出力 Serial.print("[measure]co2: "); Serial.print(last_co2); Serial.print(" ppm, tem: "); Serial.print(last_temp, 6); Serial.print(" °C, hum: "); Serial.print(last_hum, 6); Serial.print(" %"); Serial.println(); } /** * @brief 照度・近接一体型センサから気圧を取得し、最新値を更新 */ void measure_illuminance(void) { byte rc; unsigned short ps_val; float als_val; // 照度・近接一体型センサから測定値を取得 rc = rpr0521rs.get_psalsval(&ps_val, &als_val); if (rc != 0) { Serial.print("cannot read measurement ("); Serial.print(rc); Serial.println(")"); return; } // 最新値の更新 last_illuminance = als_val; // 最新値をシリアルモニタへ出力 Serial.print("[measure]illuminance:"); Serial.print(last_illuminance, 6); Serial.print(" lux"); Serial.println(); delay(1000); } //音センサ void measure_sound(void) { if (millis() - lastSendTime >= SEND_INTERVAL_MS) { lastSendTime = millis(); int current = getSoundLevel(); if(current > sound_max_eltres){ sound_max_eltres = current; } ///////////dB変換 sound_max = 0; for (int i = 0; i < 100; i++) { int v = abs(analogRead(SOUND_PIN) - sound_ref); if (v > sound_max) sound_max = v; delayMicroseconds(200); } if (sound_max < 1) sound_max = 1; // 擬似dB変換 sound_db = 20.0 * log10((float)sound_max); Serial.print("Sound max = "); Serial.print(sound_max); Serial.print(" , dB = "); Serial.println(sound_db); //////////// // ===== ホールド処理 ===== if (current > holdSoundLevel) { // 大きくなったら即反映 holdSoundLevel = current; } else { // 小さくなったらゆっくり下げる holdSoundLevel -= DECAY_STEP; if (holdSoundLevel < current) { holdSoundLevel = current; } if (holdSoundLevel < 0) holdSoundLevel = 0; } const char* state = evaluateSound(holdSoundLevel); Serial.print("raw="); Serial.print(current); Serial.print(", hold="); Serial.print(holdSoundLevel); Serial.print(", state="); Serial.println(state); } } ``` ```arduino:不適な場所での睡眠防止カメラ&ブザー #include <SDHCI.h> #include <DNNRT.h> #include <Camera.h> // AIモデルへの入力サイズ #define INPUT_WIDTH (80) #define INPUT_HEIGHT (60) // 拡縮比率(2のべき乗) #define INPUT_RESIZE_RATIO (4) // 撮影サイズ #define PICTURE_WIDTH (CAM_IMGSIZE_QVGA_H) #define PICTURE_HEIGTH (CAM_IMGSIZE_QVGA_V) // 切り取りサイズ #define PICTURE_CLIP_WIDTH (INPUT_WIDTH * INPUT_RESIZE_RATIO) #define PICTURE_CLIP_HEIGHT (INPUT_HEIGHT * INPUT_RESIZE_RATIO) // ブザー const int SLEEP_ALARM_TIME = 10000; // 任意の寝落ち判断時間(左は10秒間の例) const int buzzerPin = 6; unsigned long sleepStartTime = 0; bool buzzerState = false; // DNNRTクラスのインスタンス DNNRT dnnrt; // SDカードクラスのインスタンス SDClass SD; // AIモデルの入力データ用領域 DNNVariable dnn_input(INPUT_WIDTH * INPUT_HEIGHT); /** * @brief セットアップ処理 */ void setup() { int ret; CamErr cam_err; Serial.begin(115200); while (!Serial) { ; // シリアルモニタ接続待ち } // AIモデルファイル読み込み File nnbfile = SD.open("network.nnb"); if (nnbfile == NULL) { // ファイル無しエラー Serial.print("nnb is not found"); exit(0); } // DNNRTライブラリ(AIモデルでの判定を行うライブラリ)の初期設定 ret = dnnrt.begin(nnbfile); if (ret < 0) { // DNNRT開始エラー Serial.print("DNNRT initialization failure.: "); Serial.println(ret); exit(0); } nnbfile.close(); // カメラの初期設定 cam_err = theCamera.begin(0); if (cam_err != CAM_ERR_SUCCESS) { // カメラ開始エラー Serial.print("CAMERA initialization failure.: "); Serial.println(cam_err); exit(0); } // 撮影パラメタ(サイズ、形式)設定 cam_err = theCamera.setStillPictureImageFormat(PICTURE_WIDTH, PICTURE_HEIGTH, CAM_IMAGE_PIX_FMT_YUV422); if (cam_err != CAM_ERR_SUCCESS) { // カメラ設定エラー Serial.print("CAMERA set parameters failure.: "); Serial.println(cam_err); exit(0); } // ブザー pinMode(buzzerPin, OUTPUT); digitalWrite(buzzerPin, HIGH); } /** * @brief ループ処理 */ void loop() { CamErr cam_err; CamImage coverted; // 1秒待機 sleep(1); // カメラ撮影 CamImage camImage = theCamera.takePicture(); if (!camImage.isAvailable()) { // 撮影失敗 Serial.println("CAMERA take picture failure."); return; } // 画像の切り取りと縮小処理 int lefttop_x = (PICTURE_WIDTH - PICTURE_CLIP_WIDTH) / 2; int lefttop_y = (PICTURE_HEIGTH - PICTURE_CLIP_HEIGHT) / 2; int rightbottom_x = lefttop_x + PICTURE_CLIP_WIDTH - 1; int rightbottom_y = lefttop_y + PICTURE_CLIP_HEIGHT - 1; cam_err = camImage.clipAndResizeImageByHW(coverted, lefttop_x, lefttop_y, rightbottom_x, rightbottom_y, INPUT_WIDTH, INPUT_HEIGHT); if (cam_err != CAM_ERR_SUCCESS) { Serial.print("CAMERA resize failure. : "); Serial.println(cam_err); return; } // グレースケール形式への変換 cam_err = coverted.convertPixFormat(CAM_IMAGE_PIX_FMT_GRAY); if (cam_err != CAM_ERR_SUCCESS) { Serial.print("CAMERA convert format failure. : "); Serial.println(cam_err); return; } // AIモデルの入力データ設定 float* input_data = dnn_input.data(); uint8_t* camera_buf = coverted.getImgBuff(); for (int h = 0; h < INPUT_HEIGHT; h++) { for (int w=0; w < INPUT_WIDTH; w++) { input_data[h * INPUT_WIDTH + w] = camera_buf[h * INPUT_WIDTH + w] / 255.0; } } dnnrt.inputVariable(dnn_input, 0); // 推論実行 dnnrt.forward(); // 推論結果取得 DNNVariable output = dnnrt.outputVariable(0); float value = output[0]; // 結果表示 unsigned long now = millis(); Serial.print("[recognition] person is "); if (value < 0.5f) { Serial.print("sleep."); // sleep になった瞬間を記録 if (sleepStartTime == 0) { sleepStartTime = now; } // sleepが一定時間以上続いたらブザーON if ((now - sleepStartTime) >= SLEEP_ALARM_TIME) { digitalWrite(buzzerPin, LOW); buzzerState = true; } } else { Serial.print("wake. "); // wakeになったらリセット sleepStartTime = 0; digitalWrite(buzzerPin, HIGH); buzzerState = false; } Serial.print(" ( value: "); Serial.print(value); Serial.println(")"); } ``` ```arduino:Switchbot連携 /* * * - VL53L0Xで距離測定 * - 距離 < 100mm が連続10秒続いたらトリガ * - その瞬間にSwitchBot Cloud APIへPOSTして「ボット press」 * - 実行後30秒待って停止 * * 重要: * - ConsoleLog/ConsolePrintf はGS2200ライブラリ側に存在するため * スケッチ側では定義しない(多重定義エラー回避) */ #include <Wire.h> #include <VL53L0X.h> #include <SDHCI.h> #include <RTC.h> #include <HttpGs2200.h> #include <TelitWiFi.h> //HTTPSecureのサンプルプログラムをもとに要自作 #include "config.h" #define BAUDRATE 115200 // ===== 距離判定 ===== #define PROXIMITY_THRESHOLD_MM (100) #define PROXIMITY_HOLD_MS (10UL * 1000UL) // 10秒 #define POST_ACTION_DELAY_MS (30UL * 1000UL) // 実行後30秒 // ===== LED ===== #define LED_READY PIN_LED0 #define LED_ACTION PIN_LED1 #define LED_ERR PIN_LED3 // ===== HTTPS受信バッファ ===== static const uint16_t RECEIVE_PACKET_SIZE = 1500; static uint8_t Receive_Data[RECEIVE_PACKET_SIZE]; // ===== GS2200 / HTTP ===== TelitWiFi gs2200; TWIFI_Params gsparams; HttpGs2200 theHttpGs2200(&gs2200); HTTPGS2200_HostParams hostParams; // ===== SD / RTC ===== SDClass theSD; // ===== VL53L0X ===== static VL53L0X distance_sensor; static uint16_t last_distance = 0xFFFF; // ===== トリガ管理 ===== static uint32_t proximity_start_ms = 0; static bool action_triggered = false; // ===== RTC表示 ===== static void print2digits(int v) { if (v < 10) Serial.print('0'); Serial.print(v); } static void printRtcDateTime(const RtcTime &t) { Serial.print(t.year()); Serial.print('/'); print2digits(t.month()); Serial.print('/'); print2digits(t.day()); Serial.print(' '); print2digits(t.hour()); Serial.print(':'); print2digits(t.minute()); Serial.print(':'); print2digits(t.second()); } // ===== SwitchBot POST path ===== static void build_post_path(char* out, size_t out_sz) { snprintf(out, out_sz, "/v1.0/devices/%s/commands", BOT_DEVICE_ID); } // ===== SwitchBot 送信(ボット press)===== static bool sendSwitchBotBotPress() { char path[128]; build_post_path(path, sizeof(path)); // Bot press const char body[] = "{\"command\":\"press\",\"parameter\":\"default\",\"commandType\":\"command\"}"; // begin(endする運用なので毎回呼ぶ) hostParams.host = (char*)HTTP_HOST; hostParams.port = (char*)HTTP_PORT; theHttpGs2200.begin(&hostParams); // headers theHttpGs2200.config(HTTP_HEADER_AUTHORIZATION, SWITCHBOT_TOKEN); theHttpGs2200.config(HTTP_HEADER_TRANSFER_ENCODING, "identity"); theHttpGs2200.config(HTTP_HEADER_CONTENT_TYPE, "application/json; charset=utf8"); theHttpGs2200.config(HTTP_HEADER_HOST, HTTP_HOST); Serial.print("[SWITCHBOT] POST "); Serial.println(path); if (!theHttpGs2200.post(path, (char*)body)) { Serial.println("[SWITCHBOT] POST FAILED"); theHttpGs2200.end(); return false; } // response while (theHttpGs2200.receive(5000)) { memset(Receive_Data, 0, RECEIVE_PACKET_SIZE); theHttpGs2200.read_data(Receive_Data, RECEIVE_PACKET_SIZE); Serial.print((char*)Receive_Data); } theHttpGs2200.end(); return true; } // ===== 初期化:距離センサ ===== static void init_distance_sensor_or_die() { Wire.begin(); distance_sensor.setTimeout(500); if (!distance_sensor.init()) { digitalWrite(LED_ERR, HIGH); Serial.println("[SENSOR] VL53L0X init FAILED"); while (1) delay(1000); } distance_sensor.startContinuous(); Serial.println("[SENSOR] VL53L0X init OK"); } // ===== 初期化:Wi-Fi + TLS ===== static void init_wifi_tls_or_die() { // SD while (!theSD.begin()) { Serial.println("[SD] Insert SD card."); delay(1000); } Serial.println("[SD] mount OK"); // RTC RTC.begin(); { RtcTime cur = RTC.getTime(); if (cur.year() < 2020) { RtcTime compiled(__DATE__, __TIME__); RTC.setTime(compiled); } Serial.print("[RTC] "); printRtcDateTime(RTC.getTime()); Serial.println(); } // Wi-Fi init Init_GS2200_SPI_type(iS110B_TypeC); gsparams.mode = ATCMD_MODE_STATION; gsparams.psave = ATCMD_PSAVE_DEFAULT; if (gs2200.begin(gsparams)) { digitalWrite(LED_ERR, HIGH); Serial.println("[WIFI] GS2200 init FAILED"); while (1) delay(1000); } Serial.println("[WIFI] GS2200 init OK"); // connect AP if (gs2200.activate_station(AP_SSID, PASSPHRASE)) { digitalWrite(LED_ERR, HIGH); Serial.println("[WIFI] Association FAILED"); while (1) delay(1000); } Serial.println("[WIFI] Association OK"); Serial.println("[WIFI] Wait for DHCP (10 sec)..."); delay(10000); // Root CA File rootCertsFile = theSD.open(ROOTCA_FILE, FILE_READ); if (!rootCertsFile) { digitalWrite(LED_ERR, HIGH); Serial.println("[TLS] ROOTCA_FILE open FAILED"); while (1) delay(1000); } Serial.println("[TLS] ROOTCA_FILE open OK"); char time_string[128]; RtcTime rtc = RTC.getTime(); snprintf(time_string, sizeof(time_string), "%02d/%02d/%04d,%02d:%02d:%02d", rtc.day(), rtc.month(), rtc.year(), rtc.hour(), rtc.minute(), rtc.second()); Serial.print("[TLS] RTC: "); Serial.println(time_string); hostParams.host = (char*)HTTP_HOST; hostParams.port = (char*)HTTP_PORT; theHttpGs2200.begin(&hostParams); theHttpGs2200.set_cert((char*)"TLS_CA", time_string, 0, 1, &rootCertsFile); rootCertsFile.close(); theHttpGs2200.end(); // 送信時にbeginし直す Serial.println("[TLS] set_cert OK"); } // ===== 距離測定 ===== static void measure_distance() { last_distance = distance_sensor.readRangeContinuousMillimeters(); Serial.print("[measure] distance: "); Serial.print(last_distance); Serial.println(" mm"); } // ===== トリガ後の処理 ===== static void run_lights_off_process() { digitalWrite(LED_ACTION, HIGH); Serial.println("[ACTION] lights off process triggered!"); Serial.print("[ACTION] RTC = "); printRtcDateTime(RTC.getTime()); Serial.println(); bool ok = sendSwitchBotBotPress(); Serial.print("[ACTION] SwitchBot result = "); Serial.println(ok ? "OK" : "FAILED"); Serial.print("[ACTION] waiting "); Serial.print(POST_ACTION_DELAY_MS / 1000); Serial.println(" seconds, then stop."); delay(POST_ACTION_DELAY_MS); Serial.println("[STATE] program stopped."); while (1) delay(1000); // 完全停止 } void setup() { Serial.begin(BAUDRATE); delay(2000); pinMode(LED_READY, OUTPUT); pinMode(LED_ACTION, OUTPUT); pinMode(LED_ERR, OUTPUT); digitalWrite(LED_READY, LOW); digitalWrite(LED_ACTION, LOW); digitalWrite(LED_ERR, LOW); Serial.println("[BOOT] start"); init_wifi_tls_or_die(); init_distance_sensor_or_die(); proximity_start_ms = 0; action_triggered = false; digitalWrite(LED_READY, HIGH); Serial.println("[READY]"); } void loop() { measure_distance(); uint32_t now = millis(); bool is_close = (last_distance < PROXIMITY_THRESHOLD_MM); if (is_close) { if (proximity_start_ms == 0) { proximity_start_ms = now; action_triggered = false; Serial.println("[STATE] in range start"); } if (!action_triggered && (now - proximity_start_ms >= PROXIMITY_HOLD_MS)) { action_triggered = true; run_lights_off_process(); } } else { if (proximity_start_ms != 0) Serial.println("[STATE] out of range -> reset"); proximity_start_ms = 0; action_triggered = false; } delay(100); } ```