無線LANゲートウェイ TWELITE SPOTとGoogle App Scriptで作る農業向け温湿度測定・監視システム

<h2>TWELITE SPOTについて</h2>

[概要ホームページ](https://mono-wireless.com/jp/products/twelite-spot/index.html)
[データシート](https://twelite.net/data-sheets/twelite-spot/blue/latest.html)
[スタートガイド](https://twelite.net/start-guides/twelite-spot.html)
[ESP32 を使ったファームウェア開発の基礎](https://twelite.net/start-guides/twelite-spot/esp32-hello-world.html)
![IMG_20230811_203716.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/404614d8-a2bf-ed4a-4559-1a414ecac386.jpeg)

<h2>注意事項：7Pインターフェイスの接続方向

[接続例](https://twelite.net/start-guides/twelite-spot/esp32-hello-world.html#2-twelite-r3--r2-%E3%82%92%E3%81%A4%E3%81%AA%E3%81%90-)
※誤った向きで接続すると破損の原因

<h2>Arduino IDEを用いたESP32側の開発の基礎</h2>

1. Arduino IDEをインストール
2. ボートマネージャーでESP32を追加
3. ツールバーの ツール -> ボード -> ESP32 Arduino -> ESP32 Dev Module を選択
ボード設定を以下と同様にする。Flash size を 4MB (32Mb) から 16MB (128Mb) に変更
4. ESP用7PインターフェイスにTWELITE R3を接続。向きに注意。TWELITE R3の表面を接続対象に向ける。
5. USB-Cコネクタに5V電源をつなぐ
6. ESP32をプログラムモードで起動。ESP32 リセットスイッチ EN(RST) と ESP32 ブートスイッチ BOOT を押し、EN(RST) -> BOOT の順で離す。BOOT を押した状態でリセットすることが重要
7. ArduinoIDEからマイコンボードに書き込む
8. ESP32を再起動する。ESP32 リセットスイッチ EN(RST) を押して離し、ESP32 をリセット

<h1>TWELITE SPOTとTWELITE ARIAを使って温湿度をGoogleスプレッドシートに記録するArduinoプログラム</h1>


```
// TWELITE SPOT to Google App Scritp
// 親機にTWELITE SPOT、子機にTWELITE ARIAを使用
// デバッグ時のシリアル通信用の定義
#define _DEBUG 0  /* _DEBUG = 1のとき、シリアルポートに計測データを出力する*/
#if _DEBUG
#define DBG(...) { Serial.println(__VA_ARGS__); } /* DBG(引数)をSerial.println(引数)と定義*/
#else
#define DBG(...)
#endif

// Arduino,ESP32の公式ライブラリをインクルード
#include <Arduino.h>  //Arduinoの基本ライブラリ
#include <NTPClient.h>  //現在時刻を取得するためにNTPを使う
#include <WiFi.h> //ESP32 の WiFi を使う
#include <WiFiUdp.h>  //UDP を使う、NTPClient に必要
#include <Arduino_JSON.h> //JSON 文字列を扱う	ArduinoJsonとは異なる
#include <HTTPClient.h> //HTTPクライアント　JsonのPOSTに必要
// Third-party library：サードパーティのライブラリをインクルード
#include <TimeLib.h>  //UNIX 時間をフォーマットする

// Mono Wireless TWELITE Wings API for 32-bit Arduinosライブラリをインクルード
#include <MWings.h>

// ユーザ設定を定義。実行時にはこの設定ファイルの内容を書き換える
// ファイルの内容はWIFI_SSID,WIFI_PASSWORD,GAS_URLの定義
#include "config.h"

// ESP32のピン番号を定義
const uint8_t TWE_RST = 5;  //TWELITEのRSTピンが接続されているピンの番号
const uint8_t TWE_PRG = 4;  //TWELITEのPRGピンが接続されているピンの番号
const uint8_t LED = 18; //基板上のESP32用LED が接続されているピンの番号
const uint8_t ESP_RXD1 = 16;  //TWELITEのTXピンが接続されているピンの番号
const uint8_t ESP_TXD1 = 17;  //TWELITEのRXピンが接続されているピンの番号

// TWELITE親機に適用する設定を定義
const uint8_t TWE_CH = 18;  //TWELITEの周波数チャネル（TWELITE標準アプリの初期値）
const uint32_t TWE_APPID = 0x67720102;  //TWELITEのアプリケーションID（TWELITE標準アプリの初期値）
const uint8_t TWE_RETRY = 2;  //TWELITEの再送回数（送信時）
const uint8_t TWE_POWER = 3;  //TWELITEの送信出力
constexpr int ARIA_BUFFER_PACKETS = 32;    //ARIA からのパケットを格納するキューの長さ(32個まで保持可能)

// Type definitions：型を宣言
struct ParsedAppAriaPacketWithTime {  //受信したパケットデータを受信時刻と合わせてキューへ格納するための型
    ParsedAppAriaPacket packet;
    uint32_t elapsedMillis; //パケット受信時の起動からの経過時間（ミリ秒）
    uint32_t unixTime;  //パケット受信時の UNIX 時間（秒）
};

// Global objects：グローバルオブジェクトを宣言
WiFiUDP ntpUDP; //NTPのためのUDPインタフェース
NTPClient timeClient(ntpUDP, "ntp.nict.jp", 32400); //NTPのインタフェース。UDPインタフェース、NTPサーバー、日本の時差32400秒=9時間を指定。メインループloop()内で必ず、ntp.update()を回す
QueueHandle_t ariaPacketQueue;       //ARIAから受信したパケットと受信時刻を格納するキュー。FreeRTOSのQueue機能を使う

// 関数プロトタイプを宣言
void anotherLoop(); //非同期でTWELITEの処理を行う、もうひとつのloop()。 TWELITEの受信処理を止めないために、FreeRTOSのマルチタスク機能を使ってanotherLoop()を作成し、そのなかでTwelite.update()を実行する
String createJsonFrom(const ParsedAppAriaPacketWithTime& packetWithTime); //App_ARIAのパケットデータからJSON文字列を作る関数
bool addDataGAS();  //GASへデータを送信する

// 起動時に実行する内容
void setup() {
  // シリアルポートの初期化
  Serial.begin(115200); //SerialはArduino IDEのシリアルモニタとの通信
  Serial.println("Hello, this is TWELITE SPOT.");

  // 受信したパケットデータを受信時刻と合わせて格納するためのキューを初期化
  Serial.println("Initializing queue...");
  ariaPacketQueue = xQueueCreate(ARIA_BUFFER_PACKETS, sizeof(ParsedAppAriaPacketWithTime)); //FreeRTOSの機能。マルチタスクに対応したキューを作成
  if (ariaPacketQueue == 0) { Serial.println("Failed to init a queue."); }
  Serial.println("Completed.");

  // TWELITE親機の設定と起動
  Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);  //Serial2はTWELITE SPOTに搭載されたTWELITE親機との通信
  if (Twelite.begin(Serial2, LED, TWE_RST, TWE_PRG, TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
    Serial.println("Started TWELITE.");
  }

  // パケット受信時のイベントの登録 Twelite.on()を呼び出し、送られたデータに対して行う処理を登録
  Twelite.on([](const ParsedAppAriaPacket& packet) {
    DBG("Got a new packet from ARIA.");
    ParsedAppAriaPacketWithTime packetWithTime; //packetWithTimeオブジェクトを生成
    packetWithTime.elapsedMillis = millis();  //起動開始からの経過時間をミリ秒単位で取得
    packetWithTime.unixTime = timeClient.getEpochTime();  //NTPインターフェスからUNIX時間を取得
    packetWithTime.packet = packet; //パケット本体を取得

    //xQueueSend()により、受信したパケットデータを受信時刻と合わせてキューの末尾へ格納、送信成功時にpdPASSを返す
    //第一引数はキューのハンドラ、第二引数は送信データへのポインタ、第三引数はキューが満杯だったときの待機時間
    if (not(xQueueSend(ariaPacketQueue, &packetWithTime, 10) == pdPASS)) { 
      DBG("Failed to add packet data to the queue.");
    }
  });

    // 無線LANの設定
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    Serial.print("Connecting to WiFi ..");
    while (WiFi.status() != WL_CONNECTED) { //ネットワークに接続できるまで、while文を実行
        static int count = 0;
        Serial.print('.');
        delay(500);
        // ネットワークに接続できるまで5秒に一度、再接続
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.print("\nConnected. IP: ");
    Serial.println(WiFi.localIP().toString().c_str());  //IPアドレスを表示
    
    // NTPの設定
    Serial.print("Initializing NTP...");
    timeClient.begin();
    timeClient.update();
    Serial.print("Completed. UNIX time: ");
    Serial.println(timeClient.getEpochTime());  //NTPから取得したUNIX時間を表示

    // Assign a FreeRTOS task to the Core 0 for the another loop procedure updating TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    // TWELITE のデータを非同期で更新するためのタスクを登録
    xTaskCreatePinnedToCore(
      [](void* params) {
        while (true) {
          anotherLoop();  //anotherLoop() を無限に呼ぶタスクを登録
          vTaskDelay(1);  //無限ループを含むタスクでは、必ずvTaskDelay(1); などを挿入し、ウォッチドッグタイマの入る隙を与える
        }
      },
      "Task for anotherLoop()", 8192, nullptr, 18, nullptr, 0);
      //タスクの名称はTask for anotherLoop()で、スタックサイズは8192、タスクへのパラメータはなく、優先度は18（大きいほど高く、無線LAN関連の処理は1個上の19）、タスクを操作するインタフェースもなく、実行するCPUコアは無線LANなどのRF処理と同じCore0（ loop()等はCore 1）
}

void loop() {
  addDataGAS(); //センサーデータをJsonでGASにPOST
  timeClient.update();  //NTPライブラリの更新
}

// Another loop procedure; Priority is higher than loop()
void anotherLoop() {
  Twelite.update();    //TWELITEのデータの更新 With multi-tasking (async)
}

// Create JSON string from parsed App_ARIA packets
String createJsonFrom(const ParsedAppAriaPacketWithTime& packetWithTime) {
    JSONVar jsonData;
    jsonData["app_type"] = "app_aria_twelite_aria_mode";
    char dateTimeCString[20]; //日時を記録する文字列の変数を宣言
    setTime(packetWithTime.unixTime);
    sprintf(dateTimeCString, "%04d/%02d/%02d %02d:%02d:%02d",    // yyyy/MM/dd hh:mm:ss DateTimeオブジェクトを文字列に変換
                year(), month(), day(),
                hour(), minute(), second());
    jsonData["ArriveTime"] = dateTimeCString;
    jsonData["LogicalID"] = packetWithTime.packet.u8SourceLogicalId;  //	送信元の論理デバイスID
    jsonData["SerialID"] = packetWithTime.packet.u32SourceSerialId; //送信元のシリアルID
    jsonData["LQI"] = packetWithTime.packet.u8Lqi;  //電波通信品質 50未満(悪い -80dbm 未満)、50～100(やや悪い)、100～150(良好)、150以上(アンテナの近傍)
    jsonData["SequenceNumber"] = packetWithTime.packet.u16SequenceNumber; //シーケンス番号
    jsonData["Power"] = packetWithTime.packet.u16SupplyVoltage; //電源電圧mV
    jsonData["mag_status"] = packetWithTime.packet.u8MagnetState; //磁気イベントID
    char temp[10];
    sprintf(temp, "%.2f", packetWithTime.packet.i16Temp100x/ 100.0f); //100倍された温度(整数4桁で表現)を100で割り（浮動小数点）、小数点2桁までの文字列に変換
    jsonData["Temperature"] = temp;
    char humid[10];
    sprintf(humid, "%.2f", packetWithTime.packet.u16Humid100x/ 100.0f); //100倍された相対湿度 (%)
    jsonData["Humidity"] = humid;
    return JSON.stringify(jsonData);        //JSONデータの中身を全部確認する時は、String型に変換
}

// GASへデータをPOST
bool addDataGAS(){
    UBaseType_t availableAriaPackets = uxQueueMessagesWaiting(ariaPacketQueue);  //キューの中で読み出しを待っているデータの個数を返す
    if (not(availableAriaPackets > 0)) {
        DBG("There's no ARIA packet. Skip.");
        return false;
    }
    
    for (int i = 0; i < availableAriaPackets; i++) {
      ParsedAppAriaPacketWithTime packetWithTime;
      if (not(xQueueReceive(ariaPacketQueue, &packetWithTime, 10) == pdPASS)) {
          DBG("Failed to get packet data from the queue.");
          break;
      }

      // 受信したデータからJSON 文字列を生成
      String jsonStr = createJsonFrom(packetWithTime);  
      DBG(jsonStr); //JSONデータを表示

      // JSON文字列をGASにPOST
      HTTPClient http;
      http.begin(GAS_URL);
      http.addHeader("Content-Type", "application/json");
      int status_code = http.POST(jsonStr);  //POST
      if( status_code == 200 ){ //200以外ではシリアルにエラーを出力
        DBG("[POST]Send to server");
      }
      else{
        DBG("[POST]failed to send to server");
      }
      http.end();
    }
    return true;
}
```



<h2>データを受信するGoogleスプレッドシートのプログラム

過去に作成したものをそのまま流用
[モノワイヤレス TWELITEとGoogle App Scriptで作る農業向け温湿度測定・監視システム](https://qiita.com/mbbzsgi/items/687d77afa432be78a8b7)