セクションの複数ページをまとめています。 印刷またはPDF形式で保存...

もとのページに戻る

2024-11-14 現在

TWELITE に加えて無線 LAN を活用するスケッチの解説

TWELITE SPOT の応用的なサンプルスケッチの解説
TWELITE NET に加えて無線 LAN を活用した高度なサンプルスケッチを解説します。

1 - プリインストール済みスケッチ

子機からのデータを Web ページに表示するローカルサーバのサンプルスケッチ spot-server の解説
無線 LAN アクセスポイントとして振る舞い、Web ページ上に子機からのデータを表示するサンプルスケッチ spot-server の解説です。

1.1 - プリインストール済みスケッチ

最新(ESP32 Arduino Core v3.x.x)版
無線 LAN アクセスポイントとして振る舞い、Web ページ上に子機からのデータを表示するサンプルスケッチ spot-server の解説です。

ソースコードの入手

GitHub (monowireless/spot-server) から入手できます。

システムの概要

spot-server は、TWELITE からのデータ受信と転送を行う Arduino スケッチ (.ino) と、スマホに配信する Web ページ (.html / .css / .js) で構成しています。

イメージ図

イメージ図

TWELITE 子機が送信したデータは Arduino スケッチで受信され、Arduino スケッチは公開中の Web ページに対してイベントを発火します。公開された Web ページでは、発火されたイベントに応じて HTML の内容を動的に書き換えています。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino) に libraries フォルダがない場合は、これを作成します。

非同期 TCP 通信ライブラリ

  1. GitHub (me-no-dev/AsyncTCP) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を AsyncTCP-master から AsyncTCP に変更します
  3. libraries フォルダに AsyncTCP フォルダを配置します

非同期 Web サーバライブラリ

  1. GitHub (me-no-dev/ESPAsyncWebServer) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を AsyncWebServer-master から AsyncWebServer に変更します
  3. libraries フォルダに AsyncWebServer フォルダを配置します

OLED ディスプレイライブラリ

  1. GitHub (Seeed-Studio/OLED_Display_96X96) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を OLED_Display_96X96-master から OLED_Display_96X96 に変更します
  3. libraries フォルダに OLED_Display_96X96 フォルダを配置します

JSON ライブラリ

ライブラリマネージャを開き、Arduino_JSON をインストールします。

プラグインの導入

ファイルシステム書き込みプラグイン

HTML などのファイルを ESP32 のフラッシュ領域に書き込むには、Arduino プラグインが必要です。

ここでは、lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system を利用します(LittleFSに対応したプラグイン)。

インストール方法は TWELITE SPOT マニュアル ESP32 へのファイルの書き込み方法 をご覧ください。

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-server) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-server-main から spot-server に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-server フォルダを配置します

プロジェクトファイルの書き込み方法

スケッチ

ESP32 へのスケッチの書き込み方法 をご覧ください。

Web ページ

ESP32 へのファイルの書き込み方法 をご覧ください。

スケッチ

Arduino スケッチ spot-server.ino の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-9行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <Arduino_JSON.h>
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <WiFi.h>
#include "esp_wifi.h"
#include <Wire.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
Arduino_JSON.hJSON 文字列を扱うArduinoJsonとは異なる
ESPmDNS.hmDNS を使うホスト名を使うために必要
LittleFS.hLittleFS ファイルシステムを扱うページ公開に必要
WiFi.hESP32 の WiFi を使う
esp_wifi.hWiFiの高度な設定を行うロケール設定に必要
Wire.hI2C を使うOLED ディスプレイ用

サードパーティのライブラリ

13-15行目では、サードパーティのライブラリをインクルードしています。

#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SeeedGrayOLED.h>
ヘッダファイル内容備考
AsyncTCP.h非同期 TCP 通信を行う
ESPAsyncWebServer.h非同期 Web サーバを立てるAsyncTCP に依存
SeeedGrayOLED.hOLED ディスプレイを使う

MWings ライブラリ

18行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ピン番号の定義

21-25行目では、ピン番号を定義しています。

const uint8_t TWE_RST = 5;
const uint8_t TWE_PRG = 4;
const uint8_t LED = 18;
const uint8_t ESP_RXD1 = 16;
const uint8_t ESP_TXD1 = 17;
名称内容
TWE_RSTTWELITE の RST ピンが接続されているピンの番号
TWE_PRGTWELITE の PRG ピンが接続されているピンの番号
LED基板上の ESP32 用 LED が接続されているピンの番号
ESP_RXD1TWELITE の TX ピンが接続されているピンの番号
ESP_TXD1TWELITE の RX ピンが接続されているピンの番号

TWELITE 設定の定義

28-31行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
const uint8_t TWE_RETRY = 2;
const uint8_t TWE_POWER = 3;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション ID
TWE_RETRYTWELITE の 再送回数(送信時)
TWE_POWERTWELITE の 送信出力

無線 LAN 設定の定義

34-46行目では、TWELITE SPOT に搭載された ESP32 に適用する無線 LAN 設定を定義しています。

wifi_country_t WIFI_COUNTRY_JP = {
  cc: "JP",         // Contry code
  schan: 1,         // Starting channel
  nchan: 14,        // Number of channels
  max_tx_power: 20, // Maximum power in dBm
  policy: WIFI_COUNTRY_POLICY_MANUAL
};
const char* WIFI_SSID_BASE = "TWELITE SPOT";
const char* WIFI_PASSWORD = "twelitespot";
const uint8_t WIFI_CH = 13;
const IPAddress WIFI_IP = IPAddress(192, 168, 1, 1);
const IPAddress WIFI_MASK = IPAddress(255, 255, 255, 0);
const char* HOSTNAME = "spot";    // spot.local
名称内容
WIFI_COUNTRY_JPロケール設定(日本)
WIFI_SSID_BASESSID の共通部分の文字列
WIFI_PASSWORDパスワード
WIFI_CHESP32 の周波数チャネル
WIFI_IPIP アドレス
WIFI_MASKサブネットマスク
HOSTNAMEホスト名

グローバルオブジェクトの宣言

49-50行目では、グローバルオブジェクトを宣言しています。

AsyncWebServer server(80);
AsyncEventSource events("/events");
名称内容
server80番ポートで開く非同期 Web サーバのインタフェース
events/eventsで開くサーバー送信イベント ?のインタフェース

関数プロトタイプの宣言

53-57行目では、関数プロトタイプを宣言しています。

uint16_t createUidFromMac();
String createJsonFrom(const ParsedAppTwelitePacket& packet);
String createJsonFrom(const ParsedAppAriaPacket& packet);
String createJsonFrom(const ParsedAppCuePacket& packet);
String createJsonFrom(const BarePacket& packet);
名称内容
createUidFromMac()MAC アドレスから SSID に使う識別子を作ります
createJsonFrom()<ParsedAppTwelitePacket&>App_Twelite のパケットデータから JSON 文字列を作ります
createJsonFrom()<ParsedAppAriaPacket&>App_ARIA のパケットデータから JSON 文字列を作ります
createJsonFrom()<ParsedAppCuePacket&>App_CUE のパケットデータから JSON 文字列を作ります
createJsonFrom()<BarePacket&>すべてのパケットデータから JSON 文字列を作ります

TWELITE の設定

66-71行目では、Twelite.begin() を呼び出し、TWELITE SPOT に搭載された TWELITE 親機の設定と起動を行っています。

    Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);
    if (Twelite.begin(Serial2,
                      LED, TWE_RST, TWE_PRG,
                      TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
        Serial.println("Started TWELITE.");
    }
引数内容
Serial2HardwareSerial&TWELITE との通信に使うシリアルポート
LEDintステータス LED を接続したピンの番号
TWE_RSTintTWELITE の RST ピンを接続したピンの番号
TWE_PRGintTWELITE の PRG ピンを接続したピンの番号
TWE_CHANNELuint8_tTWELITE の 周波数チャネル
TWE_APP_IDuint32_tTWELITE の アプリケーション ID
TWE_RETRYuint8_tTWELITE の 再送回数(送信時)
TWE_POWERuint8_tTWELITE の 送信出力

App_Twelite:イベントハンドラの登録

73-80行目では、Twelite.on() <ParsedAppTwelitePacket> を呼び出し、超簡単!標準アプリの子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppTwelitePacket& packet) {
    Serial.println("Received a packet from App_Twelite");
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_app_twelite", millis());
    }
    events.send("parsed_app_twelite", "data_parsing_result", millis());
});

JSON 文字列の作成

75行目では、受信したデータからJSON 文字列を生成しています。

String jsonStr = createJsonFrom(packet);

受信したデータをWeb ページに表示するにはクライアント側の JavaScript にデータを送る必要がありますが、このとき文字列データのほうが扱いやすいため、JSON 文字列としています。

Web ページへのイベント送信

76-78行目では、生成した JSON 文字列を “Signal Viewer” ページへ送信しています。

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_twelite", millis());
}

イベント名は data_app_twelite です。

79行目では、App_Twelite からのパケットを受信したことを “Serial Viewer” ページへ送信しています。

events.send("parsed_app_twelite", "data_parsing_result", millis());

App_ARIA:イベントハンドラの登録

82-92行目では、Twelite.on() <ParsedAppAriaPacket> を呼び出し、アリアアプリ(TWELITE ARIA モード)の子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppAriaPacket& packet) {
        Serial.println("Received a packet from App_ARIA");
        static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
        if (packet.u32SourceSerialId == firstSourceSerialId) {
            String jsonStr = createJsonFrom(packet);
            if (not(jsonStr.length() <= 0)) {
                events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
            }
        }
        events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());
    });

