M_orangeのアイコン画像
M_orange 2024年01月31日作成
製作品 製作品 閲覧数 265
M_orange 2024年01月31日作成 製作品 製作品 閲覧数 265

火災報知器とIoTで人々を守る

火災報知器とIoTで人々を守る

作品概要

この作品のテーマは「街の大規模火災から人々を守る」である。火災発生の検知を温度センサーで行い、風向のデータをAPIによって得て、それらのデータを用いてデータ分析をして、LINEで火災の発生と風向を通知するシステムである。このシステムを用いることで、火災発生時にこの後燃え広がる可能性がある場所にいる人々への危険通知と安全な場所への避難を促すことができる。

大規模火災から人々を守るシステム

このシステムの利用者は「その街にいる人々」である。その街にいる人々が大規模火災の発生時に危険をいち早く知り、安全な場所へ避難することをサポートする。

社会的必要性

今までの大規模火災

約100年前、関東大震災では火災の発生によって大きな被害を受けた。それ以降も現在に至るまで、1976年の酒田大火、2016年の糸魚川市大規模火災、2023年のマウイ島山火事など日本国内でも世界でも大規模火災は起きている。今年になってからも日本国内で大規模な火災が発生したという知らせを聞き、大変心を痛めているところである。

大規模火災の燃え広がる原因

まず、大規模火災において火が燃え広がる原因はである。とあるところで火災が発生したとすると、火が燃え広がる方角はいつも一緒…ということはない。風向、風速、火災旋風などが影響して燃え広がる方向や速さは変わってくる。2016年の糸魚川市大規模火災のときは、出火地点からフェーン現象を伴う強い南風にあおられ次々に飛び火が発生したことにより燃え広がったという記録がある。

大規模火災の対策と現在の問題

燃え広がる原因が「」であり、風は「その時によって変化するもの」であるのに対して、現在の対策は火災報知器の設置やハザードマップ、避難訓練の実施にとどまっており、燃え広がる原因に対して現在の対策では対応しきれていないと言える。つまり、街の大規模火災にはその時の火災と風の状況に合わせた対策と避難が必要である。
現在の技術では、建物規模の大規模火災については、火災報知器の設置や、通信を用いた対策としては、火災放置機の作動を建物内の人へ知らせるシステムや火元をお知らせする火災報知器などの対策が進んできている。しかし、街単位の大規模火災については前例がなく、対策が進んでいない。ここで、IoTを用いて現在燃えている箇所を温度センサーで検知し、現在の風向とともに通知すれば、火災発生時に街にいる人が危険を知り、どの場所にいれば安全かを知ることができ、安全な場所への避難に役立てることができる。

実現方法

以下の図(街の様子の全体図)が今回提案するシステムの全体図である。
まず、温度センサーを用いて、ある一定以上の温度を検知したらそのデバイスがある場所、つまりその建物で火災が発生したと認識する。今回は設置場所の緯度・経度とデバイスIDが紐付いている、つまり、そのデバイスがどの緯度・経度の場所にあるかをあらかじめわかっていると仮定して実装している。火災が発生している場合と発生していない場合の2通りのpayloadを用意し、検知した温度によってその2種類のうちどちらかのpayloadデータをWi-FiまたはELTRESを用いてCLIP Viewer Liteに送信する。
次に風向データをOpenWeatherを用いて入手する。風向データの入手はOpenWeatherの会員登録をし、 One Call API 3.0を購読する。発行されたAPIキーから「Current 」、つまり現在の天気のデータを抽出し、そこから現在の風向データを抽出することで行う。得られた風向データの値の範囲は0〜360度となっており、0度は北、90度は東、180度は南、270度は西を示すので、今回の実装では0〜45度と316〜360度の範囲を北、46〜135度を東、136〜225度の範囲を南、226〜315度の範囲を西として表すことにした。
そして、Pythonを用いて、CLIP Viewer Liteに送信されたデータを先ほどの風向データをAPIを用いて組み合わせる。火災が発生した時のpayloadデータをあらかじめ閾値として設定し、CLIP Viewer Liteに送信されたpayloadの値が閾値と一致した時にLINEに通知を送信するように設定する。APIを用いて得られた風向データを、先ほどの方角の分岐の条件を用いてLINEでの通知に情報として載せる。通知する文章を「〇〇で火災が発生しました。風向は「風向」向きです。逃げて!」と通知することで、どこで(〇〇はデバイスがある緯度・経度が指す場所)、どの方角に風が吹いているのかをLINEで通知する。

街の様子の全体図

部品

センサからのデータ送信にWi-Fiを用いた場合

部品名 個数
Spresense メインボード 1
Spresense 拡張ボード 1
LM75B温度センサモジュール 1
SPRESENSE Wi-Fi Add-onボード iS110B 1
ワイヤ 4

センサからのデータ送信にELTRESを用いた場合

部品名 個数
Spresense メインボード 1
Spresense 拡張ボード 1
LM75B温度センサモジュール 1
ELTRESアドオンボード 1
GNSS用受信アンテナ 1
LPWA用受信アンテナ 1
ワイヤ 4

設計図

システム構成

システム構成の全体図はそれぞれ以下のようになっている。
システム構成図(Wi-Fi)
システム構成図(ELTRES)

配線の詳細

配線の詳細は以下の通りである。Wi-Fiの場合もELTRESの場合もセンサーとの繋ぎ方は同じである。
Spresense Vout(5V) - LM75B VCC(ワイヤ赤)
Spresense SCL - LM75B SCL (ワイヤ黄)
Spresense SDA - LM75B SDA(ワイヤ青)
Spresense GND - LM75B GND(ワイヤ白)
配線詳細(Wi-Fi)
配線詳細(ELTRES)
LM75Bの配線詳細

