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とは?仕組みは?
通常の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台のボードを使う例では、両方のボードでこれを行い、アドレスを控えておきます。
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}構造体でデータを送る方法と250バイトの上限
ESP-NOWが送るのはバイトの配列だけで、その中身が何かは関知しません。いくつかの異なる値を一度に送るいちばんきれいな方法は、それらを構造体(struct)にまとめ、構造体のバイトをメモリ上の並びのまま送ることです。受信側は同じ構造体を定義し、届いたバイトをmemcpyでその上にコピーします。これがうまくいくには、構造体が両側で同じ順番、同じ型、同じサイズでなければなりません。コンパイラは、アクセスを速くするためにフィールドの間に見えないパディングのバイトを入れることがあります。以下の例のPaket構造体(Paketはトルコ語で「パケット」)は通常36バイトになりますが、__attribute__((packed))を付けるとちょうど30バイトに縮みます。packedを使えば、ESP32とESP8266の間でも、ArduinoとMicroPythonの間でも、すべてのバイトが同じ位置に並ぶことが保証されます。
| 型 | サイズ | メモ |
|---|---|---|
| bool、uint8_t、int8_t、char | 1バイト | フラグ、小さな数、1文字 |
| int16_t、uint16_t | 2バイト | −32768〜32767または0〜65535 |
| int、int32_t、uint32_t | 4バイト | intはESP32とESP8266では4バイト、Arduino Unoでは2バイト |
| float | 4バイト | 精度は約7桁 |
| double | 8バイト | 本当に必要な場合以外は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:送信側のスケッチ
- Arduino IDEで、2台のESP32ボードともESP32 Dev Moduleが選ばれていることを確認します。ESP-NOWのライブラリはボードパッケージに含まれているので、ほかにインストールするものはありません。
- 受信側のボードから読み取ったMACアドレスを、下のコードのaliciMac配列(aliciはトルコ語で「受信側」)に入力します。
- 送信側のGPIO 4とGNDの間に押しボタンをつなぎます。INPUT_PULLUPを使うので、追加の抵抗は不要です。詳しくはArduinoに押しボタンをつなぐ方法を見てください。
- スケッチを送信側に書き込み、シリアルモニタを115200ボーで開きます。受信側がまだ動いていない間は、2秒ごとに「Teslim edilemedi / failed」(届かなかった)と表示されるのが正常です。
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の受信側のスケッチ
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)もわかります。この情報は関数の中でだけ有効なので、後で必要ならコピーしておきます。受信側は送信側をピアとして追加する必要はありません。暗号化されていないユニキャストのパケットとブロードキャストは、リストにないボードからでも受け取れます。
双方向の通信:2台のESP32が互いに制御し合う
双方向の通信では、どのボードも送信側であり受信側でもあります。下のスケッチを両方のボードに書き込みます。違いは、それぞれのボードのkarsiMac配列(karsiはトルコ語で「相手側」)に相手のボードのアドレスを入れることだけです。一方のボードでボタンを押すと、もう一方のボードのLEDが点灯または消灯し、LEDを切り替えたボードが受信確認のメッセージを返します。配線は両方のボードで同じです。
| 部品 | ESP32のピン | メモ |
|---|---|---|
| ボタン | GPIO 4とGND | 内部プルアップを使うので、追加の抵抗は不要 |
| LED | GPIO 2 | 多くのDevKitでは内蔵の青色LED、外付けのLEDには220 Ωの抵抗を使う |
| 電源 | USB | ボードは別々のUSBポートやACアダプターから給電できる |
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台ずつの送信とブロードキャストの両方を行います。
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 km | Espressifによる見通しでの数値。速度は0.5または0.25 Mbpsに下がる |
| 初期設定の通信速度 | 1 Mbps | 速度が高いほど通信距離は短くなる |
| 250バイトのパケットの送信時間 | 約2.5 ms | ACKと待ち時間を含めると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と同じ |
| 送信元のアドレス | mac | bilgi->src_addr | bilgi->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」(ピアのチャンネルが現在のチャンネルと違う)というエラーも出ます。トラブルのない構成にするには、次のルールを守ってください。
- ゲートウェイは先にWi-Fiに接続し、その後でESP-NOWを開始して、WiFi.channel()でルーターのチャンネルをシリアルモニタに表示します。
- センサーはルーターに接続しません。ネットワーク名でスキャンしてチャンネルを見つける(WiFi.scanNetworks()とWiFi.channel(i))か、コードで固定のチャンネルを使います。このためにWi-Fiのパスワードは必要ありません。
- ルーターのチャンネルを「自動」から固定のチャンネル(1、6、11のどれか)に変えます。自動のままだと、ルーターがチャンネルを変えたときにセンサーがゲートウェイを見失います。
- ゲートウェイではWiFi.setSleep(false)で省電力機能をオフにします。初期設定では、ルーターに接続したESP32は一定の間隔で無線をスリープさせるので、その間に届いたESP-NOWのパケットを取りこぼすことがあります。
- センサーにはゲートウェイのSTA MACアドレスを入力します。下のゲートウェイのスケッチは、起動時にこのアドレスをシリアルモニタに表示します。
下のゲートウェイのスケッチは、自宅のネットワークに接続して最大5個のセンサーの測定値を保存し、同じネットワーク上のスマホから開ける簡単なWebページに表示します。ネットワーク名とパスワードを自分のものに書き換え、書き込んだ後、シリアルモニタに表示されたIPアドレスをスマホのブラウザで開いてください。
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 " °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のピン | メモ |
|---|---|---|
| DHT22のVCC(+) | 3V3 | 3.3〜5 Vで動作 |
| DHT22のDATA | GPIO 4 | センサー単体の場合は、DATAと3V3の間に10 kΩの抵抗を入れる |
| DHT22のGND(−) | GND | 共通グラウンド |
| 電池の+ | GPIO 34(分圧回路経由) | 100 kΩの抵抗2本。4.2 Vの電池がピンでは2.1 Vになる |
| 電池の− | GND | 分圧回路の下側の抵抗もGNDへ |
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か月もちます。自分の値で計算するには電池寿命計算ツールを使ってください。
ESP8266でESP-NOWを使う

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配列に入力してください。
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()を使う |
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を点滅
ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

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

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

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

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

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

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

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

