ESP-NOW на ESP32: беспроводная связь между платами без роутера
ESP-NOW на ESP32: как узнать MAC-адрес, отправить структуру до 250 байт в одну или обе стороны, подключить до 20 пиров, какая дальность, что изменилось в core 3.x и как совместить с Wi-Fi.
ESP-NOW — беспроводной протокол от Espressif, с которым платы ESP32 и ESP8266 отправляют данные напрямую друг другу без роутера, точки доступа и интернета. Платы используют свой Wi-Fi-радиомодуль, но ни к какой сети не подключаются: каждое устройство находит другое по MAC-адресу и передаёт небольшие пакеты до 250 байт за несколько миллисекунд. Устанавливать соединение, получать IP-адрес и вводить пароль не нужно, поэтому нажатие кнопки на одной плате мгновенно включает реле на другой, а датчик на батарейке может проснуться, отправить показание и сразу уснуть снова.
В этом руководстве настроим ESP-NOW с нуля: сначала узнаешь MAC-адреса своих плат, затем на примерах односторонней и двусторонней связи отправишь в одном пакете разные значения — температуру, счётчик и текст — с помощью структуры (struct). Дальше разберёмся с ограничением в 20 пиров, широковещательными сообщениями, дальностью и скоростью, функциями, которые изменились в ESP32 Arduino core 3.x, и согласованием каналов — единственным правилом для одновременной работы ESP-NOW и Wi-Fi. В последней части удалённый датчик на батарейке будет просыпаться из глубокого сна, отправлять показания на шлюз, подключённый к Wi-Fi, а смотреть их можно будет с телефона. Если это первый проект на ESP32, сначала выполни настройку Arduino IDE из статьи Что такое ESP32?.