実装

Wi-Fiを用いた場合

今回は室温で実験を行ったため、「ある一定以上の温度を検知すると火災が発生したと判断する」の温度を「22.0℃」として実験した。(この温度を火災発生を検知するのに適切な温度に設定すれば適切に作動するはずである)温度センサーを読み取るとシリアルモニタには以下の図のように表示され、22.0℃以下の時は「Normal」、22.0℃以上の時は「High Temp!」となり、火災が発生した時としていない時を分けて認識できていることが確認できる。
センサー読み取り時のシリアルモニタの表示
以下の図がCLIP Viewer Liteに送信されたデータである。火災が発生したと検知した時は全て1、そうでない時は全て0がpayloadに格納されてCLIP Viewer Liteに送信できていることが確認できる。
CLIP Viewer Liteの表示
作成したPythonコードを温度データを取るときに実行しておくと、以下の図のようにLINEに通知が表示される。上のCLIP Viewer Liteの表示の図の送信時間に着目すると、ペイロードが全て0のときは、火災が発生していないので通知が行われていないが、16時13分には火災が発生したと検知され、ペイロードの値が全て1となっているから、16時13分には以下の図のように通知が届いている。以上より、火災が発生した時のみLINEにその時の風向の情報も合わせて通知されていることが確認できた。
LINE通知の表示

ELTRESを用いた場合

Wi-Fiを用いた場合と同じ条件で実験を行った。しかし、実行してもLED0は消灯・LED3は点灯する状態、つまりELTRESエラーが発生してしまい、CLIP Viewer Liteに正常にデータを送ることができなかった。ELTRESアドオンボードの接触不良が原因と考え、何度か抜き差しを行ったが改善しなかった。シリアルモニタには以下のように表示され、温度データの測定は正しく行えていることが確認できた。
シリアルモニタの表示

ソースコード

今回の実装についてCLIP学習教材「動かして学ぶIoTとAI」のサンプルプログラムのうち、以下のものを参考にさせていただいた。
PCアプリケーション作成例 - PythonによるLINE通知連携プログラム
サンプルプログラム(IoT編) - ELTRES送信
また、Wi-Fi送信については、https://github.com/TomonobuHayakawa/GS2200-WiFiも参考にさせていただいた。

inoファイル(Wi-Fi)

下記「xxxxxxx」部分には、接続先のエンドポイント名を記入してください。

wifi_LM75B.ino

