ガイド · 読了まで22分

ESP32の非同期WebサーバーとWebSocket:リアルタイムの操作パネル

ESP32の非同期WebサーバーとWebSocketで、ページを再読み込みせずにリレーを操作し、センサーの値をリアルタイムで確認します。セットアップ、PROGMEMとLittleFS、esp32.local、パスワード、よくあるエラーの対処法を紹介します。

ESPAsyncWebServerライブラリで作るESP32の非同期Webサーバーは、ESP32を、複数のスマホやパソコンに同時に応答できる高速なWebサーバーに変えます。ライブラリに含まれるWebSocketの機能を使うと、ブラウザとボードの間に常に開いた回線ができます。ページのボタンを押すとすぐにリレーがカチッと動き、温度が変わるとページの数字が再読み込みなしで自動的に更新されます。同じパネルを開いているほかの端末にも、変更は同じ1秒のうちに反映されます。つまりこの組み合わせは、クラウドサービスを使わずに、家のネットワークの中で自分だけのスマートホームのパネルを作る、最も実用的な方法です。

このガイドでは、まず非同期サーバーが従来のWebServerライブラリとどう違うのか、WebSocketがふつうのHTTPリクエストより速いのはなぜかを説明します。続いて必要なライブラリをインストールし、いちばんシンプルなサーバーを動かしてから、リレーとDHT22センサーを使ったリアルタイムの操作パネルを作ります。そのあと、HTMLをPROGMEMやLittleFSに置く方法、IPアドレスの代わりにesp32.localを使う方法、複数のクライアントの扱い方、パネルに簡単なパスワードをかける方法、よくあるコンパイルエラーや接続エラーの対処法を学びます。コードは、Arduino IDEのESP32コアの2.xと3.xのどちらでも動くように書いています。

Webサーバーのプロジェクトに使うESP32開発ボード
ESP32 DevKit:Wi-Fiチップを内蔵しているので、単体でWebサーバーを動かせます。スマホで開くパネルは、このボードのフラッシュメモリから直接送られてきます。

ESP32の非同期Webサーバーとは?その仕組みは?

ESP32コアに付属するWebServer.hライブラリは同期型です。届いたリクエストは、loop()の中でserver.handleClient()が呼ばれたときに、1つずつ順番に処理されるだけです。loop()に2秒のdelay()があればページも2秒待たされ、2人が同時に接続すると、片方はもう片方の処理が終わるまで待ちます。一方、ESPAsyncWebServerは、AsyncTCPライブラリが作る別のFreeRTOSタスク(async_tcp)で動きます。リクエストが届くとバックグラウンドでヘッダーが解析され、リクエストの準備ができた時点で自分で書いた関数(コールバック)が呼ばれ、レスポンスもバックグラウンドで送られます。その間もloop()は自分の処理を続けます。そのため複数の接続を同時に開いておくことができ、WebSocketやServer-Sent Events(SSE)のような常時接続も使えるようになります。ESP32の2つのコアとタスクについて知りたい場合は、「ESP32のFreeRTOS」の記事をご覧ください。

特徴WebServer.h(同期型)ESPAsyncWebServer(非同期型)
インストールESP32コアに付属ライブラリマネージャから2つのライブラリ
リクエストの処理loop()の中のhandleClient()で1つずつバックグラウンドのasync_tcpタスクで、複数を同時に
loop()の中のdelay()ページの応答が遅れるサーバーには影響しない
WebSocketなし。別のライブラリが必要(例:Markus Sattler氏のWebSocketsライブラリ)内蔵(AsyncWebSocket)
Server-Sent Events(SSE)なし内蔵(AsyncEventSource)
静的ファイルの配信serveStaticで配信serveStatic、.gzの自動対応、キャッシュの制御
注意点いちばん覚えやすいコールバックの中でdelay()や時間のかかる処理をしない

WebSocketとは?HTTPリクエストとの違いは?

ふつうのHTTPリクエストでは、ブラウザが接続を開き、「このページをください」と頼み、レスポンスを受け取ったところでやり取りが終わります。サーバーのほうから自発的にブラウザに何かを伝えることはできず、新しい値を見るにはブラウザがもう一度尋ねる必要があります。WebSocket(RFC 6455)は、HTTPとして始まるハンドシェイクです。ブラウザがリクエストにUpgrade: websocketヘッダーを付け、ESP32が101 Switching Protocolsのレスポンスで受け入れると、同じTCP接続が開いたままになります。そこから先は、どちらの側も好きなときにメッセージを送れます(全二重)。メッセージ1つあたりの付加データはわずか2〜14バイトですが、HTTPリクエストは毎回数百バイトのヘッダーを運びます。そのためWebSocketのメッセージは数ミリ秒で届き、1秒に何回も更新を送るパネルには最も効率のよい方法です。ページをセンサーの値で最新に保つ4つの方法を比べると、次のようになります。

方法仕組み遅延と負荷向いている場面
ページの再読み込み(meta refresh)ブラウザが数秒ごとにページ全体をダウンロードし直す大きい。毎回ページがちらつく避けたほうがよい
fetchによるポーリングJavaScriptが1〜2秒ごとに小さなJSONを要求する中程度。リクエストのたびに新しいヘッダーが付くめったに変わらない1つの値
Server-Sent Events(SSE)サーバーが開いた接続で一方向のイベントを送る小さい。サーバーからブラウザへの一方向のみ監視専用のダッシュボード
WebSocket開いた接続での双方向のメッセージのやり取り最も小さい。数バイトのフレームボタン、スライダー、リアルタイムのデータを組み合わせる場合

必要なライブラリ:ESPAsyncWebServerとAsyncTCPのインストール

  1. Arduino IDEにESP32のボードパッケージ(Espressif Systems)がインストールされていることを確認します。手順は「ESP32とは?」の記事にあります。
  2. ライブラリマネージャ(Ctrl+Shift+I)を開いてESPAsyncWebServerを検索します。作者がESP32AsyncのESP Async WebServerライブラリをインストールします。
  3. このライブラリは依存ライブラリをライブラリマネージャに登録していないので、IDEは追加のライブラリを提案しません。同じくESP32Asyncが公開しているAsync TCPライブラリを別にインストールしてください。ESP8266の場合は、代わりにESP32AsyncのESP Async TCPライブラリ(ESPAsyncTCP.h)が必要です。
  4. パネルの例のために、AdafruitのDHT sensor libraryと、それが求める依存ライブラリAdafruit Unified Sensorをインストールします。ライブラリのインストールが初めての場合は、「Arduinoにライブラリを追加する方法」のガイドをご覧ください。
  5. ドキュメント/Arduino/librariesフォルダに、以前ZIPで追加したESPAsyncWebServer-master、AsyncTCP-master、ESPAsyncWebSrvのフォルダがあれば削除します。同じライブラリが2つあると、コンパイラが間違ったほうを選んでしまいます。