Что такое ESP-NOW и как он работает?
В обычном Wi-Fi плата ESP32 сначала подключается к роутеру, проверяет пароль, получает IP-адрес по DHCP и только после этого может отправлять данные; обычно на это уходит 1–3 секунды. ESP-NOW пропускает все эти шаги. Он кладёт данные в короткий Wi-Fi-пакет стандарта IEEE 802.11, который называется vendor-specific action frame (кадр действия, определяемый производителем), и отправляет его прямо на MAC-адрес другой платы. Принимающая плата отвечает аппаратным кадром подтверждения (ACK), а функция обратного вызова (callback) на стороне отправителя сообщает, доставлен ли пакет. Если ACK не пришёл, уровень Wi-Fi автоматически повторяет отправку несколько раз. Проще говоря, ESP-NOW работает как невидимый почтовый ящик между двумя платами: пишешь адрес, опускаешь пакет и получаешь ответ, когда другая сторона его приняла.
| Характеристика | Значение |
|---|---|
| Разработчик | Espressif Systems |
| Частота | 2,4 GHz (Wi-Fi-радиомодуль, каналы 1–13) |
| Данных в одном пакете | 250 байт (ESP-NOW v1); 1470 байт с v2 на ESP-IDF 5.4 и новее |
| Зарегистрированные пиры | До 20; по умолчанию до 7 зашифрованных |
| Скорость по умолчанию | 1 Mbps |
| Подтверждение доставки | Есть для unicast (ACK), нет для broadcast |
| Шифрование | По желанию, CCMP (AES-128); ключи PMK и LMK по 16 байт |
| Роутер / точка доступа | Не нужен |
| Поддерживаемые чипы | Чипы ESP32 с Wi-Fi (ESP32, S2, S3, C2, C3, C6…) и ESP8266 |
Главные преимущества ESP-NOW — быстрый отклик и низкое энергопотребление. При нажатии кнопки светодиод на другой стороне загорается через несколько миллисекунд; то же самое по Wi-Fi требует сначала подключиться, а потом отправить запрос на сервер. Для датчика на батарейке разница ещё больше: во время подключения к Wi-Fi радиомодуль секундами потребляет больше 100 mA, а с ESP-NOW плата просыпается, отправляет показание и снова засыпает в сумме за несколько сотен миллисекунд. С другой стороны, ESP-NOW не выходит в интернет: чтобы данные попали в интернет или на телефон, где-то в сети должна быть ESP32-шлюз, подключённая к Wi-Fi. Такую схему соберём дальше в этом руководстве.
ESP-NOW, Wi-Fi, Bluetooth и nRF24L01: сравнение
| Способ | Доп. модуль | Роутер | Размер пакета | Типичная дальность (открытое пространство) | Лучше всего для |
|---|---|---|---|---|---|
| ESP-NOW | Не нужен | Не нужен | 250 байт | 100–200 м, в режиме LR больше | Команды и данные датчиков между платами |
| Wi-Fi (HTTP, MQTT) | Не нужен | Нужен | Практически не ограничен | Зона покрытия роутера | Интернет, веб-страницы, облако |
| Bluetooth (BLE) | Не нужен | Не нужен | 20 байт по умолчанию | 10–20 м в помещении | Подключение к телефону |
| 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 | Нет | Можно подключить к ESP32 через последовательный порт (UART) |
ESP-NOW работает и между разными чипами ESP: датчик на ESP32-C3 может отправлять пакеты на шлюз на классической ESP32 или на плату ESP8266. Условий всего два: обе стороны работают на одном Wi-Fi-канале, а структура данных на обеих сторонах объявлена совершенно одинаково. В примерах ниже используются две платы ESP32 DevKit; какие пины твоей платы безопасно использовать, можно посмотреть в утилите распиновки.
Шаг 1: узнаём MAC-адрес ESP32
В ESP-NOW платы находят друг друга по 6-байтному MAC-адресу; этот адрес записывается в каждую ESP32 на заводе и никогда не меняется. Загрузи скетч ниже в плату, которая будет приёмником, и открой монитор порта на скорости 115200 бод. Скетч выводит адрес и в читаемом виде, и строкой-массивом, которую можно сразу вставить в код другой платы. Для примеров с двумя платами сделай это на обеих и запиши адреса.
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. Чтобы это работало, у структуры на обеих сторонах должны быть одинаковые порядок, типы и размер. Для ускорения доступа компилятор может вставлять между полями невидимые выравнивающие байты (padding): структура Paket из примеров ниже обычно занимает 36 байт, но с __attribute__((packed)) сокращается ровно до 30 байт. С packed каждый байт гарантированно стоит на одном и том же месте у ESP32 и ESP8266, у Arduino и MicroPython.
| Тип | Размер | Примечание |
|---|---|---|
| bool, uint8_t, int8_t, char | 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, если double действительно не нужен |
| char mesaj[16] | 16 байт | Текст фиксированной длины; должен заканчиваться на '\0' |
| String, char*, указатель | Отправить нельзя | Уходит только адрес в памяти, который на другой стороне ничего не значит |
В пакет помещается максимум 250 байт. Это ограничение пришло из первой версии ESP-NOW (v1) и действует на всех платах, включая ESP8266. Arduino core 3.2 и новее на базе ESP-IDF 5.4 умеет отправлять пакеты до 1470 байт по ESP-NOW v2, но плата с v1 (ESP8266 или ESP32 со старым ядром) обрезает пакеты длиннее 250 байт или вообще их отбрасывает. Поэтому в смешанной сети надёжнее всего укладываться в 250 байт. Строка static_assert в скетче отправителя останавливает компиляцию с понятным сообщением, если структура вырастет больше 250 байт. Если нужно передать данные крупнее, раздели их на пронумерованные части и собери по порядку на приёмнике; для показаний датчиков, состояний кнопок или команд нескольких десятков байт более чем достаточно.
Односторонний ESP-NOW: скетч отправителя
- Проверь, что обе платы ESP32 выбраны в Arduino IDE как ESP32 Dev Module. Библиотека ESP-NOW входит в пакет платы, так что больше ничего устанавливать не нужно.
- Впиши MAC-адрес, считанный с платы-приёмника, в массив aliciMac в коде ниже.
- Подключи на отправителе тактовую кнопку между GPIO 4 и GND; так как используется INPUT_PULLUP, дополнительный резистор не нужен. Подробности — в статье Как подключить кнопку к Arduino.
- Загрузи скетч в отправителя и открой монитор порта на скорости 115200 бод. Пока приёмник не запущен, сообщение «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: без Wi-Fi-радиомодуля ESP-NOW работать не может, поэтому 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 в начале выбирает нужную форму функции обратного вызова для отправки, которая зависит от версии ядра; это объясняется в разделе про core 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 и сообщить об этом loop() через флаг volatile. В core 3.x параметр bilgi также сообщает, кто отправил пакет (bilgi->src_addr), и уровень сигнала (bilgi->rx_ctrl->rssi, в dBm); эти данные действительны только внутри функции, так что, если они понадобятся позже, скопируй их. Приёмнику не нужно добавлять отправителя в пиры: незашифрованные адресные (unicast) и широковещательные пакеты принимаются и от плат, которых нет в списке.
Двусторонняя связь: две платы ESP32 управляют друг другом
При двусторонней связи каждая плата одновременно и отправитель, и приёмник. Скетч ниже загружается в обе платы; разница только в том, что на каждой в массив karsiMac записывается адрес другой платы. Нажимаешь кнопку на одной плате — светодиод на другой включается или выключается, а плата, которая переключила светодиод, отвечает сообщением-подтверждением. Подключение на обеих платах одинаковое:
| Деталь | Пин ESP32 | Примечание |
|---|---|---|
| Кнопка | GPIO 4 и GND | Используется встроенная подтяжка, дополнительный резистор не нужен |
| Светодиод | GPIO 2 | На большинстве DevKit это встроенный синий светодиод; для внешнего светодиода нужен резистор 220 Ω |
| Питание | USB | Платы можно питать от разных USB-портов или блоков питания |
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 в структуре сообщения говорит, что это за пакет: 1 — команда, 2 — подтверждение. Плата, на которой нажали кнопку, отправляет состояние, в которое должен перейти светодиод другой платы; плата, получившая команду, устанавливает светодиод и отправляет то же состояние обратно с типом 2. Так отправитель получает и аппаратный ACK, и подтверждение того, что «светодиод действительно переключился». Пришедшая команда снова передаётся в loop() через флаги volatile, и digitalWrite с ответом выполняются уже там. Это надёжнее, чем вызывать esp_now_send() внутри функции обратного вызова. Пауза 30 мс для кнопки отсекает дребезг контактов. Подключи вместо светодиода реле — и тот же код превратится в беспроводной выключатель света; как подключать реле, рассказано в статье Что такое модуль реле?.
Несколько устройств: ограничение в 20 пиров и широковещательная рассылка
ESP-NOW поддерживает все три схемы: один отправитель и много приёмников (пульт и пять плат с реле), много отправителей и один приёмник (пять датчиков и один шлюз) и сети, где все общаются со всеми. Список пиров платы вмещает максимум 20 записей, и по умолчанию зашифрованными могут быть только 7 из них. Это ограничение касается только плат, которым ты отправляешь. Шлюзу не нужно добавлять датчики, которые шлют ему незашифрованные пакеты, поэтому он может слушать больше 20 датчиков. Если список заполнен, esp_now_add_peer() возвращает ESP_ERR_ESPNOW_FULL; тогда ненужные пиры можно удалить через esp_now_del_peer() и добавить новые. Скетч ниже добавляет в список три приёмника и широковещательный адрес, а затем отправляет данные и каждому по очереди, и широковещательно:
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 тоже нужно добавить в пиры перед отправкой на него. Один широковещательный пакет доходит до всех приёмников ESP-NOW на том же канале; MAC-адреса приёмников знать не нужно, поэтому настроить такой способ проще всего. Цена в том, что у broadcast нет подтверждения доставки, результат отправки всегда выглядит успешным, а широковещательные пакеты нельзя зашифровать. При отправке по очереди каждый пакет получает свой ACK. Если передать в esp_now_send() вместо адреса NULL, пакет отправится отдельно каждому пиру из списка; если в списке есть и широковещательный адрес, пакет также один раз уйдёт широковещательно. Практичный подход для многих проектов: при первом запуске платы находят друг друга широковещательной рассылкой, затем запоминают MAC-адреса и переходят на адресную отправку с подтверждением.
Дальность и скорость ESP-NOW
| Параметр | Типичное значение | Примечание |
|---|---|---|
| Дальность в помещении | 20–40 м (1–3 стены) | Сильнее всего сигнал ослабляют железобетонные стены и металлические предметы |
| Дальность на открытом пространстве | 100–200 м | Со встроенной печатной антенной; растёт, если поднять платы выше над землёй |
| Режим LR (Long Range) | До 1 км | Данные Espressif для прямой видимости; скорость падает до 0,5 или 0,25 Mbps |
| Скорость по умолчанию | 1 Mbps | Чем выше скорость, тем меньше дальность |
| Время передачи пакета 250 байт | Около 2,5 мс | Около 3 мс на пакет с учётом ACK и ожидания |
| Теоретическая частота пакетов | ~300 пакетов в секунду (~70 кБ/с) | С пакетами по 250 байт; на практике меньше |
| Задержка команды | Несколько миллисекунд | Не нужны ни установка соединения, ни сервер |
Эти значения сильно зависят от обстановки, но расчёт простой. На скорости 1 Mbps по умолчанию каждый байт находится в эфире 8 микросекунд; 250 байт данных вместе с заголовками и синхропреамбулой занимают около 2,5 мс. С учётом ACK от приёмника и ожидания свободного канала на пакет уходит примерно 3 мс, значит, в секунду можно отправить не больше 300 пакетов. Для данных датчиков и команд этого с избытком, но для потокового аудио или видео ESP-NOW не подходит. Чтобы увеличить дальность, поднимай платы повыше над землёй, не закрывай антенну металлическим корпусом, батарейками или кабелями, используй модули с внешней антенной (например, ESP32-WROOM-32U) и включи режим Long Range (LR) от Espressif на обеих сторонах командой esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR). LR работает только между двумя платами ESP32; интерфейс, настроенный только на LR, не может подключиться к обычным роутерам, а ESP8266 и ESP32-C2 этот режим не поддерживают. При проверке дальности на core 3.x выводи на приёмнике bilgi->rx_ctrl->rssi: −50 dBm — очень сильный сигнал, −80 dBm — близко к пределу, а около −90 dBm начинаются потери пакетов.
Что изменилось в ESP32 Arduino core 3.x
Большинство примеров ESP-NOW в интернете написано для ESP32 Arduino core 2.x. Если через Менеджер плат установлена версия 3.x, такой код не скомпилируется: вместе с ESP-IDF 5 компания Espressif изменила форму функций обратного вызова. Изменения пришли в два этапа: в core 3.0 поменялась функция обратного вызова для приёма, а в core 3.3 на базе ESP-IDF 5.5 — для отправки. Какая версия установлена, видно рядом с пакетом esp32 в разделе «Инструменты › Плата › Менеджер плат» (Tools › Board › Boards Manager).
| Что | Core 2.x | Core 3.0–3.2 | Core 3.3 и новее |
|---|---|---|---|
| Колбэк приёма | (const uint8_t *mac, const uint8_t *veri, int uzunluk) (MAC, данные, длина) | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) (сведения о пакете, данные, длина) | Как в 3.0 |
| Адрес отправителя | mac | bilgi->src_addr | bilgi->src_addr |
| Колбэк отправки | (const uint8_t *mac, esp_now_send_status_t durum) (MAC, статус) | Как в 2.x | (const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) (сведения об отправке, статус) |
| Максимальный пакет | 250 байт | 250 байт; 1470 байт с v2 на 3.2 | 1470 байт с v2 |
| Установка канала | esp_wifi_set_channel() | Также WiFi.setChannel() | Как в 3.0 |
| Готовая библиотека классов | Нет | ESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial) | ESP32_NOW.h с поддержкой v2 |
При компиляции старого кода на 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. При переходе на core 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+. В core 3.x также появилась библиотека на классах ESP32_NOW.h: onReceive() и onSent() пишутся в классе, унаследованном от ESP_NOW_Peer, а класс ESP_NOW_Serial позволяет работать со связью ESP-NOW так же, как с Serial. Её примеры находятся в разделе ESP_NOW меню «Файл › Примеры» (File › Examples). Мы используем функции esp_now.h напрямую, потому что они совместимы с ESP8266 и со старым кодом.
ESP-NOW и Wi-Fi одновременно: согласование канала
У ESP32 один радиомодуль на 2,4 GHz, и в каждый момент он работает только на одном канале. Как только плата подключается к роутеру, канал выбираешь уже не ты, а роутер: если роутер на 6-м канале, ESP-NOW тоже работает на 6-м канале. Поэтому каждый датчик, который шлёт пакеты на шлюз, подключённый к Wi-Fi, тоже должен перейти на канал роутера. Если каналы не совпадают, отправитель выводит «failed»; если пир добавлен с ненулевым каналом, в мониторе порта появляется ещё и ошибка 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, пришедшие в это время.
- Укажи на датчиках MAC-адрес STA шлюза; скетч шлюза ниже выводит этот адрес в монитор порта при запуске.
Скетч шлюза ниже подключается к домашней сети, хранит показания до пяти датчиков и показывает их на простой веб-странице, которую можно открыть с телефона в той же сети. Замени имя сети и пароль на свои, а после загрузки открой в браузере телефона 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}В колбэке приёма шлюз только кладёт пакет в очередь (queue) FreeRTOS и сразу выходит. loop() разбирает очередь и сохраняет последнее показание и время прихода для каждого датчика, а веб-сервер показывает эту таблицу. Очередь гарантирует, что ни один пакет не потеряется, если несколько датчиков отправят данные в одно и то же мгновение; к тому же это практичный способ следовать совету Espressif не выполнять долгую работу в колбэках. Страница обновляется сама каждые 10 секунд, а по столбцу «Son paket» (последний пакет) сразу видно, что датчик замолчал (села батарейка или он вне зоны досягаемости?). Если шлюз вообще не подключается к Wi-Fi, дело не в ESP-NOW: пройди проверки из статьи ESP32 не подключается к Wi-Fi. Если данные нужно передавать в систему умного дома, вместо веб-страницы можно использовать MQTT и Home Assistant.
Пример удалённого датчика: DHT22, глубокий сон и ESP-NOW
Теперь соберём датчик на батарейке, который отправляет данные на шлюз. При каждом пробуждении датчик измеряет температуру, влажность и напряжение батареи, отправляет один пакет ESP-NOW, ждёт подтверждения доставки (ACK) и уходит в глубокий сон на 60 секунд. Для температуры и влажности используем DHT22; из Менеджера библиотек нужно установить DHT sensor library от Adafruit. Как работает датчик, рассказано в нашем руководстве Датчик температуры и влажности DHT11; у DHT22 подключение такое же, а в коде достаточно указать тип датчика DHT22.