#include <GS2200Hal.h> #include <GS2200AtCmd.h> #include <TelitWiFi.h> #include <Wire.h> #include <stdio.h> #include "wifi_config.h" #include "certificates.h" // SPI通信最大データ長 #define WIFI_SPI_DATA_MAX (1400) // BULKデータ最大サイズ #define WIFI_BULK_SIZE_MAX (1024) // BULKデータ転送試行最大回数 #define BULK_TX_TRY_MAX (10) // BULKデータ転送試行間隔 (ミリ秒) #define BLUK_TX_RETRY_INTERVAL (1000) // PIN定義:LED(プログラム状態) #define LED_RUN PIN_LED0 // PIN定義:LED(Wi-Fi接続状態) #define LED_WIFI PIN_LED1 // PIN定義:LED(送信状態) #define LED_SEND PIN_LED2 // 復帰値定義: 正常 #define RET_OK (0) // 復帰値定義: 異常 #define RET_ERROR (-1) // 復帰値定義: パラメタ異常 #define RET_INVALID_PARAM (-10) // 復帰値定義: メモリ不足 #define RET_NO_MEMORY (-11) // 復帰値定義: Wi-Fi未接続 #define RET_NO_WIFI (-12) // 復帰値定義: HTTPステータスコード異常 #define RET_HTTP_STATUS_NG (-13) // 証明書登録名:サーバー証明書 #define CERT_NAME_CA ("SSL_CLIP_CA") // 証明書登録名:クライアント証明書 #define CERT_NAME_CLIENT ("SSL_CLIP_CLIENT") // 証明書登録名:秘密鍵 #define CERT_NAME_KEY ("SSL_CLIP_KEY") // SNTPサーバー名 // 参照: https://jjy.nict.go.jp/tsp/PubNtp/index.html #define SNTP_HOST_NAME ("ntp.nict.jp") // 接続先エンドポイント名 #define ENDPOINT_HOST_NAME ("xxxxxxx") // 接続先エンドポイントポート番号 #define ENDPOINT_PORT (8443) // 外部参照 (GS2200-WiFi内GS2200AtCmd.cpp) extern uint8_t *RespBuffer[]; extern int RespBuffer_Index; // 外部参照 (GS2200-WiFi内GS2200Hal.cpp) extern uint8_t ESCBuffer[]; extern uint32_t ESCBufferCnt; // Wi-Fi制御用インスタンス TelitWiFi gs2200; // Wi-Fi用設定 TWIFI_Params gsparams; // Wi-Fi用コマンド作成領域 static char gs2200_cmd[128]; // Wi-Fi用コマンド応答受信領域 static char gs2200_resp[128]; // デバイスID(MACアドレス)保持領域 static String device_id; // Wi-Fi Add-onボードとの通信用作業領域 static uint8_t work_buf[WIFI_SPI_DATA_MAX]; //LM75Bの定義部分 #define LM75B_address 0x48 // A0=A1=A2=Low #define temp_reg 0x00 //Temperture register #define conf_reg 0x01 //Configuration register #define thyst_reg 0x02 //Hysterisis register #define tos_reg 0x03 //Overtemperature shutdown register // 温度がtosに達するとLM75BのOSピンがLowになり、その後温度がthystを下回るとOSピンがHighに戻る。(OS active Low) double tos = 30.0; //割り込み発生温度(高) 0.5℃刻み デフォルトでは80℃ double thyst = 28.0; //割り込み発生温度(低) 0.5℃刻み デフォルトでは75℃ signed int tos_data = (signed int)(tos / 0.5) << 7; //レジスタ用に変換 signed int thyst_data = (signed int)(thyst / 0.5) << 7; //レジスタ用に変換 int osPin = 2; //LM75BのOSピンと接続するピン /** * @brief セットアップ処理 */ void setup() { //以下LM75Bに関する部分 pinMode(osPin,INPUT_PULLUP); attachInterrupt(0,temp_interrupt,CHANGE); //割り込み関数登録 2番ピンの状態変化で割り込み Wire.begin(); Serial.begin(115200); Wire.beginTransmission(LM75B_address); //***************************************** Wire.write(tos_reg); Wire.write(tos_data >> 8); //tosの温度設定 Wire.write(tos_data); Wire.endTransmission(); //***************************************** Wire.beginTransmission(LM75B_address); //----------------------------------------- Wire.write(thyst_reg); Wire.write(thyst_data >> 8); //thystの温度設定 Wire.write(thyst_data); Wire.endTransmission(); //----------------------------------------- Wire.beginTransmission(LM75B_address); //***************************************** Wire.write(temp_reg); //温度読み出しモードに設定 Wire.endTransmission(); //***************************************** //以下wifiに関する部分 int ret; // LED初期設定 pinMode(LED_RUN, OUTPUT); digitalWrite(LED_RUN, HIGH); pinMode(LED_WIFI, OUTPUT); digitalWrite(LED_WIFI, LOW); pinMode(LED_SEND, OUTPUT); digitalWrite(LED_SEND, LOW); // Wi-Fiモジュール初期設定 if (initialize_wifi_module() != RET_OK) { start_error_routine(); } // Wi-Fi接続処理 wifi_connect(); digitalWrite(LED_WIFI, HIGH); // 時刻補正処理 do { bool is_connected; ret = time_sync_routine(); // Wi-Fi切断時は再接続 is_wifi_connected(is_connected); if (is_connected == false) { digitalWrite(LED_WIFI, LOW); wifi_connect(); digitalWrite(LED_WIFI, HIGH); } } while (ret != RET_OK); } /** * @brief ループ処理 */ void loop() { //以下LM75Bに関する部分 char str[20] = { 0 }; signed int temp_data = 0; //LM75Bの温度レジスタの値用変数 double temp = 0.0; //温度用変数 Wire.requestFrom(LM75B_address,2); while(Wire.available()){ temp_data |= (Wire.read() << 8); //温度レジスタの上位8bit取得 temp_data |= Wire.read(); //温度レジスタの下位8bit取得(有効3bit) } temp = (temp_data >> 5) * 0.125; //レジスタの値を温度情報に変換 Serial.println(temp); delay(1000); //以下Wi-Fiに関する部分 int ret; byte payload[16]; bool is_connected; // Wi-Fi切断時は再接続 is_wifi_connected(is_connected); if (is_connected == false) { digitalWrite(LED_WIFI, LOW); wifi_connect(); digitalWrite(LED_WIFI, HIGH); } // データ準備 if (temp > 40.0 ) { //ここの数字を火災発生を検知するのに適切な値に変更することができる Serial.print("High Temp!"); memset(payload, 0xFF, sizeof(payload)); } else { Serial.print("Normal"); memset(payload, 0, sizeof(payload)); } digitalWrite(LED_SEND, HIGH); // データ送信 if (send_data(payload, sizeof(payload)) == RET_OK) { Serial.println("[sample]completed sending."); } else { Serial.println("[sample]failed sending."); } digitalWrite(LED_SEND, LOW); // 60秒待機 sleep(60); } /** * @brief エラー時処理 */ void start_error_routine(void) { digitalWrite(LED_RUN, LOW); Serial.flush(); sleep(1); exit(1); } /** * @brief Wi-Fiモジュール初期設定 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int initialize_wifi_module(void) { // Wi-Fi子機設定 Init_GS2200_SPI_type(iS110B_TypeC); gsparams.mode = ATCMD_MODE_STATION; gsparams.psave = ATCMD_PSAVE_DEFAULT; if (gs2200.begin(gsparams)) { // 初期設定エラー (Wi-Fi子機設定失敗) Serial.println("[sample]cannnot initialize iS110B."); return RET_ERROR; } // MACアドレス取得とデバイスID生成 device_id = String(); memset(gs2200_resp, 0, sizeof(gs2200_resp)); while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } if (AtCmd_NMAC_Q(gs2200_resp) == ATCMD_RESP_OK) { char *ptr = gs2200_resp; while (*ptr != NULL) { char c = *ptr; ptr++; if (c == ':') { continue; } if (isAlphaNumeric(c)) { device_id.concat((char)tolower(c)); } } } else { // 初期設定エラー (MACアドレス取得失敗) Serial.println("[sample]cannnot get mac address."); return RET_ERROR; } // HTTPS接続設定 if (setup_https_connection() != RET_OK) { // 初期設定エラー (HTTPS接続設定失敗) Serial.println("[sample]cannnot register certifications."); return RET_ERROR; } return RET_OK; } /** * @brief HTTPS接続設定 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int setup_https_connection(void) { // HTTPS接続用証明書登録 if (add_certificate(CERT_NAME_CA, server_cert, sizeof(server_cert)) != RET_OK) { return RET_ERROR; } if (add_certificate(CERT_NAME_CLIENT, client_cert, sizeof(client_cert)) != RET_OK) { return RET_ERROR; } if (add_certificate(CERT_NAME_KEY, client_key, sizeof(client_key)) != RET_OK) { return RET_ERROR; } return RET_OK; } /** * @brief 証明書追加 * @param name 登録名 * @param der 証明書(DER形式) * @param length 長さ */ int add_certificate(char *name, const uint8_t *der, const size_t length) { size_t dst_index; size_t src_index; ATCMD_RESP_E resp=ATCMD_RESP_UNMATCH; SPI_RESP_STATUS_E s; uint8_t *tx_buf = work_buf; size_t tx_buf_len = sizeof(work_buf); if (tx_buf_len > WIFI_SPI_DATA_MAX) { tx_buf_len = WIFI_SPI_DATA_MAX; } while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } memset(gs2200_cmd, 0, sizeof(gs2200_cmd)); snprintf(gs2200_cmd, sizeof(gs2200_cmd), "AT+TCERTADD=%s,0,%d,1\r\n", name, length); resp = AtCmd_SendCommand( gs2200_cmd ); if (resp != ATCMD_RESP_OK) { return RET_ERROR; } // フォーマットに合わせて転送 snprintf((char*)tx_buf, tx_buf_len, "%cW", ATCMD_ESC); dst_index = 2; src_index = 0; do { size_t cpy_size = length - src_index; if (cpy_size > tx_buf_len - dst_index) { cpy_size = tx_buf_len - dst_index; } memcpy(tx_buf + dst_index, der + src_index, cpy_size); dst_index += cpy_size; src_index += cpy_size; s = WiFi_Write( (char *)tx_buf, dst_index ); if ( s ==SPI_RESP_STATUS_OK ) { dst_index = 0; } else { return RET_ERROR; } } while (length - src_index > 0); return RET_OK; } /** * @brief Wi-Fi接続 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int wifi_connect(void) { int ret; bool is_connected = false; do { while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } // Wi-Fi接続要求 ret = gs2200.activate_station(WIFI_AP_SSID, WIFI_AP_PASS); if (ret != OK) { sleep(1); continue; } // Wi-Fi接続待ち(最大30秒) for (int count=0; count < 30; count++) { sleep(1); is_wifi_connected(is_connected); if (is_connected) { break; } } } while (is_connected == false); return RET_OK; } /** * @brief Wi-Fi接続状態取得 * @param is_connected [out]接続状態(true:接続中,false:切断中) * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int is_wifi_connected(bool &is_connected) { ATCMD_RESP_E resp; ATCMD_NetworkStatus status; is_connected = false; while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } resp = AtCmd_NSTAT(&status); if (resp != ATCMD_RESP_OK) { return RET_ERROR; } if (status.connected) { is_connected = true; } return RET_OK; } /** * @brief Wi-Fiモジュール内の時刻同期処理 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int time_sync_routine(void) { int ret; uint64_t st_time; uint32_t epoch = 0; const uint32_t threshold = 1672531200; while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } // ホスト名からIPアドレスへ変換 memset(gs2200_resp, 0, sizeof(gs2200_resp)); if (AtCmd_DNSLOOKUP(SNTP_HOST_NAME, gs2200_resp) != ATCMD_RESP_OK) { return RET_ERROR; } while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } // 時刻同期要求 memset(gs2200_cmd, 0, sizeof(gs2200_cmd)); ret = snprintf(gs2200_cmd, sizeof(gs2200_cmd)-1, "AT+NTIMESYNC=1,%s,30,0\r\n", gs2200_resp); if (AtCmd_SendCommand(gs2200_cmd) != ATCMD_RESP_OK) { return RET_ERROR; } // 時刻同期待ち(最大30秒) st_time = millis(); do { sleep(1); gettime_from_modulue(epoch); } while (epoch < threshold && (millis() - st_time < 30 * 1000)); if (epoch < threshold) { return RET_ERROR; } return RET_OK; } /** * @brief Wi-Fiモジュールから時間取得 * @param epoch EPOCH時間 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int gettime_from_modulue(uint32_t &epoch) { int ret; uint64_t epoch_ms; while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } memset(gs2200_resp, 0, sizeof(gs2200_resp)); memset(gs2200_cmd, 0, sizeof(gs2200_cmd)); if (AtCmd_SendCommand("AT+GETTIME=?\r\n") != ATCMD_RESP_OK) { return RET_ERROR; } if (RespBuffer_Index == 0) { return RET_ERROR; } // 応答データ解析 char *ptr = (char*)RespBuffer[0]; ptr = strrchr(ptr, ','); if (ptr == NULL) { return RET_ERROR; } ptr++; epoch_ms = strtoull(ptr, NULL, 10); epoch = (uint32_t) (epoch_ms / 1000); return RET_OK; } /** * @brief データ送信 * @param data データの先頭アドレス * @param length データ長 * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int send_data(byte* data, size_t length) { int ret; char cid = 0; String body = String(); int ret_func = RET_ERROR; // パラメタチェック if (data == NULL || length == 0) { return RET_INVALID_PARAM; } // フォーマット変換 ret = create_http_body(body, data, length); if (ret != RET_OK) { return RET_NO_MEMORY; } // 接続処理 ret = connect_server(ENDPOINT_HOST_NAME, ENDPOINT_PORT, cid); if (ret != RET_OK) { disconnect_server(0); return RET_ERROR; } // HTTPリクエスト送信 ret = send_http_request(cid, ENDPOINT_HOST_NAME, ENDPOINT_PORT, body.c_str()); if (ret == RET_OK) { ret_func = RET_OK; } // 切断処理 disconnect_server(cid); return ret_func; } /** * @brief サーバーへの接続処理 * @param host [in] 接続先ホスト名 * @param port [in] 接続先ポート番号 * @param cid [out] 接続ID * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int connect_server(const char *host, const int port, char &cid) { ATCMD_RESP_E resp; while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } // ホスト名 => IPアドレス変換 memset(gs2200_resp, 0, sizeof(gs2200_resp)); resp = AtCmd_DNSLOOKUP((char*)host, gs2200_resp); if (resp != ATCMD_RESP_OK) { return RET_ERROR; } String ip_str = String(gs2200_resp); String port_str = String(port); resp = AtCmd_NCTCP((char*)ip_str.c_str(), (char*)port_str.c_str(), &cid); if (resp != ATCMD_RESP_OK) { return RET_ERROR; } while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } memset(gs2200_cmd, 0, sizeof(gs2200_cmd)); snprintf(gs2200_cmd, sizeof(gs2200_cmd), "AT+SSLOPEN=%c,%s,%s,%s\r\n", cid, CERT_NAME_CA, CERT_NAME_CLIENT, CERT_NAME_KEY); resp = AtCmd_SendCommand(gs2200_cmd); if (resp != ATCMD_RESP_OK) { disconnect_server(cid); return RET_ERROR; } return RET_OK; } /** * @brief HTTPリクエスト送信 * @param cid [in] 接続ID * @param host [in] ホスト名 * @param port [in] ポート番号 * @param body [in] HTTPボディ * @retval RET_OK 成功 * @retval RET_NO_MEMORY メモリ不足 * @retval RET_ERROR 失敗 */ int send_http_request(const char cid, const char *host, const int port, const char *body) { ATCMD_RESP_E resp; String http_req, http_resp; char *buf = (char*)work_buf; size_t buf_len = sizeof(work_buf); int32_t send_index = 0; uint64_t rcv_st_time; int ret_func = RET_ERROR; bool recv_resp = false; // POST メッセージ設定 if (http_req.concat("POST /topics/clip/wifi?qos=1 HTTP/1.1\r\n") == 0) { return RET_NO_MEMORY; } // HTTP HEADERS設定 if (http_req.concat("Content-Type: application/json\r\n") == 0) { return RET_NO_MEMORY; } memset(buf, 0, buf_len); snprintf(buf, buf_len, "Content-Length: %d\r\n", strlen(body)); if (http_req.concat(buf) == 0) { return RET_NO_MEMORY; } memset(buf, 0, buf_len); snprintf(buf, buf_len, "Host: %s\r\n", host); if (http_req.concat(buf) == 0) { return RET_NO_MEMORY; } // 区切り行設定 if (http_req.concat("\r\n") == 0) { return RET_NO_MEMORY; } // BODYデータ設定 if (http_req.concat(body) == 0) { return RET_NO_MEMORY; } while (Get_GPIO37Status()) { AtCmd_RecvResponse(); } WiFi_InitESCBuffer(); // サイズ制限があるため分割して転送 do { ATCMD_RESP_E atcmd_resp; int32_t send_size = http_req.length() - send_index; if (send_size > WIFI_BULK_SIZE_MAX) { send_size = WIFI_BULK_SIZE_MAX; } while (Get_GPIO37Status()) { atcmd_resp = AtCmd_RecvResponse(); if (atcmd_resp == ATCMD_RESP_ESC_FAIL) { return RET_ERROR; } } // 分割2回目以降は少し時間を開けて実施 if (send_index > 0) { usleep(100 * 1000); } resp = ATCMD_RESP_SPI_ERROR; for (int try_cnt=0; try_cnt < BULK_TX_TRY_MAX; try_cnt++) { resp = AtCmd_SendBulkData(cid, http_req.c_str() + send_index, send_size); if (resp == ATCMD_RESP_OK) { break; } usleep(BLUK_TX_RETRY_INTERVAL * 1000); } if (resp != ATCMD_RESP_OK) { return RET_ERROR; } send_index += send_size; } while(http_req.length() - send_index > 0); // レスポンス受信処理 ret_func = RET_ERROR; rcv_st_time = millis(); do { while (Get_GPIO37Status()) { resp = AtCmd_RecvResponse(); if (resp == ATCMD_RESP_BULK_DATA_RX && Check_CID(cid)) { // レスポンスのステータスコード確認 http_resp.concat((const char*)(ESCBuffer+1)); if (http_resp.length() != ESCBufferCnt -1) { Serial.println("[sample]invalid response."); } if (http_resp.indexOf("200 OK") >= 0) { ret_func = RET_OK; } else { ret_func = RET_HTTP_STATUS_NG; } recv_resp = true; break; } } } while ( !recv_resp && (millis() - rcv_st_time < 30 * 1000)); return ret_func; } /** * @brief サーバーとの切断処理 * @param cid [in] 接続ID * @retval RET_OK 成功 * @retval RET_ERROR 失敗 */ int disconnect_server(const char cid) { ATCMD_RESP_E resp; if (cid) { resp = AtCmd_NCLOSE(cid); } else { resp = AtCmd_NCLOSEALL(); } if (resp != ATCMD_RESP_OK) { return RET_ERROR; } return RET_OK; } /** * @brief HTTPボディデータ作成 * @details CLIP Viewer向けフォーマットへ合わせて作成 * @param body [out] 格納先 * @param data [in] payloadデータ(バイナリ形式) * @param length [in] payloadデータ長 * @retval RET_OK 成功 * @retval RET_NO_MEMORY メモリ不足 */ int create_http_body(String &body, byte* data, size_t length) { int index = 0; char hex_buf[3]; body = String(); if (body.concat("{\"id\":\"") == 0) { return RET_NO_MEMORY; } if (body.concat(device_id) == 0) { return RET_NO_MEMORY; } if (body.concat("\",\"payload\":\"") == 0) { return RET_NO_MEMORY; } for (index = 0; index < length; index++) { snprintf(hex_buf, sizeof(hex_buf), "%02x", data[index]); if (body.concat(hex_buf) == 0) { return RET_NO_MEMORY; } } if (body.concat("\"}") == 0) { return RET_NO_MEMORY; } return RET_OK; } //以下LM75Bに関する関数 void temp_interrupt() //割り込み関数 { if(digitalRead(osPin) == 0){ //LM75Bの温度がtosの値を超えるとOSピンがLowになり Serial.print("TEMP : OVER "); Serial.print(tos); Serial.println(" deg"); } else{ //LM75Bの温度がthystを下回るとOSピンがHighになる。 Serial.print("TEMP : UNDER "); Serial.print(thyst); Serial.println(" deg"); } }