対象の絞り込み

84-85行目では、処理の対象を 最初に受信した子機 に限定しています。

static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
if (packet.u32SourceSerialId == firstSourceSerialId) {

こうしておかないと、複数の子機があった際にグラフの一貫性が失われてしまうからです。

JSON 文字列の作成

86行目では、受信したデータからJSON 文字列を生成しています。

String jsonStr = createJsonFrom(packet);

Web ページへのイベント送信

87-89行目では、生成した JSON 文字列を “ARIA Viewer” ページへ送信しています。

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
}

イベント名は data_app_aria_twelite_aria_mode です。

91行目では、App_Twelite からのパケットを受信したことを “Serial Viewer” ページへ送信しています。

events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());

App_CUE:イベントハンドラの登録

94-104行目では、Twelite.on() <ParsedAppCuePacket> を呼び出し、キューアプリ(TWELITE CUE モード)の子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppCuePacket& packet) {
    Serial.println("Received a packet from App_CUE");
    static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
    if (packet.u32SourceSerialId == firstSourceSerialId) {
        String jsonStr = createJsonFrom(packet);
        if (not(jsonStr.length() <= 0)) {
            events.send(jsonStr.c_str(), "data_app_cue_twelite_cue_mode", millis());
        }
    }
    events.send("parsed_app_cue_twelite_cue_mode", "data_parsing_result", millis());
});

その他:イベントハンドラの登録

106-134行目では、その他のアプリの子機からのパケットを受信した際に行う処理を登録しています。

アリアアプリ等と同様に “Serial Viewer” へイベントを送信しています。

すべて:イベントハンドラの登録

136-142行目では、すべてのアプリの子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const BarePacket& packet) {
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_bare_packet", millis());
    }
    events.send("unparsed_bare_packet", "data_parsing_result", millis());
});

ここでも、“Serial Viewer” に対するパケットデータ文字列の送信を行っています。

OLED ディスプレイの設定

145-150行目では、OLED ディスプレイの設定を行っています。

    Wire.begin();
    SeeedGrayOled.init(SSD1327);
    SeeedGrayOled.setNormalDisplay();
    SeeedGrayOled.setVerticalMode();
    SeeedGrayOled.setGrayLevel(0x0F);
    SeeedGrayOled.clearDisplay();

無線 LAN の設定

154-165行目では、無線 LAN の設定を行っています。

    WiFi.mode(WIFI_AP);
    esp_wifi_set_country(&WIFI_COUNTRY_JP);
    char uidCString[8];
    sprintf(uidCString, " (%02X)", createUidFromMac());
    char ssidCString[20];
    sprintf(ssidCString, "%s%s", WIFI_SSID_BASE, uidCString);
    if (not WiFi.softAP(ssidCString, WIFI_PASSWORD, WIFI_CH, false, 10)) {
      Serial.println("Failed to start AP");
    }
    delay(100);    // IMPORTANT: Waiting for SYSTEM_EVENT_AP_START
    WiFi.softAPConfig(WIFI_IP, WIFI_IP, WIFI_MASK);
    MDNS.begin(HOSTNAME);

ファイルシステムの設定

198行目では、LittleFS ファイルシステムを設定しています。

if (LittleFS.begin()) { Serial.println("Mounted file system."); }

フラッシュ領域内に書き込んだ HTML などのファイルをページとして取得することができるようになります。

Web サーバの設定

201-228行目では、Web サーバの設定を行っています。

GET リクエストのハンドリング

例えば、206-210行目 では /signal-viewer への GET リクエストに対して、LittleFS ファイルシステム上の /signal-viewer.html を返しています。

server.on("/signal-viewer", HTTP_GET,
          [](AsyncWebServerRequest* request) {
              Serial.println("HTTP_GET: signal-viewer.html");
              request->send(LittleFS, "/signal-viewer.html", "text/html");
          });

サーバの初期化

226-228行目では、ファイルシステム上のルートをサーバのルートとして設定したあと、イベントソースを登録してサーバを立ち上げています。

server.serveStatic("/", LittleFS, "/");
server.addHandler(&events);
server.begin();

TWELITE のデータの更新

234行目では、Twelite.update() を呼び出しています。

    Twelite.update();

Twelite.update() は、TWELITE 親機から送信されるパケットデータ(ModBus ASCII 形式)を順次1バイトずつ読み出す関数です。

Web ページ

Web ページに関しては、詳しい解説を行いません。重要なポイントに絞って解説します。

HTML:グリッドシステム

このサンプルの HTML では、Flexbox Grid を使っています(ソースファイルは data/css/flexboxgrid.min.css)。

下記のようにして Bootstrap に似た 12 分割のグリッドシステムを使用しています。

      <div class="col-xs-6 col-sm-6 col-md-5 col-lg-4">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 npr npl">
            <img src="./images/logo-lands.svg" class="logo" />
          </div>
        </div>
      </div>

      <div class="col-xs-6 col-sm-6 col-md-7 col-lg-8">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="medium bold">TWELITE SPOT</span>
          </div>
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="small bold">CUE Viewer</span>
          </div>
        </div>
      </div>

ここでは、ロゴを中心とした要素の幅を 6/12 、文字列を中心とした要素の幅を 6/12 、すなわち両者を等しい幅で一列に配置しています。また、文字列 TWELITE SPOT を中心とした要素と CUE Viewer を中心とした要素の幅はどちらも 12/12 、すなわち1行ずつ2行に分けて配置しています。

HTML:データ表示部

TWELITE 子機から受信したデータを表示する要素には、一意の ID を付与しています。

以下は TWELITE CUE から受信した X 軸加速度を表示する部分の抜粋です。

<div class="col-xs-4 nwp npr npl">
  <code class="medium"
        id="latest-accel-x">±--.--</code>
  <code class="small">G</code>
</div>

ここでは、ID として latest-accel-x を付与しています。この ID を使って、スクリプトから値を書き換えます。

JS:グローバル変数

4-8行目では、最新の加速度値を保存するためのグローバル変数を宣言しています。

let latest_accel = {
    x: 0.0,
    y: 0.0,
    z: 0.0
};

この値はグラフからも利用するため、実装を簡素にするためにグローバル変数を使用しています。

JS:グラフ設定

11-133行目では、グラフ描画ライブラリ Chart.js | Chart.js およびそのプラグイン chartjs-plugin-streaming の設定を行っています。

JS:ページ内容の更新

136-235行目の関数 processDataAppCueTweliteCueMode() は、スケッチから data_app_cue_twelite_cue_mode イベントを受信した際にページ内容を更新する関数です。

例えば、184-208行目では、TWELITE CUE の電源電圧に応じて電圧値と絵文字を更新しています。