ネット上の古いチュートリアルの多くは、me-no-devアカウントにある元のESPAsyncWebServerライブラリを紹介しています。このライブラリはもうメンテナンスされておらず、ESP32コアの3.xではコンパイルできません。ビルドは'mbedtls_md5_starts_ret' was not declared in this scope(宣言されていない)というエラーで止まります。古いコピーを削除して、ESP32Asyncの版をインストールすれば解決します。新しい版ではsend_P()も非推奨(deprecated)になりました。ESP32では、PROGMEMの配列をrequest->send(200, "text/html", index_html)でそのまま送れます。

最初のステップ:AsyncWebServerでいちばんシンプルなサーバー

パネルに進む前に、小さな例でライブラリの仕組みを見てみましょう。下のコードはESP32を家のネットワークにつなぎ、ホームページにあいさつを表示し、/led?durum=1と/led?durum=0のアドレスで内蔵LEDを点灯・消灯します(durumはトルコ語で「状態」)。書き込んだらシリアルモニタを115200ボーで開き、表示されたアドレスを同じWi-Fiにつないだスマホのブラウザに入力します。ボードがネットワークにつながらない場合は、「ESP32がWi-Fiにつながらないとき」のガイドにある診断用のコードを使ってください。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi";          // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123";   // Wi-Fi şifren / your Wi-Fi password
7const int LED_PIN = 2;               // çoğu DevKit'te dahili LED / onboard LED on most DevKits
8
9AsyncWebServer server(80);           // 80 = standart HTTP portu / standard HTTP port
10bool ledAcik = false;
11
12void setup() {
13  Serial.begin(115200);
14  pinMode(LED_PIN, OUTPUT);
15
16  WiFi.mode(WIFI_STA);
17  WiFi.begin(ssid, password);
18  while (WiFi.status() != WL_CONNECTED) {
19    delay(500);
20    Serial.print(".");
21  }
22  Serial.println();
23  Serial.print("Adres / address: http://");
24  Serial.println(WiFi.localIP());
25
26  // Ana sayfa / home page
27  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
28    request->send(200, "text/plain", "Merhaba, ben ESP32 / Hello from ESP32");
29  });
30
31  // /led?durum=1 yakar, /led?durum=0 söndürür / turns the LED on or off
32  server.on("/led", HTTP_GET, [](AsyncWebServerRequest *request) {
33    if (request->hasParam("durum")) {
34      ledAcik = request->getParam("durum")->value() == "1";
35      digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
36    }
37    request->send(200, "text/plain", ledAcik ? "LED: ON" : "LED: OFF");
38  });
39
40  // Tanımsız adresler / unknown addresses
41  server.onNotFound([](AsyncWebServerRequest *request) {
42    request->send(404, "text/plain", "Sayfa yok / Not found");
43  });
44
45  server.begin();
46}
47
48void loop() {
49  // Sunucu arka planda çalışır, loop() boş kalabilir / the server runs in the background
50}
命令働き
AsyncWebServer server(80)(サーバーの作成)HTTPの80番ポートで待ち受けるサーバーを作る
server.on("/led", HTTP_GET, ...)(ルートの登録)アドレスとリクエストの種類ごとに実行する関数(コールバック)を登録する
request->hasParam("durum")(パラメーターの確認)アドレスに?durum=...のパラメーターがあるかを調べる
request->getParam("durum")->value()(値の取得)パラメーターの値をStringとして返す
request->send(コード, 種類, 内容)レスポンスを送る。200は成功、404は見つからない。種類はtext/plain、text/html、application/jsonなど
server.onNotFound(...)(未定義のアドレス)定義されていないすべてのアドレスに対して実行される
server.begin()(サーバーの開始)サーバーを開始する。すべてのserver.on()の行のあとで呼ぶ
loop()が空でも間違いではありません。サーバーは自分のタスクで動いています。逆に、コールバックの中ではdelay()、長いwhileループ、インターネットからデータを取ってくるリクエストを使わないでください。その間async_tcpタスクが止まってしまい、数秒後にESP32がTask watchdog got triggered(ウォッチドッグの発動)というメッセージとともに再起動します。時間のかかる処理はフラグ変数でloop()に任せ、タイミングはmillis()で管理しましょう。

操作パネルの部品と配線

パネルの例では、リレーモジュールで電灯やファンをオン・オフし、DHT22で温度と湿度を測ります。必要なのは、ESP32 DevKit、1チャンネルの5 Vリレーモジュール、DHT22(またはDHT11)のセンサーモジュール、ブレッドボード、ジャンパーワイヤーです。リレーは、フォトカプラ付きのモジュールを選びましょう。DHT22は−40〜80 °Cを±0.5 °Cの精度で測れ、読み取りは2秒に1回までです。DHT11は安価ですが、精度は0〜50 °Cの範囲で±2 °Cです。センサーの詳細は「DHT11温湿度センサー」の記事を、ボードのピン配置は「ピン配置」ツールをご覧ください。

モジュールのピンESP32 DevKitESP8266 NodeMCUメモ
リレーのVCCVIN(5 V)VINまたはVU(5 V)リレーのコイルには5 Vが必要。USBから給電
リレーのGNDGNDGND共通GND
リレーのINGPIO 26D1(GPIO 5)ほとんどのモジュールはLOWの信号でオンになる
DHT22のVCC(+)3V33V33.3 Vで給電すると、データ線も3.3 Vになる
DHT22のDATA(OUT)GPIO 4D2(GPIO 4)センサー単体の場合は、DATAとVCCの間に10 kΩの抵抗
DHT22のGND(−)GNDGND共通GND
ESP32のピンの出力は3.3 Vです。5 Vのリレーモジュールの中にはこのレベルをHIGHと認識しないものがあり、その場合リレーがいつまでも戻りません。そのようなモジュールでは、3.3 V対応のリレー、JD-VCCジャンパー付きのモジュールを使うか、間にトランジスタを入れてください。リレーで商用電源(230 Vや120 V)を切り替える場合は、電源を切った状態で配線し、ネジ端子をむき出しにしないでください。自信がなければ、まず低電圧の負荷(LEDテープや12 Vのファン)で試しましょう(日本の商用電源は100V(50/60Hz)です)。詳しくは「リレーモジュールとは?」の記事をご覧ください。