certificates.h

/** * certificates.h - 認証関連設定ファイル * Copyright 2023 CRESCO LTD. */ #ifndef __CERTIFICATES_H__ #define __CERTIFICATES_H__ // サーバー証明書 static const uint8_t server_cert[] = { *******************// サーバー証明書の中身 }; // クライアント証明書 static const uint8_t client_cert[] = { *******************// クライアント証明書の中身 }; // クライアント証明書の秘密鍵 static const uint8_t client_key[] = { *******************// クライアント証明書の秘密鍵の中身 }; #endif // __CERTIFICATES_H__

wifi_config.h

/** * wifi_config.h - Wi-Fi関連設定ファイル * Copyright 2023 CRESCO LTD. */ #ifndef __WIFI_CONFIG_H__ #define __WIFI_CONFIG_H__ // 接続先Wi-FiアクセスポイントのSSID #define WIFI_AP_SSID "ssid" // 接続先Wi-Fiアクセスポイントのパスワード #define WIFI_AP_PASS "pass" #endif // __WIFI_CONFIG_H__

inoファイル(ELTRES)

eltres_LM75B.ino

#include <EltresAddonBoard.h> #include <Wire.h> #include <stdio.h> // 以下LM75Bに関する定義 #define LM75B_address 0x48 // A0=A1=A2=Low #define temp_reg 0x00 //Temperture register #define conf_reg 0x01 //Configuration register #define thyst_reg 0x02 //Hysterisis register #define tos_reg 0x03 //Overtemperature shutdown register // 温度がtosに達するとLM75BのOSピンがLowになり、その後温度がthystを下回るとOSピンがHighに戻る。(OS active Low) double tos = 30.0; //割り込み発生温度(高) 0.5℃刻み デフォルトでは80℃ double thyst = 28.0; //割り込み発生温度(低) 0.5℃刻み デフォルトでは75℃ signed int tos_data = (signed int)(tos / 0.5) << 7; //レジスタ用に変換 signed int thyst_data = (signed int)(thyst / 0.5) << 7; //レジスタ用に変換 int osPin = 2; //LM75BのOSピンと接続するピン // 以下ELTRESに関する定義 // 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_STOPPING (2) // プログラム内部状態:終了 #define PROGRAM_STS_STOPPED (3) // プログラム内部状態 int program_sts = PROGRAM_STS_INIT; // GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ bool gnss_recevie_timeout = false; // 点滅処理で最後に変更した時間 uint64_t last_change_blink_time = 0; // イベント通知での送信直前通知(5秒前)受信フラグ bool event_send_ready = false; // イベント通知でのアイドル状態受信フラグ bool event_idle = false; // 送信回数 int send_count = 0; // ペイロードデータ格納場所 uint8_t payload[16]; /** * @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); event_idle = true; 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 GGA情報受信コールバック * @param gga_info GGA情報のポインタ */ void gga_event_cb(const eltres_board_gga_info *gga_info) { Serial.print("[gga]"); if (gga_info->m_pos_status) { // 測位状態 // GGA情報をシリアルモニタへ出力 Serial.print("utc: "); Serial.println((const char *)gga_info->m_utc); Serial.print("lat: "); Serial.print((const char *)gga_info->m_n_s); Serial.print((const char *)gga_info->m_lat); Serial.print(", lon: "); Serial.print((const char *)gga_info->m_e_w); Serial.println((const char *)gga_info->m_lon); Serial.print("pos_status: "); Serial.print(gga_info->m_pos_status); Serial.print(", sat_used: "); Serial.println(gga_info->m_sat_used); Serial.print("hdop: "); Serial.print(gga_info->m_hdop); Serial.print(", height: "); Serial.print(gga_info->m_height); Serial.print(" m, geoid: "); Serial.print(gga_info->m_geoid); Serial.println(" m"); } else { // 非測位状態 // "invalid data"をシリアルモニタへ出力 Serial.println("invalid data."); } } /** * @brief setup()関数 */ void setup() { // 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); // ELTRES起動処理 eltres_board_result ret = EltresAddonBoard.begin(ELTRES_BOARD_SEND_MODE_1MIN,eltres_event_cb, gga_event_cb); 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(")."); } else { // 正常 program_sts = PROGRAM_STS_RUNNING; } //以下LM75Bに関する部分 pinMode(osPin,INPUT_PULLUP); attachInterrupt(0,temp_interrupt,CHANGE); //割り込み関数登録 2番ピンの状態変化で割り込み Wire.begin(); Serial.begin(115200); Wire.beginTransmission(LM75B_address); //***************************************** Wire.write(tos_reg); Wire.write(tos_data >> 8); //tosの温度設定 Wire.write(tos_data); Wire.endTransmission(); //***************************************** Wire.beginTransmission(LM75B_address); //----------------------------------------- Wire.write(thyst_reg); Wire.write(thyst_data >> 8); //thystの温度設定 Wire.write(thyst_data); Wire.endTransmission(); //----------------------------------------- Wire.beginTransmission(LM75B_address); //***************************************** Wire.write(temp_reg); //温度読み出しモードに設定 Wire.endTransmission(); //***************************************** } /** * @brief loop()関数 */ void loop() { //以下LM75Bに関する部分 char str[20] = { 0 }; signed int temp_data = 0; //LM75Bの温度レジスタの値用変数 double temp = 0.0; //温度用変数 Wire.requestFrom(LM75B_address,2); while(Wire.available()){ temp_data |= (Wire.read() << 8); //温度レジスタの上位8bit取得 temp_data |= Wire.read(); //温度レジスタの下位8bit取得(有効3bit) } temp = (temp_data >> 5) * 0.125; //レジスタの値を温度情報に変換 Serial.println(temp); delay(1000); //以下ELTRESに関する部分 uint32_t gnss_time; int32_t remaining; switch (program_sts) { case PROGRAM_STS_RUNNING: // プログラム内部状態:起動中 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; send_count += 1; // データ準備 if (temp > 40.0 ) { //ここの数字を火災発生を検知するのに適切な値に変更することができる Serial.print("High Temp!"); memset(payload, 0x11, sizeof(payload)); } else { Serial.print("Normal"); memset(payload, 0, sizeof(payload)); } // 送信ペイロードの設定 EltresAddonBoard.set_payload(payload); } if (event_idle) { // 送信完了時の処理 event_idle = false; // GNSS時刻(epoch秒)の取得 EltresAddonBoard.get_gnss_time(&gnss_time); Serial.print("gnss time: "); Serial.print(gnss_time); Serial.println(" sec"); // 次送信までの残り時間の取得 EltresAddonBoard.get_remaing_time(&remaining); Serial.print("remaining time: "); Serial.print(remaining); Serial.println(" sec"); } break; case PROGRAM_STS_STOPPING: // プログラム内部状態:終了処理中 // ELTRES停止処理(注意:この処理を行わないとELTRESが自動送信し続ける) EltresAddonBoard.end(); digitalWrite(LED_RUN, LOW); digitalWrite(LED_GNSS, LOW); program_sts = PROGRAM_STS_STOPPED; break; case PROGRAM_STS_STOPPED: // プログラム内部状態:終了 break; } // 次のループ処理まで100ミリ秒待機 delay(100); } //以下LM75Bに関する関数 void temp_interrupt() //割り込み関数 { if(digitalRead(osPin) == 0){ //LM75Bの温度がtosの値を超えるとOSピンがLowになり Serial.print("TEMP : OVER "); Serial.print(tos); Serial.println(" deg"); } else{ //LM75Bの温度がthystを下回るとOSピンがHighになる。 Serial.print("TEMP : UNDER "); Serial.print(thyst); Serial.println(" deg"); } }

