ガイド · 読了まで27分

ESP-NOW入門:ルーターなしで通信するESP32の無線通信

ESP32でESP-NOWを使う方法。MACアドレスの調べ方、最大250バイトの構造体の一方向・双方向の送信、最大20台のピア、通信距離、コア3.xでの変更点、Wi-Fiのチャンネルを解説します。

ESP-NOWは、Espressifが開発した無線通信プロトコルで、ESP32やESP8266のボード同士がルーターもアクセスポイントもインターネット接続もなしに直接データを送り合えます。ボードはWi-Fiの無線を使いますが、ネットワークには参加しません。各デバイスはMACアドレスで相手を見つけ、最大250バイトの小さなパケットを数ミリ秒で届けます。接続の確立もIPアドレスの取得もパスワードの入力も不要なので、一方のボードでボタンを押すと別のボードのリレーがすぐに切り替わり、電池で動くセンサーは起きて測定値を送り、そのまますぐにスリープに戻れます。

このガイドでは、ESP-NOWを一から設定します。まずボードのMACアドレスを読み取り、次に一方向と双方向の例で、温度、カウンター、テキストのような異なる値を構造体(struct)で1つのパケットにまとめて送ります。その後、20台のピアの上限、ブロードキャスト、通信距離と速度、ESP32 Arduinoコア3.xで変わった関数、そしてESP-NOWとWi-Fiを同時に使うための唯一のルールであるチャンネル合わせを学びます。最後のパートでは、ディープスリープで電池駆動するリモートセンサーの測定値を、Wi-Fiにつながったゲートウェイへ送り、スマホから確認します。ESP32のプロジェクトが初めてなら、先にESP32とは?でArduino IDEのセットアップを済ませてください。

ESP-NOWで通信するESP32開発ボード
ESP-NOWに追加のモジュールは不要です。ESP32に内蔵されたWi-Fiの無線を使って、ボード同士がルーターなしで直接通信します。

ESP-NOWとは?仕組みは?

通常のWi-Fiでは、ESP32はまずルーターに接続し、パスワードを確認し、DHCPでIPアドレスを取得してから、ようやくデータを送れます。これには普通1〜3秒かかります。ESP-NOWはこれらのステップをすべて省きます。データをIEEE 802.11規格のベンダー固有のアクションフレーム(vendor-specific action frame)と呼ばれる短いWi-Fiパケットに入れ、相手のボードのMACアドレスへ直接送ります。受信したボードはハードウェアレベルの受信確認(ACK)フレームで応答し、送信側のコールバック関数がパケットが届いたかどうかを知らせます。ACKが返ってこなければ、Wi-Fiの層が自動的にパケットを何回か再送します。つまりESP-NOWは、2台のボードの間にある見えない郵便受けのようなものです。宛先を書いてパケットを入れれば、相手が受け取ったときに知らせが届きます。

項目値
開発元Espressif Systems社
周波数2.4 GHz(Wi-Fiの無線、チャンネル1〜13)
1パケットのデータ量250バイト(ESP-NOW v1)、ESP-IDF 5.4以降のv2では1470バイト
登録できるピア最大20台、暗号化ピアは初期設定で最大7台
初期設定の通信速度1 Mbps
受信確認ユニキャストではあり(ACK)、ブロードキャストではなし
暗号化オプション、CCMP(AES-128)、それぞれ16バイトのPMKキーとLMKキー
ルーター/アクセスポイント不要
対応チップWi-Fi搭載のESP32チップ(ESP32、S2、S3、C2、C3、C6など)とESP8266

ESP-NOWの本当の強みは、反応の速さと消費電力の少なさです。ボタンを押すと、相手側のLEDが数ミリ秒以内に点灯します。同じことをWi-Fiで行うには、まず接続してからサーバーにリクエストを送る必要があります。電池で動くセンサーでは差がさらに大きくなります。Wi-Fiへの接続中は無線が数秒にわたって100 mA以上を消費しますが、ESP-NOWなら起動、測定値の送信、スリープへの復帰が合計数百ミリ秒で終わります。一方で、ESP-NOWはインターネットには届きません。データをインターネットやスマホに届けたいなら、ネットワークのどこかにWi-FiにつながったゲートウェイのESP32が必要です。この構成はガイドの後半で作ります。

ESP-NOW、Wi-Fi、Bluetooth、nRF24L01の比較

方式追加のモジュールルーターパケットサイズ一般的な通信距離(屋外の開けた場所)向いている用途
ESP-NOW不要不要250バイト100〜200 m、LRモードではさらに長いボード間のコマンドとセンサーデータ
Wi-Fi(HTTP、MQTT)不要必要実質的に無制限ルーターの電波が届く範囲インターネット、Webページ、クラウド
Bluetooth(BLE)不要不要初期設定で20バイト屋内で10〜20 mスマホとの接続
nRF24L01必要(SPI)不要32バイト数十メートル、PA+LNA版ならはるかに長いArduino UnoやNanoのような無線のないボード
LoRa(SX1278)必要(SPI)不要最大255バイト数キロメートル非常に長い距離、たまに送るデータ

スマホと通信するならBluetooth、インターネットが必要ならWi-Fiが適しています。しかし、互いに通信するだけのESP32ボードが何台かあるなら、たいていはESP-NOWがいちばん簡単です。追加のモジュールもケーブルもルーターもいらず、コードは数行だけです。Arduino Unoのような無線のないボードにはnRF24L01が、キロメートル単位の距離にはLoRaモジュールが向いています。すべての選択肢を並べて比べるにはArduinoの無線通信モジュールを、スマホとの接続についてはESP32のBluetoothのガイドを見てください。

ESP-NOWに対応しているボードは?

ボード/チップESP-NOWメモ
ESP32(WROOM-32、DevKit)対応Long Range(LR)モードにも対応
ESP32-S2、S3、C3、C6対応Arduinoのコードは同じで、ボードの選択だけが変わる
ESP32-C2(ESP8684)対応LRモードなし
ESP32-H2非対応Wi-Fiの無線がないため、ESP-NOWも使えない
ESP8266(NodeMCU、Wemos D1 Mini)対応別のライブラリ(espnow.h)、v1と250バイトのみ
Raspberry Pi Pico W/Pico 2 W非対応Wi-Fiチップ(CYW43439)でESP-NOWは動かない
Arduino Uno、Nano、Mega非対応シリアルポート(UART)でESP32に接続できる

ESP-NOWは異なるESPチップの間でも動作します。ESP32-C3のセンサーから無印ESP32のゲートウェイやESP8266のボードへパケットを送れます。条件は、両側が同じWi-Fiチャンネルにいることと、データの構造が両側でまったく同じに定義されていることだけです。以下の例では2台のESP32 DevKitを使います。ボードのどのピンを安全に使えるかはピン配置ツールで確認できます。

ステップ1:ESP32のMACアドレスを調べる