リアルタイムの操作パネル:リレー、DHT22、WebSocket

いよいよメインのプロジェクトです。下のコードは1つのファイルで、HTML、CSS、JavaScriptはPROGMEMの配列としてコードの中に入っています。ページが開くと、ブラウザは/wsへのWebSocket接続を開きます。ESP32は2秒ごとにDHT22を読み取り、温度、湿度、リレーの状態、接続中の端末の数を1つのJSONメッセージにまとめて、すべてのブラウザに送ります。ボタンを押すとブラウザがtoggleというメッセージを送り、ESP32はリレーを切り替えて、新しい状態をまた全員に知らせます。ネットワーク名とパスワードを書き換えて書き込み、ブラウザでhttp://esp32.localかシリアルモニタに表示されたIPアドレスを開きます。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <ESPmDNS.h>
5#include <DHT.h>
6
7const char* ssid = "AgAdi";          // Wi-Fi adın / your Wi-Fi name
8const char* password = "Sifre123";   // Wi-Fi şifren / your Wi-Fi password
9
10const int ROLE_PIN = 26;             // röle modülü IN / relay module IN
11const bool ROLE_AKTIF_LOW = true;    // çoğu modül LOW ile çeker / most modules switch on LOW
12const int DHT_PIN = 4;               // DHT22 veri pini / DHT22 data pin
13
14DHT dht(DHT_PIN, DHT22);             // DHT11 için DHT11 yaz / write DHT11 for a DHT11
15AsyncWebServer server(80);
16AsyncWebSocket ws("/ws");            // WebSocket adresi / WebSocket path
17
18bool roleAcik = false;
19float sicaklik = NAN;
20float nem = NAN;
21volatile bool gonder = false;        // loop() yayın yapsın mı? / should loop() broadcast?
22unsigned long sonOlcum = 0;
23unsigned long sonTemizlik = 0;
24
25// Tarayıcıya giden sayfa / the page sent to the browser
26const char index_html[] PROGMEM = R"rawliteral(
27<!DOCTYPE html>
28<html lang="tr">
29<head>
30<meta charset="utf-8">
31<meta name="viewport" content="width=device-width, initial-scale=1">
32<title>ESP32 Panel</title>
33<style>
34body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;text-align:center;margin:0;padding:16px}
35.kart{background:#1e293b;border-radius:12px;padding:16px;margin:12px auto;max-width:320px}
36.deger{font-size:2.4em;font-weight:bold}
37button{font-size:1.2em;padding:14px 32px;border:0;border-radius:8px;color:#fff;width:100%}
38.acik{background:#16a34a}.kapali{background:#475569}
39</style>
40</head>
41<body>
42<h2>ESP32 Panel</h2>
43<p id="baglanti">Bağlanıyor / Connecting...</p>
44<div class="kart">Sıcaklık / Temperature<div class="deger"><span id="sicaklik">--</span> &deg;C</div></div>
45<div class="kart">Nem / Humidity<div class="deger"><span id="nem">--</span> %</div></div>
46<div class="kart">Röle / Relay<p><button id="dugme" class="kapali" onclick="degistir()">KAPALI / OFF</button></p></div>
47<p>Bağlı cihaz / Clients: <span id="istemci">0</span></p>
48<script>
49let ws;
50const $ = (id) => document.getElementById(id);
51function baglan() {
52  ws = new WebSocket('ws://' + location.host + '/ws');
53  ws.onopen = () => { $('baglanti').textContent = 'Bağlı / Connected'; };
54  ws.onclose = () => {
55    $('baglanti').textContent = 'Koptu, yeniden deneniyor / Reconnecting...';
56    setTimeout(baglan, 2000);
57  };
58  ws.onmessage = (e) => {
59    const d = JSON.parse(e.data);
60    $('sicaklik').textContent = d.sicaklik === null ? '--' : d.sicaklik.toFixed(1);
61    $('nem').textContent = d.nem === null ? '--' : d.nem.toFixed(0);
62    $('istemci').textContent = d.istemci;
63    $('dugme').textContent = d.role ? 'AÇIK / ON' : 'KAPALI / OFF';
64    $('dugme').className = d.role ? 'acik' : 'kapali';
65  };
66}
67function degistir() {
68  if (ws.readyState === WebSocket.OPEN) ws.send('toggle');
69}
70baglan();
71</script>
72</body>
73</html>
74)rawliteral";
75
76void roleAyarla(bool acik) {
77  roleAcik = acik;
78  digitalWrite(ROLE_PIN, ROLE_AKTIF_LOW ? !acik : acik);
79}
80
81// Durumu JSON metnine çevir / build the state as JSON text
82String durumJson() {
83  String s = "{\"role\":";
84  s += roleAcik ? "true" : "false";
85  s += ",\"sicaklik\":";
86  s += isnan(sicaklik) ? String("null") : String(sicaklik, 1);
87  s += ",\"nem\":";
88  s += isnan(nem) ? String("null") : String(nem, 0);
89  s += ",\"istemci\":";
90  s += (unsigned long)ws.count();
91  s += "}";
92  return s;
93}
94
95// WebSocket olayları (async_tcp görevinde çalışır) / WebSocket events (run in the async_tcp task)
96void wsOlay(AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
97            void *arg, uint8_t *data, size_t len) {
98  switch (type) {
99    case WS_EVT_CONNECT:
100      Serial.printf("Istemci #%u baglandi / connected: %s\n",
101                    (unsigned)client->id(), client->remoteIP().toString().c_str());
102      gonder = true;                 // yeni gelen güncel durumu alsın / newcomer gets the state
103      break;
104    case WS_EVT_DISCONNECT:
105      Serial.printf("Istemci #%u ayrildi / disconnected\n", (unsigned)client->id());
106      gonder = true;
107      break;
108    case WS_EVT_DATA: {
109      AwsFrameInfo *info = (AwsFrameInfo *)arg;
110      // Tek parça, tam bir metin mesajı mı? / a complete single-frame text message?
111      if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
112        if (len == 6 && memcmp(data, "toggle", 6) == 0) {
113          roleAyarla(!roleAcik);
114          gonder = true;             // tüm ekranlar eşitlensin / sync every screen
115        }
116      }
117      break;
118    }
119    default:                         // PONG, ERROR vb. / PONG, ERROR etc.
120      break;
121  }
122}
123
124void setup() {
125  Serial.begin(115200);
126  pinMode(ROLE_PIN, OUTPUT);
127  roleAyarla(false);                 // açılışta röle kapalı / relay off at start
128  dht.begin();
129
130  WiFi.mode(WIFI_STA);
131  WiFi.begin(ssid, password);
132  Serial.print("Baglaniyor / connecting");
133  while (WiFi.status() != WL_CONNECTED) {
134    delay(500);
135    Serial.print(".");
136  }
137  WiFi.setSleep(false);              // daha hızlı tepki / lower latency
138  Serial.println();
139  Serial.print("http://");
140  Serial.println(WiFi.localIP());
141
142  if (MDNS.begin("esp32")) {         // http://esp32.local
143    MDNS.addService("http", "tcp", 80);
144    Serial.println("http://esp32.local");
145  }
146
147  ws.onEvent(wsOlay);
148  server.addHandler(&ws);
149
150  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
151    request->send(200, "text/html", index_html);
152  });
153  server.onNotFound([](AsyncWebServerRequest *request) {
154    request->send(404, "text/plain", "Sayfa yok / Not found");
155  });
156  server.begin();
157}
158
159void loop() {
160  if (millis() - sonOlcum >= 2000) {       // DHT22 en fazla 2 sn'de bir / DHT22: once per 2 s
161    sonOlcum = millis();
162    float t = dht.readTemperature();
163    float h = dht.readHumidity();
164    if (!isnan(t) && !isnan(h)) {          // hatalı okumayı atla / skip failed reads
165      sicaklik = t;
166      nem = h;
167    }
168    gonder = true;
169  }
170
171  if (gonder) {
172    gonder = false;
173    ws.textAll(durumJson());               // tüm tarayıcılara yolla / send to every browser
174  }
175
176  if (millis() - sonTemizlik >= 1000) {
177    sonTemizlik = millis();
178    ws.cleanupClients();                   // kopuk istemcileri temizle / drop stale clients
179  }
180}

