プリインストール済みスケッチ
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