ESP-NOWでは、各ボードは6バイトのMACアドレスで相手を見つけます。このアドレスは工場ですべてのESP32に書き込まれていて、変わることはありません。下のスケッチを受信側に使うボードに書き込み、シリアルモニタを115200ボーで開いてください。アドレスが読みやすい形式と、相手のボードのコードにそのまま貼り付けられる配列の行の両方で表示されます。2台のボードを使う例では、両方のボードでこれを行い、アドレスを控えておきます。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_wifi.h>
3
4void setup() {
5  Serial.begin(115200);
6  delay(1000);
7  WiFi.mode(WIFI_STA);                 // ESP-NOW istasyon arayüzünü kullanır / ESP-NOW uses the station interface
8
9  uint8_t mac[6];
10  if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) {
11    Serial.printf("STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
12                  mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
13    // Diğer karta kopyalanacak satır / line to paste into the other board's code
14    Serial.printf("uint8_t karsiMac[] = {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X};\n",
15                  mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
16  } else {
17    Serial.println("MAC okunamadi / could not read MAC");
18  }
19}
20
21void loop() {
22}
ESP32には、ステーション(STA)とアクセスポイント(AP)の2つの別々のWi-Fi MACアドレスがあります。APのアドレスは、たいていSTAのアドレスの最後のバイトに1を足したものです。このガイドの例はすべてボードをWIFI_STAモードで動かすので、相手のボードには必ずSTAのアドレスを入力してください。違うインターフェースのアドレスを使うことが、「failed」(届かなかった)メッセージのいちばん多い原因です。ESP32 Arduinoコア3.xでは、Wi-Fiが完全に起動する前にWiFi.macAddress()を呼ぶと00:00:00:00:00:00が返ることがあります。上のスケッチのesp_wifi_get_mac()にはこの問題はありません。

構造体でデータを送る方法と250バイトの上限

ESP-NOWが送るのはバイトの配列だけで、その中身が何かは関知しません。いくつかの異なる値を一度に送るいちばんきれいな方法は、それらを構造体(struct)にまとめ、構造体のバイトをメモリ上の並びのまま送ることです。受信側は同じ構造体を定義し、届いたバイトをmemcpyでその上にコピーします。これがうまくいくには、構造体が両側で同じ順番、同じ型、同じサイズでなければなりません。コンパイラは、アクセスを速くするためにフィールドの間に見えないパディングのバイトを入れることがあります。以下の例のPaket構造体(Paketはトルコ語で「パケット」)は通常36バイトになりますが、__attribute__((packed))を付けるとちょうど30バイトに縮みます。packedを使えば、ESP32とESP8266の間でも、ArduinoとMicroPythonの間でも、すべてのバイトが同じ位置に並ぶことが保証されます。

型サイズメモ
bool、uint8_t、int8_t、char1バイトフラグ、小さな数、1文字
int16_t、uint16_t2バイト−32768〜32767または0〜65535
int、int32_t、uint32_t4バイトintはESP32とESP8266では4バイト、Arduino Unoでは2バイト
float4バイト精度は約7桁
double8バイト本当に必要な場合以外はfloatを使う
char mesaj[16](mesajはトルコ語で「メッセージ」)16バイト固定長のテキスト、最後は'\0'で終わる必要がある
String、char*、ポインタ送れない送られるのはメモリアドレスだけで、相手側では意味がない

1つのパケットに入るのは最大250バイトです。この上限はESP-NOWの最初のバージョン(v1)に由来し、ESP8266を含むすべてのボードに当てはまります。ESP-IDF 5.4をベースにしたArduinoコア3.2以降では、ESP-NOW v2で最大1470バイトのパケットを送れます。ただし、v1で動くボード(ESP8266や古いコアのESP32)は、250バイトより長いパケットを切り詰めるか、まるごと捨ててしまいます。そのため、いろいろなボードが混ざったネットワークでは250バイト以内に収めるのがいちばん安全です。送信側スケッチのstatic_assertの行は、構造体が250バイトを超えるとわかりやすいメッセージでコンパイルを止めます。もっと大きなデータを送る必要があるなら、番号付きの断片に分けて、受信側で順番に組み立て直します。センサーの値、ボタンの状態、コマンドなら、数十バイトで十分です。

一方向のESP-NOW:送信側のスケッチ

  1. Arduino IDEで、2台のESP32ボードともESP32 Dev Moduleが選ばれていることを確認します。ESP-NOWのライブラリはボードパッケージに含まれているので、ほかにインストールするものはありません。
  2. 受信側のボードから読み取ったMACアドレスを、下のコードのaliciMac配列(aliciはトルコ語で「受信側」)に入力します。
  3. 送信側のGPIO 4とGNDの間に押しボタンをつなぎます。INPUT_PULLUPを使うので、追加の抵抗は不要です。詳しくはArduinoに押しボタンをつなぐ方法を見てください。
  4. スケッチを送信側に書き込み、シリアルモニタを115200ボーで開きます。受信側がまだ動いていない間は、2秒ごとに「Teslim edilemedi / failed」(届かなかった)と表示されるのが正常です。
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Alıcının STA MAC adresi (mac_oku çıktısı) / receiver's STA MAC address (from the MAC sketch)
6uint8_t aliciMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
7
8const int BUTON_PIN = 4;               // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
9
10// İki kartta da birebir aynı olmalı / must be identical on both boards
11typedef struct __attribute__((packed)) {
12  uint8_t  id;                         // gönderen kart numarası / sender board number
13  uint32_t sayac;                      // paket sayacı / packet counter
14  float    sicaklik;                   // °C
15  float    nem;                        // % bağıl nem / % relative humidity
16  bool     buton;                      // buton basılı mı / is the button pressed
17  char     mesaj[16];                  // kısa metin, String değil / short text, not String
18} Paket;
19static_assert(sizeof(Paket) <= 250, "Paket 250 bayti asamaz / packet must not exceed 250 bytes");
20
21Paket paket;
22
23// Teslim sonucu: alıcı paketi aldıysa SUCCESS / delivery result: SUCCESS if the receiver got it
24#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
25void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) {   // çekirdek 3.3+ / core 3.3+
26#else
27void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) {                // çekirdek 2.x-3.2 / core 2.x-3.2
28#endif
29  Serial.println(durum == ESP_NOW_SEND_SUCCESS ? "Teslim edildi / delivered" : "Teslim edilemedi / failed");
30}
31
32void setup() {
33  Serial.begin(115200);
34  pinMode(BUTON_PIN, INPUT_PULLUP);
35  WiFi.mode(WIFI_STA);                 // önce Wi-Fi, sonra ESP-NOW / Wi-Fi first, then ESP-NOW
36
37  if (esp_now_init() != ESP_OK) {
38    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
39    return;
40  }
41  esp_now_register_send_cb(gonderimSonucu);
42
43  esp_now_peer_info_t es = {};         // tüm alanları sıfırla / zero every field
44  memcpy(es.peer_addr, aliciMac, 6);
45  es.channel = 0;                      // 0 = şu anki kanal / 0 = current channel
46  es.ifidx = WIFI_IF_STA;
47  es.encrypt = false;
48  if (esp_now_add_peer(&es) != ESP_OK) {
49    Serial.println("Es eklenemedi / failed to add peer");
50    return;
51  }
52  paket.id = 1;
53}
54
55void loop() {
56  paket.sayac++;
57  paket.sicaklik = 20.0 + random(0, 100) / 10.0;   // deneme değeri / test value
58  paket.nem = 40.0 + random(0, 200) / 10.0;
59  paket.buton = (digitalRead(BUTON_PIN) == LOW);
60  snprintf(paket.mesaj, sizeof(paket.mesaj), "Selam %lu", (unsigned long)paket.sayac);
61
62  esp_err_t sonuc = esp_now_send(aliciMac, (uint8_t *)&paket, sizeof(paket));
63  if (sonuc != ESP_OK) {
64    Serial.printf("Gonderme hatasi / send error: %s\n", esp_err_to_name(sonuc));
65  }
66  delay(2000);
67}

スケッチはまずWi-FiをWIFI_STAモードで起動します。ESP-NOWはWi-Fiの無線なしでは動かないので、esp_now_init()は必ずWiFi.mode()の行の後に置きます。次に、受信側をピア(peer)としてリストに追加します。esp_now_peer_info_t構造体を= {}でクリアすることが大切です。クリアしていないフィールドに残ったでたらめな値が「Peer interface is invalid」エラーの原因になります。channel = 0は「ボードの現在のチャンネルを使う」という意味です。esp_now_send()はパケットをキューに入れてすぐに戻ります。ESP_OKが返っても、パケットが送信のために受け付けられたというだけです。本当の結果は数ミリ秒後にgonderimSonucu()関数(トルコ語で「送信結果」)に届きます。ESP_NOW_SEND_SUCCESSは、受信側がハードウェアレベルでパケットの受信を確認したという意味です。先頭の#ifブロックは、コアのバージョンによって変わる送信コールバックの正しい形を選びます。これについてはコア3.xのセクションで説明します。