コードの仕組み

  • AsyncWebSocket ws("/ws")でWebSocketのエンドポイントを作り、server.addHandler(&ws)でサーバーに登録します。JavaScriptのアドレスは、このパスと完全に一致している必要があります。
  • wsOlay()関数(olayはトルコ語で「イベント」)は、接続、切断、メッセージの受信のたびに呼ばれます。WS_EVT_CONNECTは新しいクライアント、WS_EVT_DISCONNECTはクライアントの離脱、WS_EVT_DATAはメッセージの受信を表します。
  • AwsFrameInfoは、受信したフレームの情報を持っています。final、index == 0、lenを確認すると、メッセージが1つのフレームで欠けずに届いたことが、WS_TEXTを確認するとテキストであることがわかります。短い命令は必ず1つのフレームに収まります。
  • 受信したデータはmemcmp()で長さも含めて比較するので、データの最後にヌル文字があるかどうかに左右されません。
  • コールバックはasync_tcpタスクで動くので、リレーを切り替えてgonderフラグ(gonderはトルコ語で「送信」)を立てるだけにしています。実際の送信はloop()の中のws.textAll()で行うので、すべてのメッセージが1か所から順番に送られます。
  • ws.cleanupClients()は1秒に1回呼びます。切れた接続のメモリを解放し、クライアントの数が上限を超えたときにいちばん古い接続を閉じます。
  • JavaScript側では、oncloseイベントで2秒後に再接続します。ESP32が再起動したり、Wi-Fiが一瞬切れたりしても、ページは自動的に復帰します。アドレスをlocation.hostから組み立てているので、ページはIPアドレスでもesp32.localでも動きます。
  • WiFi.setSleep(false)は、Wi-Fiの省電力機能をオフにします。標準のモデムスリープでは、応答に最大で数百ミリ秒の遅れが出ることがあります。コンセントから給電するパネルでこれをオフにすると、ボタンがすぐに反応するようになります。

メッセージの形式にJSONを選ぶと、処理が簡単になります。ブラウザはJSON.parse()を使って、受信したテキストを1行でオブジェクトに変換できます。メッセージの例は{"role":true,"sicaklik":23.4,"nem":48,"istemci":2}のようになります(トルコ語のキーで、roleは「リレー」、sicaklikは「温度」、nemは「湿度」、istemciは「クライアント」)。センサーが読み取れないときは値をnullとして送り、ページには「--」と表示されます。読み取りの失敗がNaNとして送られると、JSONが無効になってページがまったく更新されなくなります。メッセージ1つあたりのデータは約60バイトなので、1秒に何回か更新してもネットワークの負担になりません。もっと複雑なメッセージにはArduinoJsonライブラリに切り替えることもできますが、この規模のパネルならStringをつなげるだけで十分です。リレーの代わりにLEDやブザーをつないで、同じコードをそのまま試すこともできます。

HTMLはどこに置く?PROGMEMとLittleFS

パネルのコードでは、ページはR"rawliteral( ... )rawliteral"という形の生文字列リテラルに入っています。この書き方なら、エスケープ文字を使わずに引用符や改行を書けます。AVRベースのArduinoでは、テキストをRAMではなくフラッシュに置くためにPROGMEMが必要です。ESP32では定数の配列はもともとフラッシュから読まれるので、PROGMEMは互換性のために書いているだけで、害はありません。ページが大きくなると、コードの中でHTMLを編集するのが大変になります。そうなったら、ファイルをフラッシュ内のLittleFSファイルシステムに移すほうがすっきりします。

