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

mametarou963 が 2022年09月26日00時06分02秒 に編集

コメント無し

本文の変更

# 背景 子供や認証の方の行方不明者の人数は令和4年現在でも横ばいに推移しています。 8割以上は発見されていますが、対策が実施されているにも関わらず、値が改善されないことから、 個々人の対策も重要になってくると考えられます。 キッズ向けのGPSは多数発表されていますが、「spresenseが省電力」であること、「SPRESENSE用ELTRESアドオンボード」が小型で、ランニングコストが安価であり、数分単位で位置情報をクラウド上に通知できることから、キッズみまもりに十分な性能を備えており、今回開発してみようと考えました。 # ユースケース * 広い屋外で子供と遊ぶ家族 * 子供の帰りを待つ家族 # システム概要 見守りたい子供に「キッズみまもり子機」を持ってもらい、見守りたい親に「キッズみまもり親機」を持ってもらいます。 ![キャプションを入力できます](https://camo.elchika.com/246b2c6962f2354465ba911c8348312e4eeb820b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f33313762653434632d313865372d343862392d616633372d353131623764616565386665/) 親(親機)と子供(子機)が事前に設定した距離以上離れた場合、親機のLEDが点灯し、親に離れてしまったことを通知します。 親機には以下の位置・距離情報が表示されています。 * 自分(親機)の位置 * 子供(子機)の位置 * 離れている距離(m) * 離れている方角(角度) 親は上記の情報を頼りに素早く子供のところに到達できます。 ![キャプションを入力できます](https://camo.elchika.com/e8a0230105d3d3b3b9f768076d3303b72fc275e8/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f62393065353366612d386661362d346530362d613561662d343365316636386636666335/) また、親機の位置情報を自宅や固定の場所に設定することで、 親を原点にするのではなく、特定の場所を原点にして一定距離離れた場合にお知らせるようにもできます。例として、家を原点にし、設定距離を学校にしておけば、通学路から、逸脱した遠くに連れ去られてしまった場合に、素早く気づくことができます。 # 機器構成 |機器名|配置位置|役割| |:---|:---|:---| |キッズみまもり子機|対象となる子供|対象となる子供の位置情報をリアルタイムでクラウド(CLIP Viewer Lite)に通知する| |キッズみまもり親機|対象となる親|サーバーPC経由で受信した子機の位置情報と、自機に取り付けられたGPSモジュールの位置情報を比較し、子機-親機間の位置・距離情報を算出・表示する。必要に応じて、LEDを点灯させ、距離がしきち値より離れていることを示す| |CLIP Viewer Liteデータ・通信用PC|任意|定期的に、CLIP Viewer Liteからcsv形式をダウンロードし、サーバーPCのMQTT Brokerに子供の位置情報を送信する| |サーバーPC|任意|MQTT Brokerでmqttのメッセージを送受信する| ## キッズみまもり子機 * [SPRESENSEメインボード](https://www.switch-science.com/products/3900?_pos=17&_sid=6f79d1600&_ss=r) * [SPRESENSE用ELTRESアドオンボード](https://www.switch-science.com/products/7580) * 任意のモバイルバッテリー ![キャプションを入力できます](https://camo.elchika.com/b441940f0a8686883785b4e578535f1765970568/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f63373935383135362d333634322d343231652d386339392d643938386461303837316335/) ### 結線 ※ SPRESENSE用ELTRESアドオンボードの公式通りのため省略 ## キッズみまもり親機 * [M5StickC](https://shop.m5stack.com/products/stick-c?variant=17203451265114) ※[M5StickC Plus](https://www.switch-science.com/products/6470?_pos=1&_sid=f92a9b9ba&_ss=r)でも可 * [UNIT CatM](https://docs.m5stack.com/en/unit/cat_m) * [povo SIM](https://povo.jp/) * [GPS受信機キット](https://akizukidenshi.com/catalog/g/gK-09991/) * 任意のモバイルバッテリー ![キャプションを入力できます](https://camo.elchika.com/9e52e9686a5eb071876ab6ccb5cc51fc6e51c00c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f30323232316138612d333033622d346366312d396361662d646463396539343433643038/) ### ピンアサイン |UNIT CatM|M5StickC| |:---|:---| |TXD|G33| |RXD|G32| |5V|Vout| |GND|GND| |GPS受信機キット|M5StickC| |:---|:---| |5V|5V→| |GND|GND| |RXD|G26| |TXD|G0| |1PPS|-| ## CLIP Viewer Liteデータ・通信用PC ※ 任意のPC ## サーバーPC ※ 任意のレンタルサーバー # システム構成