ESP-NOWの受信側のスケッチ

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Vericidekiyle birebir aynı yapı / exactly the same struct as the sender
6typedef struct __attribute__((packed)) {
7  uint8_t  id;
8  uint32_t sayac;
9  float    sicaklik;
10  float    nem;
11  bool     buton;
12  char     mesaj[16];
13} Paket;
14
15Paket gelen;
16volatile bool yeniPaket = false;
17
18// Wi-Fi görevinde çalışır: kısa tut / runs in the Wi-Fi task: keep it short
19#if ESP_IDF_VERSION_MAJOR >= 5
20void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) {   // çekirdek 3.x / core 3.x
21#else
22void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) {                // çekirdek 2.x / core 2.x
23#endif
24  if (uzunluk != sizeof(Paket)) return;  // farklı boyut: başka bir paket / wrong size: some other packet
25  memcpy(&gelen, veri, sizeof(Paket));   // veriyi hemen kopyala / copy the data right away
26  yeniPaket = true;
27}
28
29void setup() {
30  Serial.begin(115200);
31  WiFi.mode(WIFI_STA);
32
33  if (esp_now_init() != ESP_OK) {
34    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
35    return;
36  }
37  esp_now_register_recv_cb(veriGeldi);
38  Serial.println("Alici hazir / receiver ready");
39}
40
41void loop() {
42  if (yeniPaket) {
43    yeniPaket = false;
44    Paket p = gelen;                     // kopya üzerinde çalış / work on a copy
45    p.mesaj[sizeof(p.mesaj) - 1] = '\0'; // metin her zaman sonlansın / always terminate the text
46    Serial.printf("id=%u sayac=%lu sicaklik=%.1f nem=%.1f buton=%d mesaj=%s\n",
47                  (unsigned)p.id, (unsigned long)p.sayac, p.sicaklik, p.nem, (int)p.buton, p.mesaj);
48  }
49}

受信側では、パケットはveriGeldi()関数(トルコ語で「データが届いた」)に届きます。この関数はloop()の中ではなく、Wi-Fiの優先度の高いタスクの中で実行されるので、中でSerial.print、delay、時間のかかる計算を使ってはいけません。正しいやり方は、パケットのサイズが期待する構造体と一致するかを確認し、memcpyで自分の変数にデータをコピーし、volatileのフラグでloop()に知らせることです。コア3.xでは、bilgi引数(トルコ語で「情報」)から、パケットの送信元(bilgi->src_addr)と信号の強さ(bilgi->rx_ctrl->rssi、単位dBm)もわかります。この情報は関数の中でだけ有効なので、後で必要ならコピーしておきます。受信側は送信側をピアとして追加する必要はありません。暗号化されていないユニキャストのパケットとブロードキャストは、リストにないボードからでも受け取れます。

「Delivered」(届いた)は、パケットが受信側のWi-Fiハードウェアに届いたという意味で、受信側のプログラムがそれを処理したという意味ではありません。相手のボードに電源が入っていて同じチャンネルにいれば、ESP-NOWを一度も起動していなくてもACKは返ってきます。重要なコマンドでは、ソフトウェアレベルの受信確認メッセージを別に使い、パケットにカウンターを加えてください。受信側はカウンターが飛んでいれば失われたパケットの数がわかり、同じ番号が2回届けば重複を無視できます。

双方向の通信:2台のESP32が互いに制御し合う

双方向の通信では、どのボードも送信側であり受信側でもあります。下のスケッチを両方のボードに書き込みます。違いは、それぞれのボードのkarsiMac配列(karsiはトルコ語で「相手側」)に相手のボードのアドレスを入れることだけです。一方のボードでボタンを押すと、もう一方のボードのLEDが点灯または消灯し、LEDを切り替えたボードが受信確認のメッセージを返します。配線は両方のボードで同じです。

部品ESP32のピンメモ
ボタンGPIO 4とGND内部プルアップを使うので、追加の抵抗は不要
LEDGPIO 2多くのDevKitでは内蔵の青色LED、外付けのLEDには220 Ωの抵抗を使う
電源USBボードは別々のUSBポートやACアダプターから給電できる
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Bu kodu iki karta da yükle; yalnız karşı kartın MAC adresini değiştir
6// Upload to both boards; change only the other board's MAC address
7uint8_t karsiMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
8
9const int BUTON_PIN = 4;               // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
10const int LED_PIN = 2;                 // dahili LED / onboard LED
11
12typedef struct __attribute__((packed)) {
13  uint8_t tur;                         // 1 = komut, 2 = onay / 1 = command, 2 = acknowledgement
14  bool    ledAcik;                     // istenen ya da uygulanan durum / requested or applied state
15} Mesaj;
16
17volatile bool komutGeldi = false;
18volatile bool onayGeldi = false;
19volatile bool gelenLed = false;
20bool karsiLed = false;                 // karşı karttaki LED için son isteğimiz / our last request for the other LED
21int sonButon = HIGH;
22
23#if ESP_IDF_VERSION_MAJOR >= 5
24void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) {   // çekirdek 3.x / core 3.x
25#else
26void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) {                // çekirdek 2.x / core 2.x
27#endif
28  if (uzunluk != sizeof(Mesaj)) return;
29  Mesaj m;
30  memcpy(&m, veri, sizeof(m));
31  if (m.tur == 1) {
32    gelenLed = m.ledAcik;
33    komutGeldi = true;
34  } else if (m.tur == 2) {
35    onayGeldi = true;
36  }
37}
38
39void gonder(uint8_t tur, bool ledAcik) {
40  Mesaj m = {tur, ledAcik};
41  esp_now_send(karsiMac, (uint8_t *)&m, sizeof(m));
42}
43
44void setup() {
45  Serial.begin(115200);
46  pinMode(BUTON_PIN, INPUT_PULLUP);
47  pinMode(LED_PIN, OUTPUT);
48  WiFi.mode(WIFI_STA);
49
50  if (esp_now_init() != ESP_OK) {
51    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
52    return;
53  }
54  esp_now_register_recv_cb(veriGeldi);
55
56  esp_now_peer_info_t es = {};
57  memcpy(es.peer_addr, karsiMac, 6);
58  es.ifidx = WIFI_IF_STA;              // kanal 0, şifresiz / channel 0, unencrypted
59  esp_now_add_peer(&es);
60}
61
62void loop() {
63  // 1) Butona basılınca karşı kartın LED'ini değiştir / on press, toggle the other board's LED
64  int buton = digitalRead(BUTON_PIN);
65  if (buton != sonButon) {
66    delay(30);                         // kontak sıçramasını bekle / wait out contact bounce
67    buton = digitalRead(BUTON_PIN);
68    if (sonButon == HIGH && buton == LOW) {
69      karsiLed = !karsiLed;
70      gonder(1, karsiLed);
71      Serial.println(karsiLed ? "Komut: AC / command: ON" : "Komut: KAPAT / command: OFF");
72    }
73    sonButon = buton;
74  }
75
76  // 2) Gelen komutu uygula ve onay gönder / apply an incoming command and reply
77  if (komutGeldi) {
78    komutGeldi = false;
79    digitalWrite(LED_PIN, gelenLed ? HIGH : LOW);
80    gonder(2, gelenLed);
81  }
82
83  // 3) Karşı karttan gelen onay / acknowledgement from the other board
84  if (onayGeldi) {
85    onayGeldi = false;
86    Serial.println("Karsi kart uyguladi / the other board applied it");
87  }
88}