比較の観点PROGMEM(コードの中)LittleFS(ファイルシステム)
準備追加の手順なし、ファイルは1つdataフォルダと書き込み用のプラグインが必要
編集HTMLはC++の配列の中index.html、style.css、app.jsを別々のファイルにできる
更新変更のたびにスケッチを書き込み直すファイルシステムだけを書き込み直す
サイズ数KBのページに最適標準のパーティション構成で約1.5 MBの領域
画像とフォント実用的ではないアイコン、画像、.gzファイルを配信できる
使う場面最初のテストや小さなパネル複数ページの、または大きくなっていくUI

LittleFSでHTML、CSS、JavaScriptのファイルを配信する

  1. スケッチのフォルダ(.inoファイルがあるフォルダ)の中にdataという名前のフォルダを作り、パネルのコードのR"rawliteral(と)rawliteral"の間にあるHTMLをdata/index.htmlとして保存します。必要なら、CSSとJavaScriptを別のファイルに分けてもかまいません。
  2. Arduino IDE 2は、以前の「ESP32 Sketch Data Upload」メニューに対応していません。代わりにarduino-littlefs-uploadプラグインの.vsixファイルをダウンロードし、WindowsならC:\Users\(ユーザー名)\.arduinoIDE\plugins、macOSとLinuxなら~/.arduinoIDE/pluginsに置き(フォルダがなければ作成)、IDEを再起動します。このプラグインにはIDE 2.2.1以降が必要です。
  3. シリアルモニタが開いていれば閉じ、Ctrl+Shift+P(macOSではCmd+Shift+P)でコマンドパレットを開いて、Upload LittleFS to Pico/ESP8266/ESP32(LittleFSを書き込む)を選びます。ファイルがフラッシュのファイルシステム用パーティションに書き込まれます。
  4. 「ツール」>「Partition Scheme」で、ファイルシステム用のパーティションがある構成(たとえば「Default 4MB with spiffs」)が選ばれていることを確認します。LittleFSは、spiffsという名前のこのパーティションを使います。
  5. 下のコードをいつもどおり書き込みます。「ツール」メニューの「Erase All Flash」がオフになっていれば、スケッチを書き込んでもファイルシステムは消えません。ファイルを変更したときは、ステップ3だけを繰り返せば済みます。
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <LittleFS.h>
5
6const char* ssid = "AgAdi";          // Wi-Fi adın / your Wi-Fi name
7const char* password = "Sifre123";   // Wi-Fi şifren / your Wi-Fi password
8
9AsyncWebServer server(80);
10
11void setup() {
12  Serial.begin(115200);
13
14  if (!LittleFS.begin(true)) {       // true: bağlanamazsa biçimlendir / format if mount fails
15    Serial.println("LittleFS hatasi / LittleFS error");
16    return;
17  }
18
19  WiFi.mode(WIFI_STA);
20  WiFi.begin(ssid, password);
21  while (WiFi.status() != WL_CONNECTED) {
22    delay(500);
23  }
24  Serial.println(WiFi.localIP());
25
26  // data/ klasörünün içeriğini sun / serve the contents of the data/ folder
27  server.serveStatic("/", LittleFS, "/")
28        .setDefaultFile("index.html")    // varsayılan index.htm'dir / default is index.htm
29        .setCacheControl("max-age=600"); // tarayıcı 10 dk önbelleğe alsın / cache for 10 min
30
31  server.onNotFound([](AsyncWebServerRequest *request) {
32    request->send(404, "text/plain", "Sayfa yok / Not found");
33  });
34  server.begin();
35}
36
37void loop() {
38}
ファイルが要求されると、serveStatic()はまず同じ名前で拡張子が.gzの圧縮版を探します。index.htmlをgzipで圧縮してindex.html.gzとして書き込むと、ページのサイズはたいてい3分の1から4分の1に減り、読み込みが速くなります。フォルダのアドレスが要求されたときの標準のファイル名はindex.htmなので、setDefaultFile("index.html")の行を忘れないでください。ライブラリのテンプレート機能を使う場合は、ページ内の%DEGER%のようなプレースホルダーが置き換えられます(degerはトルコ語で「値」)。その場合、CSSのwidth:100%のようなパーセント記号は%%と書く必要があります。

mDNS:IPアドレスの代わりにesp32.local

ルーターは起動のたびにESP32に違うIPアドレスを割り当てることがあり、192.168.1.47のような数字は覚えにくいものです。mDNS(マルチキャストDNS)を使うと、端末がローカルネットワーク上で自分の名前を知らせることができます。パネルのコードにあるMDNS.begin("esp32")の行のおかげで、同じネットワークの端末からhttp://esp32.localでページを開けます。また、MDNS.addService("http", "tcp", 80)の行は、ネットワークをブラウズするアプリに、ここにWebサーバーがあることを知らせます。名前に「.local」を付けず、使うのは小文字の英字、数字、ハイフンだけにしてください。ESP32を何台も使う場合は、それぞれに違う名前(kitchen、living-roomなど)を付けましょう。同じ名前の端末が2台あると互いに干渉します。mDNSの対応状況はシステムによって異なります。

システム.localへの対応動かない場合
iPhone、iPad、macOS標準で対応(Bonjour)同じWi-Fiネットワークにつながっているか確認する
Windows 10と11最新の版なら通常は動くBonjourサービスをインストールするか、IPアドレスを使う
Linuxavahi-daemonがインストールされていれば動くavahi-daemonとnss-mdnsのパッケージをインストールする
Androidバージョンやブラウザによって異なるIPアドレスを使うか、ルーターでDHCPの予約を設定する

複数のクライアント:スマホ、タブレット、パソコンから同時に