if (data.vcc >= 3000) {
    document.getElementById("latest-vcc-icon").innerHTML = "🔋";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.add("green");
} else if (data.vcc >= 2700) {
    document.getElementById("latest-vcc-icon").innerHTML = "🔋";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else if (data.vcc >= 2400) {
    document.getElementById("latest-vcc-icon").innerHTML = "🪫";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.add("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else {
    document.getElementById("latest-vcc-icon").innerHTML = "🪫";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.add("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
}

ここでは、電源電圧が 2700mV 未満に降下した際に絵文字を 🔋 から 🪫 に変えているほか、3000mV → 2700mV → 2400mV と電圧降下に従って電圧値の文字色を適用する CSS クラスを入れ替えています。

イベントリスナーの登録

254-257行目では、スケッチからのイベントを受信した際の処理を登録しています。

source.addEventListener("data_app_cue_twelite_cue_mode", (e) => {
    console.log("data_app_cue_twelite_cue_mode", e.data);
    processDataAppCueTweliteCueMode(JSON.parse(e.data));
}, false);

ここでは、スケッチより 受信したイベントメッセージから JSON 文字列を取り出し、パースしたデータを先ほどの関数 processDataAppCueTweliteCueMode() へ渡しています。

関連情報

Arduino

ESP32

コミュニティ

ライブラリ

プラグイン

Web関連

ECMAScript (JavaScript)

コミュニティ

1.2 - プリインストール済みスケッチ

ESP32 Arduino Core v2.x.x 版
無線 LAN アクセスポイントとして振る舞い、Web ページ上に子機からのデータを表示するサンプルスケッチ spot-server の解説です。

ソースコードの入手

GitHub (monowireless/spot-server) から入手できます。

システムの概要

spot-server は、TWELITE からのデータ受信と転送を行う Arduino スケッチ (.ino) と、スマホに配信する Web ページ (.html / .css / .js) で構成しています。

イメージ図

イメージ図

TWELITE 子機が送信したデータは Arduino スケッチで受信され、Arduino スケッチは公開中の Web ページに対してイベントを発火します。公開された Web ページでは、発火されたイベントに応じて HTML の内容を動的に書き換えています。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino) に libraries フォルダがない場合は、これを作成します。

非同期 TCP 通信ライブラリ

  1. GitHub (me-no-dev/AsyncTCP) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を AsyncTCP-master から AsyncTCP に変更します
  3. libraries フォルダに AsyncTCP フォルダを配置します

非同期 Web サーバライブラリ

  1. GitHub (me-no-dev/ESPAsyncWebServer) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を AsyncWebServer-master から AsyncWebServer に変更します
  3. libraries フォルダに AsyncWebServer フォルダを配置します

OLED ディスプレイライブラリ

  1. GitHub (Seeed-Studio/OLED_Display_96X96) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を OLED_Display_96X96-master から OLED_Display_96X96 に変更します
  3. libraries フォルダに OLED_Display_96X96 フォルダを配置します

JSON ライブラリ

ライブラリマネージャを開き、Arduino_JSON をインストールします。

プラグインの導入

ファイルシステム書き込みプラグイン

HTML などのファイルを ESP32 のフラッシュ領域に書き込むには、Arduino プラグインが必要です。

ここでは、lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system を利用します。

インストール方法は TWELITE SPOT マニュアル ESP32 へのファイルの書き込み方法 をご覧ください。

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-server) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-server-main から spot-server に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-server フォルダを配置します

プロジェクトファイルの書き込み方法

スケッチ

ESP32 へのスケッチの書き込み方法 をご覧ください。

Web ページ

ESP32 へのファイルの書き込み方法 をご覧ください。

スケッチ

Arduino スケッチ spot-server.ino の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-9行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <Arduino_JSON.h>
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <WiFi.h>
#include <Wire.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
Arduino_JSON.hJSON 文字列を扱うArduinoJsonとは異なる
ESPmDNS.hmDNS を使うホスト名を使うために必要
LittleFS.hLittleFS ファイルシステムを扱うページ公開に必要
WiFi.hESP32 の WiFi を使う
Wire.hI2C を使うOLED ディスプレイ用

サードパーティのライブラリ

12-14行目では、サードパーティのライブラリをインクルードしています。

#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SeeedGrayOLED.h>
ヘッダファイル内容備考
AsyncTCP.h非同期 TCP 通信を行う
ESPAsyncWebServer.h非同期 Web サーバを立てるAsyncTCP に依存
SeeedGrayOLED.hOLED ディスプレイを使う

MWings ライブラリ

17行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ピン番号の定義

20-24行目では、ピン番号を定義しています。

const uint8_t TWE_RST = 5;
const uint8_t TWE_PRG = 4;
const uint8_t LED = 18;
const uint8_t ESP_RXD1 = 16;
const uint8_t ESP_TXD1 = 17;
名称内容
TWE_RSTTWELITE の RST ピンが接続されているピンの番号
TWE_PRGTWELITE の PRG ピンが接続されているピンの番号
LED基板上の ESP32 用 LED が接続されているピンの番号
ESP_RXD1TWELITE の TX ピンが接続されているピンの番号
ESP_TXD1TWELITE の RX ピンが接続されているピンの番号

TWELITE 設定の定義

27-30行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
const uint8_t TWE_RETRY = 2;
const uint8_t TWE_POWER = 3;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション ID
TWE_RETRYTWELITE の 再送回数(送信時)
TWE_POWERTWELITE の 送信出力

無線 LAN 設定の定義

33-38行目では、TWELITE SPOT に搭載された ESP32 に適用する無線 LAN 設定を定義しています。

const char* WIFI_SSID_BASE = "TWELITE SPOT";
const char* WIFI_PASSWORD = "twelitespot";
const uint8_t WIFI_CH = 13;
const IPAddress WIFI_IP = IPAddress(192, 168, 1, 1);
const IPAddress WIFI_MASK = IPAddress(255, 255, 255, 0);
const char* HOSTNAME = "spot";    // spot.local
名称内容
WIFI_SSID_BASESSID の共通部分の文字列
WIFI_PASSWORDパスワード
WIFI_CHESP32 の周波数チャネル
WIFI_IPIP アドレス
WIFI_MASKサブネットマスク
HOSTNAMEホスト名

グローバルオブジェクトの宣言

41-42行目では、グローバルオブジェクトを宣言しています。

AsyncWebServer server(80);
AsyncEventSource events("/events");
名称内容
server80番ポートで開く非同期 Web サーバのインタフェース
events/eventsで開くサーバー送信イベント ?のインタフェース

関数プロトタイプの宣言

45-49行目では、関数プロトタイプを宣言しています。

uint16_t createUidFromMac();
String createJsonFrom(const ParsedAppTwelitePacket& packet);
String createJsonFrom(const ParsedAppAriaPacket& packet);
String createJsonFrom(const ParsedAppCuePacket& packet);
String createJsonFrom(const BarePacket& packet);
名称内容
createUidFromMac()MAC アドレスから SSID に使う識別子を作ります
createJsonFrom()<ParsedAppTwelitePacket&>App_Twelite のパケットデータから JSON 文字列を作ります
createJsonFrom()<ParsedAppAriaPacket&>App_ARIA のパケットデータから JSON 文字列を作ります
createJsonFrom()<ParsedAppCuePacket&>App_CUE のパケットデータから JSON 文字列を作ります
createJsonFrom()<BarePacket&>すべてのパケットデータから JSON 文字列を作ります

TWELITE の設定

58-63行目では、Twelite.begin() を呼び出し、TWELITE SPOT に搭載された TWELITE 親機の設定と起動を行っています。

Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);
    if (Twelite.begin(Serial2,
                      LED, TWE_RST, TWE_PRG,
                      TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
        Serial.println("Started TWELITE.");
    }
引数内容
Serial2HardwareSerial&TWELITE との通信に使うシリアルポート
LEDintステータス LED を接続したピンの番号
TWE_RSTintTWELITE の RST ピンを接続したピンの番号
TWE_PRGintTWELITE の PRG ピンを接続したピンの番号
TWE_CHANNELuint8_tTWELITE の 周波数チャネル
TWE_APP_IDuint32_tTWELITE の アプリケーション ID
TWE_RETRYuint8_tTWELITE の 再送回数(送信時)
TWE_POWERuint8_tTWELITE の 送信出力

App_Twelite:イベントハンドラの登録

65-72行目では、Twelite.on() <ParsedAppTwelitePacket> を呼び出し、超簡単!標準アプリの子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppTwelitePacket& packet) {
    Serial.println("Received a packet from App_Twelite");
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_app_twelite", millis());
    }
    events.send("parsed_app_twelite", "data_parsing_result", millis());
});

JSON 文字列の作成

67行目では、受信したデータからJSON 文字列を生成しています。

String jsonStr = createJsonFrom(packet);

受信したデータをWeb ページに表示するにはクライアント側の JavaScript にデータを送る必要がありますが、このとき文字列データのほうが扱いやすいため、JSON 文字列としています。

Web ページへのイベント送信

68-70行目では、生成した JSON 文字列を “Signal Viewer” ページへ送信しています。

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_twelite", millis());
}

イベント名は data_app_twelite です。

71行目では、App_Twelite からのパケットを受信したことを “Serial Viewer” ページへ送信しています。

events.send("parsed_app_twelite", "data_parsing_result", millis());

App_ARIA:イベントハンドラの登録

74-84行目では、Twelite.on() <ParsedAppAriaPacket> を呼び出し、アリアアプリ(TWELITE ARIA モード)の子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppAriaPacket& packet) {
        Serial.println("Received a packet from App_ARIA");
        static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
        if (packet.u32SourceSerialId == firstSourceSerialId) {
            String jsonStr = createJsonFrom(packet);
            if (not(jsonStr.length() <= 0)) {
                events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
            }
        }
        events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());
    });

対象の絞り込み

76-77行目では、処理の対象を 最初に受信した子機 に限定しています。

static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
if (packet.u32SourceSerialId == firstSourceSerialId) {

こうしておかないと、複数の子機があった際にグラフの一貫性が失われてしまうからです。

JSON 文字列の作成

78行目では、受信したデータからJSON 文字列を生成しています。

String jsonStr = createJsonFrom(packet);

Web ページへのイベント送信

79-81行目では、生成した JSON 文字列を “ARIA Viewer” ページへ送信しています。

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
}

イベント名は data_app_aria_twelite_aria_mode です。

83行目では、App_Twelite からのパケットを受信したことを “Serial Viewer” ページへ送信しています。

events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());

App_CUE:イベントハンドラの登録

86-96行目では、Twelite.on() <ParsedAppCuePacket> を呼び出し、キューアプリ(TWELITE CUE モード)の子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppCuePacket& packet) {
    Serial.println("Received a packet from App_CUE");
    static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
    if (packet.u32SourceSerialId == firstSourceSerialId) {
        String jsonStr = createJsonFrom(packet);
        if (not(jsonStr.length() <= 0)) {
            events.send(jsonStr.c_str(), "data_app_cue_twelite_cue_mode", millis());
        }
    }
    events.send("parsed_app_cue_twelite_cue_mode", "data_parsing_result", millis());
});

その他:イベントハンドラの登録

98-126行目では、その他のアプリの子機からのパケットを受信した際に行う処理を登録しています。

アリアアプリ等と同様に “Serial Viewer” へイベントを送信しています。

すべて:イベントハンドラの登録

128-134行目では、すべてのアプリの子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const BarePacket& packet) {
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_bare_packet", millis());
    }
    events.send("unparsed_bare_packet", "data_parsing_result", millis());
});

ここでも、“Serial Viewer” に対するパケットデータ文字列の送信を行っています。

OLED ディスプレイの設定

137-142行目では、OLED ディスプレイの設定を行っています。

    Wire.begin();
    SeeedGrayOled.init(SSD1327);
    SeeedGrayOled.setNormalDisplay();
    SeeedGrayOled.setVerticalMode();
    SeeedGrayOled.setGrayLevel(0x0F);
    SeeedGrayOled.clearDisplay();

無線 LAN の設定

146-154行目では、無線 LAN の設定を行っています。