| Соединение | Пин ESP32 | Примечание |
|---|---|---|
| DHT22 VCC (+) | 3V3 | Работает от 3,3–5 V |
| DHT22 DATA | GPIO 4 | Для датчика без платы добавь резистор 10 kΩ между DATA и 3V3 |
| DHT22 GND (−) | GND | Общая земля |
| Батарея + | GPIO 34 (через делитель) | Два резистора по 100 kΩ; 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 секунд), а при следующих пробуждениях сразу использует найденный канал; если пакет не удалось доставить, он сбрасывает переменную канала и при следующем пробуждении сканирует снова. Поэтому, даже если роутер сменит канал, датчик восстановится за один цикл. Напряжение батареи считывается уже откалиброванным через analogReadMilliVolts() и умножается на 2, потому что делитель делит его пополам; номиналы резисторов можно проверить в утилите делителя напряжения. GPIO 34 — пин ADC1; это важно, потому что пины ADC2 не могут делать аналоговые измерения, пока включён Wi-Fi-радиомодуль. Подробности о режимах сна — в нашем руководстве Глубокий сон ESP32.
Грубо оценим время работы от батареи. Пробуждение без сканирования (загрузка, измерение и отправка) длится около 0,3 секунды и потребляет в среднем около 100 mA, то есть примерно 0,008 mAh на одно пробуждение. При пробуждении раз в минуту получается 1440 пробуждений в сутки, в сумме около 12 mAh. Голый модуль ESP32 в глубоком сне потребляет около 10 µA; вместе с током покоя DHT22 и резисторами делителя расход во сне остаётся ниже 2 mAh в сутки. Итого датчик тратит около 14 mAh в сутки, и аккумулятора 18650 на 2000 mAh хватает на 4–5 месяцев. Чтобы посчитать со своими значениями, воспользуйся калькулятором времени работы от батареи.
ESP-NOW на ESP8266