WebSocketのいちばん喜ばれる点は、パネルを開いた全員が同じ状態を見られることです。スマホでリレーをオンにすると、ESP32はtextAll()で新しい状態をすべての接続に送り、パソコンのタブも同じ瞬間に緑に変わります。タブは1つ1つが別のクライアントで、同じブラウザでタブを3つ開くとカウンターは3を示します。ライブラリが標準で許可するWebSocketクライアントの同時接続数は、ESP32で8、ESP8266で4です(DEFAULT_MAX_WS_CLIENTS)。この上限は自動的には適用されず、cleanupClients()を呼んだときに上限を超えた分のいちばん古い接続が閉じられます。ほかにも次の点に注意してください。

  • スマホがスリープしたとき:画面が消えたスマホは、接続を正しく閉じないことがあります。こうした中途半端に残った接続は、cleanupClients()と定期的なデータ送信によって、時間がたてば片付きます。
  • 送信の頻度:キューで待てるメッセージは、クライアント1つにつき最大32個(ESP8266では8個)です。loop()の周回のたびにtextAll()を呼ぶとキューがいっぱいになり、新しいメッセージは何も知らせずに捨てられます。「ツール」>「Core Debug Level」をWarn以上の詳しいレベルにすると、シリアルモニタにToo many messages queued(キューのメッセージが多すぎる)という警告が表示されます。値は変わったときだけ、または1秒に数回だけ送りましょう。
  • 新しく接続したクライアント:接続したページにはすぐに現在の状態を送りましょう。そうしないと、最初の測定まで空のパネルを見続けることになります。パネルのコードでは、WS_EVT_CONNECTの中で立てるフラグがこれを行っています。
  • 1つのクライアントだけに返信:全員ではなく、尋ねてきたブラウザだけに答えるにはclient->text("...")を使います。クライアントの番号はclient->id()で取得できます。
  • メモリ:開いている接続は、それぞれTCPソケットとバッファ用のメモリを使います。ESP32の520 KBのRAMは数台のクライアントには十分すぎるほどですが、ESP.getFreeHeap()でときどき空きメモリを表示しておくと、メモリリークを早く見つけられます。

簡単なパスワード保護

家のネットワークにいる人なら、パネルのアドレスを知っていれば誰でもリレーを操作できてしまいます。ライブラリのrequest->authenticate()とrequest->requestAuthentication()関数を使うと、ページにユーザー名とパスワードを設定できます。ブラウザは専用のログイン画面を開き、セッションの間は入力した情報を覚えています。標準の方式はDigest認証なので、パスワードが平文のままネットワークを流れることはありません。ただし、ページを守るだけでは不十分です。WebSocketのアドレスは別の入口なので、ws://ip/wsに直接つながれるおそれがあります。下の例ではこの入口もふさぎます。ESP32は起動のたびにランダムなキーを作り、ログインしたブラウザにだけ/anahtar(anahtarはトルコ語で「鍵」)のアドレスでそのキーを渡し、setFilter()で正しいキーを持たないWebSocket接続を拒否します。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi";          // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123";   // Wi-Fi şifren / your Wi-Fi password
7const char* KULLANICI = "admin";     // panel kullanıcı adı / panel username
8const char* PAROLA = "Gizli-2026";   // panel parolası / panel password
9const int LED_PIN = 2;
10
11AsyncWebServer server(80);
12AsyncWebSocket ws("/ws");
13String wsAnahtar;                    // her açılışta yeni anahtar / a new key on every boot
14bool ledAcik = false;
15
16const char index_html[] PROGMEM = R"rawliteral(
17<!DOCTYPE html>
18<html><head><meta charset="utf-8">
19<meta name="viewport" content="width=device-width, initial-scale=1"></head>
20<body style="font-family:sans-serif;text-align:center">
21<h2>Korumali panel / Protected panel</h2>
22<button style="font-size:1.4em;padding:12px 32px" onclick="ws && ws.send('toggle')">LED</button>
23<p id="durum">...</p>
24<script>
25let ws;
26fetch('/anahtar').then(r => r.text()).then(k => {
27  ws = new WebSocket('ws://' + location.host + '/ws?k=' + k);
28  ws.onmessage = (e) => { document.getElementById('durum').textContent = e.data; };
29});
30</script>
31</body></html>
32)rawliteral";
33
34bool girisVar(AsyncWebServerRequest *request) {
35  return request->authenticate(KULLANICI, PAROLA);
36}
37
38void setup() {
39  Serial.begin(115200);
40  pinMode(LED_PIN, OUTPUT);
41
42  WiFi.mode(WIFI_STA);
43  WiFi.begin(ssid, password);
44  while (WiFi.status() != WL_CONNECTED) {
45    delay(500);
46  }
47  Serial.println(WiFi.localIP());
48
49  // Wi-Fi açıkken random() donanım üretecini kullanır / random() uses the hardware RNG
50  wsAnahtar = String(random(0x7FFFFFFF), HEX) + String(random(0x7FFFFFFF), HEX);
51
52  // Sayfa: tarayıcı kullanıcı adı ve parola sorar / page: the browser asks for login
53  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
54    if (!girisVar(request)) return request->requestAuthentication();
55    request->send(200, "text/html", index_html);
56  });
57
58  // Anahtar yalnız giriş yapana verilir / the key is given only after login
59  server.on("/anahtar", HTTP_GET, [](AsyncWebServerRequest *request) {
60    if (!girisVar(request)) return request->requestAuthentication();
61    request->send(200, "text/plain", wsAnahtar);
62  });
63
64  // Doğru anahtarı getirmeyen bağlantıyı reddet / reject sockets without the right key
65  ws.setFilter([](AsyncWebServerRequest *request) {
66    return request->hasParam("k") && request->getParam("k")->value() == wsAnahtar;
67  });
68
69  ws.onEvent([](AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
70                void *arg, uint8_t *data, size_t len) {
71    if (type == WS_EVT_DATA && len == 6 && memcmp(data, "toggle", 6) == 0) {
72      ledAcik = !ledAcik;
73      digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
74      sunucu->textAll(ledAcik ? "LED: ON" : "LED: OFF");
75    }
76  });
77
78  server.addHandler(&ws);
79  server.begin();
80}
81
82void loop() {
83  ws.cleanupClients();               // saniyede bir yeterli / once per second is enough
84  delay(1000);
85}
この保護は家のネットワーク用の鍵としては十分ですが、暗号化ではありません。ESPAsyncWebServerはESP32でHTTPSに対応しておらず、WebSocketのメッセージは平文で送られます。パスワードはコードの中にあるので、コードをシェアする前に忘れずに消してください。ルーターのポート転送でパネルをインターネットに公開するのはやめましょう。外から操作したいなら、「ESP32のTelegramボット」、「MQTTとHome Assistant」、またはルーターのVPN機能のほうがずっと安全です。新しい版のライブラリでは、AsyncAuthenticationMiddlewareクラスでも同じことができます。

ESP8266とRaspberry Pi Pico W向けのメモ