WiFi.mode(WIFI_AP);
char uidCString[8];
sprintf(uidCString, " (%02X)", createUidFromMac());
char ssidCString[20];
sprintf(ssidCString, "%s%s", WIFI_SSID_BASE, uidCString);
WiFi.softAP(ssidCString, WIFI_PASSWORD, WIFI_CH, false, 8);
delay(100);    // IMPORTANT: Waiting for SYSTEM_EVENT_AP_START
WiFi.softAPConfig(WIFI_IP, WIFI_IP, WIFI_MASK);
MDNS.begin(HOSTNAME);

ファイルシステムの設定

187行目では、LittleFS ファイルシステムを設定しています。

if (LittleFS.begin()) { Serial.println("Mounted file system."); }

フラッシュ領域内に書き込んだ HTML などのファイルをページとして取得することができるようになります。

Web サーバの設定

190-217行目では、Web サーバの設定を行っています。

GET リクエストのハンドリング

例えば、195-199行目 では /signal-viewer への GET リクエストに対して、LittleFS ファイルシステム上の /signal-viewer.html を返しています。

server.on("/signal-viewer", HTTP_GET,
          [](AsyncWebServerRequest* request) {
              Serial.println("HTTP_GET: signal-viewer.html");
              request->send(LittleFS, "/signal-viewer.html", "text/html");
          });

サーバの初期化

215-217行目では、ファイルシステム上のルートをサーバのルートとして設定したあと、イベントソースを登録してサーバを立ち上げています。

server.serveStatic("/", LittleFS, "/");
server.addHandler(&events);
server.begin();

TWELITE のデータの更新

223行目では、Twelite.update() を呼び出しています。

    Twelite.update();

Twelite.update() は、TWELITE 親機から送信されるパケットデータ(ModBus ASCII 形式)を順次1バイトずつ読み出す関数です。

Web ページ

Web ページに関しては、詳しい解説を行いません。重要なポイントに絞って解説します。

HTML:グリッドシステム

このサンプルの HTML では、Flexbox Grid を使っています(ソースファイルは data/css/flexboxgrid.min.css)。

下記のようにして Bootstrap に似た 12 分割のグリッドシステムを使用しています。

      <div class="col-xs-6 col-sm-6 col-md-5 col-lg-4">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 npr npl">
            <img src="./images/logo-lands.svg" class="logo" />
          </div>
        </div>
      </div>

      <div class="col-xs-6 col-sm-6 col-md-7 col-lg-8">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="medium bold">TWELITE SPOT</span>
          </div>
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="small bold">CUE Viewer</span>
          </div>
        </div>
      </div>

ここでは、ロゴを中心とした要素の幅を 6/12 、文字列を中心とした要素の幅を 6/12 、すなわち両者を等しい幅で一列に配置しています。また、文字列 TWELITE SPOT を中心とした要素と CUE Viewer を中心とした要素の幅はどちらも 12/12 、すなわち1行ずつ2行に分けて配置しています。

HTML:データ表示部

TWELITE 子機から受信したデータを表示する要素には、一意の ID を付与しています。

以下は TWELITE CUE から受信した X 軸加速度を表示する部分の抜粋です。

<div class="col-xs-4 nwp npr npl">
  <code class="medium"
        id="latest-accel-x">±--.--</code>
  <code class="small">G</code>
</div>

ここでは、ID として latest-accel-x を付与しています。この ID を使って、スクリプトから値を書き換えます。

JS:グローバル変数

4-8行目では、最新の加速度値を保存するためのグローバル変数を宣言しています。

let latest_accel = {
    x: 0.0,
    y: 0.0,
    z: 0.0
};

この値はグラフからも利用するため、実装を簡素にするためにグローバル変数を使用しています。

JS:グラフ設定

11-133行目では、グラフ描画ライブラリ Chart.js | Chart.js およびそのプラグイン chartjs-plugin-streaming の設定を行っています。

JS:ページ内容の更新

136-235行目の関数 processDataAppCueTweliteCueMode() は、スケッチから data_app_cue_twelite_cue_mode イベントを受信した際にページ内容を更新する関数です。

例えば、184-208行目では、TWELITE CUE の電源電圧に応じて電圧値と絵文字を更新しています。

if (data.vcc >= 3000) {
    document.getElementById("latest-vcc-icon").innerHTML = "🔋";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.add("green");
} else if (data.vcc >= 2700) {
    document.getElementById("latest-vcc-icon").innerHTML = "🔋";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else if (data.vcc >= 2400) {
    document.getElementById("latest-vcc-icon").innerHTML = "🪫";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.add("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else {
    document.getElementById("latest-vcc-icon").innerHTML = "🪫";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.add("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
}

ここでは、電源電圧が 2700mV 未満に降下した際に絵文字を 🔋 から 🪫 に変えているほか、3000mV → 2700mV → 2400mV と電圧降下に従って電圧値の文字色を適用する CSS クラスを入れ替えています。

イベントリスナーの登録

254-257行目では、スケッチからのイベントを受信した際の処理を登録しています。

source.addEventListener("data_app_cue_twelite_cue_mode", (e) => {
    console.log("data_app_cue_twelite_cue_mode", e.data);
    processDataAppCueTweliteCueMode(JSON.parse(e.data));
}, false);

ここでは、スケッチより 受信したイベントメッセージから JSON 文字列を取り出し、パースしたデータを先ほどの関数 processDataAppCueTweliteCueMode() へ渡しています。

関連情報

Arduino

ESP32

コミュニティ

ライブラリ

プラグイン

Web関連

ECMAScript (JavaScript)

コミュニティ

2 - WebSocketによる中継

子機からのデータを WebSocket サーバに中継するサンプルスケッチ spot-router の解説
無線 LAN 子機として振る舞い、LAN 上の WebSocket サーバに受信したパケットデータ文字列を中継するサンプルスケッチ spot-router の解説です。

2.1 - WebSocketによる中継

最新版
無線 LAN 子機として振る舞い、LAN 上の WebSocket サーバに受信したパケットデータ文字列を中継するサンプルスケッチ spot-router の解説です。

ソースコードの入手

GitHub (monowireless/spot-router) から入手できます。

システムの概要

spot-router は、TWELITE 親機が受信したデータに基づき出力した文字列(App_Wings の ModBus ASCII 形式)を WebSocket サーバへ転送します。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino) に libraries フォルダがない場合は、これを作成します。

WebSocket ライブラリ

  1. GitHub (Links2004/arduinoWebSockets) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、libraries フォルダに arduinoWebSockets-<バージョン> フォルダを配置します

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-router) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-router-main から spot-router に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-router フォルダを配置します

ユーザ設定の変更

Arduino IDE 上部のタブから config.h を開き、無線 LAN や WebSocket サーバに関する設定( 詳細 )を変更してください。

プロジェクトファイルの書き込み方法

ESP32 へのスケッチの書き込み方法 をご覧ください。

スケッチ

Arduino スケッチ spot-router.ino の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-5行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <WiFi.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
WiFi.hESP32 の WiFi を使う

サードパーティのライブラリ

8行目では、サードパーティのライブラリをインクルードしています。

#include <WebSocketsClient.h>
ヘッダファイル内容備考
WebSocketsClient.hWebSocket クライアントになる

MWings ライブラリ

11行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ユーザ設定の定義

14行目では、config.h をインクルードしています。

#include "config.h"

無線 LAN 設定の定義

config.h の4-5行目では、TWELITE SPOT に搭載された ESP32 に適用する無線 LAN 設定を定義しています。

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it
名称内容
WIFI_SSID接続するネットワークの SSID
WIFI_PASSWORD接続するネットワークの パスワード

WebSocket 設定の定義

config.h の8-10行目では、WebSocket クライアントの設定を定義しています。

const char* WS_SERVER_IP = "YOUR ADDRESS";    // Modify it
const int WS_SERVER_PORT = 8080;
const char* WS_SERVER_PATH = "/";
名称内容
WS_SERVER_IP送信するサーバの IP アドレス
WS_SERVER_PORT送信するサーバのポート番号
WS_SERVER_PATH送信するサーバの WebSocket サーバのパス

ピン番号の定義

17-21行目では、ピン番号を定義しています。

const uint8_t TWE_RST = 5;
const uint8_t TWE_PRG = 4;
const uint8_t LED = 18;
const uint8_t ESP_RXD1 = 16;
const uint8_t ESP_TXD1 = 17;
名称内容
TWE_RSTTWELITE の RST ピンが接続されているピンの番号
TWE_PRGTWELITE の PRG ピンが接続されているピンの番号
LED基板上の ESP32 用 LED が接続されているピンの番号
ESP_RXD1TWELITE の TX ピンが接続されているピンの番号
ESP_TXD1TWELITE の RX ピンが接続されているピンの番号

TWELITE 設定の定義

24-27行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
const uint8_t TWE_RETRY = 2;
const uint8_t TWE_POWER = 3;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション ID
TWE_RETRYTWELITE の 再送回数(送信時)
TWE_POWERTWELITE の 送信出力

グローバルオブジェクトの宣言

30行目では、グローバルオブジェクトを宣言しています。

WebSocketsClient webSocket;
名称内容
webSocketWebSocket クライアントのインタフェース

関数プロトタイプの宣言

33行目では、関数プロトタイプを宣言しています。

String createPacketStringFrom(const BarePacket& packet);
名称内容
createPacketStringFrom()受信したパケットデータから書式文字列を再構築します

TWELITE の設定

42-47行目では、Twelite.begin() を呼び出し、TWELITE SPOT に搭載された TWELITE 親機の設定と起動を行っています。

Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);
    if (Twelite.begin(Serial2,
                      LED, TWE_RST, TWE_PRG,
                      TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
        Serial.println("Started TWELITE.");
    }
引数内容
Serial2HardwareSerial&TWELITE との通信に使うシリアルポート
LEDintステータス LED を接続したピンの番号
TWE_RSTintTWELITE の RST ピンを接続したピンの番号
TWE_PRGintTWELITE の PRG ピンを接続したピンの番号
TWE_CHANNELuint8_tTWELITE の 周波数チャネル
TWE_APP_IDuint32_tTWELITE の アプリケーション ID
TWE_RETRYuint8_tTWELITE の 再送回数(送信時)
TWE_POWERuint8_tTWELITE の 送信出力

イベントハンドラの登録

49-54行目では、すべてのアプリの子機からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const BarePacket& packet) {
    String packetStr = createPacketStringFrom(packet);
    if (not(packetStr.length() <= 0)) {
        webSocket.sendTXT(packetStr.c_str());
    }
});

