spot-httpbin の解説です。本稿では、サードパーティのオープンソースソフトウェアを使用します。
サードパーティのソフトウェアについて、その詳しい使用方法を弊社からご案内することはいたしかねます。また、サードパーティのソフトウェアを使用されたことによるいかなる損害についても、弊社は一切の責任を負いません。
ソースコードの入手
GitHub リポジトリ monowireless/spot-httpbin から入手できます。
システムの概要
spot-httpbin は、TWELITE 親機が受信したデータの一部と NTP による現在時刻を HTTP GET リクエストとしてモックサーバへ送信し、そのレスポンスをシリアルモニタへ表示します。
開発に必要なもの
- 
無線LANゲートウェイ TWELITE SPOT- 電源用 USB-C ケーブル
- USB AC アダプタ(1A 以上供給できるもの)
 
- 
磁気・温度・湿度センサー無線タグ TWELITE ARIAなどの子機 (お持ちでない場合はご購入ください 👉 販売店一覧)- CR2032 コイン電池 などの電源
 
- 
USBアダプター TWELITE R3 (お持ちでない場合はご購入ください 👉 販売店一覧)- 通信用 USB-C ケーブル
 
- 💻 開発用コンピュータ
環境整備
IDE とツールチェインの導入
Arduino IDE 1.x による開発環境の構築方法 をご覧ください。
ライブラリの導入
このサンプルでは、依存するライブラリをはじめから同梱しています。
src フォルダの内容は IDE に表示されませんが、再帰的にビルドされます。プロジェクトファイルの入手
- GitHub (monowireless/spot-httpbin) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を spot-httpbin-mainからspot-httpbinに変更します
- Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)にspot-httpbinフォルダを配置します
ユーザ設定の変更
Arduino IDE 上部のタブから config.h を開き、Wi-Fi の SSID と パスワードを設定してください。WPA2-PSK ネットワークを想定しています。また、ルート証明書も登録してください。ルート証明書は、Chrome などのウェブブラウザの各ページに対するセキュリティ画面から入手できます。
ルート証明書(拡張子.cer)は、下記のような形式のテキストファイルです。
-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----
プロジェクトファイルの書き込み方法
ESP32 へのスケッチの書き込み方法 をご覧ください。
スケッチ
Arduino スケッチ spot-httpbin.ino および config.h の解説です。
ライブラリのインクルード
Arduino および ESP32 公式ライブラリ
4-6行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。
#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
| ヘッダファイル | 内容 | 備考 | 
|---|---|---|
| Arduino.h | Arduino の基本ライブラリ | 省略できる場合もあるが念のため記載 | 
| WiFiClientSecure.h | ESP32 で SSL通信を行う | |
| WiFiUdp.h | UDP 通信を行う | NTP に必要 | 
サードパーティのライブラリ
9-10行目では、同梱されたサードパーティのライブラリをインクルードしています。
#include "src/NTPClient/NTPClient.h"
#include "src/Time/TimeLib.h"
| ヘッダファイル | 内容 | 備考 | 
|---|---|---|
| NTPClient.h | NTP サーバへアクセスする | |
| TimeLib.h | エポック時間を変換する | 
MWings ライブラリ
13行目では、MWings ライブラリをインクルードしています。
#include <MWings.h>
ユーザ設定の定義
16行目では、config.h をインクルードしています。
#include "config.h"
config.h では、ユーザ設定を定義しています。実行時には、この設定を書き換えてください。データ型の定義
19-26行目では、子機から受信したデータを保管しておく構造体の型を定義しています。
struct DataFromAria {
    uint32_t serialId;
    uint8_t logicalId;
    uint16_t supplyVoltage;
    uint8_t linkQuality;
    int16_t temp100x;
    uint16_t humid100x;
};
| 名称 | 内容 | 
|---|---|
| serialId | シリアルID | 
| logicalId | 論理デバイスID | 
| supplyVoltage | 電源電圧 | 
| linkQuality | LQI | 
| temp100x | 100倍された温度 | 
| humid100x | 100倍された湿度 | 
ここでは、TWELITE ARIA を使用します。
config.h
再起動間隔の定義
config.h の4行目では、ESP32 の再起動間隔を指定しています。
const uint32_t REBOOT_INTERVAL = 21600; // seconds
ここでは、21600秒=6時間としています。
長期間の運用では、メモリリークが累積して不具合を起こしてしまう場合があります。
そこで Wi-Fi ルータのように定期的な再起動を行うようにしています。
TWELITE 設定の定義
config.h の7-8行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。
const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
| 名称 | 内容 | 
|---|---|
| TWE_CH | TWELITE の 周波数チャネル | 
| TWE_APPID | TWELITE の アプリケーション ID | 
Wi-Fi 設定の定義
config.h の11-12行目では、TWELITE SPOT に搭載された ESP32 に適用するWi-Fi 設定を定義しています。
const char* WIFI_SSID = "YOUR SSID";
const char* WIFI_PASSWORD = "YOUR PASSWORD";
| 名称 | 内容 | 
|---|---|
| WIFI_SSID | 接続するネットワークの SSID | 
| WIFI_PASSWORD | 接続するネットワークの パスワード | 
ルート証明書
config.h の14-16行目では、ルート証明書の内容を記述するためのテンプレートを用意しています。
const char *CA_CERT =
    "-----BEGIN CERTIFICATE-----\n"
    "-----END CERTIFICATE-----\n";