メッセージ構造体のturフィールド(turはトルコ語で「種類」)は、そのパケットが何かを表します。1はコマンド、2は受信確認です。ボタンを押されたボードは、相手のボードのLEDをどの状態にしたいかを送ります。コマンドを受け取ったボードはLEDを設定し、同じ状態を種類2で送り返します。こうして送信側は、ハードウェアのACKと「LEDが本当に切り替わった」という確認の両方を受け取ります。届いたコマンドはここでもvolatileのフラグでloop()に渡し、digitalWriteと返信はloop()の中で行います。このほうが、コールバックの中からesp_now_send()を呼ぶより安全です。ボタンの30 msの待ち時間は接点のチャタリングを取り除きます。LEDの代わりにリレーをつなげば、同じコードがワイヤレスの照明スイッチになります。リレーの配線はリレーモジュールとは?を見てください。

複数のデバイス:20台のピアの上限とブロードキャスト

ESP-NOWは3つの構成すべてに対応しています。1台の送信側から多数の受信側へ(1つのリモコンから5台のリレーボードへ)、多数の送信側から1台の受信側へ(5個のセンサーから1台のゲートウェイへ)、そして全員が全員と通信するネットワークです。ボードのピアリストに入るのは最大20件で、初期設定ではそのうち暗号化できるのは7件だけです。この上限は送信先のボードにだけ当てはまります。ゲートウェイは、暗号化なしのパケットを送ってくるセンサーを追加する必要がないので、20個より多くのセンサーを受信できます。リストがいっぱいになると、esp_now_add_peer()はESP_ERR_ESPNOW_FULLを返します。その場合は、もう必要のないピアをesp_now_del_peer()で削除して、新しいピアを追加できます。下のスケッチは3台の受信側とブロードキャストアドレスをリストに追加し、1台ずつの送信とブロードキャストの両方を行います。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3
4// Üç alıcının STA MAC adresleri / STA MAC addresses of three receivers
5uint8_t alicilar[3][6] = {
6  {0x24, 0x6F, 0x28, 0x11, 0x11, 0x11},
7  {0x24, 0x6F, 0x28, 0x22, 0x22, 0x22},
8  {0x24, 0x6F, 0x28, 0x33, 0x33, 0x33}
9};
10uint8_t yayinMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};   // yayın adresi / broadcast address
11
12typedef struct __attribute__((packed)) {
13  uint8_t  hedef;                      // 0 = herkes, 1-3 = tek alıcı / 0 = everyone, 1-3 = one receiver
14  uint32_t sayac;
15} Komut;
16
17Komut komut;
18
19bool esEkle(const uint8_t *mac) {
20  esp_now_peer_info_t es = {};
21  memcpy(es.peer_addr, mac, 6);
22  es.ifidx = WIFI_IF_STA;
23  return esp_now_add_peer(&es) == ESP_OK;
24}
25
26void setup() {
27  Serial.begin(115200);
28  WiFi.mode(WIFI_STA);
29  if (esp_now_init() != ESP_OK) {
30    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
31    return;
32  }
33  for (int i = 0; i < 3; i++) esEkle(alicilar[i]);
34  esEkle(yayinMac);                    // yayın da eş olarak eklenmeli / broadcast must be added as a peer too
35
36  esp_now_peer_num_t sayi;
37  esp_now_get_peer_num(&sayi);
38  Serial.printf("Kayitli es / registered peers: %d / 20\n", sayi.total_num);
39}
40
41void loop() {
42  komut.sayac++;
43
44  // 1) Tek tek: her alıcıya ayrı paket ve ayrı teslim onayı / one by one: separate packet and ACK each
45  for (int i = 0; i < 3; i++) {
46    komut.hedef = i + 1;
47    esp_now_send(alicilar[i], (uint8_t *)&komut, sizeof(komut));
48    delay(10);
49  }
50  delay(2000);
51
52  // 2) Yayın: tek paket, aynı kanaldaki herkes alır, onay yok / broadcast: one packet, everyone on the channel, no ACK
53  komut.hedef = 0;
54  esp_now_send(yayinMac, (uint8_t *)&komut, sizeof(komut));
55  delay(2000);
56}

ブロードキャストアドレスFF:FF:FF:FF:FF:FFも、送信する前にピアとして追加しておく必要があります。ブロードキャストのパケット1つで、同じチャンネルにいるすべてのESP-NOW受信側に届きます。受信側のMACアドレスを知らなくてよいので、いちばん手軽に設定できる方法です。その代わり、ブロードキャストには受信確認がなく、送信結果は常に成功に見え、ブロードキャストのパケットは暗号化できません。1台ずつ送る場合は、パケットごとにACKが返ります。esp_now_send()のアドレスにNULLを渡すと、パケットはリストのすべてのピアに個別に送られます。ブロードキャストアドレスもリストにあれば、パケットはブロードキャストとしても1回送られます。多くのプロジェクトで実用的なのは、最初の起動時にブロードキャストでボード同士を見つけ、MACアドレスを覚えてから、受信確認のあるユニキャストに切り替える方法です。

ESP-NOWの通信距離と速度

指標一般的な値メモ
屋内の通信距離20〜40 m(壁1〜3枚)鉄筋コンクリートの壁と金属の物がいちばん信号を弱める
屋外の開けた場所での通信距離100〜200 m基板上のPCBアンテナの場合。ボードを地面から高く置くと伸びる
LR(Long Range)モード最大1 kmEspressifによる見通しでの数値。速度は0.5または0.25 Mbpsに下がる
初期設定の通信速度1 Mbps速度が高いほど通信距離は短くなる
250バイトのパケットの送信時間約2.5 msACKと待ち時間を含めると1パケットあたり約3 ms
理論上のパケット数毎秒約300パケット(約70 kB/s)250バイトのパケットの場合。実際はもっと少ない
コマンドの遅延数ミリ秒接続の確立もサーバーも不要

これらの値は環境によって大きく変わりますが、計算は簡単です。初期設定の1 Mbpsでは、1バイトが空中にいる時間は8マイクロ秒です。250バイトのデータにヘッダーと同期用のプリアンブルを加えると、約2.5 msかかります。受信側のACKと、チャンネルが空くのを待つ時間を含めると1パケットあたり約3 msが必要なので、1秒間に送れるのは最大で約300パケットです。センサーデータやコマンドには十分すぎるほどですが、ESP-NOWは音声や映像のストリーミングには向きません。通信距離を伸ばすには、ボードを地面から高い位置に置き、アンテナを金属のケースや電池、ケーブルで覆わず、外付けアンテナのモジュール(ESP32-WROOM-32Uなど)を使い、両側でesp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR)によってEspressifのLong Range(LR)モードを有効にします。LRは2台のESP32の間でしか動きません。LRだけに設定したインターフェースは通常のルーターに接続できず、ESP8266とESP32-C2はこのモードに対応していません。コア3.xで通信距離をテストするときは、受信側でbilgi->rx_ctrl->rssiを表示します。−50 dBmはとても強い信号、−80 dBmは限界に近く、−90 dBm前後からパケットの取りこぼしが始まります。

ESP32 Arduinoコア3.xでの変更点

インターネット上のESP-NOWの例の多くは、ESP32 Arduinoコア2.x向けに書かれています。ボードマネージャからバージョン3.xをインストールした場合、そのコードはコンパイルできません。EspressifがESP-IDF 5でコールバック関数の形を変えたからです。変更は2段階で行われました。コア3.0で受信コールバックが、ESP-IDF 5.5をベースにしたコア3.3で送信コールバックが変わりました。どのバージョンを使っているかは、「ツール」>「ボード」>「ボードマネージャ」のesp32パッケージの横で確認できます。