ここでは、パケットデータから書式文字列(ModBus ASCII 形式)を再構成し、WebSocket サーバへ送信しています。

無線 LAN の設定

57-71行目では、無線 LAN の設定を行っています。

WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
    static int count = 0;
    Serial.print('.');
    delay(500);
    // Retry every 5 seconds
    if (count++ % 10 == 0) {
        WiFi.disconnect();
        WiFi.reconnect();
        Serial.print('!');
    }
}

ここでは、無線 LAN 子機として設定したうえで、指定のネットワークへ接続しています。

WebSocket の設定

76-77行目では、WebSocket を設定しています。

webSocket.begin(WS_SERVER_IP, WS_SERVER_PORT, WS_SERVER_PATH);
webSocket.setReconnectInterval(5000);

ここでは、WebSocket サーバと再接続間隔を指定しています。

また、78-97行目では、サーバとの接続が切断されたとき、サーバと接続したとき、そしてメッセージを受信したときのイベントを登録しています。

webSocket.onEvent([](WStype_t type, uint8_t* payload, size_t length) {
    switch (type) {
    case WStype_DISCONNECTED: {
        Serial.println("Disconnected!");
        break;
    }
    case WStype_CONNECTED: {
        Serial.print("Connected to url: ");
        Serial.println(reinterpret_cast<char*>(payload));
        webSocket.sendTXT("This is TWELITE SPOT to ground control");
        break;
    }
    case WStype_TEXT: {
        Serial.print("Got text: ");
        Serial.println(reinterpret_cast<char*>(payload));
        break;
    }
    default: break;
    }
});

なかでも、サーバと接続したときには、サーバへメッセージを送るようにしています。

webSocket.sendTXT("This is TWELITE SPOT to ground control");

TWELITE のデータの更新

102行目では、Twelite.update() を呼び出しています。

Twelite.update();

Twelite.update() は、TWELITE 親機から送信されるパケットデータ(ModBus ASCII 形式)を順次1バイトずつ読み出す関数です。

WebSocket のデータの更新

103行目では、WebSocket のデータを更新する処理を呼び出しています。

webSocket.loop();

<付録> WebSocket サーバによる動作確認

extra/python-websocket-server/server.py は、Python スクリプトによって WebSocket サーバを立て、ESP32 からのパケットデータ文字列を表示するサンプルスクリプトです。このスクリプトを使うことで、スケッチの動作を確認できます。

# -*- coding: utf-8-unix -*-
# Python 3.11

import logging
from websocket_server import WebsocketServer

def new_client(client, server):
    server.send_message_to_all("This is ground control to TWELITE SPOT")

def new_message(client, server, message):
    print("Received an message:")
    print(message)

server = WebsocketServer(host="YOUR IP ADDRESS", port=8080, loglevel=logging.INFO)
server.set_fn_new_client(new_client)
server.set_fn_message_received(new_message)
server.run_forever()

coding 変数を指定しているのは、筆者の環境が Emacs だからです。おまじないではありません。

動作確認の手順

スクリプトの実行

依存モジュール をインストールしてから実行します。


pip3 install websocket-server
python3 server.py

実行すると、下記のようなメッセージが表示されます。

INFO:websocket_server.websocket_server:Listening on port 8080 for clients..
INFO:websocket_server.websocket_server:Starting WebsocketServer on main thread.

クライアントの接続を確認

ESP32 が 無線 LAN への接続に成功すると、WebSocket サーバへの接続を試みます。

接続に成功すると、クライアント側のシリアルコンソールには下記のように出力されます。

Started TWELITE.
Connecting to WiFi .....
Connected. IP: xxx.xxx.xxx.xxx
Connected to url: /
Got text: This is ground control to TWELITE SPOT

一方で、サーバ側のターミナルには下記のように出力されます。

Received an message:
This is TWELITE SPOT to ground control

以降、TWELITE SPOT が子機からのパケットを受信すると、下記のようにサーバ側のターミナルへパケットデータ文字列が出力されます。

Received an message:
:80000000DE10098201BC8201800607003400038135001205350401000000113008020A8C1130010203AF0000000180050100020AC60102000211D7AF30

Received an message:
:80000000E4100A8201BC8201800607003400038135001205350401000000113008020A8C1130010203AC0000000180050100020AC40102000211DB0DCC

関連情報

TWELITE

Arduino

ESP32

コミュニティ

ライブラリ

プラグイン

ネットワーク関連

WebSocket

コミュニティ

3 - REST API の使用

子機からのデータを HTTP GET リクエストに利用するサンプルスケッチ spot-httpbin の解説
無線 LAN 子機として振る舞い、Web 上のモックサーバ httpbin.org へ受信したパケットのデータを送信するサンプルスケッチ spot-httpbin の解説です。

3.1 - REST API の使用

最新版
無線 LAN 子機として振る舞い、Web 上のモックサーバ httpbin.org へ受信したパケットのデータを送信するサンプルスケッチ spot-httpbin の解説です。

ソースコードの入手

GitHub リポジトリ monowireless/spot-httpbin から入手できます。

システムの概要

spot-httpbin は、TWELITE 親機が受信したデータの一部と NTP による現在時刻を HTTP GET リクエストとしてモックサーバへ送信し、そのレスポンスをシリアルモニタへ表示します。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

このサンプルでは、依存するライブラリをはじめから同梱しています。

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-httpbin) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-httpbin-main から spot-httpbin に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-httpbin フォルダを配置します

ユーザ設定の変更

Arduino IDE 上部のタブから config.h を開き、Wi-Fi の SSID と パスワードを設定してください。WPA2-PSK ネットワークを想定しています。また、ルート証明書も登録してください。ルート証明書は、Chrome などのウェブブラウザの各ページに対するセキュリティ画面から入手できます。

プロジェクトファイルの書き込み方法

ESP32 へのスケッチの書き込み方法 をご覧ください。

スケッチ

Arduino スケッチ spot-httpbin.ino および config.h の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-6行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
WiFiClientSecure.hESP32 で SSL通信を行う
WiFiUdp.hUDP 通信を行うNTP に必要

サードパーティのライブラリ

9-10行目では、同梱されたサードパーティのライブラリをインクルードしています。

#include "src/NTPClient/NTPClient.h"
#include "src/Time/TimeLib.h"
ヘッダファイル内容備考
NTPClient.hNTP サーバへアクセスする
TimeLib.hエポック時間を変換する

MWings ライブラリ

13行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ユーザ設定の定義

16行目では、config.h をインクルードしています。

#include "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電源電圧
linkQualityLQI
temp100x100倍された温度
humid100x100倍された湿度

ここでは、TWELITE ARIA を使用します。

config.h

再起動間隔の定義

config.h の4行目では、ESP32 の再起動間隔を指定しています。

const uint32_t REBOOT_INTERVAL = 21600; // seconds

ここでは、21600秒=6時間としています。

TWELITE 設定の定義

config.h の7-8行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション 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_INTERVALNTP時刻の取得間隔
QUERIES_MAX_LENGTHクエリ文字列の最大長(ヌル文字含まず)
CONNECT_TIMEOUTサーバへの接続時のタイムアウト
RECONNECT_MIN_INTERVALWi-Fiアクセスポイントへ再接続する際の最短間隔
SEND_MIN_INTERVALリクエスト間隔の最短間隔
REQUEST_TIMEOUTリクエストからレスポンスまでのタイムアウト

ピン番号の定義

29-34行目では、ピン番号を定義しています。

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;

static const int8_t RX1_PIN = 16;
static const int8_t TX1_PIN = 17;
名称内容
RST_PINTWELITE の RST ピンが接続されているピンの番号
PRG_PINTWELITE の PRG ピンが接続されているピンの番号
LED_PIN基板上の ESP32 用 LED が接続されているピンの番号
RX1_PINTWELITE の RX1 ピンが接続されているピンの番号
TX1_PINTWELITE の TX1 ピンが接続されているピンの番号

グローバルオブジェクトの宣言

37-40行目では、グローバルオブジェクトを宣言しています。

static WiFiClientSecure client;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "ntp.nict.jp",
                            32400, NTP_UPDATE_INTERVAL); // JST(UTC+9)
名称内容
clientHTTPS通信のインタフェース
ntpUDPNTP用のUDP通信のインタフェース
timeClientNTPのインタフェース

グローバル変数の宣言

43-44行目では、グローバル変数を宣言しています。

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
名称内容
LatestDataFromAriaTWELITE ARIA から受信した最新のデータ
IsThereNewDataFromAriaTWELITE ARIA から新たなデータを受信したことを示すフラグ

関数プロトタイプの宣言

47-59行目では、関数プロトタイプを宣言しています。

