/

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

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

コミュニティ