項目コア2.xコア3.0〜3.2コア3.3以降
受信コールバック(const uint8_t *mac, const uint8_t *veri, int uzunluk)(veri=データ、uzunluk=長さ)(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)(bilgi=情報)3.0と同じ
送信元のアドレスmacbilgi->src_addrbilgi->src_addr
送信コールバック(const uint8_t *mac, esp_now_send_status_t durum)(durum=状態)2.xと同じ(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum)(新しい形)
最大のパケット250バイト250バイト、3.2のv2では1470バイトv2で1470バイト
チャンネルの設定esp_wifi_set_channel()WiFi.setChannel()も使える3.0と同じ
クラス形式の既製ライブラリなしESP32_NOW.h(ESP_NOW_Peer、ESP_NOW_Serial)v2対応のESP32_NOW.h

古いコードを3.xでコンパイルすると、受信コールバックについてinvalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t'(型を変換できない)というエラーが出ます。直すには、関数の最初の引数をconst esp_now_recv_info_t *にして、送信元のアドレスをbilgi->src_addrから読み取ります。コア3.3に移ると、今度はesp_now_send_cb_tについて同じようなエラーが出ます。送信コールバックの最初の引数はconst esp_now_send_info_t *でなければならず、受信側のアドレスはbilgi->des_addrに入っています。このガイドのスケッチはESP_IDF_VERSIONマクロで正しい形を自動的に選ぶので、2.x、3.0〜3.2、3.3以降のどれでもそのままコンパイルできます。コア3.xでは、ESP32_NOW.hというクラス形式のライブラリも追加されました。ESP_NOW_Peerから派生したクラスにonReceive()とonSent()を書き、ESP_NOW_Serialクラスを使うとESP-NOWの通信をSerialと同じように使えます。例は「ファイル」>「スケッチ例」メニューのESP_NOWの下にあります。このガイドでは、ESP8266や古いコードとの互換性を保てるので、esp_now.hの関数を直接使っています。

ESP-NOWとWi-Fiを同時に使う:チャンネルを合わせる

ESP32には2.4 GHzの無線が1つしかなく、その無線は一度に1つのチャンネルでしか動けません。ボードがルーターに接続すると、チャンネルを決めるのは自分ではなくルーターです。ルーターがチャンネル6なら、ESP-NOWもチャンネル6で動きます。そのため、Wi-Fiにつながったゲートウェイへパケットを送るセンサーも、すべてルーターのチャンネルに合わせる必要があります。チャンネルが合わないと送信側には「failed」と表示され、0以外のチャンネルでピアを追加した場合は、シリアルモニタに「ESPNOW: Peer channel is not equal to the home channel, send fail」(ピアのチャンネルが現在のチャンネルと違う)というエラーも出ます。トラブルのない構成にするには、次のルールを守ってください。

  1. ゲートウェイは先にWi-Fiに接続し、その後でESP-NOWを開始して、WiFi.channel()でルーターのチャンネルをシリアルモニタに表示します。
  2. センサーはルーターに接続しません。ネットワーク名でスキャンしてチャンネルを見つける(WiFi.scanNetworks()とWiFi.channel(i))か、コードで固定のチャンネルを使います。このためにWi-Fiのパスワードは必要ありません。
  3. ルーターのチャンネルを「自動」から固定のチャンネル(1、6、11のどれか)に変えます。自動のままだと、ルーターがチャンネルを変えたときにセンサーがゲートウェイを見失います。
  4. ゲートウェイではWiFi.setSleep(false)で省電力機能をオフにします。初期設定では、ルーターに接続したESP32は一定の間隔で無線をスリープさせるので、その間に届いたESP-NOWのパケットを取りこぼすことがあります。
  5. センサーにはゲートウェイのSTA MACアドレスを入力します。下のゲートウェイのスケッチは、起動時にこのアドレスをシリアルモニタに表示します。

下のゲートウェイのスケッチは、自宅のネットワークに接続して最大5個のセンサーの測定値を保存し、同じネットワーク上のスマホから開ける簡単なWebページに表示します。ネットワーク名とパスワードを自分のものに書き換え、書き込んだ後、シリアルモニタに表示されたIPアドレスをスマホのブラウザで開いてください。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <WebServer.h>
3#include <esp_now.h>
4#include <esp_idf_version.h>
5
6const char *ssid = "AgAdi";            // Wi-Fi adın / your Wi-Fi name
7const char *sifre = "Sifre123";        // Wi-Fi şifren / your Wi-Fi password
8
9// Sensör kodundakiyle birebir aynı / identical to the sensor sketch
10typedef struct __attribute__((packed)) {
11  uint8_t  id;                         // sensör numarası 1-5 / sensor number 1-5
12  uint32_t sayac;
13  float    sicaklik;
14  float    nem;
15  float    pil;                        // pil gerilimi (V) / battery voltage (V)
16} SensorPaketi;
17
18const int SENSOR_SAYISI = 5;
19SensorPaketi sonVeri[SENSOR_SAYISI + 1];       // 1..5 kullanılır / indexes 1..5 are used
20unsigned long sonZaman[SENSOR_SAYISI + 1];     // son paketin zamanı / time of the last packet
21QueueHandle_t kuyruk;
22WebServer server(80);
23
24#if ESP_IDF_VERSION_MAJOR >= 5
25void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) {   // çekirdek 3.x / core 3.x
26#else
27void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) {                // çekirdek 2.x / core 2.x
28#endif
29  if (uzunluk != sizeof(SensorPaketi)) return;
30  SensorPaketi p;
31  memcpy(&p, veri, sizeof(p));
32  xQueueSend(kuyruk, &p, 0);           // kuyruğa at, Wi-Fi görevini bekletme / queue it, never block the Wi-Fi task
33}
34
35void anaSayfa() {
36  String html = "<meta name='viewport' content='width=device-width'>"
37                "<meta http-equiv='refresh' content='10'>"
38                "<h2>ESP-NOW sensorleri</h2><table border=1 cellpadding=6>"
39                "<tr><th>ID</th><th>Sicaklik</th><th>Nem</th><th>Pil</th><th>Son paket</th></tr>";
40  for (int i = 1; i <= SENSOR_SAYISI; i++) {
41    if (sonZaman[i] == 0) continue;    // bu sensörden veri yok / no data from this sensor yet
42    html += "<tr><td>" + String(i) + "</td><td>" + String(sonVeri[i].sicaklik, 1) +
43            " &deg;C</td><td>%" + String(sonVeri[i].nem, 0) + "</td><td>" + String(sonVeri[i].pil, 2) +
44            " V</td><td>" + String((millis() - sonZaman[i]) / 1000) + " sn once</td></tr>";
45  }
46  html += "</table>";
47  server.send(200, "text/html", html);
48}
49
50void setup() {
51  Serial.begin(115200);
52  kuyruk = xQueueCreate(10, sizeof(SensorPaketi));
53
54  WiFi.mode(WIFI_STA);
55  WiFi.begin(ssid, sifre);
56  while (WiFi.status() != WL_CONNECTED) delay(500);
57  WiFi.setSleep(false);                // güç tasarrufu kapalı: paket kaçmaz / power save off: no missed packets
58
59  Serial.print("IP: ");
60  Serial.println(WiFi.localIP());
61  Serial.printf("Kanal / channel: %d\n", (int)WiFi.channel());
62  Serial.print("Gecit MAC / gateway MAC: ");
63  Serial.println(WiFi.macAddress());   // sensörlere bu adresi yaz / put this address in the sensors
64
65  if (esp_now_init() != ESP_OK) {
66    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
67    return;
68  }
69  esp_now_register_recv_cb(veriGeldi);
70
71  server.on("/", anaSayfa);
72  server.begin();
73}
74
75void loop() {
76  SensorPaketi p;
77  while (xQueueReceive(kuyruk, &p, 0) == pdTRUE) {
78    if (p.id >= 1 && p.id <= SENSOR_SAYISI) {
79      sonVeri[p.id] = p;
80      sonZaman[p.id] = millis();
81      Serial.printf("Sensor %u: %.1f C  %%%.0f  %.2f V\n", (unsigned)p.id, p.sicaklik, p.nem, p.pil);
82    }
83  }
84  server.handleClient();
85}