ルート証明書は、Chrome などのウェブブラウザの各ページに対するセキュリティ画面から入手してください。
すべての行をダブルクォートで囲い、末尾のダブルクォートの前には改行文字 \n を追加する必要があります。
ホストの設定
config.h の18-19行目では、ホストの設定を定義しています。
const char *SERVER_HOST = "www.httpbin.org";
const uint16_t SERVER_PORT = 443;
| 名称 | 内容 | 
|---|---|
| SERVER_HOST | サーバのホスト名 | 
| SERVER_PORT | サーバのポート番号 | 
各種定数の定義
config.h の21行目からは、各種定数を定義しています。
const uint32_t NTP_UPDATE_INTERVAL = 10000; // ms
const int QUERIES_MAX_LENGTH = 128;         // bytes (without \0)
const int32_t CONNECT_TIMEOUT = 10;     // seconds
const uint32_t RECONNECT_MIN_INTERVAL = 5; // seconds
// SEND_MIN_INTERVAL must be longer than NTP_UPDATE_INTERVAL
const uint32_t SEND_MIN_INTERVAL = 10; // seconds
const uint32_t REQUEST_TIMEOUT = 10;   // seconds
| 名称 | 内容 | 
|---|---|
| NTP_UPDATE_INTERVAL | NTP時刻の取得間隔 | 
| QUERIES_MAX_LENGTH | クエリ文字列の最大長(ヌル文字含まず) | 
| CONNECT_TIMEOUT | サーバへの接続時のタイムアウト | 
| RECONNECT_MIN_INTERVAL | Wi-Fiアクセスポイントへ再接続する際の最短間隔 | 
| SEND_MIN_INTERVAL | リクエスト間隔の最短間隔 | 
| REQUEST_TIMEOUT | リクエストからレスポンスまでのタイムアウト | 
SEND_MIN_INTERVAL は、NTP_UPDATE_INTERVAL 以上に設定しています。
リクエスト間隔が短いと、リクエスト間でタイムスタンプが重複してしまう可能性があるからです。
SEND_MIN_INTERVAL を短くすると、連続してパケットを受信した場合にサーバへ負担をかけてしまいます。
必ず適度な間隔を空けてください。
ピン番号の定義
29-31行目では、ピン番号を定義しています。
static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;
| 名称 | 内容 | 
|---|---|
| RST_PIN | TWELITE の RST ピンが接続されているピンの番号 | 
| PRG_PIN | TWELITE の PRG ピンが接続されているピンの番号 | 
| LED_PIN | 基板上の ESP32 用 LED が接続されているピンの番号 | 
グローバルオブジェクトの宣言
34-37行目では、グローバルオブジェクトを宣言しています。
static WiFiClientSecure client;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "ntp.nict.jp",
                            32400, NTP_UPDATE_INTERVAL); // JST(UTC+9)