Pythonファイル

fire_python.py

import datetime import urllib.request import urllib.parse import json import http.client import time import requests class ClvApiCtrl: """ CLIP Viewer Lite API制御クラス """ _ENDPOINT = "https://api.clip-viewer-lite.com" def __init__(self, api_key: str, username: str, password: str) -> None: """初期化処理 """ self.__api_key = api_key self.__username = username self.__password = password self.__token = '' self.__token_expired = datetime.datetime.now() def _api_get_token(self) -> bool: """エンドポイントからのトークン取得 """ result = False url = "{ep}/auth/token".format(ep=ClvApiCtrl._ENDPOINT) data = {"username": self.__username, "password": self.__password} req = urllib.request.Request(url=url,data=json.dumps(data).encode()) req.add_header("X-API-Key", self.__api_key) try: with urllib.request.urlopen(req) as res: if isinstance(res, http.client.HTTPResponse): if res.status == 200: body = res.read() res_data: dict = json.loads(body) self.__token = res_data.get("token") if self.__token: self.__token_expired = datetime.datetime.now() + datetime.timedelta(minutes=4) result = True except Exception as e: print("CLIP API error: ", e) return result def retrive_token(self) -> bool: """トークン再取得 """ result = False check_time = datetime.datetime.now() if self.__token and (check_time < self.__token_expired): # 再取得不要 result = True else: # 再取得実行 result = self._api_get_token() return result def _api_get_latest_payload(self, device_id) -> (bool, dict | None): """サーバからの最新データ取得 """ result = False payload = None url = "{ep}/payload/latest/{id}".format(ep=ClvApiCtrl._ENDPOINT, id=device_id) req = urllib.request.Request(url=url) req.add_header("X-API-Key", self.__api_key) req.add_header("Authorization", self.__token) try: with urllib.request.urlopen(req) as res: if isinstance(res, http.client.HTTPResponse): if res.status == 200: body = res.read() res_data: dict = json.loads(body) payload_list = res_data.get("payload") if payload_list: payload = payload_list[0] else: payload = {} result = True except Exception as e: print("CLIP API error: ", e) return (result, payload) def get_payload(self, device_id) -> (bool, dict | None): """データ取得 """ result = False payload = None token_avaiable = self.retrive_token() if token_avaiable: result, payload = self._api_get_latest_payload(device_id=device_id) return (result, payload) class LineNotifyCtrl: """LINE Notify API連携制御クラス """ def __init__(self, token) -> None: """初期化処理 """ self.__token = token def notify(self, message) -> bool: """通知 """ result = False data = {"message": message} url = "https://notify-api.line.me/api/notify" req = urllib.request.Request(url=url, data=urllib.parse.urlencode(data).encode()) req.add_header("Content-Type", "application/x-www-form-urlencoded") req.add_header("Authorization", "Bearer {}".format(self.__token)) try: with urllib.request.urlopen(req) as res: if isinstance(res, http.client.HTTPResponse): if res.status == 200: # 通知成功 result = True except Exception as e: print("LINE notify error:", e) return result class Config: """アクセス先、表示文字列設定 """ def __init__(self) -> None: """初期化処理 "***"に必要な情報を設定してください。 他の値も変更可能です。 """ self.username = "*******************" # CLIP Viewer Liteのユーザ名 self.password = "*******************" # CLIP Viewer Liteのパスワード self.api_key = "*******************" # CLIP Viewer LiteのAPIキー self.device = "*******************" # デバイスID self.line_token ="*******************" # LINEのアクセストークン self.wapi_key = "*******************" #天気のAPIキー self.latitude = **** #緯度 利用する緯度の値を設定 self.longitude = **** #経度 利用する経度の値を設定 self.line_target = "payload" # 最新ペイロード取得APIのレスポンスボディの項目名 self.line_missing_value = 8888 # 取得失敗時の値(エラーが認識できる値で任意に変更可) self.line_threshold = 11111111111111111111111111111111 # 閾値 # {v} == {t} 閾値と一致する場合 self.line_trigger = "{v} == {t}" # LINEに表示される文字列 self.line_message = "〇〇で火災が発生しました。風向は{W}向きです。逃げて!" def get_wind_direction(api_key, lat, lon): base_url = "https://api.openweathermap.org/data/3.0/onecall" params = { "lat": lat, "lon": lon, "exclude": "minutely,hourly,daily", "appid": api_key } try: response = requests.get(base_url, params=params) response.raise_for_status() data = response.json() wind_direction = data["current"]["wind_deg"] return wind_direction except requests.exceptions.RequestException as w: print(f"Error accessing API: {w}") return None def main() -> None: """ メイン """ config = Config() failure_cnt = 0 latest_send_time = "" clv_api = ClvApiCtrl(config.api_key, config.username, config.password) print("started") line_notify = LineNotifyCtrl(config.line_token) try: while True: ret, d = clv_api.get_payload(config.device) if ret: failure_cnt = 0 # 取得したペイロード出力 print("[payload]", d) if d: send_time = d.get("sendDateTime", "") if send_time != latest_send_time: latest_send_time = send_time value = d.get(config.line_target, config.line_missing_value) # 風向きの取得 wind_direction = get_wind_direction(config.wapi_key, config.latitude, config.longitude) if eval(config.line_trigger.format(v=value, t=config.line_threshold)): # LINE通知の条件 if 0 <= wind_direction <= 45 or 316 <= wind_direction <= 360: wind_direction_text = "北" elif 46 <= wind_direction <= 135: wind_direction_text = "東" elif 136 <= wind_direction <= 225: wind_direction_text = "南" elif 226 <= wind_direction <= 315: wind_direction_text = "西" m = config.line_message.format(W=wind_direction_text) line_notify.notify(m) time.sleep(60) else: failure_cnt += 1 if failure_cnt > 5: break time.sleep(5) except KeyboardInterrupt: pass print("stopped") if __name__ == "__main__": main()