Платы ESP8266 могут работать по ESP-NOW в одной сети с платами ESP32, но библиотека Arduino у них другая: подключается espnow.h вместо esp_now.h и ESP8266WiFi.h вместо WiFi.h, а функции берутся из SDK ESP8266, а не из ESP-IDF. Основные отличия:
| Задача | 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, channel, NULL, 0) |
| Колбэк приёма | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) (сведения о пакете, данные, длина) | (uint8_t *mac, uint8_t *data, uint8_t len) (MAC, данные, длина) |
| Колбэк отправки | (..., esp_now_send_status_t durum) | (uint8_t *mac, uint8_t status); 0 означает успех |
| Максимальный пакет | 250 байт (1470 с v2) | 250 байт |
На ESP8266 не забудь добавить WiFi.disconnect() после WiFi.mode(WIFI_STA): при запуске ESP8266 сама пытается переподключиться к последнему роутеру, а это меняет канал и может приводить к потере пакетов. Если структура данных на обеих сторонах объявлена как packed, ESP32 и ESP8266 одинаково читают одни и те же байты, ведь оба процессора 32-битные и хранят данные младшим байтом вперёд (little-endian). Пины и настройка ESP8266 описаны в нашем руководстве ESP8266 NodeMCU.
ESP-NOW в MicroPython и Raspberry Pi Pico
Если ESP32 программируется на MicroPython, для ESP-NOW есть готовый модуль espnow; он входит в сборки MicroPython для ESP32 и ESP8266. Код ниже принимает 30-байтную структуру Paket, которую отправляет пример отправителя на Arduino, и раскладывает её с помощью struct.unpack(). В строке формата < означает порядок «младшим байтом вперёд» без выравнивания; B — однобайтное число, I — 4-байтное беззнаковое число, f — float, а 16s — 16 байт текста. Поле bool из Arduino читается через B как 0 или 1. Загрузи код в плату через Thonny и впиши выведенный им MAC-адрес в массив aliciMac отправителя на Arduino:
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 тоже занимает две строки: добавь приёмник через e.add_peer(mac) и отправь ту же структуру командой e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")) — так платы на Arduino и MicroPython могут работать вместе в одной сети. На плате с MicroPython, подключённой к Wi-Fi, отключи энергосбережение через sta.config(pm=sta.PM_NONE), чтобы не пропускать пакеты. Если MicroPython в новинку, начни с нашего руководства Основы MicroPython. А вот у семейства Raspberry Pi Pico ESP-NOW нет: у обычной Pico беспроводной связи нет вообще, а чип CYW43439 на Pico W и Pico 2 W умеет только стандартные Wi-Fi и Bluetooth. Чтобы включить Pico в сеть ESP-NOW, подключи к ней маленькую ESP32-C3 через последовательную связь UART и используй её как мост или пусть Pico W общается со шлюзом по Wi-Fi; про Wi-Fi на Pico W рассказано в статье Pico W: Wi-Fi и веб-сервер. Тот же подход с мостом подходит и для Arduino Uno.
Шифрование пакетов ESP-NOW
По умолчанию пакеты ESP-NOW не шифруются: любой на том же канале может перехватить их из эфира и отправить поддельные пакеты, имитирующие твою структуру. Для проектов вроде дверных замков или гаражных ворот используй шифрование. ESP-NOW применяет CCMP, как и WPA2, и работает с двумя ключами: 16-байтным PMK, одинаковым на всех устройствах (задаётся через esp_now_set_pmk()), и отдельным 16-байтным LMK для каждого пира. При добавлении пира задай es.encrypt = true и скопируй ключ в es.lmk; другая сторона тоже должна добавить тебя как зашифрованный пир с тем же LMK. Чтобы принимать зашифрованные пакеты, приёмник обязан добавить отправителя в свой список. Широковещательные пакеты зашифровать нельзя, а зашифрованных пиров по умолчанию может быть не больше 7. Само по себе шифрование не мешает повторно отправить перехваченный пакет позже; добавить к важным командам возрастающий счётчик и отбрасывать на приёмнике старые номера — хорошая дополнительная защита.
Идеи проектов на ESP-NOW
- Беспроводной дверной звонок: при нажатии кнопки на ESP32 у двери звучит зуммер на плате внутри дома; ради времени работы от батареи сделай кнопку пином, который будит плату из глубокого сна.
- Контроль температуры в нескольких комнатах: поставь в каждую комнату датчик DHT22 или BME280, собирай показания на одном шлюзе и сравнивай их на веб-странице.
- Полив сада: датчик влажности почвы отправляет команду «пора поливать» плате рядом с насосом; про подключение — в статье Датчик влажности почвы.
- Беспроводной пульт для робота: отправляй значения джойстика 20 раз в секунду; про механику робота читай в статье Как собрать робота-машинку на Arduino.
- Сигнализация в саду: когда PIR-датчик в саду замечает движение, шлюз отправляет уведомление в Telegram.
- Синхронное световое шоу: одним широковещательным пакетом переключай ленты WS2812B на нескольких платах на один и тот же цвет одновременно.
Частые ошибки ESP-NOW и их решение
| Признак / ошибка | Причина | Решение |
|---|---|---|
| «ESPNOW: Peer interface is invalid», пир не добавляется | esp_now_peer_info_t используется без очистки | Очисти структуру через = {} или memset и задай ifidx = WIFI_IF_STA |
| Постоянные сообщения «failed» | Неверный MAC (адрес AP или опечатка), приёмник выключен или на другом канале | Заново считай MAC-адрес STA приёмника и проверь каналы обеих плат |
| Пакеты приходят, но значения — мусор | Структуры на двух сторонах отличаются или содержат String | Объяви структуру как packed с одинаковым порядком полей на обеих сторонах, а для текста используй массив char |
| «invalid conversion ... to 'esp_now_recv_cb_t'» | Код для core 2.x компилируется на 3.x | Напиши колбэк приёма с const esp_now_recv_info_t * |
| «invalid conversion ... to 'esp_now_send_cb_t'» | Код для core 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 (modem sleep) | Используй 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() или используй broadcast |
| ESP_ERR_ESPNOW_NO_MEM | Пакеты отправляются подряд, пока очередь отправки ещё не освободилась | Дожидайся результата отправки, прежде чем слать следующий пакет |
| MAC-адрес показывается как 00:00:00:00:00:00 | В core 3.x функция WiFi.macAddress() вызвана до полного запуска Wi-Fi | Используй esp_wifi_get_mac() после WiFi.mode(WIFI_STA) |
ESP-NOW — самый быстрый способ научить несколько плат ESP32 общаться без роутера, кабелей и дополнительных модулей: узнай MAC-адрес, объяви одинаковую структуру данных на обеих сторонах, добавь пир и отправляй. Если ESP-NOW работает вместе с Wi-Fi, помни единственное правило: все должны быть на одном канале. В приложении BlueGrays можно пошагово, со схемами подключения и готовым кодом, попробовать все составные части из этого руководства, включая управление светодиодом и кнопкой на ESP32, измерение температуры с DHT22, подключение к Wi-Fi, веб-сервер и глубокий сон.
Частые вопросы
Нужны ли для ESP-NOW роутер или интернет?
Нет. ESP-NOW использует Wi-Fi-радиомодуль плат, но не подключается ни к какой сети: платы отправляют пакеты напрямую на MAC-адреса друг друга. Если данные нужно передать в интернет, понадобится одна ESP32 в роли шлюза, подключённая одновременно к Wi-Fi и к ESP-NOW.
Какая дальность у ESP-NOW?
С платами ESP32 со встроенной антенной обычно получается 20–40 метров в помещении, в зависимости от стен, и около 100–200 метров на открытом пространстве. С модулями с внешней антенной и режимом LR (Long Range), включённым на обеих сторонах, по данным Espressif, в прямой видимости возможно до 1 километра.
Сколько устройств можно подключить по ESP-NOW?
Список пиров платы вмещает до 20 устройств, и по умолчанию зашифрованными могут быть не больше 7 из них. Это ограничение касается устройств, которым ты отправляешь данные: шлюз может слушать больше 20 датчиков, шлющих незашифрованные пакеты, а широковещательный пакет одной отправкой доходит до всех устройств на том же канале.
Можно ли использовать ESP-NOW и Wi-Fi одновременно?
Да, но единственный радиомодуль ESP32 в каждый момент работает только на одном канале, поэтому все устройства ESP-NOW должны использовать канал роутера. Отключи энергосбережение на шлюзе через WiFi.setSleep(false), а на датчиках находи канал сканированием по имени сети или переключи роутер на фиксированный канал.
Могут ли ESP32 и ESP8266 общаться по ESP-NOW?
Да. Оба чипа поддерживают ESP-NOW и могут отправлять пакеты друг другу; на ESP8266 отличаются только библиотека (espnow.h) и сигнатуры функций. Объяви структуру данных как packed и одинаково на обеих сторонах и держи пакеты в пределах 250 байт.
Какой максимальный объём данных у ESP-NOW?
В ESP-NOW v1 пакет вмещает максимум 250 байт, и это ограничение действует на всех платах, включая ESP8266. Платы на ESP-IDF 5.4 и новее (Arduino core 3.2+) могут отправлять до 1470 байт по ESP-NOW v2, но платы с v1 не принимают пакеты длиннее 250 байт.
Что собрать после прочтения
Все проекты
Мигающий светодиод на ESP32
Учит базовой работе с выходами GPIO на ESP32. Эффект мигания получается включением и выключением светодиода с цифрового пина. Это ESP32-версия классического проекта мигания на Arduino UNO; базовые команды мигания в среде Arduino те же, но расположение пинов ESP32 и уровни GPIO 3,3 V отличаются от Arduino UNO.