| 名称 | 内容 | 
|---|---|
| client | HTTPS通信のインタフェース | 
| ntpUDP | NTP用のUDP通信のインタフェース | 
| timeClient | NTPのインタフェース | 
グローバル変数の宣言
40-41行目では、グローバル変数を宣言しています。
static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
| 名称 | 内容 | 
|---|---|
| LatestDataFromAria | TWELITE ARIA から受信した最新のデータ | 
| IsThereNewDataFromAria | TWELITE ARIA から新たなデータを受信したことを示すフラグ | 
関数プロトタイプの宣言
44-56行目では、関数プロトタイプを宣言しています。
void anotherLoopForTWELITE();
void anotherLoopForNTP();
| 名称 | 内容 | 
|---|---|
| anotherLoopForTWELITE | TWELITEのデータを処理するためのループ関数 | 
| anotherLoopForNTP | NTPで時刻を取得するためのループ関数 | 
xTaskCreatePinnedToCore() により、別のタスクとして登録しています。
void initTWELITE();
void initWiFi();
void initNTP();
| 名称 | 内容 | 
|---|---|
| initTWELITE | TWELITEの初期化関数 | 
| initWiFi | Wi-Fiの初期化関数 | 
| initNTP | NTPの初期化関数 | 
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
| 名称 | 内容 | 
|---|---|
| onAppAriaPacket | TWELITE ARIA からデータを受信した際のコールバック関数 | 
void sendAriaData(const DataFromAria& data)
| 名称 | 内容 | 
|---|---|
| sendAriaData | TWELITE ARIAのデータを HTTP GET リクエストにのせて送る関数 | 
setup()
59-87行目では、全体の初期化を行います。
void setup() {
    Serial.begin(115200);
    initTWELITE();
    initWiFi();
    initNTP();
    // Attach another loop function for TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForTWELITE()", 8192, nullptr, 18, nullptr,
        0); // Priority is 18 (lower than WiFi)
    // Attach another loop function for NTP
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForNTP();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForNTP()", 8192, nullptr, 17, nullptr,
        0); // Priority is 17 (lower than WiFi and TWELITE)
}
xTaskCreatePinnedToCore() により、loop() とは別のタスクを登録しています。
下記の部分はキャプチャのない無名関数です。不要なグローバル空間の汚染を避けることができます。
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
vTaskDelay() を挿入しています。loop()
90-111行目は、主となるループ処理です。
HTTP リクエストの処理、Wi-Fi 切断時の再接続処理、定期リセットの処理を行います。
void loop() {
    static uint32_t lastTimeReconnected = 0;
    if (WiFi.status() == WL_CONNECTED) {
        // Regular operations
        // Check for new data
        if (IsThereNewDataFromAria) {
            IsThereNewDataFromAria = false; // Clear first; data is updated on another thread
            DataFromAria data = LatestDataFromAria; // Now, the buffer is open for incoming data
            sendAriaData(data);
        }
    } else if (millis() - lastTimeReconnected > RECONNECT_MIN_INTERVAL * 1000) {
        // Lost connection, reconnect periodically
        Serial.println("Disconnected. Reconnecting to WiFi...");
        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        lastTimeReconnected = millis();
    }
    // Reboot every x interval
    if (millis() > REBOOT_INTERVAL * 1000) {
        Serial.println("Rebooting...");
        ESP.restart();
    }
}
anotherLoopForTWELITE()
114-116行目は、TWELITE のためのループ処理です。
データの受信と解釈を逐次行うため、ブロッキング処理を含む loop() とは別のタスクとしています。
void anotherLoopForTWELITE() {
    Twelite.update();
}
anotherLoopForNTP()
117-120行目は、NTP のためのループ処理です。
こちらについても UDP の通信を行うため、ブロッキング処理を含む loop() とは別のタスクとしています。
void anotherLoopForNTP() {
    timeClient.update();
    setTime(timeClient.getEpochTime());
}
initTWELITE()
123-130行目は、TWELITE の初期化処理です。
TWELITE SPOT に搭載された TWELITE を指定された設定で起動し、パケット受信時のコールバック関数を登録しています。
void initTWELITE() {
    Serial2.begin(115200);
    if (Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID)) {
        Serial.println("Started TWELITE.");
    }
    // Attach event handlers to process packets
    Twelite.on(onAppAriaPacket);
}
下記リファレンスも合わせてご覧ください。
- Twelite.begin()mwings::MWings クラス | MWings API リファレンス
- Twelite.on()mwings::MWings クラス | MWings API リファレンス
initWiFi()
133-157行目は、Wi-Fi の初期化処理です。
接続されない場合は、5秒置きに再接続を試みます。
void initWiFi() {
    Serial.print("\nConnecting to the WiFi network ");
    Serial.print(WIFI_SSID);
    Serial.println("...");
    // Begin
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    // Wait for connection
    Serial.print("Connecting.");
    while (WiFi.status() != WL_CONNECTED) x
        static int count = 0;
        Serial.print('.');
        delay(500);
        // Retry every 5 seconds
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.println("\nConnected!");
    // Set Root CA certificate
    client.setCACert(CA_CERT);
}
initNTP()
160-164行目は、NTP の初期化処理です。
void initNTP() {
    timeClient.begin();
    timeClient.update();
    setTime(timeClient.getEpochTime());
}
onAppAriaPacket()
167-177行目には、TWELITE ARIA からデータを受信した際の処理を記述しています。
ここでは HTTP の送信処理を行わず、グローバル変数へセットしています。
グローバル変数へセットしたデータは、別のタスクで sendAriaData() によって処理します。
void onAppAriaPacket(const ParsedAppAriaPacket& packet)
{
    // Store data
    LatestDataFromAria.serialId = packet.u32SourceSerialId;
    LatestDataFromAria.logicalId = packet.u8SourceLogicalId;
    LatestDataFromAria.supplyVoltage = packet.u16SupplyVoltage;
    LatestDataFromAria.linkQuality = packet.u8Lqi;
    LatestDataFromAria.temp100x = packet.i16Temp100x;
    LatestDataFromAria.humid100x = packet.u16Humid100x;
    IsThereNewDataFromAria = true;
}
sendAriaData()
180-237行目は、TWELITE ARIA のデータを HTTP GET リクエストのクエリ文字列にセットして送信する関数です。
コードを簡潔とするために、HTTP GET を使用しています。
HTTP POST を使用される場合は、リクエストボディを追加してください。
サーバへの過度な負荷を防ぐため、高頻度でパケットが到着した際には送信をスキップしています。
void sendAriaData(const DataFromAria& data)
{
    static uint32_t lastTimeRequested = 0;
    if (millis() - lastTimeRequested > SEND_MIN_INTERVAL * 1000 or lastTimeRequested == 0) {
        Serial.println("Connecting to the server...");
        if (not client.connect(SERVER_HOST, SERVER_PORT, CONNECT_TIMEOUT * 1000)) {
            Serial.println("Connection failed!");
        } else {
            Serial.println("Connected to the server!");
            // Make a query string
            char queries[QUERIES_MAX_LENGTH+1];
            snprintf(queries, sizeof(queries),
                     "datetime=%04d%02d%02d%02d%02d%02d&sid=%X&lid=%d&temp=%d&humid=%d&bat=%d&lqi=%d",
                     // Note that NTP_UPDATE_INTERVAL is set for 10000ms by default; second() delays up to 10s.
                     // To prevent duplication of datetime, SEND_MIN_INTERVAL is set for 10s.
                     year(), month(), day(), hour(), minute(), second(),
                     data.serialId,
                     data.logicalId,
                     data.temp100x,
                     data.humid100x,
                     data.supplyVoltage,
                     data.linkQuality);
            // Send a request
            client.println(String("GET https://") +
                           SERVER_HOST +
                           String("/get?") +
                           queries +
                           String(" HTTP/1.1"));
            client.println("Accept: */*");
            client.println(String("Host: ") + SERVER_HOST);
            client.println("Connection: close");
            client.println();
            uint32_t timeSentRequest = millis();
            // Handle a response
            while (client.connected()) {
                String line = client.readStringUntil('\n');
                if (line == "\r") {
                    Serial.println("Headers received");
                    break;
                }
                if (millis() - timeSentRequest > REQUEST_TIMEOUT * 1000) {
                    Serial.println("Request was timed out");
                    break;
                }
            }
            while (client.available()) {
                char c = client.read();
                Serial.write(c);
            }
            client.stop();
        }
        lastTimeRequested = millis();
    } else {
        Serial.println("Requests are too frequently; skip.");
    }
}