同じライブラリはESP8266(NodeMCU、Wemos D1 mini)でも動きます。変更点は次のとおりです。Async TCPの代わりにESP32AsyncのESP Async TCPライブラリをインストールし、WiFi.hの代わりにESP8266WiFi.h、AsyncTCP.hの代わりにESPAsyncTCP.h、ESPmDNS.hの代わりにESP8266mDNS.hをインクルードして、loop()の中にMDNS.update()の行を加えます。WiFi.setSleep(false)はWiFi.setSleepMode(WIFI_NONE_SLEEP)に置き換え、ピンは配線表のNodeMCUの列に合わせて設定します。ESP8266で使えるRAMは約80 KBしかないので、ページは小さく保ち、クライアントの上限が4であることを忘れないでください。Raspberry Pi Pico Wを使う場合は、MicroPythonでWebサーバーを作る方法を「Raspberry Pi Pico WのWi-Fi」のガイドで紹介しています。ESPAsyncWebServerの新しい版は、Arduino側でRP2040とRP2350のチップにも対応しています。

ESP32のWebサーバーを使ったプロジェクトのアイデア

  • マルチチャンネルのスマートコンセント:4チャンネルのリレーボードを使い、電灯、ファン、コンセント用に別々のボタンを作ります。命令は「r1」「r2」のような短いテキストで区別します。リレーの選び方は「リレーモジュール」の記事をご覧ください。
  • 植物の水やりパネル:土壌水分センサーの値をリアルタイムで表示し、水やりのしきい値をページのスライダーで設定し、必要なら手動でポンプを動かします。
  • 気象ステーション:DHT22の代わりにBME280を使って気圧も加え、直近60回の測定値をページに小さな折れ線グラフで表示します。
  • カラーピッカーのライト:ページのカラーピッカーの値をWebSocketで送り、WS2812B NeoPixelのテープを即座にその色に染めます。
  • スライダーで動くサーボ:スライダーを動かすたびにその値を送り、サーボモーターを回します。カメラの向きの調整やドアのかんぬきに最適です。
  • タイムスタンプ付きの記録:各測定値にNTPで取得した時刻を付け、コードは毎回USBで書き込む代わりにOTAで無線で更新します。

よくあるエラーと対処法

症状原因対処法
エラー:'mbedtls_md5_starts_ret' was not declared in this scope(宣言されていない)古いme-no-devのライブラリとESP32コア3.xの組み合わせESP32AsyncのESP Async WebServerとAsync TCPをインストールし、古いフォルダを削除する
エラー:Multiple libraries were found for "AsyncTCP.h"(ライブラリが複数見つかった)同じライブラリが2つあるlibrariesフォルダにある古い-masterのコピーを削除する
send_Pに非推奨(deprecated)の警告が出る新しい版ではsend_Pが廃止される予定request->send(200, "text/html", index_html)を使う
ページがまったく開かないスマホがゲストネットワークやモバイルデータ通信につながっている、またはIPアドレスが変わった同じWi-Fiにつなぎ、シリアルモニタに表示された現在のIPアドレスを使う
ページは開くが、値が「--」のままWebSocketが接続できていない。パスが違う、またはaddHandlerを忘れているF12でコンソールを開き、ws("/ws")とJavaScriptのアドレスを一致させる
async_tcpでTask watchdog got triggered(ウォッチドッグの発動)コールバックの中のdelay()、長いループ、HTTPリクエストコールバックは短くし、処理はフラグでloop()に移す
警告:Too many messages queued(キューのメッセージが多すぎる)textAll()を呼ぶ回数が多すぎる変わったときだけ、または1秒に数回だけ送る
LittleFSのページが404になるdataフォルダが書き込まれていない、またはsetDefaultFileがないプラグインでファイルを書き込み、setDefaultFile("index.html")を追加する
シリアルモニタに「does not exist, no permits for creation」(ファイルが存在しない)要求されたファイルがLittleFSにないファイル名と大文字・小文字を確認する。.gzを含む行は問題ない
リレーがいつまでもオフにならない5 Vのモジュールが3.3 VのHIGHを認識しない3.3 V対応のモジュール、JD-VCC、またはトランジスタを使う
リレーが動くとESP32がリセットされるコイルの電流で電源電圧が下がる(ブラウンアウト)しっかりした5 VのACアダプターと470 µFのコンデンサを使う
esp32.localが開かない端末がmDNSに対応していない、または名前に.localを付けて書いたMDNS.begin("esp32")と書き、必要ならIPアドレスを使う
ボタンの反応が遅いWi-Fiのモデムスリープがオンになっている接続後にWiFi.setSleep(false)を呼ぶ
うまく動かないときは、ブラウザでF12を押して開発者ツールを開きます。「コンソール」タブにはWebSocket connection to 'ws://192.168.1.50/ws' failedのようなエラーが表示されます。「ネットワーク」タブでWebSocketの接続をクリックすると、「メッセージ」の欄で送受信されたすべてのメッセージを確認できます。ESP32側では、クライアントの接続と切断をシリアルモニタに表示しておくと、問題がブラウザ側にあるのかボード側にあるのかが数秒でわかります。リレーが動いた瞬間にボードが再起動する場合は、「ESP32のブラウンアウト」のガイドをご覧ください。

ESP32の非同期WebサーバーとWebSocketの組み合わせは、クラウドサービスもスマホアプリの開発も必要なく、複数の端末で共有できる高速な操作パネルを作るいちばんの近道です。まずは1ファイルのPROGMEMのパネルから始め、UIが大きくなったらLittleFSに移り、ネットワークに新しい端末を加えるたびにそれぞれ独自のmDNS名を付けましょう。ESP32が初めての場合は、基本を「ESP32とは?」の記事で紹介しています。BlueGraysアプリでは、非同期Webサーバー、WebSocketカウンター、mDNSでのアクセス、NTP時計、OTAアップデートなどのESP32のプロジェクトを、回路図とすぐに使えるコードつきでステップごとに試せます。

よくある質問

ESP32のESPAsyncWebServerとWebServerの違いは何ですか?

WebServerライブラリは、loop()の中のhandleClient()の呼び出しでリクエストを1つずつ処理するので、loop()が遅くなるとページも遅くなります。ESPAsyncWebServerは、バックグラウンドの別タスクでリクエストを処理し、複数の接続を同時にさばけて、WebSocketとSSEの機能も最初から備えています。リアルタイムのパネルには、非同期版のほうが向いています。