void anotherLoopForTWELITE();
void anotherLoopForNTP();
名称内容
anotherLoopForTWELITETWELITEのデータを処理するためのループ関数
anotherLoopForNTPNTPで時刻を取得するためのループ関数
void initTWELITE();
void initWiFi();
void initNTP();
名称内容
initTWELITETWELITEの初期化関数
initWiFiWi-Fiの初期化関数
initNTPNTPの初期化関数
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
名称内容
onAppAriaPacketTWELITE ARIA からデータを受信した際のコールバック関数
void sendAriaData(const DataFromAria& data)
名称内容
sendAriaDataTWELITE ARIAのデータを HTTP GET リクエストにのせて送る関数

setup()

62-90行目では、全体の初期化を行います。

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
            }
        },

loop()

93-114行目は、主となるループ処理です。

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()

117-119行目は、TWELITE のためのループ処理です。

データの受信と解釈を逐次行うため、ブロッキング処理を含む loop() とは別のタスクとしています。

void anotherLoopForTWELITE() {
    Twelite.update();
}

anotherLoopForNTP()

120-123行目は、NTP のためのループ処理です。

こちらについても UDP の通信を行うため、ブロッキング処理を含む loop() とは別のタスクとしています。

void anotherLoopForNTP() {
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

initTWELITE()

126-133行目は、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);
}

initWiFi()

136-160行目は、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()

163-167行目は、NTP の初期化処理です。

void initNTP() {
    timeClient.begin();
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

onAppAriaPacket()

170-180行目には、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()

183-240行目は、TWELITE ARIA のデータを HTTP GET リクエストのクエリ文字列にセットして送信する関数です。

サーバへの過度な負荷を防ぐため、高頻度でパケットが到着した際には送信をスキップしています。

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.");
    }
}

3.2 - REST API の使用

mwings-v1.1.3
無線 LAN 子機として振る舞い、Web 上のモックサーバ httpbin.org へ受信したパケットのデータを送信するサンプルスケッチ spot-httpbin の解説です。

ソースコードの入手

GitHub リポジトリ monowireless/spot-httpbin から入手できます。

システムの概要

spot-httpbin は、TWELITE 親機が受信したデータの一部と NTP による現在時刻を HTTP GET リクエストとしてモックサーバへ送信し、そのレスポンスをシリアルモニタへ表示します。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

このサンプルでは、依存するライブラリをはじめから同梱しています。

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-httpbin) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-httpbin-main から spot-httpbin に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-httpbin フォルダを配置します

ユーザ設定の変更

Arduino IDE 上部のタブから config.h を開き、Wi-Fi の SSID と パスワードを設定してください。WPA2-PSK ネットワークを想定しています。また、ルート証明書も登録してください。ルート証明書は、Chrome などのウェブブラウザの各ページに対するセキュリティ画面から入手できます。

プロジェクトファイルの書き込み方法

ESP32 へのスケッチの書き込み方法 をご覧ください。

スケッチ

Arduino スケッチ spot-httpbin.ino および config.h の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-6行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
WiFiClientSecure.hESP32 で SSL通信を行う
WiFiUdp.hUDP 通信を行うNTP に必要

サードパーティのライブラリ

9-10行目では、同梱されたサードパーティのライブラリをインクルードしています。

#include "src/NTPClient/NTPClient.h"
#include "src/Time/TimeLib.h"
ヘッダファイル内容備考
NTPClient.hNTP サーバへアクセスする
TimeLib.hエポック時間を変換する

MWings ライブラリ

13行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ユーザ設定の定義

16行目では、config.h をインクルードしています。

#include "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電源電圧
linkQualityLQI
temp100x100倍された温度
humid100x100倍された湿度

ここでは、TWELITE ARIA を使用します。

config.h

再起動間隔の定義

config.h の4行目では、ESP32 の再起動間隔を指定しています。

const uint32_t REBOOT_INTERVAL = 21600; // seconds

ここでは、21600秒=6時間としています。

TWELITE 設定の定義

config.h の7-8行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション 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_INTERVALNTP時刻の取得間隔
QUERIES_MAX_LENGTHクエリ文字列の最大長(ヌル文字含まず)
CONNECT_TIMEOUTサーバへの接続時のタイムアウト
RECONNECT_MIN_INTERVALWi-Fiアクセスポイントへ再接続する際の最短間隔
SEND_MIN_INTERVALリクエスト間隔の最短間隔
REQUEST_TIMEOUTリクエストからレスポンスまでのタイムアウト

ピン番号の定義

29-31行目では、ピン番号を定義しています。

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;
名称内容
RST_PINTWELITE の RST ピンが接続されているピンの番号
PRG_PINTWELITE の 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)
名称内容
clientHTTPS通信のインタフェース
ntpUDPNTP用のUDP通信のインタフェース
timeClientNTPのインタフェース

グローバル変数の宣言

40-41行目では、グローバル変数を宣言しています。

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
名称内容
LatestDataFromAriaTWELITE ARIA から受信した最新のデータ
IsThereNewDataFromAriaTWELITE ARIA から新たなデータを受信したことを示すフラグ

関数プロトタイプの宣言

44-56行目では、関数プロトタイプを宣言しています。

void anotherLoopForTWELITE();
void anotherLoopForNTP();
名称内容
anotherLoopForTWELITETWELITEのデータを処理するためのループ関数
anotherLoopForNTPNTPで時刻を取得するためのループ関数
void initTWELITE();
void initWiFi();
void initNTP();
名称内容
initTWELITETWELITEの初期化関数
initWiFiWi-Fiの初期化関数
initNTPNTPの初期化関数
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
名称内容
onAppAriaPacketTWELITE ARIA からデータを受信した際のコールバック関数
void sendAriaData(const DataFromAria& data)
名称内容
sendAriaDataTWELITE 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
            }
        },

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);
}

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 リクエストのクエリ文字列にセットして送信する関数です。

サーバへの過度な負荷を防ぐため、高頻度でパケットが到着した際には送信をスキップしています。

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.");
    }
}

4 - Google スプレッドシートの利用

TWELITE ARIA からのデータを Google スプレッドシートへアップロードするサンプルスケッチ spot-google-sheets の解説
無線 LAN 子機として振る舞い、クラウド上の Google スプレッドシートに TWELITE ARIA から受信したデータをアップロードするサンプルスケッチ spot-google-sheets の解説です。

4.1 - Google スプレッドシートの利用

TWELITE ARIA からのデータを Google スプレッドシートへアップロードするサンプルスケッチ spot-google-sheets の解説
無線 LAN 子機として振る舞い、クラウド上の Google スプレッドシートに TWELITE ARIA から受信したデータをアップロードするサンプルスケッチ spot-google-sheets の解説です。なお、このスケッチでは ESP32 の Arduino 環境から FreeRTOS の機能を利用しています。

ソースコードの入手

GitHub (monowireless/spot-google-sheets) から入手できます。

システムの概要

TWELITE SPOT は、事前に作成したサービスアカウントを使って自動的にスプレッドシートを作成し、指定したユーザアカウントへ、そのファイルを共有します。

ユーザアカウントへログインすると、Google ドライブの「共有アイテム」ページから、TWELITE SPOT によって作成されたスプレッドシートを閲覧・編集できます。

作成されるスプレッドシートのイメージ

作成されるスプレッドシートのイメージ

TWELITE SPOT は、作成したスプレッドシートへデータ行を次々と追加していきます。

開発に必要なもの

環境整備

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

ライブラリの導入

ESP-Google-Sheet-Client ライブラリ

ライブラリマネージャを開き、検索ボックスに esp-google-sheet と入力してインストールします。

なお、GitHub (mobizt/ESP-Google-Sheet-Client) からも入手できます。

公式 NTP ライブラリ

ライブラリマネージャを開き、検索ボックスに ntpclient と入力してインストールします。

TimeLib ライブラリ

ライブラリマネージャを開き、検索ボックスに timelib と入力してインストールします。

事前準備:API のセットアップ

事前に、API を使用できるように準備する必要があります。Google アカウントを使います。

ここでは、下記の作業を行います。

  • Google Cloud プロジェクトの作成
  • Google Sheets API の有効化
  • Google Drive API の有効化
  • サービスアカウントの作成と設定
  • サービスアカウントの認証情報の取得

プロジェクトの作成

API を使用するにあたって、まずは Google Cloud プロジェクトを作成します。

Google Cloud プロジェクトは、システム全体を束ねるような存在です。構築するシステムの名称をプロジェクト名にするとよいでしょう。ここでは、仮に SPOT-DEV とします。

下記のリンクにアクセスし、プロジェクトを作成してください。

https://console.cloud.google.com/projectcreate

プロジェクト作成画面の例(個人)

プロジェクト作成画面の例(個人)

Sheets API の有効化

TWELITE SPOT からスプレッドシートを操作するために、Sheets API を有効化します。

下記のリンクにアクセスし、API を有効化してください。

https://console.cloud.google.com/apis/library/sheets.googleapis.com

Sheets APIの有効化を行う画面の例

Sheets APIの有効化を行う画面の例

Drive API の有効化

TWELITE SPOT からスプレッドシートを共有するために、Drive API を有効化します。

下記のリンクにアクセスし、API を有効化してください。

https://console.cloud.google.com/apis/library/drive.googleapis.com

Drive APIの有効化を行う画面の例

Drive APIの有効化を行う画面の例

サービスアカウントの作成と設定

TWELITE SPOT からスプレッドシートを作成するために、サービスアカウントを作成します。

下記のリンクにアクセスし、プロジェクト名(ここでは SPOT-DEV )を選択してサービスアカウント一覧画面を表示したのち、ページ上部のボタンからサービスアカウントの作成を開始します。

https://console.cloud.google.com/iam-admin/serviceaccounts