Управление светодиодом кнопкой на ESP32 (переключение)
При каждом нажатии кнопки состояние светодиода меняется: если горел — гаснет, если не горел — загорается. Классический шаблон переключения. Антидребезг убирает механический шум кнопки, чтобы одно нажатие давало одно переключение.

Двусторонняя связь между двумя Arduino
Две Arduino соединены проводами и разговаривают друг с другом: нажми кнопку на одной, и на другой загорится свет, нажми снова — погаснет. На обеих платах одна и та же программа, поэтому каждая и отпра

Температура и влажность с DHT22 на ESP32 (с LCD-дисплеем)
Этот проект измеряет температуру и влажность воздуха датчиком DHT22 и в реальном времени показывает данные на I2C LCD 16x2. Данные читаются с датчика по однопроводному протоколу через библиотеку Adafruit DHT. Проект служит основой для IoT-метеостанций, умного дома или систем мониторинга среды в теплицах. Связь с DHT22 идёт через пин GPIO 4 у ESP32.

Подключение ESP32 к Wi-Fi в режиме STA (с экраном LCD)
В этом проекте ESP32 надёжно и устойчиво к сбоям подключается к домашней сети Wi-Fi в режиме станции (STA). SSID и пароль хранятся в отдельном файле secrets.h, чтобы не утекли в Git; настроено автоматическое переподключение, а уровень сигнала RSSI и IP-адрес выводятся на I2C LCD-экран. Встроенный светодиод служит индикатором состояния. Это менеджер подключения уровня реальных устройств.