-

![キャプションを入力できます](https://camo.elchika.com/e1ed50e6986b79ec15218b5d5b08a7278634e38b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f31646163626664662d353563372d343532662d616466372d336634393330623464303335/)

+

![キャプションを入力できます](https://camo.elchika.com/7a4db1bc8f2d72dbee260fce1b4190a04490452a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f32656539303735632d383731302d343165322d623564302d376632306135396134316237/)

## CLIP Viewer Liteデータ・通信用PCの動作 CLIP Viewer LiteにはWeb APIが備わっています。 サーバーPC側から最新の値を要求し、サーバーPCのMQTT Brokerに値を送信すれば、やりたいことは達成できます。 しかし、CLIP Viewer LiteのWeb APIは2022年9月現在有料であり、 「2022年 SPRESENSE™ 活用コンテスト」では標準では有効になっていないことから、 他の方法で通知する必要がありました。 CLIP Viewer LiteではWeb APIの他に最新の値をcsvでダウンロードする機能も提供されていることから、Windows操作の自動化ツールである[UWSC](https://www.vector.co.jp/soft/winnt/util/se115105.html)を使用し、1分や3分などの固定間隔でcsvのダウンロード→最新の位置情報の抜き取り→サーバーPCのMQTT Brokerに値を送信することにしました。 ### UWSCへ記録・実行した操作手順 1. webページ[CLIP Viewer Lite](https://clip-viewer-lite.com/)にアクセス 1. ユーザー名の入力欄をクリック 1. 自分のユーザー名を入力 1. パスワードの入力欄をクリック 1. 自分のパスワード名を入力 1. 「サインイン」をクリック 1. ダッシュボードが表示されるので、「CSVダウンロード」ボタンをクリック 1. 画面右上のハンバーガーボタンをクリックし、「サインアウト」ボタンをクリック 1. ブラウザを閉じる 1. 本記事の末に示す、「CLIP Viewer Liteの最新CSVファイル読取およびサーバーPCのMQTT Brokerへの位置情報送信スクリプト」を実行する # 動作の様子 ## 親機-子機間が近い場合 ![キャプションを入力できます](https://camo.elchika.com/62e4f1ef5fe7c817587db68aaf625cdf8de9bc3e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f66613938643735392d346339302d343662612d623430622d663936663536313133396431/) ほぼ、同じ空間にいる場合の表示のようすです。 距離が8mと出ており、探しに行く必要がありません。 ## 親機-子機間が遠い場合 ![キャプションを入力できます](https://camo.elchika.com/0ce7f04becd152ae6834ed8707d125b8f755138f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f66313635623631322d396434302d346138302d383437632d633835326363363935663734/) かなり、遠くにいる場合の表示のようすです。 LEDにより、距離がしきい値を上回っていることがわかります。(※しきい値は300mに設定) 距離が317m、角度321°と出ており、北西の方向に300m程度いったところに子機があることがわかりますので、その方向・距離に探しに行く必要があることがわかります。 # プログラム ## 子機(spresense)のプログラム ※eltres_sample_gps.inoママ ```arduino #include <EltresAddonBoard.h> // 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; // GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ bool gnss_recevie_timeout = false; // 点滅処理で最後に変更した時間 uint64_t last_change_blink_time = 0; // イベント通知での送信直前通知(5秒前)受信フラグ bool event_send_ready = false; // ペイロードデータ格納場所 uint8_t payload[16]; // 最新のGGA情報 eltres_board_gga_info last_gga_info; /** * @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 GGA情報受信コールバック * @param gga_info GGA情報のポインタ */ void gga_event_cb(const eltres_board_gga_info *gga_info) { Serial.print("[gga]"); last_gga_info = *gga_info; 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() { // シリアルモニタ出力設定 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); // 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; } } /** * @brief loop()関数 */ void loop() { 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; setup_payload_gps(); // 送信ペイロードの設定 EltresAddonBoard.set_payload(payload); } break; case PROGRAM_STS_STOPPED: // プログラム内部状態:終了 break; } // 次のループ処理まで100ミリ秒待機 delay(100); } /** * @brief GPSペイロード設定 */ void setup_payload_gps() { String lat_string = String((char*)last_gga_info.m_lat); String lon_string = String((char*)last_gga_info.m_lon); int index; uint32_t gnss_time; uint32_t utc_time; // GNSS時刻(epoch秒)の取得 EltresAddonBoard.get_gnss_time(&gnss_time); // UTC時刻を計算(閏秒補正) utc_time = gnss_time - 18; // 設定情報をシリアルモニタへ出力 Serial.print("[setup_payload_gps]"); Serial.print("lat:"); Serial.print(lat_string); Serial.print(",lon:"); Serial.print(lon_string); Serial.print(",utc:"); Serial.print(utc_time); Serial.print(",pos:"); Serial.print(last_gga_info.m_pos_status); Serial.println(); // ペイロード領域初期化 memset(payload, 0x00, sizeof(payload)); // ペイロード種別[GPSペイロード]設定 payload[0] = 0x81; // 緯度設定 index = 0; payload[1] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[2] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; index += 1; // skip "." payload[3] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[4] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); // 経度設定 index = 0; payload[5] = (uint8_t)(lon_string.substring(index,index+1).toInt() & 0xff); index += 1; payload[6] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[7] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; index += 1; // skip "." payload[8] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[9] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); // 時刻(EPOCH秒)設定 payload[10] = (uint8_t)((utc_time >> 24) & 0xff); payload[11] = (uint8_t)((utc_time >> 16) & 0xff); payload[12] = (uint8_t)((utc_time >> 8) & 0xff); payload[13] = (uint8_t)(utc_time & 0xff); // 拡張用領域(0固定)設定 payload[14] = 0x00; // 品質設定 payload[15] = last_gga_info.m_pos_status; } ``` ## 親機(M5StickC)のプログラム ```arduino #include <M5StickC.h> #include <ArduinoJson.h> #include "mqtt-config.h" /* ■CAT-M */ #define TINY_GSM_MODEM_SIM7080 #define TINY_GSM_RX_BUFFER 650 // なくても動いたけど、あったほうが安定する気がする #define TINY_GSM_YIELD() { delay(2); } // なくても動いたけど、あったほうが安定する気がする #include <TinyGsmClient.h> #include <PubSubClient.h> #define THRESHOLD_DIST 300 const char apn[] = "povo.jp"; const char* broker = MY_BROKER; const char* topicTest = "eltres"; #define GSM_AUTOBAUD_MIN 9600 #define GSM_AUTOBAUD_MAX 115200 // TinyGsmClient TinyGsm modem(Serial2); TinyGsmClient client(modem); PubSubClient mqtt(client); uint32_t lastReconnectAttempt = 0; /* ■GPS */ #include <TinyGPS++.h> TinyGPSPlus gps; double currentLat = 0.0; double currentLong = 0.0; void mqttCallback(char* topic, byte* payload, unsigned int len) { M5.Lcd.fillScreen(BLACK); M5.Lcd.setCursor(0, 0); M5.Lcd.print("Message arv\n"); char payloadBuf[256] = {0}; strncpy(payloadBuf,(const char *)payload,len); StaticJsonDocument<128> doc; // メモリを確保(静的) deserializeJson(doc, payloadBuf); // json形式に変換 const char* latitudeChar = doc["latitude"]; const char* longitudeChar = doc["longitude"]; const char* timeStamp = doc["timeStamp"]; double latitude = String(latitudeChar).toDouble(); double longitude = String(longitudeChar).toDouble(); char buf1[256] = {0}; sprintf(buf1,"currentLat:\n %lf",currentLat); M5.Lcd.printf("%s\n",buf1); char buf2[256] = {0}; sprintf(buf2,"currentlong:\n %lf\n",currentLong); M5.Lcd.printf("%s\n",buf2); char buf3[256] = {0}; sprintf(buf3,"targetLat:\n %lf",latitude); M5.Lcd.printf("%s\n",buf3); char buf4[256] = {0}; sprintf(buf4,"targetlong:\n %lf\n",longitude); M5.Lcd.printf("%s\n",buf4); long dist = CalcDistance(currentLat,currentLong,latitude,longitude); int bearing = CalcBearing(currentLat,currentLong,latitude,longitude); M5.Lcd.print("------------\n"); M5.Lcd.println(String(timeStamp)); char buf[256] = {0}; sprintf(buf,"dist: \n %ld [m]",dist); M5.Lcd.printf("%s\n",buf); M5.Lcd.printf("bearing: \n %d [deg]\n\n",bearing); M5.Lcd.print("------------\n"); if(dist < THRESHOLD_DIST) { M5.Lcd.printf("LED OFF\n"); digitalWrite(GPIO_NUM_10, HIGH); // OFF }else { M5.Lcd.printf("LED ON\n"); digitalWrite(GPIO_NUM_10, LOW); // ON } } boolean mqttConnect() { M5.Lcd.print("Connecting to "); M5.Lcd.println(broker); // Connect to MQTT Broker boolean status = mqtt.connect("GsmClientTest"); // Or, if you want to authenticate MQTT: // boolean status = mqtt.connect("GsmClientName", "mqtt_user", "mqtt_pass"); if (status == false) { M5.Lcd.println(" fail"); return false; } M5.Lcd.println(" success"); mqtt.subscribe(topicTest); return mqtt.connected(); } // the setup routine runs once when M5StickC starts up void setup(){ // Initialize the M5StickC object M5.begin(); // GPS通信用 Serial1.begin(9600, SERIAL_8N1, 0, 26); // EXT_IO Serial2.begin(9600, SERIAL_8N1, 33, 32); // LCD display M5.Lcd.print("Hello World"); Serial.println("Hello World"); M5.Lcd.println("Wait..."); // Print text on the screen (string) 在屏幕上打印文本(字符串) // Set GSM module baud rate // モデムのリスタート M5.Lcd.println("Initializing modem..."); // Print text on the screen (string) 在屏幕上打印文本(字符串) modem.restart(); // モデムの情報の取得 String modemInfo = modem.getModemInfo(); M5.Lcd.print("Modem Info: "); M5.Lcd.println(modemInfo); // GPRS connection parameters are usually set after network registration M5.Lcd.print(F("Connecting to ")); M5.Lcd.print(apn); if (!modem.gprsConnect(apn, "", "")) { M5.Lcd.println("-> fail"); delay(10000); return; } M5.Lcd.println("-> success"); if (modem.isGprsConnected()) { M5.Lcd.println("GPRS connected"); } mqtt.setServer(broker, 1883); mqtt.setCallback(mqttCallback); pinMode(GPIO_NUM_10, OUTPUT); digitalWrite(GPIO_NUM_10, HIGH); // OFF M5.Lcd.print("work start1"); Serial.println("work start1"); } // function from https://forum.arduino.cc/index.php?topic=45760.0 //convert degrees to radians double dtor(double fdegrees) { return(fdegrees * M_PI / 180); } //Convert radians to degrees double rtod(double fradians) { return(fradians * 180.0 / M_PI); } //Calculate distance form lat1/lon1 to lat2/lon2 using haversine formula //Note lat1/lon1/lat2/lon2 must be in radians //Returns distance in feet long CalcDistance(double lat1, double lon1, double lat2, double lon2) { double dlon, dlat, a, c; double dist = 0.0; dlon = dtor(lon2 - lon1); dlat = dtor(lat2 - lat1); a = pow(sin(dlat/2),2) + cos(dtor(lat1)) * cos(dtor(lat2)) * pow(sin(dlon/2),2); c = 2 * atan2(sqrt(a), sqrt(1-a)); //dist = 20925656.2 * c; //radius of the earth (6378140 meters) in feet 20925656.2 dist = 6378140.0 * c; //radius of the earth (6378140 meters) in feet 20925656.2 return( (long) dist + 0.5); } //Calculate bearing from lat1/lon1 to lat2/lon2 //Note lat1/lon1/lat2/lon2 must be in radians //Returns bearing in degrees int CalcBearing(double lat1, double lon1, double lat2, double lon2) { lat1 = dtor(lat1); lon1 = dtor(lon1); lat2 = dtor(lat2); lon2 = dtor(lon2); //determine angle double bearing = atan2(sin(lon2-lon1)*cos(lat2), (cos(lat1)*sin(lat2))-(sin(lat1)*cos(lat2)*cos(lon2-lon1))); //convert to degrees bearing = rtod(bearing); //use mod to turn -90 = 270 bearing = fmod((bearing + 360.0), 360); return (int) bearing + 0.5; } // the loop routine runs over and over again forever void loop() { while (Serial1.available() > 0){ char c = Serial1.read(); //Serial.print(c); gps.encode(c); if (gps.location.isUpdated()){ Serial.print("LAT="); Serial.println(gps.location.lat(), 6); currentLat = gps.location.lat(); Serial.print("LONG="); Serial.println(gps.location.lng(), 6); currentLong = gps.location.lng(); Serial.print("ALT="); Serial.println(gps.altitude.meters()); } } // Make sure we're still registered on the network if (!modem.isNetworkConnected()) { M5.Lcd.println("Network disconnected"); if (!modem.waitForNetwork(180000L, true)) { M5.Lcd.println(" fail"); delay(10000); return; } if (modem.isNetworkConnected()) { M5.Lcd.println("Network re-connected"); } // and make sure GPRS/EPS is still connected if (!modem.isGprsConnected()) { M5.Lcd.println("GPRS disconnected!"); M5.Lcd.print(F("Connecting to ")); M5.Lcd.print(apn); if (!modem.gprsConnect(apn, "", "")) { M5.Lcd.println(" fail"); delay(10000); return; } if (modem.isGprsConnected()) { M5.Lcd.println("GPRS reconnected"); } } } if (!mqtt.connected()) { M5.Lcd.println("=== MQTT NOT CONNECTED ==="); // Reconnect every 10 seconds uint32_t t = millis(); if (t - lastReconnectAttempt > 10000L) { lastReconnectAttempt = t; if (mqttConnect()) { lastReconnectAttempt = 0; } } delay(100); return; } mqtt.loop(); } ``` ## CLIP Viewer Liteの最新CSVファイル読取およびサーバーPCのMQTT Brokerへの位置情報送信スクリプト ```python path = "c:/Users/{UserName}}/Downloads" url = "XXX.com" #import import os import csv import json import paho.mqtt.client as mqtt # MQTTのライブラリをインポート from time import sleep # 3秒間のウェイトのために使う import time import sys connected = False class TudeValue: def __init__( self,degrees,minutes,seconds): self.degrees = degrees self.minutes = minutes self.seconds = seconds class GpsDataSet: def __init__( self, latDegrees , latMinutesSeconds, longDegrees,longMinutesSeconds, timeStamp) : latMinuts,latSecondsBase = latMinutesSeconds.split('.') latSeconds = str(round(float(latSecondsBase) * 60 / 10000,1)) print("gps2") print(latDegrees) print(latMinuts) print(latSeconds) self.latitude = TudeValue(latDegrees,latMinuts,latSeconds) longMinuts,longSecondsBase = longMinutesSeconds.split('.') longSeconds = str(round(float(longSecondsBase) * 60 / 10000,1)) print("gps3") print(longDegrees) print(longMinuts) print(longSeconds) self.longitude = TudeValue(longDegrees,longMinuts,longSeconds) self.timeStamp = timeStamp # 与えられたフォルダから最新のCSVファイルのフルパスを取得 def getLatestCsvFile(folderPath): filePath = None files = os.listdir(folderPath) for item in files: itemFilePath = path + "/" + item # 該当のファイルのフルパスを取得 root, ext = os.path.splitext(itemFilePath) # extに拡張子の取出 if ext != ".csv": # .csvファイル以外は対象から除外 continue ts = os.path.getmtime(itemFilePath) if filePath is None: filePath = itemFilePath else : latestFileTs = os.path.getmtime(filePath) fileTs = os.path.getmtime(itemFilePath) if(fileTs - latestFileTs > 0): filePath = itemFilePath return filePath def getLastestGpsData(csvFilePath): with open(csvFilePath, "r", encoding="UTF-8", errors="", newline="" ) as csvFile: reader = csv.reader(csvFile) csvList = [row for row in reader] latDegrees,latMinutesSeconds,longDegrees,longMinutesSeconds = csvList[1][8].split(' ') return GpsDataSet(latDegrees,latMinutesSeconds,longDegrees,longMinutesSeconds,csvList[1][9]) def getGpsDataJson(gpsDataSet): latitude = round( float(gpsDataSet.latitude.degrees) + ((float(gpsDataSet.latitude.minutes) / 60)) + ((float(gpsDataSet.latitude.seconds) / 60 / 60)) ,6) longitude = round( float(gpsDataSet.longitude.degrees) + (float(gpsDataSet.longitude.minutes) / 60) + (float(gpsDataSet.longitude.seconds) /60 / 60) ,6) obj = { "latitude" : str(latitude), "longitude" : str(longitude), "timeStamp" : gpsDataSet.timeStamp } json_data = json.dumps(obj).encode("utf-8") return json_data # ブローカーに接続できたときの処理 def on_connect(client, userdata, flag, rc): print("Connected with result code " + str(rc)) # ブローカーが切断したときの処理 def on_disconnect(client, userdata, rc): if rc != 0: print("Unexpected disconnection.") # publishが完了したときの処理 def on_publish(client, userdata, mid): print("publish: {0}".format(mid)) # 対象のフォルダから最新のCSVファイルを取得 latestCsvFile = getLatestCsvFile(path) # 取得したCSVファイルの最新の緯度経度とタイムスタンプを取得 print(latestCsvFile) gpsDataSet = getLastestGpsData(latestCsvFile) gpsDataSetJson = getGpsDataJson(gpsDataSet) # mqtt各種設定 client = mqtt.Client() # クラスのインスタンス(実体)の作成 client.on_connect = on_connect # 接続時のコールバック関数を登録 client.on_disconnect = on_disconnect # 切断時のコールバックを登録 client.on_publish = on_publish # メッセージ送信時のコールバック client.connect(url, 1883, 60) # 通信処理スタート client.loop_start() # subはloop_forever()だが,pubはloop_start()で起動だけさせる time.sleep(5) print("publish") print(gpsDataSetJson) client.publish("eltres",gpsDataSetJson) time.sleep(5) ```