ゲートウェイは受信コールバックの中で、パケットをFreeRTOSのキュー(queue)に入れてすぐに戻るだけです。loop()がキューを空にして、各センサーの最新の測定値と受信時刻を保存し、Webサーバーがその表を表示します。キューのおかげで、複数のセンサーが同時に送ってきてもパケットは失われません。これは、コールバックの中で時間のかかる処理をしないというEspressifの推奨を実践する方法でもあります。ページは10秒ごとに自動で更新され、「Son paket」(最後のパケット)列を見れば、センサーが黙ってしまったかどうか(電池切れか、範囲外か)がすぐにわかります。ゲートウェイがそもそもWi-Fiに接続できないなら、問題はESP-NOWではありません。ESP32がWi-Fiにつながらないときの確認項目を順に試してください。データをホームオートメーションのシステムに送りたい場合は、Webページの代わりにMQTTとHome Assistantを使えます。

リモートセンサーの例:DHT22、ディープスリープ、ESP-NOW

では、ゲートウェイにデータを送る電池駆動のセンサーを作りましょう。センサーは起きるたびに温度、湿度、電池の電圧を測り、ESP-NOWのパケットを1つ送り、受信確認のACKを待ってから60秒間ディープスリープに入ります。温度と湿度にはDHT22を使います。ライブラリマネージャからAdafruitのDHT sensor libraryをインストールしてください。センサーの仕組みはDHT11温湿度センサーのガイドで説明しています。DHT22でも配線は同じで、コードではセンサーの種類をDHT22にするだけです。

ESP32 DevKit
ESP32 DevKit
DHT22(AM2302)
DHT22(AM2302)
18650リチウムイオン電池
18650リチウムイオン電池
TP4056充電モジュール
TP4056充電モジュール
接続ESP32のピンメモ
DHT22のVCC(+)3V33.3〜5 Vで動作
DHT22のDATAGPIO 4センサー単体の場合は、DATAと3V3の間に10 kΩの抵抗を入れる
DHT22のGND(−)GND共通グラウンド
電池の+GPIO 34(分圧回路経由)100 kΩの抵抗2本。4.2 Vの電池がピンでは2.1 Vになる
電池の−GND分圧回路の下側の抵抗もGNDへ
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_wifi.h>
4#include <esp_idf_version.h>
5#include <DHT.h>
6
7const char *AG_ADI = "AgAdi";          // ağ geçidinin bağlandığı Wi-Fi (şifre gerekmez) / Wi-Fi the gateway uses (no password needed)
8uint8_t gecitMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};   // ağ geçidinin STA MAC adresi / gateway STA MAC
9const uint8_t SENSOR_ID = 1;           // her sensöre farklı numara / a different number for each sensor
10const uint64_t UYKU_US = 60ULL * 1000000ULL;   // 60 saniye uyku / sleep for 60 seconds
11
12#define DHT_PIN 4
13#define PIL_PIN 34                     // ADC1 pini, 100k + 100k bölücü / ADC1 pin, 100k + 100k divider
14DHT dht(DHT_PIN, DHT22);
15
16typedef struct __attribute__((packed)) {
17  uint8_t  id;
18  uint32_t sayac;
19  float    sicaklik;
20  float    nem;
21  float    pil;
22} SensorPaketi;
23
24RTC_DATA_ATTR uint8_t kanal = 0;       // derin uykuda silinmez / kept during deep sleep
25RTC_DATA_ATTR uint32_t paketSayaci = 0;
26volatile bool sonucGeldi = false;
27volatile bool teslimEdildi = false;
28
29#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
30void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) {   // çekirdek 3.3+ / core 3.3+
31#else
32void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) {                // çekirdek 2.x-3.2 / core 2.x-3.2
33#endif
34  teslimEdildi = (durum == ESP_NOW_SEND_SUCCESS);
35  sonucGeldi = true;
36}
37
38uint8_t kanalBul(const char *ag) {     // ağın kanalını tarayarak bul / find the network's channel by scanning
39  int n = WiFi.scanNetworks();
40  for (int i = 0; i < n; i++) {
41    if (WiFi.SSID(i) == ag) return WiFi.channel(i);
42  }
43  return 1;                            // bulunamazsa kanal 1 / channel 1 if not found
44}
45
46void uyu() {
47  esp_sleep_enable_timer_wakeup(UYKU_US);
48  esp_deep_sleep_start();
49}
50
51void setup() {
52  Serial.begin(115200);
53  dht.begin();
54  WiFi.mode(WIFI_STA);
55
56  if (kanal == 0) kanal = kanalBul(AG_ADI);   // yalnız ilk açılışta ya da hatadan sonra tara / scan only at first boot or after a failure
57  esp_wifi_set_promiscuous(true);
58  esp_wifi_set_channel(kanal, WIFI_SECOND_CHAN_NONE);
59  esp_wifi_set_promiscuous(false);
60
61  if (esp_now_init() != ESP_OK) uyu();
62  esp_now_register_send_cb(gonderimSonucu);
63
64  esp_now_peer_info_t es = {};
65  memcpy(es.peer_addr, gecitMac, 6);
66  es.channel = kanal;
67  es.ifidx = WIFI_IF_STA;
68  esp_now_add_peer(&es);
69
70  SensorPaketi p;
71  p.id = SENSOR_ID;
72  p.sayac = ++paketSayaci;
73  p.sicaklik = dht.readTemperature();
74  p.nem = dht.readHumidity();
75  p.pil = analogReadMilliVolts(PIL_PIN) * 2 / 1000.0;   // bölücü yarıya indirir / the divider halves it
76
77  esp_now_send(gecitMac, (uint8_t *)&p, sizeof(p));
78  unsigned long baslangic = millis();
79  while (!sonucGeldi && millis() - baslangic < 200) delay(1);   // en fazla 200 ms bekle / wait up to 200 ms
80
81  Serial.printf("Kanal %u, %.1f C, %%%.0f, %.2f V, %s\n", (unsigned)kanal, p.sicaklik, p.nem, p.pil,
82                teslimEdildi ? "teslim edildi / delivered" : "teslim edilemedi / failed");
83  if (!teslimEdildi) kanal = 0;        // teslim edilemedi: sonraki uyanışta yeniden tara / not delivered: rescan next wake
84  Serial.flush();
85  uyu();
86}
87
88void loop() {
89}

このスケッチの鍵は、RTC_DATA_ATTRの付いた変数です。ディープスリープ中はESP32のメインメモリが消えますが、RTCメモリにあるこれらの変数は残ります。センサーがスキャンでチャンネルを探すのは最初の起動時だけで(約2秒かかります)、その後の起動ではそのチャンネルを直接使います。パケットが届かなければチャンネルの変数をリセットし、次の起動時にもう一度スキャンします。そのため、ルーターがチャンネルを変えても、センサーは1サイクルで復帰します。電池の電圧はanalogReadMilliVolts()でキャリブレーション済みの値として読み取り、分圧回路で半分になっているので2倍します。抵抗の値は分圧回路ツールで確認できます。GPIO 34はADC1のピンです。Wi-Fiの無線がオンの間はADC2のピンでアナログ値を読み取れないので、これは重要です。スリープモードの詳細はESP32のディープスリープのガイドを見てください。