Асинхронный веб-сервер на ESP32 (с выводом IP на LCD)
В этом проекте ESP32 превращается в асинхронный веб-сервер, которым можно управлять с телефона или компьютера в той же сети Wi-Fi. Благодаря библиотеке ESPAsyncWebServer сервер может обрабатывать несколько запросов одновременно (в отличие от классического WebServer.h, который блокируется на одном запросе). Открыв IP-адрес ESP32 в браузере, ты нажимаешь кнопки ON/OFF (маршруты /on и /off) и включаешь и выключаешь светодиод на GPIO 5.

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
Режим глубокого сна (Deep Sleep) у ESP32 снижает потребление до ~10 µA, выключая процессор и большую часть периферии, — это важно для IoT-устройств на батарейках. В этом проекте используются три источника пробуждения: таймер (15 секунд), внешний пин (EXT0 — кнопка GPIO 33) и касание (T0/GPIO 4). Переменные с RTC_DATA_ATTR сохраняются во время глубокого сна — счётчики загрузок, пробуждений по таймеру и по кнопке ведутся непрерывно.

Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
Собери на ESP32 регистратор температуры с отметками времени! Каждую минуту читай температуру и влажность с DHT22, бери реальное время из DS1307 RTC и записывай всё на карту microSD в формате CSV. OLED показывает текущую температуру (крупным шрифтом), влажность в процентах, время и счётчик записей. Файл /temp_log.csv на SD-карте открывается в Excel, Google Sheets или Python pandas — идеально для теплиц, комнат, лабораторий и хранения продуктов.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