ESP32のWebサーバーには何台まで同時に接続できますか?

ESPAsyncWebServerは標準で、ESP32では8台までのWebSocketクライアントを許可し、cleanupClients()が呼ばれたときに超えた分を閉じます。ESP8266では上限は4台です。通常のページのリクエストは短時間で終わるので、もっと多くの端末が順番にページを開けます。実際には、家庭用のパネルなら3〜5台の同時接続で問題なく動きます。

ESP32のWebサーバーにインターネットからアクセスできますか?

ルーターでポート転送を設定すれば技術的には可能ですが、おすすめしません。ライブラリはESP32でHTTPSに対応しておらず、端末がインターネットからの攻撃にさらされたままになるからです。外から操作したいなら、Telegramボット、MQTTを使ったシステム、またはルーターのVPN機能のほうがずっと安全です。

esp32.localが使えないのはなぜですか?

最もよくある理由は、スマホやパソコンがmDNSの名前を解決できないことで、特に一部のAndroidスマホや古いWindowsで起こります。また、コードにMDNS.begin("esp32.local")と書くのも間違いです。名前は.localを付けずに指定します。それでも名前が解決できない場合は、シリアルモニタに表示されたIPアドレスを使うか、ルーターでESP32に固定IPを予約しましょう。

ESP32のWebサーバーはルーターなしで動きますか?

はい。WiFi.softAP("ESP32-Panel", "12345678")を使うと、ESP32が自分のWi-Fiネットワークを作ります。スマホをそのネットワークにつなぎ、ブラウザで192.168.4.1を開きます。このアクセスポイントモードではインターネットは使えませんが、パネルとWebSocketはまったく同じように動きます。ネットワークのパスワードは8文字以上にする必要があります。

ESPAsyncWebServerはどのライブラリをインストールすればいいですか?

Arduinoのライブラリマネージャで、作者がESP32AsyncのESP Async WebServerとAsync TCPのライブラリをインストールします。me-no-devアカウントにある元の版はもうメンテナンスされておらず、ESP32コア3.xではコンパイルできません。ZIPで追加した古いコピーがあれば、librariesフォルダから削除してください。

全プロジェクトを見る
ESP32非同期Webサーバー(LCDにIPアドレスを表示)
#129上級

ESP32非同期Webサーバー(LCDにIPアドレスを表示)

このプロジェクトでは、ESP32を同じWi-Fiネットワーク上のスマートフォンやパソコンから操作できる非同期Webサーバーにします。ESPAsyncWebServerライブラリを使うと、従来のWebServer.hのように1つのリクエストで処理がブロックされず、複数のリクエストを同時に処理できます。ブラウザーでESP32のIPアドレスを開き、ON/OFFボタン(/onと/offのルート)を押すと、GPIO 5に接続したLEDを点灯・消灯できます。

4.8(16)
26
ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)
#130上級
プレミアム

ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)

このプロジェクトでは、ESP32に接続した2つのボタンでカウンターの値を変更します(GPIO 5で増加、GPIO 4で減少)。値は16x2 I2C LCDに即座に反映され、ESPAsyncWebServerのWebSocket機能により、接続中のすべてのブラウザーでもページを再読み込みせずに更新されます。HTTPポーリングの代わりに1つの接続を維持し、ESP32がws.textAll()で新しい値を全員に送信します。

4.5(14)
23
ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)
#131上級
プレミアム

ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)

このプロジェクトでは、ESP32にmDNS(Multicast DNS)サービスを追加し、IPアドレスの代わりにbluegrays-esp.localのような覚えやすいホスト名でアクセスできるようにします。DHCPで割り当てられるIPアドレスが変わっても、同じ名前でデバイスにアクセスできます。小さなWebサーバーも動かし、ブラウザーでhttp://bluegrays-esp.local/を開いてLEDを操作します。I2C LCDディスプレイにはホスト名、IPアドレス、LEDの状態をトルコ語と英語で表示します。iOS、macOS、LinuxはmDNSに標準対応しています。

4.8(14)
23
ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
#127上級

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)

このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。

4.4(21)
34
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

ESP32 DHT22温湿度計(LCDディスプレイ付き)

DHT22センサーで周囲の温度と湿度を測定し、16x2 I2C LCDにリアルタイムで表示するプロジェクトです。Adafruit DHTライブラリを使い、1本の信号線を使うプロトコルでセンサーからデータを読み取ります。IoT気象観測装置やスマートホームの自動化、温室の環境監視システムの基礎となります。ESP32のGPIO 4ピンを介してDHT22と通信します。

4.8(38)
56
ESP32スマート照明(PIR + LDR + リレー + OLED)
#141上級
プレミアム

ESP32スマート照明(PIR + LDR + リレー + OLED)

ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。

4.3(6)
11
ESP32 OTA更新:無線でファームウェアを書き込み(LCD付き)
#132上級
プレミアム

ESP32 OTA更新:無線でファームウェアを書き込み(LCD付き)

このプロジェクトでは、USBケーブルを使わず、Wi-Fi経由でESP32に新しいファームウェアを書き込む方法を学びます。ArduinoOTAライブラリを使うと、ESP32がネットワーク上で検出され、Arduino IDEの「ネットワークポート」タブにbluegrays-otaとして自動表示されます。初回はUSBで書き込み、その後の更新はすべて無線で行えます。設置済みのデバイスにとって非常に重要な機能です。I2C LCDディスプレイには、Wi-Fiの状態、IPアドレス、ホスト名、OTA書き込みの進捗率、エラーメッセージをトルコ語と英語で表示します。

4.6(14)
22
ESP32とNTPでインターネットから正確な時刻を取得
#128上級
プレミアム

ESP32とNTPでインターネットから正確な時刻を取得

ESP32をWi-Fiに接続し、NTP(Network Time Protocol)サーバーからミリ秒精度の現在時刻を取得します。configTzTime()関数でタイムゾーンを自動適用し、夏時間を採用する国ではPOSIX TZ文字列で夏時間と標準時間の切り替えも自動管理します。時刻はシリアルモニタと、必要に応じて接続するI2C LCDに表示します。configTzTime()がSNTPサービスを開始し、ネットワークにアクセスできる間は定期的に再同期します。

4.5(13)
21