電池の持ちをおおまかに見積もってみましょう。スキャンなしの起動(ブート、測定、送信)は約0.3秒かかり、その間に平均で約100 mAを消費します。これは起動1回あたり約0.008 mAhです。1分に1回起きると1日に1440回の起動になり、合計で約12 mAhです。ESP32モジュール単体のディープスリープ中の消費電流は約10 µAで、DHT22の待機電流と分圧回路の抵抗を合わせても、スリープ中の消費は1日2 mAh未満に収まります。つまりセンサーは1日に約14 mAhを使い、2000 mAhの18650電池なら4〜5か月もちます。自分の値で計算するには電池寿命計算ツールを使ってください。

この見積もりはモジュール単体か低消費電力のボードの場合です。普通のESP32 DevKitでは、レギュレーター、USBシリアル変換チップ、電源LEDがスリープ中でも数mAを消費し、電池の持ちは数週間まで縮みます。長持ちするセンサーにするには、自己消費電流の小さいレギュレーター(HT7333など)を使うか、電源LEDを外すか、既製の低消費電力ボードを選びます。測定間隔を60秒から5分に延ばすと、電池の持ちは3倍以上になります。18650電池を安全に充電するには、保護回路付きのTP4056モジュールを使えます。

ESP8266でESP-NOWを使う

Wemos D1 MiniのESP8266ボード
Wemos D1 MiniのようなESP8266ボードもESP-NOWに対応していますが、Arduinoのライブラリと関数のシグネチャはESP32と違います。

ESP8266ボードはESP32ボードと同じネットワークでESP-NOWを使えますが、Arduinoのライブラリが違います。esp_now.hの代わりにespnow.hを、WiFi.hの代わりにESP8266WiFi.hをインクルードし、関数はESP-IDFではなくESP8266 SDKのものを使います。主な違いは次のとおりです。

処理ESP32(esp_now.h)ESP8266(espnow.h)
初期化esp_now_init() == ESP_OK(成功)esp_now_init() == 0(成功)
役割不要esp_now_set_self_role(ESP_NOW_ROLE_COMBO)(送受信の両方)
ピアの追加構造体を使ってesp_now_add_peer(&es)esp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, チャンネル, NULL, 0)
受信コールバック(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)(コア3.x)(uint8_t *mac, uint8_t *data, uint8_t len)(constなし)
送信コールバック(..., esp_now_send_status_t durum)(状態)(uint8_t *mac, uint8_t status)、0が成功
最大のパケット250バイト(v2では1470)250バイト

ESP8266では、WiFi.mode(WIFI_STA)の後にWiFi.disconnect()を忘れずに加えてください。起動時にESP8266は最後に使ったルーターへ自動的に再接続しようとするので、チャンネルが変わってパケットを失う原因になります。データ構造体を両側でpackedとして定義すれば、ESP32とESP8266はどちらも32ビットのリトルエンディアンのプロセッサなので、同じバイトを同じように読み取ります。ESP8266のピンとセットアップについては、ESP8266 NodeMCUのガイドを見てください。

MicroPythonとRaspberry Pi PicoでのESP-NOW

ESP32をMicroPythonでプログラミングしているなら、ESP-NOW用の既製のespnowモジュールがあります。ESP32とESP8266向けのMicroPythonのビルドに含まれています。下のコードは、Arduinoの送信側の例が送る30バイトのPaket構造体を受信し、struct.unpack()で復元します。書式文字列の<はパディングなしのリトルエンディアン、Bは1バイトの数、Iは4バイトの符号なしの数、fはfloat、16sは16バイトのテキストを表します。Arduinoのboolフィールドは、Bで0か1として読み取ります。Thonnyでコードをボードに書き込み、表示されたMACアドレスをArduinoの送信側のaliciMac配列に入力してください。

Python
1import network
2import espnow
3import struct
4import binascii
5
6sta = network.WLAN(network.STA_IF)     # ESP-NOW istasyon arayüzünü kullanır / ESP-NOW uses the station interface
7sta.active(True)
8print("MAC:", binascii.hexlify(sta.config("mac"), ":").decode())   # bu adresi vericiye yaz / put it in the sender
9
10e = espnow.ESPNow()
11e.active(True)
12
13BICIM = "<BIffB16s"                    # Arduino'daki Paket yapısı / the Paket struct from Arduino
14BOYUT = struct.calcsize(BICIM)         # 30 bayt / 30 bytes
15
16while True:
17    mac, msg = e.recv()                # paket gelene kadar bekle / wait for a packet
18    if msg and len(msg) == BOYUT:
19        kimlik, sayac, sicaklik, nem, buton, mesaj = struct.unpack(BICIM, msg)
20        metin = mesaj.split(b"\x00")[0].decode()
21        print(binascii.hexlify(mac, ":").decode(), kimlik, sayac,
22              round(sicaklik, 1), round(nem, 1), bool(buton), metin)

MicroPythonからパケットを送るのも2行で済みます。e.add_peer(mac)で受信側を追加し、e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam"))で同じ構造体を送ります(BICIMはトルコ語で「書式」、Selamは「やあ」)。これでArduinoとMicroPythonのボードが同じネットワークで一緒に動けます。Wi-Fiにつながったボードでは、パケットを取りこぼさないようにsta.config(pm=sta.PM_NONE)で省電力機能をオフにします。MicroPythonが初めてなら、MicroPythonの基本のガイドから始めてください。一方、Raspberry Pi PicoのシリーズにはESP-NOWがありません。無印のPicoには無線がまったくなく、Pico WとPico 2 WのCYW43439チップは標準のWi-FiとBluetoothしか動かせません。PicoをESP-NOWのネットワークに加えるには、小さなESP32-C3をUARTのシリアル通信でつないでブリッジとして使うか、Pico WにWi-Fiでゲートウェイと通信させます。Pico WのWi-FiについてはPico WのWi-FiとWebサーバーで解説しています。同じブリッジの方法はArduino Unoにも使えます。

ESP-NOWのパケットを暗号化する

初期設定では、ESP-NOWのパケットは暗号化されていません。同じチャンネルにいる人なら誰でも空中でパケットを受信でき、構造体をまねた偽のパケットを送ることもできます。ドアロックやガレージのシャッターのようなプロジェクトでは暗号化を使ってください。ESP-NOWはWPA2と同じCCMPを使い、2つのキーで動作します。すべてのデバイスで共通の16バイトのPMK(esp_now_set_pmk()で設定)と、ピアごとに別の16バイトのLMKです。ピアを追加するときにes.encrypt = trueにして、キーをes.lmkにコピーします。相手側も同じLMKで自分を暗号化ピアとして追加する必要があります。暗号化されたパケットを受信するには、受信側が送信側をリストに追加しておかなければなりません。ブロードキャストのパケットは暗号化できず、暗号化ピアは初期設定で最大7台です。暗号化だけでは、受信したパケットを後で再送する攻撃(リプレイ)は防げません。重要なコマンドに増え続けるカウンターを加え、受信側で古い番号を拒否するのが、よい追加の安全策になります。

