tktk360 が 2024年01月23日00時12分05秒 に編集
初版
タイトルの変更
Spresense Family Monitor
タグの変更
SPRESENSE
LoRa
enebular
GPS
メイン画像の変更
記事種類の変更
製作品
ライセンスの変更
(MIT) The MIT License
本文の変更
この作品は、「家の近場で遊びに行く、子供の見守りを行う機器を、LoRaを使って実現する」ことを目的としたもの です。 2023年 SONY Spresenseコンテストで提供いただいたモニター品を活用し制作しています。 機能は現在も改良しパワーアップ中なのですが、コンテスト期間中に記事を完遂するために、一通り機能を満たした所でのソースコードで書くことにしました。 最初に、LoRa Add-onボード(DTH-SSLR)と、W5500-Etherですが、各々、単独では正常に動作確認ができましたが、 1つのSpresenseに同時に接続すると使うことができませんでした。 具体的には、W5500-Etherでネットに接続すると応答が返ってこず固まります。試した内容も末尾に記載しています。 そのため、W5500-Etherの使用を諦め、ネット接続は、ESP32のWi-fiをSoftwareSerial経由で接続し、実現しています。 @[youtube](https://youtu.be/eR4RvUdE6oA) # 目的・方針 ・対象者が、何処にいるか確認する手段があること ・維持費に、極力、お金をかけないこと(通信費にお金がかかるなら、既製品で良いため) ・既製品との差別化ができること(機能アップができること) # 機能 ![システム構成図](https://camo.elchika.com/bbc62471498b0097f7204444c83ee0d375e6efbf/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f34353537393737382d643534332d343961392d623735332d613237383563643337333166/) 通信費0円で、対象が何処にいるか確認可能な方法があるのが特徴です。 送信機、受信機間は、LoRaを使用しているため、電波が届けば、通信費はかかりません。 受信機は、クラウドへ位置情報を送り、データを蓄積しています。 クラウドは、enebularのクラウド実行環境で構築しており、今回の利用用途では、無料枠の範囲でも問題ない試算です。 そして、スマホのブラウザ経由でクラウドへアクセスし、位置情報を地図表示することで、何処からでも見守りができるようにしています。 # LoRa受信機 / 位置情報(スマホ確認) ![機能紹介](https://camo.elchika.com/aba861b434ed3ccccb5afeb5d81b1afce8d49c11/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f35393963613233372d363832392d346231632d616638322d636564653034313863663766/) LoRa受信機 は、LoRa送信機からの受信感度を少しでも高めるため自宅の窓側に設置しています。 そして、受信情報をディスプレイに表示しています。 GPSの位置値だけでは、現在位置が把握が難しいため、画像を表示することで、視認性を向上させています。 スマホでは、ブラウザで、自宅、学校、現在位置のピンを表示することで、何処にいるかを閲覧できるようにしています。 # 使用状況 使用場所は、都心に近い環境ではありますが、見守りたい範囲はカバーができました。 2km圏内になると繁華街があり、受信は困難でしたが、普段の通学経路や、遊ぶ公園は1km圏内にあり受信状況は良好な結果となりました。 ![見守り1km圏内でのGPS即位値のLoRa受信状況](https://camo.elchika.com/ba40653d47f01827544fa8da7d213a3bdb48bdf6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f33366233353735382d613737322d343732312d386638322d646434346562616533663939/) # 部品 - 作成に使用したパーツは下記となります。 ![送信機/受信機に使用した部品](https://camo.elchika.com/b29363dd3fe22f6730ece91cd34aa6cb8a0abaa4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f30346336633336642d343835622d343135332d383334332d626162353931386134356431/) | NO | 品目 | 価格 | |:---:|:---:|:---| | 1 | [SONY SPRESENSE メインボード](https://akizukidenshi.com/catalog/g/gM-14584/) | 6,050 x2 | | 2 | [SONY SPRESENSE 拡張ボード](https://akizukidenshi.com/catalog/g/gM-14585/) | 3,850 | | 3 | ~~[W5500-Ether](https://crane-elec.co.jp/products/vol-20/)~~ | ~~7,678~~ | | 4 | [ソニー SPRESENSE™用 LoRa Add-onボード(DTH-SSLR)](https://dragon-torch.tech/cat-components/extension-boards/dth-sslr/) | 5,280 x 2| | 5 | [LoRa用アンテナ [TX915-JKS-20]](https://www.switch-science.com/products/8676) | 715 x 2| | 6 | [ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807](https://akizukidenshi.com/catalog/g/gM-16265/) | 1,450 | | 7 | [ad keyboard simulate five key](https://www.aliexpress.com/item/2255800619344733.html?gatewayAdapt=4itemAdapt) | 165 | | 8 | [タカチ電機工業 PF型ネットワークケース PF13-4-13W](https://www.amazon.co.jp/dp/B07BD5S83C?ref=ppx_yo2ov_dt_b_product_details&th=1) | 1,031 | | 9 | [Tzt-防水プラスチックケース収納ボックス](https://ja.aliexpress.com/item/1005002939638254.html?src=google&src=google&albch=shopping&acnt=494-037-6276&slnk=&plac=&mtctp=&albbt=Google_7_shopping&albagn=888888&isSmbAutoCall=false&needSmbHouyi=false&albcp=19207436862&albag=&trgt=&crea=ja1005002939638254&netw=x&device=c&albpg=&albpd=ja1005002939638254&gad_source=1&gclid=CjwKCAiAhJWsBhAaEiwAmrNyq0tew-R5zMTI_iBCaExcO1KWFVLxzPuG4aDvL2QXY1A8R3D0wJ--0BoCt9kQAvD_BwE&gclsrc=aw.ds&aff_fcid=6044dc89222b412f9ac0143f1193d64f-1703290799688-00853-UneMJZVf&aff_fsk=UneMJZVf&aff_platform=aaf&sk=UneMJZVf&aff_trace_key=6044dc89222b412f9ac0143f1193d64f-1703290799688-00853-UneMJZVf&terminal_id=5c5116efdbc64e7f9298852a88b6c91d&afSmartRedirect=y) | 99 | | 10 | [ESP-WROOM-32D開発ボード](https://akizukidenshi.com/catalog/g/gM-13628/) | 1600 | | | 合計| 32,285 | # 設計図 部品を元に、下記配線を行います。 ### 送信機 難しい配線は不要です。 「SONY SPRESENSE メインボード」に、「ソニー SPRESENSE™用 LoRa Add-onボード(DTH-SSLR)」を載せ、 アンテナ 「LoRa用アンテナ [TX915-JKS-20]」を付けます。 ケースは、「Tzt-防水プラスチックケース収納ボックス」を使用し、加工することで、収納しました。 ### 受信機 「SONY SPRESENSE 拡張ボード」に「SONY SPRESENSE メインボード」を装着します。 「ソニー SPRESENSE™用 LoRa Add-onボード(DTH-SSLR)」を載せ、 アンテナ 「LoRa用アンテナ [TX915-JKS-20]」を付けます。 ケースは、「タカチ電機工業 PF型ネットワークケース PF13-4-13W」を使用し、加工することで、収納しました。 液晶と、ボタンの配線は下記を参考にしてください。 ESP32の配線 | SPRESENSE | ESP32 | |:---:|:---| | D05(RX) | 16(TX) | | D06(TX) | 17(RX) | ILI9341の配線 | SPRESENSE | ILI9341 | |:---:|:---| | AREF | VCC | | GND | GND | | SCK | SCK | | MISO | MISO | | MOSI | MOSI | | CS | CS | | PWM2 | DC | | GPIO | RESET | | 3.3V | VCC | ad keyboard simulate five keyの配線 | SPRESENSE | ad keyboard simulate five key | |:---:|:---| | A0 | OUT | | GND | GND | | Vout | VCC | ==使用ライブラリ== Arduino Library Manager ・ボードマネージャ-Spresense Commuity 3.1.0 ・SSLClient 1.6.11 [ソニー SPRESENSE™用 LoRa Add-onボード(DTH-SSLR)用 ライブラリ](https://dragon-torch.tech/cat-components/extension-boards/dth-sslr/) [W5500 Eth Arduino IDE開発用 Ethernet-spi5.ino](https://crane-elec.co.jp/wp/wp-content/uploads/2022/03/Ethernet-spi5.zip) グラフィック -[Adafruit_ILI9341-spresense](https://github.com/kzhioki/Adafruit_ILI9341) -[Adafruit-GFX-Library-spresense](https://github.com/kzhioki/Adafruit-GFX-Library) ==enebular== クラウドは、enebularを利用しています。 月当たりの利用上限 月間のクラウド実行環境の使用状況には、以下の上限があります。 | | エンタープライズ | トライアルおよびフリー | |:---:|:---:|:---| |HTTPリクエスト回数|3,000,000回|50,000回| |実行時間|1000時間|24時間| |ログサイズ |5GB|0.1GB| |送信メッセージ数|100000|10000| |送信メッセージサイズ|512MB|64MB| ### 見守り 今回の必要となるスペックを考え、下記と定めました。 フリープランの範囲でも賄える内容に収まりました。 | 項目 | 数値 | |:---:|:---:| |月の見守り日数(平日)| 20 日| |1日の見守り時間|最大10 時間| |送信間隔|5 分/回| ==プログラム== プログラム全体を下記にのせています。 [Github: SpresenseFamilyMonitor](https://github.com/TKTK360/SpresenseFamilyMonitor) からソースコードを取得してください。 以降は、中心となるコードについての抜粋となります。 - 送信機 Spresense搭載のGPSを使って、受信時間と位置情報を取得し、受信機へ送信します。 GPSデータは、[GPGGA形式](https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm)で取得されます。 UTC時刻となるため、日本時間はUTC時刻+9時間となります。 緯度経度は、OpenstreetmapやGoogle map の地図で見るため、10進法に変換します。 計算式は下記となります。 ```arduino:緯度経度の10進法の変換式 Mapの緯度、経度 = (GPSロガーの緯度 ÷ 100.0)の整数部分 + (GPSロガーの緯度 ÷ 100)の小数部分÷60.0×100.0 ``` プログラム内の parameterの値は、任意に変更してください POSITIONING_INTERVAL は、GPSの更新間隔の時間です。 SEND_INTERVAL は、受信機への送信間隔の時間です。 ```arduino:送信機(SpresenseTracker.ino) #include <Arduino.h> #include <vector> #include <GNSS.h> #include <GNSSPositionData.h> #include "gnss_logger.h" #include "gnss_nmea.h" #include "spresense_e220900t22s_jp_lib.h" // ----------------------------------------------------------------------- // parameter #define POSITIONING_INTERVAL 10 // positioning interval in seconds #define SEND_INTERVAL 10 // send interval in seconds // ----------------------------------------------------------------------- // enum LoopState enum LoopState { eStateActive // Loop is activated }; // GNSS SpGnss Gnss; // SpGnss object String g_time; String g_lat; String g_lot; // LORA CLoRa lora; // LoRa設定値 struct LoRaConfigItem_t config = { 0xa215, // own_address 0 0b011, // baud_rate 9600 bps 0b10000, // air_data_rate SF:9 BW:125 0b00, // subpacket_size 200 0b1, // rssi_ambient_noise_flag 有効 0b0, // transmission_pause_flag 有効 0b01, // transmitting_power 13 dBm 0x00, // own_channel 0 0b1, // rssi_byte_flag 有効 0b1, // transmission_method_type 固定送信モード 0b0, // lbt_flag 有効 0b011, // wor_cycle 2000 ms 0x0000, // encryption_key 0 0x0000, // target_address 0 0x00 // target_channel 0 }; //------------------------------------------------------------- // Turn on / off the LED1 for positioning state notification. // [in] state Positioning state //------------------------------------------------------------- static void Led_isPosfix(bool state){ if (state == 1){ ledOn(PIN_LED0); } else { ledOff(PIN_LED0); } } //------------------------------------------------------------- // Turn on / off the LED2 for file SD access notification. // [in] state SD access state //------------------------------------------------------------- static void Led_isStorageAccess(bool state){ if (state == 1) { ledOn(PIN_LED3); } else { ledOff(PIN_LED3); } } //------------------------------------------------------------- // Turn on / off the LED3 for error notification. // [in] state Error state //------------------------------------------------------------- static void Led_isError(bool state){ } //------------------------------------------------------------- // Setup positioning. // return 0 if success, 1 if failure //------------------------------------------------------------- static int SetupPositioning(void){ int error_flag = 0; // Set Gnss debug mode. Gnss.setDebugMode(PrintNone); if (Gnss.begin(Serial) != 0) { Serial.println("Gnss begin error!!"); error_flag = 1; } else { Serial.println("Gnss begin OK."); // GPS + QZSS(L1C/A) + QZAA(L1S) Gnss.select(GPS); Gnss.select(QZ_L1CA); Gnss.select(QZ_L1S); Gnss.setInterval(POSITIONING_INTERVAL); if (Gnss.start(HOT_START) != OK) { Serial.println("Gnss start error!!"); error_flag = 1; } } return error_flag; } //------------------------------------------------------------- //StrSplit //------------------------------------------------------------- int StrSplit(String data, char delimiter, String *dst){ int index = 0; int datalength = data.length(); for (int i = 0; i < datalength; i++) { char tmp = data.charAt(i); if ( tmp == delimiter ) { index++; } else { dst[index] += tmp; } } return (index + 1); } //------------------------------------------------------------- //ConvertGps //------------------------------------------------------------- float ConvertGps(String data){ float f = atof(data.c_str()); float deg = (int)f / 100; float min = f - deg * 100; return deg + min / 60; } void floatToBytes(float val, uint8_t* bytes){ memcpy(bytes, &val, sizeof(float)); } void floatArrayToBytes(float* values, uint8_t* bytes){ for (int i = 0; i < 2; ++i) { floatToBytes(values[i], bytes + i * sizeof(float)); } } void floatValuesToCommaString(float* values, char* resultString){ String floatStrings[3]; for (int i = 0; i < 3; ++i) { floatStrings[i] = String(values[i], 5); // Adjust precision as needed } sprintf(resultString, "%s,%s,%s\0", floatStrings[0].c_str(), floatStrings[1].c_str(), floatStrings[2].c_str()); } //------------------------------------------------------------- //SendGnssLoraData //------------------------------------------------------------- void SendGnssLoraData(String time, String lat, String lot){ float values[3] = { atof(time.c_str()) + 90000, ConvertGps(lat), ConvertGps(lot)}; char resultString[50]; // Adjust the size as needed floatValuesToCommaString(values, resultString); Serial.print("result = "); Serial.println(resultString); if (lora.SendFrame(config, resultString, strlen(resultString)) == 0) { Serial.println("send succeeded."); } else { Serial.printf("send failed."); } } //------------------------------------------------------------- //Setup //------------------------------------------------------------- void setup(){ int error_flag = 0; // Open serial communications and wait for port to open Serial.begin(9600); Serial.println("-- READY --"); // Turn on all LED:Setup start. ledOn(PIN_LED0); ledOn(PIN_LED1); ledOn(PIN_LED2); ledOn(PIN_LED3); error_flag = SetupPositioning(); // Turn off all LED:Setup done. ledOff(PIN_LED0); ledOff(PIN_LED1); ledOff(PIN_LED2); ledOff(PIN_LED3); // Set error LED. if (error_flag == 1) { Led_isError(true); } // E220-900T22S(JP)へのLoRa初期設定 if (lora.InitLoRaModule(config)) { SerialMon.printf("init error\n"); return; } else { Serial.printf("init ok\n"); } // ノーマルモード(M0=0,M1=0)へ移行する lora.SwitchToNormalMode(); Serial.println("-- START --"); } //------------------------------------------------------------- //loop //------------------------------------------------------------- void loop() { static bool PosFixflag = false; if (Gnss.waitUpdate(POSITIONING_INTERVAL * 1000)) { // Get NavData. SpNavData NavData; Gnss.getNavData(&NavData); // Position Fixed bool LedSet = ((NavData.posDataExist) && (NavData.posFixMode != 0)); if (PosFixflag != LedSet) { Led_isPosfix(LedSet); PosFixflag = LedSet; } if (PosFixflag) { //unsigned short sat = NavData.posSatelliteType; unsigned short sat = NavData.satelliteType; // GPS, QZ_L1CA --> LED1 if (0 != (sat & (GPS | QZ_L1CA))) { ledOn(PIN_LED1); } else { ledOff(PIN_LED1); } // QZ_L1S --> LED2 if (0 != (sat & QZ_L1S)) { ledOn(PIN_LED2); } else { ledOff(PIN_LED2); } } // Convert Nav-Data to Nmea-String. String NmeaString = getNmeaGga(&NavData); if (strlen(NmeaString.c_str()) == 0) { Serial.println("getNmea error"); Led_isError(true); } else { Serial.print(NmeaString); // 分割数 = 分割処理(文字列, 区切り文字, 配列) String cmds[25] = {"\0"}; // 分割された文字列を格納する配列 int index = StrSplit(NmeaString, ',', cmds); // 結果表示 //for(int i = 0; i < index; i++){ // Serial.println(cmds[i]); //} if (4 < index) { g_time = cmds[1]; g_lat = cmds[2]; g_lot = cmds[4]; if (strlen(g_lat.c_str()) != 0 && strlen(g_lot.c_str()) != 0) { //Serial.print("lat="); Serial.println(g_lat); //Serial.print("lot="); Serial.println(g_lot); SendGnssLoraData(g_time, g_lat, g_lot); delay(SEND_INTERVAL * 1000); return; } } } } delay(SEND_INTERVAL * 1000); } ``` - 受信機 構成が少し複雑です。 Spresenseは、Mainコアと、Subコア1つを利用しています。 Subコア1では、送信機から送られてくる位置情報をLoraで受信し、Mainコアに送ります。 Mainコアは、送信機の位置情報をSubコアから受け取ります。 また、グラフィックとして、情報を表示します。 情報は、RSSI、時刻、緯度経度、何処にいるかを画像で表示します。 画像は、家、学校、移動中の3パターン用意しており、プログラム内に埋め込んでいます。 画像からプログラムデータへの変換方法は、末尾をしてください。 クラウドへのアップは、SoftwareSerial経由で緯度経度情報をESP32へ渡しています。 ESP32では、HTTPS接続で、クラウド実行環境のEnebularへ位置情報を送信しています。 parameterのhome_lat,lotには、自宅の位置情報を記載してください。 parameterのschool_lat,lotには、学校の位置情報を記載してください。 ボタンは、5個ありますが、1個だけ使用したものになります。 クラウドへ今の最新情報を即送信する機能です。 実際に送信機を動かさないでいても、GPSが受信した位置情報の揺らぎがあります。 そのため、近しい位置の場合には、クラウドへ送らないようにしています。 SPACE_LAT,LOTの値で調整ができます。 ```arduino:受信機(Mainコア:SpresenseHomeStationMain.ino) #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include <SPI.h> #include <Arduino.h> #include <vector> #include <MP.h> #include <SoftwareSerial.h> //Display #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" #include "IMG_WALK.h" #include "IMG_SCHOOL.h" #include "IMG_HOME.h" SoftwareSerial SerialAT(5, 6); // RX, TX // ----------------------------------------------------------------------- // parameter #define SPACE_LAT 0.0015 #define SPACE_LOT 0.0015 #define home_lat 34.XXXXXXXXXXXXXX #define home_lot 135.XXXXXXXXXXXXXX #define school_lat 34.XXXXXXXXXXXXXX #define school_lot 135.XXXXXXXXXXXXXX // ----------------------------------------------------------------------- //Button #define BTN_PIN A0 // BUTTONN KEY ID #define CMD_1 1 #define CMD_2 2 #define CMD_3 3 #define CMD_4 4 #define CMD_5 5 #define CMD_6 6 //TFT #define TFT_RST 8 #define TFT_DC 9 #define TFT_CS 10 //Dispaly #define TFT_BACKLIGHT_PIN 7 #define IMG_TYPE_HOME 1 #define IMG_TYPE_SCHOOL 2 #define IMG_TYPE_MOVE 3 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST); bool g_isTftLight = false; int subcore = 1; // Communication with SubCore1 struct MyPacket { int timed; float lat; float lot; int rssi; }; MyPacket packet; int g_rssi; int g_time; float g_flat = home_lat; float g_flot = home_lot; int g_artime[3]; float g_arflat[3]; float g_arflot[3]; int g_imgType = 0; int g_cnt_gps = 0; int g_push_index = 0; int g_sendCmd = 0; int g_cnt_send = 0; //------------------------------------------------------------- //GpsDataFunction //------------------------------------------------------------- int GpsDataFunction(int timed, float flat, float flot, int rssi){ int ret = 0; // 自宅近く if (abs(flat - home_lat) < SPACE_LAT && abs(flot - home_lot) < SPACE_LOT) { ret = IMG_TYPE_HOME; MPLog("HOME\n"); } // 学校近く else if (abs(flat - school_lat) < SPACE_LAT && abs(flot - school_lot) < SPACE_LOT) { ret = IMG_TYPE_SCHOOL; MPLog("SCHOOL\n"); } // 動いていない else if (abs(flat - g_flat) < SPACE_LAT && abs(flot - g_flot) < SPACE_LOT) { ret = IMG_TYPE_MOVE; MPLog("MOVE\n"); } // Now Parameter g_rssi = rssi; g_time = timed; g_flat = flat; g_flot = flot; int updateIndex = g_cnt_gps-2; g_artime[updateIndex] = timed; g_arflat[updateIndex] = flat; g_arflot[updateIndex] = flot; MPLog("Gps:%d,%d,%f,%f\n", g_rssi, g_time, g_flat, g_flot); return ret; } //------------------------------------------------------------- //DisplayImageFunction //------------------------------------------------------------- bool DisplayImageFunction(int mode, bool isForce){ if (!isForce && g_imgType == mode) { return false; } g_imgType = mode; switch(g_imgType) { case IMG_TYPE_HOME: tft.drawRGBBitmap(170, 73, IMG_HOME, 150, 137); //MPLog("Draw IMG_HOME\n"); break; case IMG_TYPE_SCHOOL: tft.drawRGBBitmap(170, 94, IMG_SCHOOL, 150, 95); //MPLog("Draw IMG_SCHOOL\n"); break; case IMG_TYPE_MOVE: tft.drawRGBBitmap(170, 76, IMG_WALK, 150, 132); //MPLog("Draw IMG_TYPE_MOVE\n"); break; } yield(); return true; } //------------------------------------------------------------- //DisplayTextFunction //------------------------------------------------------------- void DisplayTextFunction(){ tft.setTextColor(ILI9341_WHITE); tft.setTextSize(2); // rssi tft.setCursor(10, 18); tft.print("RSSI:"); tft.setCursor(80, 18); tft.print(g_rssi); tft.setCursor(138, 18); tft.print(g_time / 10000); tft.setCursor(168, 18); tft.print(":"); tft.setCursor(183, 18); tft.print((g_time % 10000) / 100); tft.setCursor(208, 18); tft.print("."); tft.setCursor(228, 18); tft.print(g_time % 100); // lat, lot int yPos = 18 + 35; char buffer[10]; tft.setCursor(10, yPos); snprintf(buffer, sizeof(buffer), "%f", g_flat); tft.print(buffer); tft.setCursor(125, yPos); tft.print(","); tft.setCursor(138, yPos); snprintf(buffer, sizeof(buffer), "%f", g_flot); tft.print(buffer); yPos += 45; tft.setTextSize(1); int i = 0; while (i++ < 3){ tft.setCursor(12, yPos); snprintf(buffer, sizeof(buffer), "%d", g_artime[i - 1]); tft.print(buffer); tft.setCursor(57, yPos); snprintf(buffer, sizeof(buffer), "%f", g_arflat[i - 1]); tft.print(buffer); tft.setCursor(110, yPos); tft.print(","); tft.setCursor(120, yPos); snprintf(buffer, sizeof(buffer), "%f", g_arflot[i - 1]); tft.print(buffer); yPos += 25; } yield(); } //------------------------------------------------------------- //DispRefresh //------------------------------------------------------------- void DispRefresh(){ tft.fillScreen(ILI9341_BLACK); DisplayImageFunction(g_imgType, true); DisplayTextFunction(); } //------------------------------------------------------------- //DispLight //------------------------------------------------------------- void DispLight(bool isTftLight){ if (isTftLight) { MPLog("light on\n"); //digitalWrite(TFT_BACKLIGHT_PIN,HIGH); DispRefresh(); } else { MPLog("light off\n"); //digitalWrite(TFT_BACKLIGHT_PIN,LOW); } } //------------------------------------------------------------- //SetupDisplay //------------------------------------------------------------- void SetupDisplay(){ tft.begin(); tft.setRotation(3); tft.fillScreen(ILI9341_BLACK); yield(); } //------------------------------------------------------------- //SendGps //------------------------------------------------------------- void SendGps(float lat, float lot){ if (g_rssi == 0 || lat < 1 || lot < 1) { MPLog("NO GPS\n"); return; } // Make a HTTP request: char buffer[35]; snprintf(buffer, sizeof(buffer), "?lat=%f&lot=%f", lat, lot); Serial.println(buffer); SerialAT.println(buffer); MPLog("SendGps\n"); } //------------------------------------------------------------- //getButtonKey //------------------------------------------------------------- int getButtonKey(){ int index = 0; int data = analogRead(BTN_PIN); if (5 <= data && data <= 70) { g_push_index = 1; } else if (90 <= data && data <= 150) { g_push_index = 2; } else if (300 <= data && data <= 350) { g_push_index = 3; } else if (360 <= data && data <= 500) { g_push_index = 4; } else if (530 <= data && data <= 700) { g_push_index = 5; } else { if (g_push_index != 0) { index = g_push_index; g_push_index = 0; Serial.print("btn= "); Serial.println(index); } } return index; } //------------------------------------------------------------- //commandFunction //------------------------------------------------------------- void commandFunction(int cmd){ if (cmd == CMD_1) { g_sendCmd = CMD_1; SendGps(g_flat, g_flot); } else if (cmd == CMD_2) { g_sendCmd = CMD_2; DispRefresh(); } else if (cmd == CMD_3) { g_sendCmd = CMD_3; } else if (cmd == CMD_4) { g_sendCmd = CMD_4; } else if (cmd == CMD_5) { g_sendCmd = CMD_5; } } //------------------------------------------------------------- //Setup //------------------------------------------------------------- void setup(){ Serial.begin(9600); //Serial2.begin(115200, SERIAL_8N1); SerialAT.begin(9600); MPLog("Button\n"); //pinMode(BTN_PIN, INPUT); MPLog("Tft\n"); SetupDisplay(); DispLight(g_isTftLight); DisplayImageFunction(IMG_TYPE_HOME, true); DisplayTextFunction(); // Launch SubCore1 MP.RecvTimeout(100); int ret = MP.begin(subcore); if (ret < 0) { printf("MP.begin error = %d\n", ret); } MPLog("=== START ===\n"); } //------------------------------------------------------------- //loop //------------------------------------------------------------- void loop() { // KEY CODE int key = getButtonKey(); commandFunction(key); // Subcore MyPacket *ppacket; int8_t rcvid; int ret = MP.Recv(&rcvid, &ppacket, subcore); if (ret >= 0) { ret = GpsDataFunction(ppacket->timed, ppacket->lat, ppacket->lot, ppacket->rssi); if (ret > 0) { g_cnt_gps++; if (g_cnt_gps > 4) { g_cnt_gps = 1; } tft.fillScreen(ILI9341_BLACK); DisplayImageFunction(ret, true); DisplayTextFunction(); g_cnt_send++; if (g_cnt_send > 3) { g_cnt_send = 0; commandFunction(CMD_1); } } delay(10); return; } delay(100); } ``` Subコアは、1を使用しています。 ```arduino:受信機(Subコア:SpresenseHomeStationSub.ino) #if (SUBCORE != 1) #error "Core selection is wrong!!" #endif #include <MP.h> #include <Arduino.h> #include <vector> //Lora #include "spresense_e220900t22s_jp_lib.h" #define MY_MSGID 10 // LORA CLoRa lora; struct RecvFrameE220900T22SJP_t g_data; // LoRa設定値 struct LoRaConfigItem_t config = { 0xa215, // own_address 0 0b011, // baud_rate 9600 bps 0b10000, // air_data_rate SF:9 BW:125 0b00, // subpacket_size 200 0b1, // rssi_ambient_noise_flag 有効 0b0, // transmission_pause_flag 有効 0b01, // transmitting_power 13 dBm 0x00, // own_channel 0 0b1, // rssi_byte_flag 有効 0b1, // transmission_method_type 固定送信モード 0b0, // lbt_flag 有効 0b011, // wor_cycle 2000 ms 0x0000, // encryption_key 0 0x0000, // target_address 0 0x00 // target_channel 0 }; int g_rssi; int g_time; float g_flat; float g_flot; struct MyPacket { int timed; float lat; float lot; int rssi; }; MyPacket packet; void errorLoop(int num){ int i; while (1) { for (i = 0; i < num; i++) { ledOn(LED0); delay(300); ledOff(LED0); delay(300); } delay(1000); } } //------------------------------------------------------------- //StrSplit //------------------------------------------------------------- int StrSplit(String data, char delimiter, String *dst){ int index = 0; int datalength = data.length(); for (int i = 0; i < datalength; i++) { char tmp = data.charAt(i); if ( tmp == delimiter ) { index++; } else { dst[index] += tmp; } } return (index + 1); } //------------------------------------------------------------- //GpsDataFunction //------------------------------------------------------------- int GpsDataFunction(String data){ String cmds[3] = {"\0"}; // 分割された文字列を格納する配列 int index = StrSplit(data, ',', cmds); if (2 > index) { return -1; } // 結果表示 String now_time = cmds[0]; String now_lat = cmds[1]; String now_lot = cmds[2]; g_time = atoi(now_time.c_str()); g_flat = atof(now_lat.c_str()); g_flot = atof(now_lot.c_str()); return 0; } //------------------------------------------------------------- //LoraFunction //------------------------------------------------------------- bool LoraFunction(){ MPLog("lora function\n"); if (lora.RecieveFrame(&g_data) != 0) { return false; } String strRev; int ret = 0; MPLog("lora recv data\n"); for (int i = 0; i < g_data.recv_data_len; i++) { strRev += (char)g_data.recv_data[i]; } // GPS Update g_rssi = g_data.rssi; ret = GpsDataFunction(strRev); if (ret == 0) { return true; } return false; } //------------------------------------------------------------- //SendGps //------------------------------------------------------------- void SendGps(int timed, float lat, float lot, int rssi){ packet.timed = timed; packet.lat = lat; packet.lot = lot; packet.rssi = rssi; MPLog("Gps:%d, %f, %f\n", rssi, lat, lot); int ret = MP.Send(MY_MSGID, &packet); if (ret < 0) { MPLog("fail lora send\n"); } } //------------------------------------------------------------- //Setup //------------------------------------------------------------- void setup(){ int ret = MP.begin(); if (ret < 0) { errorLoop(2); } // Lora MPLog("Lora\n"); // E220-900T22S(JP)へのLoRa初期設定 if (lora.InitLoRaModule(config)) { MPLog("init error\n"); } else { MPLog("init ok\n"); } // ノーマルモード(M0=0,M1=0)へ移行する MPLog("switch to normal mode\n"); lora.SwitchToNormalMode(); // LoRa受信 while (1) { if (LoraFunction()) { // Send Gps Data SendGps(g_time, g_flat, g_flot, g_rssi); } delay(100); } } //------------------------------------------------------------- //loop //------------------------------------------------------------- void loop() { } ``` ネット接続する上で、ルータのSSIDとPASSを書き換えてください。 特定サイトにHTTPSで接続する上で、WiFiClientSecureを使い実現しています。 ルート証明書の設定が必要となり、作成方法は、[参考サイト](https://www.mgo-tec.com/blog-entry-arduino-esp32-ssl-stable-root-ca.html/2)を元に作成してください。 parameterは、クラウドで使用しているEnebularの情報となります。 各自の環境によって異なりますので、適時書き換えてください。 ``` arduino:受信機(Esp32:SpresenseWebClientHttpsEsp32.ino) #include <WiFi.h> #include <WiFiClientSecure.h> #include <time.h> #include <HTTPClient.h> #include <base64.h> // parameter #define URL_GET_PARAMETER "GET /NODE_NAME/" #define HOST_SERVER "lcdpXXX.enebular.com" #define HOST_SERVER_TAG "Host: lcdpXXX.enebular.com" #define JST 3600* 9 // Your WiFi credentials. const char* ssid = "XXXXXXXXXXXXXX"; const char* pass = "XXXXXXXXXXXXXX"; const char* test_root_ca= \ "-----BEGIN CERTIFICATE-----\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" \ "MrY=\n" \ "-----END CERTIFICATE-----\n"; WiFiClientSecure client; //------------------------------------------------------------------ //setup void setup() { Serial.begin(9600); Serial2.begin(9600); Serial.println("setup-start"); // WiFi setup WiFi.mode(WIFI_STA); // Disable Access Point WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println("Connected to the WiFi network"); Serial.println("configTime"); configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp"); client.setCACert(test_root_ca); Serial.println("setup-end"); } //------------------------------------------------------------- //ReadDataFromConsole //------------------------------------------------------------- bool ReadDataFromConsole(char *msg){ if (Serial.available() > 0) { char incoming_byte = Serial.read(); if (incoming_byte == 0x00 || incoming_byte > 0x7F) return false; return true; } return false; } //------------------------------------------------------------- //SendGps //------------------------------------------------------------- void SendGps(String posData){ if (posData.length() < 5) { return; } if ((WiFi.status() != WL_CONNECTED)) { return; } // if you get a connection, report back via serial: if (!client.connect(HOST_SERVER, 443)) { // if you didn't get a connection to the server: Serial.println("connection failed"); return; } Serial.print("connected to "); // Make a HTTP request: String urlParam = URL_GET_PARAMETER; posData.trim(); urlParam += posData; urlParam += " HTTP/1.1"; Serial.println(urlParam); client.println(urlParam); client.println(HOST_SERVER_TAG); client.println("Connection: close"); client.println(); while (client.connected()) { String line = client.readStringUntil('\n'); if (line == "\r") { Serial.println("headers received"); break; } } // if there are incoming bytes available // from the server, read them and print them: while (client.available()) { char c = client.read(); Serial.write(c); } client.stop(); Serial.println("\nSendGps"); } //------------------------------------------------------------------ //loop //------------------------------------------------------------------ void loop(){ //受信 if(Serial2.available() > 0) { String data = Serial2.readString(); SendGps(data); } delay(100); } ``` - クラウド Enebularのサービスであるクラウド実行環境で実現しています。 クラウド実行環境で割り当てた1つのURIに対して、パスで分岐させ、2つの機能を実装しています。 1つ目は、Spresenseから送られるGPS位置情報の保存処理 2つ目は、スマートフォンから地図で位置情報を見るためのHTMLを返す処理です。 地図は、Openstreetmapを使用しています。 フローの再現ファイルは、Githubを参照ください。動作させるには、Enebularのアカウントを作成し、アップロードをしてください。 また、データストアの値や、拠点のGPS値は、お使いの状況に応じて書き換えが必要となります。 参考にフロー図を載せておきます。 ![Enebular-Flow図](https://camo.elchika.com/e7ae4b05ecbcdbeb9170f8c78c452ea10f193553/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f34303530353765332d363962322d343561342d613035662d653962396535633964383866/) - その他 受信機の液晶への画像の描画は、RGB565のBitmapデータを使用しています。 SDカードからファイル読み込まず、プログラムに組み込んでいます。 Bitmapファイルから、プログラムの配列データへの変換プログラムはC#で作成しており、下記となります。 ```cs:BitmapをRGB565のファイル using System.IO; using System.Text; using System.Drawing; namespace ConsoleApp1{ class Program{ static void Main(string[] args){ WriteData(@"C:\Work\src.bmp", @"C:\Work\src.h"); } static ushort color565(ushort red, ushort green, ushort blue){ return (ushort)(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3)); } static void WriteData(string bmpPath, string writePath){ using (var bitmap = new Bitmap(bmpPath)) using (var writer = new StreamWriter(writePath, false, Encoding.UTF8)){ int w = bitmap.Width, h = bitmap.Height; var fileName = System.IO.Path.GetFileNameWithoutExtension(bmpPath); writer.Write("uint16_t "); writer.Write(fileName); writer.WriteLine("[]={"); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { Color pixel = bitmap.GetPixel(x, y); // ARGB var d = color565(pixel.R, pixel.G, pixel.B); if (x + 1 != w) { writer.Write(string.Format("0x{0:x},", d)); } else { writer.Write(string.Format("0x{0:x},\n", d)); } } } writer.Write(@"};"); } } } } ``` ==W5500によるHTTPS接続== W5500-Etherを使って、HTTPSでのネット接続するために作ったサンプルコードを載せておきます。 trust_anchors.hファイルは、下記を参考に作成してください。 [STM32: ethernet w5500 with plain (HTTP) and SSL (HTTPS)](https://mischianti.org/stm32-ethernet-w5500-with-plain-http-and-ssl-https/#Simple_HTTP_request) ```arduino:受信機(W5500-Ehter) #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include <SPI.h> #include <Arduino.h> #include <vector> #include <MP.h> //Add-onボード用インクルード #include "src/Ethernet.h" #include "src/M24C64.h" #include <SSLClient.h> #include "trust_anchors.h" // ----------------------------------------------------------------------- // parameter #define URL_GET_PARAMETER "GET /NOED_NAME/?lat=" #define HOST_SERVER "lcdpXXX.enebular.com" #define HOST_SERVER_TAG "Host: lcdpXXX.enebular.com" #define SPACE_LAT 0.0015 #define SPACE_LOT 0.0015 #define home_lat 34.XXXXXXXXXXXXXX #define home_lot 135.XXXXXXXXXXXXXX #define shome_lat "34.XXXXXXXXXXXXXX" #define shome_lot "135.XXXXXXXXXXXXXX" #define school_lat 34.XXXXXXXXXXXXXX #define school_lot 135.XXXXXXXXXXXXXX //Button #define BTN_PIN A0 // BUTTONN KEY ID #define CMD_1 1 #define CMD_2 2 #define CMD_3 3 #define CMD_4 4 #define CMD_5 5 #define CMD_6 6 // ----------------------------------------------------------------------- //Add-onボード搭載EEPROMへのアクセス用 M24C64 eep; #define USE_SPRESENSE_SPI5 char server[] = HOST_SERVER; // name address for Google (using DNS) // Enter a MAC address and IP address for your controller below. // The IP address will be dependent on your local network: byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x98, 0x01 }; // Choose the analog pin to get semi-random data from for SSL // Pick a pin that's not connected or attached to a randomish voltage source const int rand_pin = A5; // Initialize the SSL client library // We input an EthernetClient, our trust anchors, and the analog pin EthernetClient base_client; SSLClient client(base_client, TAs, (size_t)TAs_NUM, rand_pin); // Variables to measure the speed unsigned long beginMicros, endMicros; unsigned long byteCount = 0; bool printWebData = true; // set to false for better speed measurement int subcore = 1; /* Communication with SubCore1 */ struct MyPacket { float lat; float lot; int rssi; }; MyPacket packet; String g_lat = shome_lat; String g_lot = shome_lot; float g_flat; float g_flot; int g_rssi; int g_imgType = 0; int g_cnt_gps = 0; int g_push_index = 0; int g_sendCmd = 0; //------------------------------------------------------------- //isI2C //------------------------------------------------------------- bool isI2C(uint8_t address){ Wire.beginTransmission(address); if (Wire.endTransmission() == 0){ return true; } return false; } //------------------------------------------------------------- //SendGps //------------------------------------------------------------- void SendGps(float lat, float lot){ // if you get a connection, report back via serial: if (!client.connect(server, 443)) { // if you didn't get a connection to the server: Serial.println("connection failed"); return; } Serial.print("connected to "); Serial.println(Ethernet.localIP()); // Make a HTTP request: String posData = URL_GET_PARAMETER; posData += lat; posData += "&lot="; posData += lot; posData += " HTTP/1.1"; client.println(posData); client.println(HOST_SERVER_TAG); client.println("Connection: close"); client.println(); } //------------------------------------------------------------- //getButtonKey //------------------------------------------------------------- int getButtonKey(){ int index = 0; int data = analogRead(BTN_PIN); if (5 <= data && data <= 70) { g_push_index = 1; } else if (90 <= data && data <= 150) { g_push_index = 2; } else if (300 <= data && data <= 350) { g_push_index = 3; } else if (360 <= data && data <= 500) { g_push_index = 4; } else if (530 <= data && data <= 700) { g_push_index = 5; } else { if (g_push_index != 0) { index = g_push_index; g_push_index = 0; Serial.print("btn= "); Serial.println(index); } } return index; } //------------------------------------------------------------- //commandFunction //------------------------------------------------------------- void commandFunction(int cmd){ if (cmd == CMD_1) { g_sendCmd = CMD_1; SendGps(packet.lat, packet.lot); } else if (cmd == CMD_2) { g_sendCmd = CMD_2; g_isTftLight = !g_isTftLight; DispLight(g_isTftLight); } else if (cmd == CMD_3) { g_sendCmd = CMD_3; } else if (cmd == CMD_4) { g_sendCmd = CMD_4; } else if (cmd == CMD_5) { g_sendCmd = CMD_5; } } //------------------------------------------------------------- //Setup //------------------------------------------------------------- void setup() { Serial.begin(9600); // W5500-Ether用の初期化方法 Serial.println("W5500-Ether"); digitalWrite(21, LOW);//W5500_Eth RESET# = HIGH delay(500); digitalWrite(21, HIGH);//W5500_Eth RESET# = HIGH Ethernet.init(19);// use I2S_DIN pin for W5500 CS pin // W5500-Ether用のMACアドレス取得処理 // I2C device scan Serial.print(F("I2C Devices(0x) : ")); Wire.begin(); //I2C spec. have reserved address!! these scanning escape it address. for (uint8_t ad = 0x08; ad < 0x77; ad++) { if (isI2C(ad)){ Serial.print(ad, HEX); Serial.write(' '); } } Serial.write('\n'); //EEPROM MAC ADDRESS read eep.init(0x57); Serial.print("MAC read from on board eeprom "); for(int i=0;i<6;i++){ mac[i] = eep.read(i); Serial.print(mac[i],HEX); Serial.print(":"); } Serial.println(""); // start the Ethernet connection and the server: Ethernet.begin(mac); delay(100); // Check for Ethernet hardware present if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :("); while (true) { delay(1); // do nothing, no point running without Ethernet hardware } } if (Ethernet.linkStatus() == LinkOFF) { Serial.println("Ethernet cable is not connected."); } Serial.print("My IP address: "); Serial.println(Ethernet.localIP()); } //------------------------------------------------------------- //loop //------------------------------------------------------------- void loop() { // Cloud if (g_sendCmd == CMD_1) { // if there are incoming bytes available // from the server, read them and print them: if (client.available()) { char c = client.read(); Serial.print(c); } // if the server's disconnected, stop the client: if (!client.connected()) { Serial.println("disconnecting."); client.stop(); g_sendCmd = 0; } delay(10); return; } // KEY CODE int key = getButtonKey(); commandFunction(key); delay(100); } ``` 単独 ==最後に== - まだまだ機能が少ないため、改良の余地があります。 - 送信機にボタンやスピーカーの搭載をするか悩みどころではありますが、学校に持っていく前提のため、誤動作を心配し、今のところやめています。そのため、受信機やクラウドを今後もパワーアップしていきたいと考えています。 - クラウドを、ノーコードツールのenebularを使用しましたが、参考情報が少ないのと仕様の把握に時間がかかる印象のため、独自でバックエンドの開発をする方向へ、今後は切り替える方が、自分には合っているかもしれないと、感じている所もあります。 ==今後の応用== ・検知する特定施設の追加 ・送信間隔の最適化 ・ジオフェンスの導入