システム概要
本システム「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 |
設計図
ソースコード
環境データ収集用
// 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);
}
}
不適な場所での睡眠防止カメラ&ブザー
#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(")");
}
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);
}


-
urano
さんが
2026/01/14
に
編集
をしました。
(メッセージ: 初版)
-
urano
さんが
2026/01/15
に
編集
をしました。
ログインしてコメントを投稿する