ESP-NOWのプロジェクトアイデア

  • ワイヤレスチャイム:玄関のESP32のボタンを押すと、室内のボードでブザーが鳴ります。電池を長持ちさせるには、ボタンをディープスリープから起こすピンとして使います。
  • 複数の部屋の温度モニター:各部屋にDHT22かBME280のセンサーを置き、測定値を1台のゲートウェイに集めて、Webページで比べます。
  • 庭の水やり:土壌水分センサーが、ポンプの横のボードに「今すぐ水やり」のコマンドを送ります。配線は土壌水分センサーを見てください。
  • ロボットのワイヤレスリモコン:ジョイスティックの値を1秒に20回送ります。ロボットの機械部分についてはArduinoロボットカーの作り方を見てください。
  • 庭の防犯アラーム:庭のPIRセンサーが動きを検知すると、ゲートウェイがTelegramで通知を送ります。
  • 同期したライトショー:ブロードキャストのパケット1つで、複数のボードのWS2812Bテープを一斉に同じ色に切り替えます。

ESP-NOWのよくあるエラーと直し方

症状/エラー原因直し方
「ESPNOW: Peer interface is invalid」と表示され、ピアを追加できないesp_now_peer_info_tをクリアせずに使っている構造体を= {}かmemsetでクリアし、ifidx = WIFI_IF_STAを設定する
「failed」メッセージが出続けるMACアドレスが間違っている(APのアドレスか入力ミス)、受信側の電源が切れている、または別のチャンネルにいる受信側のSTA MACアドレスを読み直し、両方のボードのチャンネルを確認する
パケットは届くが、値がでたらめ構造体が両側で違うか、Stringを含んでいる構造体を両側で同じ順番のpackedとして定義し、テキストにはcharの配列を使う
エラー:invalid conversion ... to 'esp_now_recv_cb_t'(型を変換できない)コア2.x向けのコードを3.xでコンパイルしている受信コールバックをconst esp_now_recv_info_t *で書く
エラー:invalid conversion ... to 'esp_now_send_cb_t'(型を変換できない)コア3.0〜3.2向けのコードを3.3以降でコンパイルしている送信コールバックをconst esp_now_send_info_t *で書くか、ESP_IDF_VERSIONマクロを使う
「Peer channel is not equal to the home channel, send fail」(ピアのチャンネルが違う)Wi-Fiに接続しているボードのチャンネルが、ピアのチャンネルと違うchannel = 0でピアを追加し、センサーをルーターのチャンネルに合わせる
Wi-Fiにつながったゲートウェイがパケットをいくつか取りこぼすWi-Fiの省電力機能(モデムスリープ)WiFi.setSleep(false)を使う
エラー:ESP_ERR_ESPNOW_NOT_INIT(初期化されていない)esp_now_init()が呼ばれていないか、Wi-Fiの起動前に呼ばれているまずWiFi.mode(WIFI_STA)、その後でesp_now_init()
エラー:ESP_ERR_ESPNOW_FULL(リストが満杯)ピアリストが20件に達しているesp_now_del_peer()で古いピアを削除するか、ブロードキャストを使う
エラー:ESP_ERR_ESPNOW_NO_MEM(メモリ不足)送信キューが空になる前にパケットを続けて送っている次のパケットを送る前に送信結果を待つ
MACアドレスが00:00:00:00:00:00と表示されるコア3.xで、Wi-Fiが完全に起動する前にWiFi.macAddress()を呼んでいるWiFi.mode(WIFI_STA)の後でesp_wifi_get_mac()を使う
トラブルシューティングは、いちばん簡単なステップから始めます。2台のボードを横に並べ、まずブロードキャストアドレスに送ってみてください。ブロードキャストのパケットが届くなら、無線、チャンネル、データ構造体は問題なく、原因はMACアドレスです。何も届かなければ、「ツール」>「Core Debug Level」でDebugを選ぶと、コアの詳しいログがシリアルモニタに表示されます。ボードが再起動を繰り返すなら、原因はおそらく電源です。無線がオンになるときの電流の急増への対策は、ESP32のブラウンアウトとGuru Meditationのガイドにあります。

ESP-NOWは、ルーターもケーブルも追加のモジュールもなしに、複数のESP32を通信させるいちばん速い方法です。MACアドレスを読み取り、データ構造体を両側で同じに定義し、ピアを追加して送るだけです。Wi-Fiと一緒に使うときは、全員が同じチャンネルにいなければならないという1つのルールを忘れないでください。BlueGraysアプリでは、ESP32でのLEDとボタンの制御、DHT22での温度測定、Wi-Fi接続、Webサーバー、ディープスリープなど、このガイドに出てきたすべての要素を、回路図とすぐに使えるコードでステップごとに試せます。

よくある質問

ESP-NOWにルーターやインターネットは必要ですか?

いいえ。ESP-NOWはボードのWi-Fiの無線を使いますが、どのネットワークにも参加しません。ボード同士が相手のMACアドレスへ直接パケットを送ります。データをインターネットに届けたい場合は、Wi-FiとESP-NOWの両方につながったゲートウェイ役のESP32が1台必要です。

ESP-NOWの通信距離はどれくらいですか?

基板上にアンテナがあるESP32ボードなら、屋内では壁によって20〜40メートル、屋外の開けた場所では100〜200メートルほどが一般的です。外付けアンテナのモジュールを使い、両側でLR(Long Range)モードを有効にすれば、見通しで最大1キロメートルまで可能だとEspressifは説明しています。

ESP-NOWでは何台のデバイスを接続できますか?

ボードのピアリストには最大20台のデバイスを登録でき、初期設定ではそのうち暗号化できるのは最大7台です。この上限は送信先のデバイスについてのものです。ゲートウェイは暗号化なしのパケットを送ってくる20個以上のセンサーを受信でき、ブロードキャストならパケット1つで同じチャンネルのすべてのデバイスに届きます。

ESP-NOWとWi-Fiは同時に使えますか?

使えます。ただし、ESP32の1つしかない無線は一度に1つのチャンネルでしか動かないので、すべてのESP-NOWデバイスがルーターのチャンネルを使う必要があります。ゲートウェイではWiFi.setSleep(false)で省電力機能をオフにし、センサーではネットワーク名でスキャンしてチャンネルを見つけるか、ルーターを固定のチャンネルに設定します。

ESP32とESP8266はESP-NOWで通信できますか?

できます。どちらのチップもESP-NOWに対応していて、互いにパケットを送れます。ESP8266で違うのはライブラリ(espnow.h)と関数のシグネチャだけです。データ構造体を両側でpackedの同じ形に定義し、パケットを250バイト未満に収めてください。

ESP-NOWで送れるデータの最大サイズは?

ESP-NOW v1では1つのパケットに最大250バイトが入り、この上限はESP8266を含むすべてのボードに当てはまります。ESP-IDF 5.4以降(Arduinoコア3.2以降)で動くボードはESP-NOW v2で最大1470バイトを送れますが、v1を使うボードは250バイトより長いパケットを受信できません。

全プロジェクトを見る
ESP32でLEDを点滅
#103初級

ESP32でLEDを点滅

ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

4.7(37)
99
ESP32でボタンによるLED制御(トグル)
#105初級

ESP32でボタンによるLED制御(トグル)

ボタンを押すたびにLEDの状態が切り替わり、点灯中なら消灯し、消灯中なら点灯します。基本的なトグル動作です。チャタリング対策でボタンの機械的なノイズを除去し、1回の操作で1回だけ切り替わるようにします。

4.6(39)
57
2台のArduinoで双方向通信
#58中級
プレミアム

2台のArduinoで双方向通信

2台のArduinoをケーブルでつないで通信します。片方のボタンを押すともう片方のライトが点灯し、もう一度押すと消灯します。両方のボードで同じプログラムを動かすので、どちらからでもメッセージを送受信できます。ボード同士でメッセージをやり取りする仕組みを、シンプルな構成で学べます。

4.5(20)
26
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

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

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

4.8(38)
56
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非同期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のディープスリープで節電(OLED + RTC RAMカウンター)
#135上級
プレミアム

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)

ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。

4.3(10)
16
ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
#144上級
プレミアム

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)

ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

4.7(7)
12