セクションの複数ページをまとめています。 印刷またはPDF形式で保存...
TWELITE に加えて無線 LAN を活用するスケッチの解説
- 1: プリインストール済みスケッチ
- 1.1: プリインストール済みスケッチ
- 1.2: プリインストール済みスケッチ
- 2: WebSocketによる中継
- 2.1: WebSocketによる中継
- 3: REST API の使用
- 3.1: REST API の使用
- 3.2: REST API の使用
- 4: Google スプレッドシートの利用
- 4.1: Google スプレッドシートの利用
1 - プリインストール済みスケッチ
spot-server
の解説です。1.1 - プリインストール済みスケッチ
spot-server
の解説です。本稿では、Arduino IDE 1.x を必要とします。Arduino IDE 2.x の技術的な制約により、2023年5月現在、Arduino IDE 2.x には対応していません。
本稿で使用するプラグインは Java で書かれているため、Arduino IDE 1.x とは異なり、Java ベースではない Arduino IDE 2.x では動作しないからです。問題の詳細については、Arduino IDE GitHub ページの Issue (Missing support for external tools / plugins · Issue #58 · arduino/arduino-ide) をご覧ください(英語)。
2024年10月追記:未検証ですが、Arduino IDE 2.x では、プラグインearlephilhower/arduino-littlefs-uploadを代わりに使用できるかもしれません。
本稿では、サードパーティのオープンソースソフトウェアを使用します。
サードパーティのソフトウェアについて、その詳しい使用方法を弊社からご案内することはいたしかねます。また、サードパーティのソフトウェアを使用されたことによるいかなる損害についても、弊社は一切の責任を負いません。
ソースコードの入手
GitHub (monowireless/spot-server) から入手できます。
システムの概要
spot-server は、TWELITE からのデータ受信と転送を行う Arduino スケッチ (.ino
) と、スマホに配信する Web ページ (.html
/ .css
/ .js
) で構成しています。
TWELITE 子機が送信したデータは Arduino スケッチで受信され、Arduino スケッチは公開中の Web ページに対してイベントを発火します。公開された Web ページでは、発火されたイベントに応じて HTML の内容を動的に書き換えています。
開発に必要なもの
-
無線LANゲートウェイ TWELITE SPOT
- 電源用 USB-C ケーブル
- USB AC アダプタ(1A 以上供給できるもの)
-
加速度センサー無線タグ TWELITE CUEなどの子機 (お持ちでない場合はご購入ください 👉 販売店一覧)
- CR2032 コイン電池 などの電源
-
USBアダプター TWELITE R3 (お持ちでない場合はご購入ください 👉 販売店一覧)
- 通信用 USB-C ケーブル
- Grove - OLED Display 1.12 (なくてもスケッチは動きます)
- Grove ケーブル
- 💻 コンピュータ
環境整備
IDE とツールチェインの導入
Arduino IDE 1.x による開発環境の構築方法 をご覧ください。
ライブラリの導入
はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino
) に libraries
フォルダがない場合は、これを作成します。
非同期 TCP 通信ライブラリ
- GitHub (me-no-dev/AsyncTCP) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
AsyncTCP-master
からAsyncTCP
に変更します libraries
フォルダにAsyncTCP
フォルダを配置します
非同期 Web サーバライブラリ
- GitHub (me-no-dev/ESPAsyncWebServer) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
AsyncWebServer-master
からAsyncWebServer
に変更します libraries
フォルダにAsyncWebServer
フォルダを配置します
OLED ディスプレイライブラリ
- GitHub (Seeed-Studio/OLED_Display_96X96) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
OLED_Display_96X96-master
からOLED_Display_96X96
に変更します libraries
フォルダにOLED_Display_96X96
フォルダを配置します
JSON ライブラリ
ライブラリマネージャを開き、Arduino_JSON
をインストールします。
ArduinoJson
ではなく、公式の Arduino_JSON
を使用します。プラグインの導入
ファイルシステム書き込みプラグイン
HTML などのファイルを ESP32 のフラッシュ領域に書き込むには、Arduino プラグインが必要です。
ここでは、lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system を利用します(LittleFSに対応したプラグイン)。
インストール方法は TWELITE SPOT マニュアル ESP32 へのファイルの書き込み方法 をご覧ください。
プロジェクトファイルの入手
- GitHub (monowireless/spot-server) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
spot-server-main
からspot-server
に変更します - 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.h | Arduino の基本ライブラリ | 省略できる場合もあるが念のため記載 |
Arduino_JSON.h | JSON 文字列を扱う | ArduinoJson とは異なる |
ESPmDNS.h | mDNS を使う | ホスト名を使うために必要 |
LittleFS.h | LittleFS ファイルシステムを扱う | ページ公開に必要 |
WiFi.h | ESP32 の WiFi を使う | |
esp_wifi.h | WiFiの高度な設定を行う | ロケール設定に必要 |
Wire.h | I2C を使う | OLED ディスプレイ用 |
サードパーティのライブラリ
13-15行目では、サードパーティのライブラリをインクルードしています。
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SeeedGrayOLED.h>
ヘッダファイル | 内容 | 備考 |
---|---|---|
AsyncTCP.h | 非同期 TCP 通信を行う | |
ESPAsyncWebServer.h | 非同期 Web サーバを立てる | AsyncTCP に依存 |
SeeedGrayOLED.h | OLED ディスプレイを使う |
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_RST | TWELITE の RST ピンが接続されているピンの番号 |
TWE_PRG | TWELITE の PRG ピンが接続されているピンの番号 |
LED | 基板上の ESP32 用 LED が接続されているピンの番号 |
ESP_RXD1 | TWELITE の TX ピンが接続されているピンの番号 |
ESP_TXD1 | TWELITE の 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_CH | TWELITE の 周波数チャネル |
TWE_APPID | TWELITE の アプリケーション ID |
TWE_RETRY | TWELITE の 再送回数(送信時) |
TWE_POWER | TWELITE の 送信出力 |
無線 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_BASE | SSID の共通部分の文字列 |
WIFI_PASSWORD | パスワード |
WIFI_CH | ESP32 の周波数チャネル |
WIFI_IP | IP アドレス |
WIFI_MASK | サブネットマスク |
HOSTNAME | ホスト名 |
ESP32 Arduino Core v3.x.x では、ロケール設定を行わないとチャネル13などの機能を使用できないことがあります。
グローバルオブジェクトの宣言
49-50行目では、グローバルオブジェクトを宣言しています。
AsyncWebServer server(80);
AsyncEventSource events("/events");
名称 | 内容 |
---|---|
server | 80番ポートで開く非同期 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 の |
createJsonFrom() <ParsedAppAriaPacket&> | App_ARIA の |
createJsonFrom() <ParsedAppCuePacket&> | App_CUE の |
createJsonFrom() <BarePacket&> | すべての |
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.");
}
引数 | 型 | 内容 |
---|---|---|
Serial2 | HardwareSerial& | TWELITE との通信に使うシリアルポート |
LED | int | ステータス LED を接続したピンの番号 |
TWE_RST | int | TWELITE の RST ピンを接続したピンの番号 |
TWE_PRG | int | TWELITE の PRG ピンを接続したピンの番号 |
TWE_CHANNEL | uint8_t | TWELITE の 周波数チャネル |
TWE_APP_ID | uint32_t | TWELITE の アプリケーション ID |
TWE_RETRY | uint8_t | TWELITE の 再送回数(送信時) |
TWE_POWER | uint8_t | TWELITE の 送信出力 |
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
です。
millis()
で取得した現在時刻を使用しています。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);
delay(100)
を省略すると初期化に失敗することがあります。ファイルシステムの設定
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バイトずつ読み出す関数です。
loop()
内で繰り返し Twelite.update()
を呼ぶことで、TWELITE 親機から送信されるパケットデータの解釈が進みます。パケットデータの解釈を終えた際に 上記 のようなイベントが呼ばれる仕組みです。delay()
などの処理でこの関数の呼び出しをブロックすると、パケットデータ文字列の読み出しが間に合わないことがあります。時間のかかる処理は必ず非同期の実装として、loop()
関数をできるだけ高速回転させるようにしてください。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行に分けて配置しています。
xs-
や sm-
などは画面の幅を指定します。レスポンシブデザインに活用できます。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:グローバル変数
ここからは各 HTML ファイルに対応したスクリプトの解説です。
例として、ここでは data/js/cue-viewer.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
- 公式サイト:Arduino - Home
ESP32
- 製品情報:ESP32 Wi-Fi & Bluetooth MCU I Espressif Systems
- データシート:esp32_datasheet_en.pdf
- Arduino 向けツールチェイン:espressif/arduino-esp32: Arduino core for the ESP32
- スタートガイド:Getting Started — Arduino-ESP32 documentation
- 導入方法:Installing — Arduino-ESP32 documentation
- API リファレンス:Libraries — Arduino-ESP32 documentation
- Wi-Fi API:Wi-Fi API — Arduino-ESP32 documentation
- チュートリアル:Tutorials — Arduino-ESP32 documentation
- トラブルシューティング:Troubleshooting — Arduino-ESP32 documentation
コミュニティ
ライブラリ
- 非同期 TCP:me-no-dev/AsyncTCP: Async TCP Library for ESP32
- 非同期 Web サーバ:me-no-dev/ESPAsyncWebServer: Async Web Server for ESP8266 and ESP32
- Seeed 96x96 / 128x128 OLED:Seeed-Studio/OLED_Display_96X96: Seeed OLED Display 96*96 library
プラグイン
- ファイル書き込み:me-no-dev/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system
- スタックトレース:me-no-dev/EspExceptionDecoder: Exception Stack Trace Decoder for ESP8266 and ESP32
Web関連
ECMAScript (JavaScript)
- API リファレンス:開発者向けのウェブ技術 | MDN
- ES2016以降のバージョン別互換性リスト:ECMAScript 2016+ compatibility table
コミュニティ
- CSS
- Web カラーとその配色例:世界の伝統色 洋色大辞典 - Traditional Colors of World
- Bootstrap のようなグリッドシステム:Flexbox Grid
- ニューモーフィズムな CSS ジェネレータ:Neumorphism/Soft UI CSS shadow generator
- ECMAScript
- グラフ描画:Chart.js | Chart.js
- リアルタイムストリーミングプラグイン:はじめに | chartjs-plugin-streaming
- 時計と時刻:Luxon Home
- グラフ描画:Chart.js | Chart.js
1.2 - プリインストール済みスケッチ
spot-server
の解説です。本稿では、Arduino IDE 1.x を必要とします。Arduino IDE 2.x の技術的な制約により、2023年5月現在、Arduino IDE 2.x には対応していません。
本稿で使用するプラグインは Java で書かれているため、Arduino IDE 1.x とは異なり、Java ベースではない Arduino IDE 2.x では動作しないからです。問題の詳細については、Arduino IDE GitHub ページの Issue (Missing support for external tools / plugins · Issue #58 · arduino/arduino-ide) をご覧ください(英語)。
2024年10月追記:未検証ですが、Arduino IDE 2.x では、プラグインearlephilhower/arduino-littlefs-uploadを代わりに使用できるかもしれません。
本稿では、サードパーティのオープンソースソフトウェアを使用します。
サードパーティのソフトウェアについて、その詳しい使用方法を弊社からご案内することはいたしかねます。また、サードパーティのソフトウェアを使用されたことによるいかなる損害についても、弊社は一切の責任を負いません。
ソースコードの入手
GitHub (monowireless/spot-server) から入手できます。
システムの概要
spot-server は、TWELITE からのデータ受信と転送を行う Arduino スケッチ (.ino
) と、スマホに配信する Web ページ (.html
/ .css
/ .js
) で構成しています。
TWELITE 子機が送信したデータは Arduino スケッチで受信され、Arduino スケッチは公開中の Web ページに対してイベントを発火します。公開された Web ページでは、発火されたイベントに応じて HTML の内容を動的に書き換えています。
開発に必要なもの
-
無線LANゲートウェイ TWELITE SPOT
- 電源用 USB-C ケーブル
- USB AC アダプタ(1A 以上供給できるもの)
-
加速度センサー無線タグ TWELITE CUEなどの子機 (お持ちでない場合はご購入ください 👉 販売店一覧)
- CR2032 コイン電池 などの電源
-
USBアダプター TWELITE R3 (お持ちでない場合はご購入ください 👉 販売店一覧)
- 通信用 USB-C ケーブル
- Grove - OLED Display 1.12 (なくてもスケッチは動きます)
- Grove ケーブル
- 💻 コンピュータ
環境整備
IDE とツールチェインの導入
Arduino IDE 1.x による開発環境の構築方法 をご覧ください。
ライブラリの導入
はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino
) に libraries
フォルダがない場合は、これを作成します。
非同期 TCP 通信ライブラリ
- GitHub (me-no-dev/AsyncTCP) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
AsyncTCP-master
からAsyncTCP
に変更します libraries
フォルダにAsyncTCP
フォルダを配置します
非同期 Web サーバライブラリ
- GitHub (me-no-dev/ESPAsyncWebServer) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
AsyncWebServer-master
からAsyncWebServer
に変更します libraries
フォルダにAsyncWebServer
フォルダを配置します
OLED ディスプレイライブラリ
- GitHub (Seeed-Studio/OLED_Display_96X96) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
OLED_Display_96X96-master
からOLED_Display_96X96
に変更します libraries
フォルダにOLED_Display_96X96
フォルダを配置します
JSON ライブラリ
ライブラリマネージャを開き、Arduino_JSON
をインストールします。
ArduinoJson
ではなく、公式の Arduino_JSON
を使用します。プラグインの導入
ファイルシステム書き込みプラグイン
HTML などのファイルを ESP32 のフラッシュ領域に書き込むには、Arduino プラグインが必要です。
ここでは、lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system を利用します。
インストール方法は TWELITE SPOT マニュアル ESP32 へのファイルの書き込み方法 をご覧ください。
プロジェクトファイルの入手
- GitHub (monowireless/spot-server) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
spot-server-main
からspot-server
に変更します - 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.h | Arduino の基本ライブラリ | 省略できる場合もあるが念のため記載 |
Arduino_JSON.h | JSON 文字列を扱う | ArduinoJson とは異なる |
ESPmDNS.h | mDNS を使う | ホスト名を使うために必要 |
LittleFS.h | LittleFS ファイルシステムを扱う | ページ公開に必要 |
WiFi.h | ESP32 の WiFi を使う | |
Wire.h | I2C を使う | OLED ディスプレイ用 |
サードパーティのライブラリ
12-14行目では、サードパーティのライブラリをインクルードしています。
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SeeedGrayOLED.h>
ヘッダファイル | 内容 | 備考 |
---|---|---|
AsyncTCP.h | 非同期 TCP 通信を行う | |
ESPAsyncWebServer.h | 非同期 Web サーバを立てる | AsyncTCP に依存 |
SeeedGrayOLED.h | OLED ディスプレイを使う |
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_RST | TWELITE の RST ピンが接続されているピンの番号 |
TWE_PRG | TWELITE の PRG ピンが接続されているピンの番号 |
LED | 基板上の ESP32 用 LED が接続されているピンの番号 |
ESP_RXD1 | TWELITE の TX ピンが接続されているピンの番号 |
ESP_TXD1 | TWELITE の 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_CH | TWELITE の 周波数チャネル |
TWE_APPID | TWELITE の アプリケーション ID |
TWE_RETRY | TWELITE の 再送回数(送信時) |
TWE_POWER | TWELITE の 送信出力 |
無線 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_BASE | SSID の共通部分の文字列 |
WIFI_PASSWORD | パスワード |
WIFI_CH | ESP32 の周波数チャネル |
WIFI_IP | IP アドレス |
WIFI_MASK | サブネットマスク |
HOSTNAME | ホスト名 |
グローバルオブジェクトの宣言
41-42行目では、グローバルオブジェクトを宣言しています。
AsyncWebServer server(80);
AsyncEventSource events("/events");
名称 | 内容 |
---|---|
server | 80番ポートで開く非同期 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 の |
createJsonFrom() <ParsedAppAriaPacket&> | App_ARIA の |
createJsonFrom() <ParsedAppCuePacket&> | App_CUE の |
createJsonFrom() <BarePacket&> | すべての |
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.");
}
引数 | 型 | 内容 |
---|---|---|
Serial2 | HardwareSerial& | TWELITE との通信に使うシリアルポート |
LED | int | ステータス LED を接続したピンの番号 |
TWE_RST | int | TWELITE の RST ピンを接続したピンの番号 |
TWE_PRG | int | TWELITE の PRG ピンを接続したピンの番号 |
TWE_CHANNEL | uint8_t | TWELITE の 周波数チャネル |
TWE_APP_ID | uint32_t | TWELITE の アプリケーション ID |
TWE_RETRY | uint8_t | TWELITE の 再送回数(送信時) |
TWE_POWER | uint8_t | TWELITE の 送信出力 |
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
です。
millis()
で取得した現在時刻を使用しています。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);
delay(100)
を省略すると初期化に失敗することがあります。ファイルシステムの設定
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バイトずつ読み出す関数です。
loop()
内で繰り返し Twelite.update()
を呼ぶことで、TWELITE 親機から送信されるパケットデータの解釈が進みます。パケットデータの解釈を終えた際に 上記 のようなイベントが呼ばれる仕組みです。delay()
などの処理でこの関数の呼び出しをブロックすると、パケットデータ文字列の読み出しが間に合わないことがあります。時間のかかる処理は必ず非同期の実装として、loop()
関数をできるだけ高速回転させるようにしてください。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行に分けて配置しています。
xs-
や sm-
などは画面の幅を指定します。レスポンシブデザインに活用できます。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:グローバル変数
ここからは各 HTML ファイルに対応したスクリプトの解説です。
例として、ここでは data/js/cue-viewer.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
- 公式サイト:Arduino - Home
ESP32
- 製品情報:ESP32 Wi-Fi & Bluetooth MCU I Espressif Systems
- データシート:esp32_datasheet_en.pdf
- Arduino 向けツールチェイン:espressif/arduino-esp32: Arduino core for the ESP32
- スタートガイド:Getting Started — Arduino-ESP32 documentation
- 導入方法:Installing — Arduino-ESP32 documentation
- API リファレンス:Libraries — Arduino-ESP32 documentation
- Wi-Fi API:Wi-Fi API — Arduino-ESP32 documentation
- チュートリアル:Tutorials — Arduino-ESP32 documentation
- トラブルシューティング:Troubleshooting — Arduino-ESP32 documentation
コミュニティ
ライブラリ
- 非同期 TCP:me-no-dev/AsyncTCP: Async TCP Library for ESP32
- 非同期 Web サーバ:me-no-dev/ESPAsyncWebServer: Async Web Server for ESP8266 and ESP32
- Seeed 96x96 / 128x128 OLED:Seeed-Studio/OLED_Display_96X96: Seeed OLED Display 96*96 library
プラグイン
- ファイル書き込み:me-no-dev/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system
- スタックトレース:me-no-dev/EspExceptionDecoder: Exception Stack Trace Decoder for ESP8266 and ESP32
Web関連
ECMAScript (JavaScript)
- API リファレンス:開発者向けのウェブ技術 | MDN
- ES2016以降のバージョン別互換性リスト:ECMAScript 2016+ compatibility table
コミュニティ
- CSS
- Web カラーとその配色例:世界の伝統色 洋色大辞典 - Traditional Colors of World
- Bootstrap のようなグリッドシステム:Flexbox Grid
- ニューモーフィズムな CSS ジェネレータ:Neumorphism/Soft UI CSS shadow generator
- ECMAScript
- グラフ描画:Chart.js | Chart.js
- リアルタイムストリーミングプラグイン:はじめに | chartjs-plugin-streaming
- 時計と時刻:Luxon Home
- グラフ描画:Chart.js | Chart.js
2 - WebSocketによる中継
spot-router
の解説です。2.1 - WebSocketによる中継
spot-router
の解説です。本稿では、サードパーティのオープンソースソフトウェアを使用します。
サードパーティのソフトウェアについて、その詳しい使用方法を弊社からご案内することはいたしかねます。また、サードパーティのソフトウェアを使用されたことによるいかなる損害についても、弊社は一切の責任を負いません。
ソースコードの入手
GitHub (monowireless/spot-router) から入手できます。
システムの概要
spot-router は、TWELITE 親機が受信したデータに基づき出力した文字列(App_Wings の ModBus ASCII 形式)を WebSocket サーバへ転送します。
開発に必要なもの
-
無線LANゲートウェイ TWELITE SPOT
- 電源用 USB-C ケーブル
- USB AC アダプタ(1A 以上供給できるもの)
-
加速度センサー無線タグ TWELITE CUEなどの子機 (お持ちでない場合はご購入ください 👉 販売店一覧)
- CR2032 コイン電池 などの電源
-
USBアダプター TWELITE R3 (お持ちでない場合はご購入ください 👉 販売店一覧)
- 通信用 USB-C ケーブル
- 💻 WebSocket サーバ
- 💻 開発用コンピュータ
環境整備
IDE とツールチェインの導入
Arduino IDE 1.x による開発環境の構築方法 をご覧ください。
ライブラリの導入
はじめに、Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino
) に libraries
フォルダがない場合は、これを作成します。
WebSocket ライブラリ
- GitHub (Links2004/arduinoWebSockets) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、
libraries
フォルダにarduinoWebSockets-<バージョン>
フォルダを配置します
プロジェクトファイルの入手
- GitHub (monowireless/spot-router) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
spot-router-main
からspot-router
に変更します - 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.h | Arduino の基本ライブラリ | 省略できる場合もあるが念のため記載 |
WiFi.h | ESP32 の WiFi を使う |
サードパーティのライブラリ
8行目では、サードパーティのライブラリをインクルードしています。
#include <WebSocketsClient.h>
ヘッダファイル | 内容 | 備考 |
---|---|---|
WebSocketsClient.h | WebSocket クライアントになる |
MWings ライブラリ
11行目では、MWings ライブラリをインクルードしています。
#include <MWings.h>
ユーザ設定の定義
14行目では、config.h
をインクルードしています。
#include "config.h"
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_RST | TWELITE の RST ピンが接続されているピンの番号 |
TWE_PRG | TWELITE の PRG ピンが接続されているピンの番号 |
LED | 基板上の ESP32 用 LED が接続されているピンの番号 |
ESP_RXD1 | TWELITE の TX ピンが接続されているピンの番号 |
ESP_TXD1 | TWELITE の 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_CH | TWELITE の 周波数チャネル |
TWE_APPID | TWELITE の アプリケーション ID |
TWE_RETRY | TWELITE の 再送回数(送信時) |
TWE_POWER | TWELITE の 送信出力 |
グローバルオブジェクトの宣言
30行目では、グローバルオブジェクトを宣言しています。
WebSocketsClient webSocket;
名称 | 内容 |
---|---|
webSocket | WebSocket クライアントのインタフェース |
関数プロトタイプの宣言
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.");
}
引数 | 型 | 内容 |
---|---|---|
Serial2 | HardwareSerial& | TWELITE との通信に使うシリアルポート |
LED | int | ステータス LED を接続したピンの番号 |
TWE_RST | int | TWELITE の RST ピンを接続したピンの番号 |
TWE_PRG | int | TWELITE の PRG ピンを接続したピンの番号 |
TWE_CHANNEL | uint8_t | TWELITE の 周波数チャネル |
TWE_APP_ID | uint32_t | TWELITE の アプリケーション ID |
TWE_RETRY | uint8_t | TWELITE の 再送回数(送信時) |
TWE_POWER | uint8_t | TWELITE の 送信出力 |
イベントハンドラの登録
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 子機として設定したうえで、指定のネットワークへ接続しています。
while
文を出ません。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バイトずつ読み出す関数です。
loop()
内で繰り返し Twelite.update()
を呼ぶことで、TWELITE 親機から送信されるパケットデータの解釈が進みます。パケットデータの解釈を終えた際に 上記 のようなイベントが呼ばれる仕組みです。delay()
などの処理でこの関数の呼び出しをブロックすると、パケットデータ文字列の読み出しが間に合わないことがあります。時間のかかる処理は必ず非同期の実装として、loop()
関数をできるだけ高速回転させるようにしてください。WebSocket のデータの更新
103行目では、WebSocket のデータを更新する処理を呼び出しています。
webSocket.loop();
Twelite.update()
と同様に、delay()
などの処理でこの関数の呼び出しをブロックすると、データ更新が間に合わないことがあります。時間のかかる処理は必ず非同期の実装として、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
- 公式サイト:Arduino - Home
ESP32
- 製品情報:ESP32 Wi-Fi & Bluetooth MCU I Espressif Systems
- データシート:esp32_datasheet_en.pdf
- Arduino 向けツールチェイン:espressif/arduino-esp32: Arduino core for the ESP32
- スタートガイド:Getting Started — Arduino-ESP32 documentation
- 導入方法:Installing — Arduino-ESP32 documentation
- API リファレンス:Libraries — Arduino-ESP32 documentation
- Wi-Fi API:Wi-Fi API — Arduino-ESP32 documentation
- チュートリアル:Tutorials — Arduino-ESP32 documentation
- トラブルシューティング:Troubleshooting — Arduino-ESP32 documentation
コミュニティ
ライブラリ
プラグイン
ネットワーク関連
WebSocket
コミュニティ
3 - REST API の使用
spot-httpbin
の解説です。3.1 - REST API の使用
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-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_PIN | TWELITE の RST ピンが接続されているピンの番号 |
PRG_PIN | TWELITE の PRG ピンが接続されているピンの番号 |
LED_PIN | 基板上の ESP32 用 LED が接続されているピンの番号 |
RX1_PIN | TWELITE の RX1 ピンが接続されているピンの番号 |
TX1_PIN | TWELITE の TX1 ピンが接続されているピンの番号 |
グローバルオブジェクトの宣言
37-40行目では、グローバルオブジェクトを宣言しています。
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のインタフェース |
グローバル変数の宣言
43-44行目では、グローバル変数を宣言しています。
static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
名称 | 内容 |
---|---|
LatestDataFromAria | TWELITE ARIA から受信した最新のデータ |
IsThereNewDataFromAria | TWELITE ARIA から新たなデータを受信したことを示すフラグ |
関数プロトタイプの宣言
47-59行目では、関数プロトタイプを宣言しています。
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()
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
}
},
vTaskDelay()
を挿入しています。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);
}
下記リファレンスも合わせてご覧ください。
Twelite.begin()
mwings::MWings クラス | MWings API リファレンスTwelite.on()
mwings::MWings クラス | MWings API リファレンス
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 リクエストのクエリ文字列にセットして送信する関数です。
コードを簡潔とするために、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.");
}
}
3.2 - REST API の使用
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.");
}
}
4 - Google スプレッドシートの利用
spot-google-sheets
の解説です。4.1 - Google スプレッドシートの利用
spot-google-sheets
の解説です。なお、このスケッチでは ESP32 の Arduino 環境から FreeRTOS の機能を利用しています。本稿では、サードパーティのオープンソースソフトウェアを使用します。
サードパーティのソフトウェアについて、その詳しい使用方法を弊社からご案内することはいたしかねます。また、サードパーティのソフトウェアを使用されたことによるいかなる損害についても、弊社は一切の責任を負いません。
ソースコードの入手
GitHub (monowireless/spot-google-sheets) から入手できます。
システムの概要
TWELITE SPOT は、事前に作成したサービスアカウントを使って自動的にスプレッドシートを作成し、指定したユーザアカウントへ、そのファイルを共有します。
ユーザアカウントへログインすると、Google ドライブの「共有アイテム」ページから、TWELITE SPOT によって作成されたスプレッドシートを閲覧・編集できます。
TWELITE SPOT は、作成したスプレッドシートへデータ行を次々と追加していきます。
2023年5月現在、Google Sheets / Drive API の利用に追加料金はかかりません。
ただし、クエリの数などを定めた使用量上限(Sheets API / Drive API)があります。
例えば Google Sheets API では、リクエストを毎分60回以内に押さえなくてはなりません。超過したリクエストはエラーとなります。
サービスアカウントは、アプリケーションが使用するための Google アカウントです。
詳しくは Google による解説をご覧ください。
開発に必要なもの
-
無線LANゲートウェイ TWELITE SPOT
- 電源用 USB-C ケーブル
- USB AC アダプタ(1A 以上供給できるもの)
-
温湿度センサー無線タグ TWELITE ARIA (お持ちでない場合はご購入ください 👉 販売店一覧)
- CR2032 コイン電池
-
USBアダプター TWELITE R3 (お持ちでない場合はご購入ください 👉 販売店一覧)
- 通信用 USB-C ケーブル
- 💻 開発用コンピュータ
- Google アカウント
本稿では TWELITE ARIA を使用しますが、ソースコードを改変することで TWELITE CUE などの子機にも対応できます。
ただし、シートへ書き込むデータが増える場合は、API の 使用量上限 に気を配る必要があります。
環境整備
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
Drive API の有効化
TWELITE SPOT からスプレッドシートを共有するために、Drive API を有効化します。
下記のリンクにアクセスし、API を有効化してください。
https://console.cloud.google.com/apis/library/drive.googleapis.com
サービスアカウントの作成と設定
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
の内容を動作確認で使用します。
動作確認
まずは動作確認を行ってみましょう。
プロジェクトファイルを入手
- GitHub (monowireless/spot-google-sheets) から Zip ファイルをダウンロードします
- Zip ファイルを展開し、フォルダ名を
spot-google-sheets-main
からspot-google-sheets
に変更します - 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 へのスケッチの書き込み方法 を参考に、スケッチを書き込んでください。
ツール -> Flash Size の設定が 16MB
でないと正常に動きません。
パーティションテーブルには
paritions.csv
を使用しますが、全体のサイズが小さいと正しく書き込めないからです。
親機と子機を起動
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 からのデータを確認できるはずです。
Ctrl (⌘) + ↓
を押します。スケッチ解説
Arduino スケッチ spot-google-sheets.ino の解説です。
ライブラリのインクルード
Arduino および ESP32 公式ライブラリ
4-7行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。
#include <Arduino.h>
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
ヘッダファイル | 内容 | 備考 |
---|---|---|
Arduino.h | Arduino の基本ライブラリ | |
NTPClient.h | NTP を使う | ファイル名と受信時刻に使用 |
WiFi.h | ESP32 の WiFi を使う | |
WiFiUdp.h | UDP を使う | NTPClient に必要 |
サードパーティのライブラリ
10-11行目では、サードパーティのライブラリをインクルードしています。
#include <ESP_Google_Sheet_Client.h>
#include <TimeLib.h>
ヘッダファイル | 内容 | 備考 |
---|---|---|
ESP_Google_Sheet_Client.h | Google へアクセスする | |
TimeLib.h | UNIX 時間をフォーマットする |
MWings ライブラリ
14行目では、MWings ライブラリをインクルードしています。
#include <MWings.h>
ユーザ設定の定義
17行目では、config.h
をインクルードしています。
#include "config.h"
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_RST | TWELITE の RST ピンが接続されているピンの番号 |
TWE_PRG | TWELITE の PRG ピンが接続されているピンの番号 |
LED | 基板上の ESP32 用 LED が接続されているピンの番号 |
ESP_RXD1 | TWELITE の TX ピンが接続されているピンの番号 |
ESP_TXD1 | TWELITE の 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_CH | TWELITE の 周波数チャネル |
TWE_APPID | TWELITE の アプリケーション ID |
TWE_RETRY | TWELITE の 再送回数(送信時) |
TWE_POWER | TWELITE の 送信出力 |
シート関連の定義
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_ID | ARIA 用シートの ID |
ARIA_SHEET_TITLE | ARIA 用シートの名称 |
ARIA_BUFFER_PACKETS | ARIA からのパケットを格納するキューの長さ |
TWELITE ARIA 以外の子機に対応するときには、同一のスプレッドシート(ワークブック)に別のシートを作成することを推奨します。
したがって、ID、タイトル、キューの長さの3点を付け加える必要があります。
型の宣言
46-50行目では、型を宣言しています。
struct ParsedAppAriaPacketWithTime {
ParsedAppAriaPacket packet;
uint32_t elapsedMillis;
uint32_t unixTime;
};
名称 | 内容 |
---|---|
ParsedAppAriaPacketWithTime | 受信したパケットデータを受信時刻と合わせてキューへ格納するための型 |
elapsedMillis
:パケット受信時の起動からの経過時間(ミリ秒)unixTime
:パケット受信時の UNIX 時間(秒)です。
TWELITE ARIA 以外の子機に対応するときには、新たにキューを作成する必要があります。
したがって、ParsedAppAriaPacketWithTime
のようなキュー格納用の型についても付け加える必要があります。
グローバルオブジェクトの宣言
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
名称 | 内容 |
---|---|
ntpUDP | NTP のための UDP インタフェース |
timeClient | NTP のインタフェース |
spreadsheetIdString | 作成したスプレッドシートのID |
readyForNewRequests | 新たなリクエストを送信できる状態になったら true |
lastTimeRequestWasSent | 最後にリクエストを送信した時間 |
ariaPacketQueue | ARIA から受信したパケットと受信時刻を格納するキュー |
rowToAddNewAriaData | 次に ARIA から受信したデータを追加する行 |
ariaPacketQueue
は、Arduino core for the ESP32 のベースになっている FreeRTOS の Queue 機能を使います。関数プロトタイプの宣言
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() | シートへデータ行を追加する |
anotherLoop()
を作成し、そのなかで Twelite.update()
を実行しています。TWELITE ARIA 以外の子機に対応するときには、例えば、下記のような方針でリクエストの内容を追加します。
createSpreadsheet()
:スプレッドシートを増やすように改変するformatSheet()
:新たなシートもフォーマットするように改変するextendSheet()
:内容はそのまま、新たなシートに対しても呼び出すaddSheet**HeaderRow()
:関数ごと付け加える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.");
}
引数 | 型 | 内容 |
---|---|---|
Serial2 | HardwareSerial& | TWELITE との通信に使うシリアルポート |
LED | int | ステータス LED を接続したピンの番号 |
TWE_RST | int | TWELITE の RST ピンを接続したピンの番号 |
TWE_PRG | int | TWELITE の PRG ピンを接続したピンの番号 |
TWE_CHANNEL | uint8_t | TWELITE の 周波数チャネル |
TWE_APP_ID | uint32_t | TWELITE の アプリケーション ID |
TWE_RETRY | uint8_t | TWELITE の 再送回数(送信時) |
TWE_POWER | uint8_t | TWELITE の 送信出力 |
イベントハンドラの登録
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 子機として設定したうえで、指定のネットワークへ接続しています。
ネットワークに接続できるまで、while
文を出ません。
上記の実装では、5秒に一度、再接続を試みています。
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.");
}
デフォルトでは、A-Z 列のセルが 1,000 行続きます。ここでは列を A-J の 10 列に絞り、行数を 100倍の 100,000 行に増やしています。
それでも足りない場合には、さらに行を増やす必要があります。ただし、セルの上限は 10,000,000 個のため、 A-J の10 列の場合、最大 1,000,000 行に制限されます。それ以上はセルを増やせないため、新たなスプレッドシート(ワークブック)を作成する必要があります。
タスクの登録
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
}
},
vTaskDelay(1);
などを挿入し、ウォッチドッグタイマの入る隙を与えてください。タスクの名称は 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バイトずつ読み出す関数です。
loop()
内で繰り返し Twelite.update()
を呼ぶことで、TWELITE 親機から送信されるパケットデータの解釈が進みます。パケットデータの解釈を終えた際に 上記 のようなイベントが呼ばれる仕組みです。delay()
などの処理でこの関数の呼び出しをブロックすると、パケットデータ文字列の読み出しが間に合わないことがあります。しかし、このスケッチでは FreeRTOS の機能によって anotherLoop()
を切り離しているため、たとえ loop()
内で同期処理があっても問題ありません。スプレッドシートの更新
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 Cloud コンソール:ようこそ – SPOT-DEV – Google Cloud コンソール
- Sheets REST API リファレンス:Google Sheets API | Google for Developers
- Sheets REST サンプル:サンプル | Google Sheets | Google for Developers
Arduino
- 公式サイト:Arduino - Home
ESP32
- 製品情報:ESP32 Wi-Fi & Bluetooth MCU I Espressif Systems
- データシート:esp32_datasheet_en.pdf
- Arduino 向けツールチェイン:espressif/arduino-esp32: Arduino core for the ESP32
- スタートガイド:Getting Started — Arduino-ESP32 documentation
- 導入方法:Installing — Arduino-ESP32 documentation
- API リファレンス:Libraries — Arduino-ESP32 documentation
- Wi-Fi API:Wi-Fi API — Arduino-ESP32 documentation
- チュートリアル:Tutorials — Arduino-ESP32 documentation
- トラブルシューティング:Troubleshooting — Arduino-ESP32 documentation
- ESP-IDF:espressif/esp-idf: Espressif IoT Development Framework
- ドキュメント:ESP-IDF Programming Guide - ESP32
- APIリファレンス:FreeRTOS (ESP-IDF) - ESP32
- ドキュメント:ESP-IDF Programming Guide - ESP32
コミュニティ
ライブラリ
- Google Sheets / Drive API:mobizt/ESP-Google-Sheet-Client: Arduino Google Sheet REST client library for ESP8266, ESP32 and Raspberry Pi Pico RP2040
- 日付と時刻:PaulStoffregen/Time: Time library for Arduino