今後の展望

今回の実装ではWi-Fiを用いての温度データの送信と、風向データを用いて、火災の発生と風向を利用者に知らせる状態までしか実装できていない。そのため、まず、ELTRESを用いての温度データの送信の動作確認を行い、正常に動作するようにしたい。また、今携帯電話を持っている人の位置情報を利用して、以下の図のように50m北で火災発生のように、火災発生場所と利用者との具体的な距離を示してよりわかりやすく情報を伝えるような改良を加えたい。また、大規模火災の燃え広がる原因として、風向・風速・火災旋風を例として挙げたが、風速・火災旋風のデータを用いて、現在燃えている場所からこれからどの範囲に燃え広がるのかを算出し、燃え広がる範囲内にいる人に通知が届くような工夫を実装したいと考えている。
LINE通知の完成予想図

参考文献

[1]世界ジオパークのまち糸魚川市.”大火の記録展示”.世界ジオパークのまち糸魚川市. 2020年12月23日. https://www.city.itoigawa.lg.jp/7373.htm , (2023年10月27日)
[2] 久留米広域消防本部(福岡県)古賀友章 . "自動火災報知設備への IoT 導入について" . 全国消防協会 . 不明 . https://www.ffaj-shobo.or.jp/ronbun/data/h30/17.pdf , (2023 年 9 月 11 日)
[3] パナソニック ホールディングス株式会社 . “業界初(※1)火元をお知らせする IoT 対応住宅用火災警報器を発売".
パナソニック ホールディングス株式会社 . 2020 年 9 月 30 日 .https://news.panasonic.com/jp/press/jn200930-4 , (2023 年 9 月 11 日)

ログインしてコメントを投稿する