サービスアカウント一覧の表示画面の例

サービスアカウント一覧の表示画面の例

「① サービスアカウントの詳細」では、サービスアカウントの名称を入力します。

下記の例では、spot-dev-sa としています。

サービスアカウント名の入力画面の例

サービスアカウント名の入力画面の例

入力したら、「作成して続行」ボタンを押して次へ進みます。

「② このサービスアカウントにプロジェクトへのアクセスを許可する(省略可)」では、サービスアカウントの権限を設定します。

ここでは、下記の例のようにして「オーナー」を選択してください。

サービスアカウント権限の入力画面の例

サービスアカウント権限の入力画面の例

選択したら、「続行」ボタンを押して次へ進みます。

「③ ユーザーにこのサービスアカウントへのアクセスを許可(省略可)」では、何も行わずに「完了」を押してスキップします。

スキップする画面の例

スキップする画面の例

サービスアカウントの作成が完了すると、サービスアカウントの一覧画面へ戻ります。作成したサービスアカウントが表示されていることを確認してください。

サービスアカウントの認証情報の取得

作成したサービスアカウントを確認したら、「メール」列のリンクをクリックし、サービスアカウントの詳細画面へ移ります。

サービスアカウントアカウント作成後の一覧画面の例

サービスアカウントアカウント作成後の一覧画面の例

上部の「キー」タブを選択して、サービスアカウントの認証に必要な秘密鍵を管理する画面へ移ります。

サービスアカウントの詳細画面の例

サービスアカウントの詳細画面の例

「鍵を追加」ボタンから「新しい鍵を作成」を選択し、秘密鍵の作成を開始します。

鍵の作成ボタンの表示例

鍵の作成ボタンの表示例

次の画面では、「JSON」を選択した状態のまま「作成」ボタンを押します。

タイプ選択画面の例

タイプ選択画面の例

「作成」ボタンを押すと、秘密鍵ファイル(.json)が自動的にダウンロードされます。

秘密鍵ファイルをテキストエディタで開くと、下記のような構成になっているはずです。

{
  "type": "service_account",
  "project_id": "???",
  "private_key_id": "???",
  "private_key": "-----BEGIN PRIVATE KEY-----\n???\n-----END PRIVATE KEY-----\n",
  "client_email": "???@???.iam.gserviceaccount.com",
  "client_id": "???",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "???",
  "universe_domain": "googleapis.com"
}

上記のうち、project_id / private_key / client_email の内容を動作確認で使用します。

動作確認

まずは動作確認を行ってみましょう。

プロジェクトファイルを入手

  1. GitHub (monowireless/spot-google-sheets) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-google-sheets-main から spot-google-sheets に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-google-sheets フォルダを配置します

スケッチの設定ファイルを修正

Arduino スケッチ spot-google-sheets.ino を開き、画面上部の config.h タブを選択して、4-11行目の値を修正してください。

4-5行目は、無線 LAN 関連の設定です。

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it

SSID と パスワードを設定しています。

一方、8-11行目はスプレッドシート関連の設定です。

const char* PROJECT_ID = "YOUR-PROJECT-ID";                                                                         // Modify it
const char* SERVICE_ACCOUNT_EMAIL = "YOUR-SERVICE-ACCOUNT@YOUR-PROJECT-ID.iam.gserviceaccount.com";                 // Modify it
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY\n-----END PRIVATE KEY-----\n";    // Modify it
const char* USER_ACCOUNT_EMAIL = "YOUR-ACCOUNT@EMAIL";                                                              // Modify it

最初の3項目には .json ファイルの内容をコピーして、最後の USER_ACCOUNT_EMAIL にはあなたがログインしている Google アカウントのメールアドレスを入力してください。

スケッチを書き込み

ESP32 へのスケッチの書き込み方法 を参考に、スケッチを書き込んでください。

親機と子機を起動

TWELITE SPOT のリセットボタン(ESP32 側)を押してください。

Arduino のシリアルコンソールに以下のような表示がされたら、起動に成功しています。

Initializing queue...
Completed.
Started TWELITE.
Connecting to WiFi ...!...
Connected. IP: xxx.xxx.xxx.xxx
Initializing NTP...Completed. UNIX time: xxxxxxxxxx
Initializing sheets...
Creating sheets...
OAuth2.0 access token on initializing
OAuth2.0 access token on signing
OAuth2.0 access token on exchange request
OAuth2.0 access token ready
Requesting to create...
Succeeded.
Adding headers for ARIA...
Requesting to add header...
Succeeded.
Formatting the sheet for ARIA...
Requesting to format...
Succeeded.
Extending the sheet for ARIA...
Requesting to extend...
Succeeded.
Completed.

TWELITE ARIA(初期設定)にもコイン電池を挿入し、電源を投入します。

コイン電池の挿入

コイン電池の挿入

TWELITE SPOT が TWELITE ARIA からのパケットを正常に受信し、データ列の追加に成功すると、下記のような表示がなされます。

Got a new packet from ARIA.
Got a new packet from ARIA.
Requesting to add data...
Got a new packet from ARIA.
Succeeded.

ちなみに、上記の例ではリクエスト中にパケットを受信しています。後述のマルチタスクに成功している証です!

Google へアクセス

Google ドライブの 共有アイテム へアクセスし、SPOT Sheet (xxx) という名前のスプレッドシートを開きます。

以下のような画面が表示されます。

スプレッドシート画面のイメージ

スプレッドシート画面のイメージ

スクロールしていくと、TWELITE ARIA からのデータを確認できるはずです。

スケッチ解説

Arduino スケッチ spot-google-sheets.ino の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

4-7行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ
NTPClient.hNTP を使うファイル名と受信時刻に使用
WiFi.hESP32 の WiFi を使う
WiFiUdp.hUDP を使うNTPClient に必要

サードパーティのライブラリ

10-11行目では、サードパーティのライブラリをインクルードしています。

#include <ESP_Google_Sheet_Client.h>
#include <TimeLib.h>
ヘッダファイル内容備考
ESP_Google_Sheet_Client.hGoogle へアクセスする
TimeLib.hUNIX 時間をフォーマットする

MWings ライブラリ

14行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ユーザ設定の定義

17行目では、config.h をインクルードしています。

#include "config.h"

無線 LAN 設定の定義

config.h の4-5行目では、TWELITE SPOT に搭載された ESP32 に適用する無線 LAN 設定を定義します。

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it
名称内容
WIFI_SSID接続するネットワークの SSID
WIFI_PASSWORD接続するネットワークの パスワード

API 設定の定義

config.h の8-11行目では、API の設定を定義しています。

const char* PROJECT_ID = "YOUR-PROJECT-ID";                                                                         // Modify it
const char* SERVICE_ACCOUNT_EMAIL = "YOUR-SERVICE-ACCOUNT@YOUR-PROJECT-ID.iam.gserviceaccount.com";                 // Modify it
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY\n-----END PRIVATE KEY-----\n";    // Modify it
const char* USER_ACCOUNT_EMAIL = "YOUR-ACCOUNT@EMAIL";                                                              // Modify it
名称内容
PROJECT_IDプロジェクト ID
SERVICE_ACCOUNT_EMAILサービスアカウントのメールアドレス
PRIVATE_KEY秘密鍵の本体
USER_ACCOUNT_EMAILスプレッドシートを共有するユーザアカウントのメールアドレス

ピン番号の定義

20-24行目では、ピン番号を定義しています。

const uint8_t TWE_RST = 5;
const uint8_t TWE_PRG = 4;
const uint8_t LED = 18;
const uint8_t ESP_RXD1 = 16;
const uint8_t ESP_TXD1 = 17;
名称内容
TWE_RSTTWELITE の RST ピンが接続されているピンの番号
TWE_PRGTWELITE の PRG ピンが接続されているピンの番号
LED基板上の ESP32 用 LED が接続されているピンの番号
ESP_RXD1TWELITE の TX ピンが接続されているピンの番号
ESP_TXD1TWELITE の RX ピンが接続されているピンの番号

TWELITE 設定の定義

27-30行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
const uint8_t TWE_RETRY = 2;
const uint8_t TWE_POWER = 3;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション ID
TWE_RETRYTWELITE の 再送回数(送信時)
TWE_POWERTWELITE の 送信出力

シート関連の定義

32-43行目では、シートに関連した情報を定義しています。

const char* SPREADSHEET_TITLE_PREFIX = "SPOT Sheet";
const char* SPREADSHEET_LOCALE = "ja_JP";
const char* SPREADSHEET_TIME_ZONE = "Asia/Tokyo";

const int MIN_REQUEST_INTERVAL = 1000;    // 60 requests per minute

const int SHEETS_DEFAULT_ROWS = 1000;            // Default length is 1000 rows
const int SHEETS_ROWS = 100000;                  // Max 1,000,000 rows at 10 columns

const int ARIA_SHEET_ID = 1;
const char* ARIA_SHEET_TITLE = "ARIA";
constexpr int ARIA_BUFFER_PACKETS = 32;    // Max number of rows per addition request
名称内容
SPREADSHEET_TITLE_PREFIXスプレッドシートのファイル名の固定部分
SPREADSHEET_LOCALEスプレッドシートのロケール
SPREADSHEET_TIME_ZONEスプレッドシートのタイムゾーン
MIN_REQUEST_INTERVALリクエスト送信の最小間隔
SHEETS_DEFAULT_ROWS各シートのデフォルトの行数
SHEETS_ROWS各シートの行数
ARIA_SHEET_IDARIA 用シートの ID
ARIA_SHEET_TITLEARIA 用シートの名称
ARIA_BUFFER_PACKETSARIA からのパケットを格納するキューの長さ

型の宣言

46-50行目では、型を宣言しています。

struct ParsedAppAriaPacketWithTime {
    ParsedAppAriaPacket packet;
    uint32_t elapsedMillis;
    uint32_t unixTime;
};
名称内容
ParsedAppAriaPacketWithTime受信したパケットデータを受信時刻と合わせてキューへ格納するための型
  • elapsedMillis :パケット受信時の起動からの経過時間(ミリ秒)
  • unixTime :パケット受信時の UNIX 時間(秒)です。

グローバルオブジェクトの宣言

53-61行目では、グローバルオブジェクトを宣言しています。

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.nict.jp", 32400);

String spreadsheetIdString;    // Identifier of newly created file
bool readyForNewRequests = false;
uint32_t lastTimeRequestWasSent = UINT32_MAX;

QueueHandle_t ariaPacketQueue;       // Store received data from ARIA
uint32_t rowToAddNewAriaData = 2;    // Starting with the Row 2
名称内容
ntpUDPNTP のための UDP インタフェース
timeClientNTP のインタフェース
spreadsheetIdString作成したスプレッドシートのID
readyForNewRequests新たなリクエストを送信できる状態になったら true
lastTimeRequestWasSent最後にリクエストを送信した時間
ariaPacketQueueARIA から受信したパケットと受信時刻を格納するキュー
rowToAddNewAriaData次に ARIA から受信したデータを追加する行

関数プロトタイプの宣言

64-71行目では、関数プロトタイプを宣言しています。

void anotherLoop();

void waitUntilNewRequestsReady();
String createSpreadsheet();
bool formatSheet(const String spreadsheetId, const int sheetId);
bool extendSheet(const String spreadsheetId, const int sheetId, const int rows);
bool addSheetAriaHeaderRow(const String spreadsheetId, const char* const sheetTitle);
bool addSheetsDataRow(const String spreadsheetId);
名称内容
anotherLoop()非同期で TWELITE の処理を行う、もうひとつの loop()
waitUntilNewRequestsReady()次のリクエストを送信可能になるまで待機する
createSpreadsheet()スプレッドシートを新規作成する
formatSheet()指定したシートの書式を設定する
extendSheet()指定したシートの行を増やし、書式を設定する
addSheetAriaHeaderRow()指定したシートへ ARIA 向けのヘッダー行を追加する
addSheetsDataRow()シートへデータ行を追加する

キューの設定

82-83行目では、受信したパケットデータを受信時刻と合わせて格納するためのキューを初期化しています。

ariaPacketQueue = xQueueCreate(ARIA_BUFFER_PACKETS, sizeof(ParsedAppAriaPacketWithTime));
if (ariaPacketQueue == 0) { Serial.println("Failed to init a queue."); }

xQueueCreate() は、ESP32 の内部で動作する FreeRTOS の機能です。マルチタスクに対応したキューを簡単に作成することができます。

TWELITE の設定

88-92行目では、Twelite.begin() を呼び出し、TWELITE SPOT に搭載された TWELITE 親機の設定と起動を行っています。

Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);
    if (Twelite.begin(Serial2,
                      LED, TWE_RST, TWE_PRG,
                      TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
        Serial.println("Started TWELITE.");
    }
引数内容
Serial2HardwareSerial&TWELITE との通信に使うシリアルポート
LEDintステータス LED を接続したピンの番号
TWE_RSTintTWELITE の RST ピンを接続したピンの番号
TWE_PRGintTWELITE の PRG ピンを接続したピンの番号
TWE_CHANNELuint8_tTWELITE の 周波数チャネル
TWE_APP_IDuint32_tTWELITE の アプリケーション ID
TWE_RETRYuint8_tTWELITE の 再送回数(送信時)
TWE_POWERuint8_tTWELITE の 送信出力

イベントハンドラの登録

94-103行目では、TWELITE ARIA からのパケットを受信した際に行う処理を登録しています。

Twelite.on([](const ParsedAppAriaPacket& packet) {
    Serial.println("Got a new packet from ARIA.");
    ParsedAppAriaPacketWithTime packetWithTime;
    packetWithTime.elapsedMillis = millis();
    packetWithTime.unixTime = timeClient.getEpochTime();
    packetWithTime.packet = packet;
    if (not(xQueueSend(ariaPacketQueue, &packetWithTime, 0) == pdPASS)) {
        Serial.println("Failed to add packet data to the queue.");
    }
});

ここでは、xQueueSend() により、受信したパケットデータを受信時刻と合わせてキューの末尾へ格納しています。

無線 LAN の設定

106-120行目では、無線 LAN の設定を行っています。

WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
    static int count = 0;
    Serial.print('.');
    delay(500);
    // Retry every 5 seconds
    if (count++ % 10 == 0) {
        WiFi.disconnect();
        WiFi.reconnect();
        Serial.print('!');
    }
}

ここでは、無線 LAN 子機として設定したうえで、指定のネットワークへ接続しています。

NTP の設定

126-127行目では、NTP の設定をしています。

timeClient.begin();
timeClient.update();

Google スプレッドシートの設定

132-145行目では、Google スプレッドシートを設定しています。

GSheet.setTokenCallback([](TokenInfo info) {
    // Print token initialization states
    if (info.status == esp_signer_token_status_error) {
        Serial.print("Token error ");
        Serial.println(GSheet.getTokenError(info));
    }
    Serial.print(GSheet.getTokenType(info));
    Serial.print(" ");
    Serial.println(GSheet.getTokenStatus(info));
});
GSheet.setPrerefreshSeconds(60);    // Set refresh rate for auth token

Serial.println("Initializing sheets...");
GSheet.begin(SERVICE_ACCOUNT_EMAIL, PROJECT_ID, PRIVATE_KEY);

132-141行目でサービスアカウントのトークンを取得する際の状態表示の処理を登録したあと、142行目でトークンの再取得間隔を設定、145行目でサービスアカウントの初期化をしています。

また、147-173行目では、スプレッドシートの作成、ARIA 向けヘッダー行の追加、セルの書式設定、そして行の拡張を行っています。

Serial.println("Creating sheets...");
waitUntilNewRequestsReady();    // Wait for token
spreadsheetIdString = createSpreadsheet();
if (not(spreadsheetIdString.length() > 0)) {
    Serial.println("Failed to create sheets.");
}

Serial.println("Adding headers for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not addSheetAriaHeaderRow(spreadsheetIdString, ARIA_SHEET_TITLE)) {
    Serial.println("Failed to add headers.");
}

Serial.println("Formatting the sheet for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not formatSheet(spreadsheetIdString, ARIA_SHEET_ID)) {
    Serial.println("Failed to format.");
}

Serial.println("Extending the sheet for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not extendSheet(spreadsheetIdString, ARIA_SHEET_ID, SHEETS_ROWS - SHEETS_DEFAULT_ROWS)) {
    Serial.println("Failed to extend.");
}

タスクの登録

179-186行目では、TWELITE のデータを非同期で更新するためのタスクを登録しています。

xTaskCreatePinnedToCore(
    [](void* params) {
        while (true) {
            anotherLoop();
            vTaskDelay(1);
        }
    },
    "Task for anotherLoop()", 8192, nullptr, 18, nullptr, 0);

xTaskCreatePinnedToCore() は、FreeRTOS のマルチタスク機能に含まれる関数です。

ここでは、180-185行目のラムダ式を渡し、anotherLoop() を無限に呼ぶタスクを登録しています。

[](void* params) {
    while (true) {
        anotherLoop();
        vTaskDelay(1);    // IMPORTANT for Watchdog
    }
},

タスクの名称は Task for anotherLoop() で、スタックサイズは 8192 、タスクへのパラメータはなく、優先度は 18(大きいほど高く、無線 LAN 関連の処理は1個上の 19)、タスクを操作するインタフェースもなく、実行する CPU コアは無線 LAN などの RF 処理と同じ Core 0 です( loop() 等は Core 1 )。

"Task for anotherLoop()", 8192, nullptr, 18, nullptr, 0);    // Priority is 18 (lower than WiFi)

TWELITE のデータの更新

203行目では、anotherLoop() 内にて Twelite.update() を呼び出しています。

Twelite.update();

Twelite.update() は、TWELITE 親機から送信されるパケットデータ(ModBus ASCII 形式)を順次1バイトずつ読み出す関数です。

スプレッドシートの更新

192-195行目では、スプレッドシートの更新処理を呼び出しています。

if (millis() - lastTimeRequestWasSent > MIN_REQUEST_INTERVAL) {
    // Add available data
    addSheetsDataRow(spreadsheetIdString);
}

192行目の if 文では、60リクエスト毎分の API 制限を守るために、前回のリクエスト送信から必ず 1 秒以上空けるようにしています(JavaScript の Throttle / Debounce における Throttle に相当します)。

194行目では、必要に応じてスプレッドシートへデータ行を追加します。

addSheetsDataRow(spreadsheetIdString);

API ライブラリの更新

196行目では、Google API 用ライブラリの更新を行い、リクエストの送信可否の状態を取得しています。

readyForNewRequests = GSheet.ready();

NTP ライブラリの更新

197行目では、NTP ライブラリの更新を行っています。

timeClient.update();

スプレッドシートの操作

217行目以降では、Sheets API によるスプレッドシートの操作を行っています。

詳しくは ライブラリの API リファレンスSheets API の REST リソース をご覧ください。

関連情報

Google

Arduino

ESP32

コミュニティ

ライブラリ

プラグイン