ESP-NOW con ESP32: guida alla comunicazione wireless senza router
ESP-NOW con ESP32: leggi l'indirizzo MAC, invia uno struct fino a 250 byte in una o due direzioni, usa 20 peer, portata, core 3.x e canale Wi-Fi.
ESP-NOW è un protocollo wireless sviluppato da Espressif che permette alle schede ESP32 ed ESP8266 di scambiarsi dati direttamente, senza router, access point né connessione a internet. Le schede usano la loro radio Wi-Fi ma non si collegano a nessuna rete: ogni dispositivo trova l'altro tramite il suo indirizzo MAC e consegna piccoli pacchetti fino a 250 byte in pochi millisecondi. Poiché non c'è una connessione da stabilire, né un indirizzo IP da ottenere né una password da inserire, premendo un pulsante su una scheda puoi attivare all'istante un relè su un'altra, e un sensore a batteria può svegliarsi, inviare la sua misura e tornare subito a dormire.
In questa guida configurerai ESP-NOW da zero: prima leggerai l'indirizzo MAC delle tue schede, poi con esempi monodirezionali e bidirezionali invierai valori diversi come temperatura, un contatore e del testo in un unico pacchetto usando uno struct. Dopo scoprirai il limite di 20 peer, i messaggi broadcast, la portata e la velocità, le funzioni cambiate con il core 3.x di Arduino per ESP32 e l'allineamento del canale, l'unica regola per usare ESP-NOW e Wi-Fi insieme. Nell'ultima parte invierai le misure di un sensore remoto a batteria in deep sleep a un gateway collegato al Wi-Fi e le seguirai dal telefono. Se è il tuo primo progetto con l'ESP32, completa prima l'installazione dell'Arduino IDE descritta in Cos'è l'ESP32?.

Cos'è ESP-NOW e come funziona?
Con il Wi-Fi normale, l'ESP32 si collega prima al router, verifica la password, ottiene un indirizzo IP tramite DHCP e solo allora può inviare dati; di solito ci vogliono 1–3 secondi. ESP-NOW salta tutti questi passaggi. Inserisce i tuoi dati in un breve pacchetto Wi-Fi chiamato action frame specifico del produttore (vendor-specific action frame) dello standard IEEE 802.11 e lo invia direttamente all'indirizzo MAC dell'altra scheda. La scheda che lo riceve risponde con una conferma (ACK) a livello hardware, e una funzione di callback sul lato del mittente ti dice se il pacchetto è stato consegnato. Se l'ACK non arriva, il livello Wi-Fi ritenta automaticamente l'invio alcune volte. In breve, ESP-NOW funziona come una cassetta postale invisibile tra due schede: scrivi l'indirizzo, imbuchi il pacchetto e vieni avvisato quando l'altra parte l'ha ricevuto.
| Caratteristica | Valore |
|---|---|
| Sviluppato da | Espressif Systems |
| Frequenza | 2,4 GHz (radio Wi-Fi, canali 1–13) |
| Dati per pacchetto | 250 byte (ESP-NOW v1); 1470 byte con v2 da ESP-IDF 5.4 in poi |
| Peer registrati | Fino a 20; fino a 7 cifrati per impostazione predefinita |
| Velocità dati predefinita | 1 Mbps |
| Conferma di consegna | Sì in unicast (ACK), no in broadcast |
| Crittografia | Facoltativa, CCMP (AES-128); chiavi PMK e LMK da 16 byte ciascuna |
| Router / access point | Non serve |
| Chip compatibili | Chip ESP32 con Wi-Fi (ESP32, S2, S3, C2, C3, C6…) ed ESP8266 |
I veri punti di forza di ESP-NOW sono la risposta rapida e il basso consumo. Quando premi un pulsante, il LED dall'altra parte si accende in pochi millisecondi; con il Wi-Fi dovresti prima collegarti e poi inviare una richiesta a un server. In un sensore a batteria la differenza è ancora più grande: durante la connessione al Wi-Fi la radio assorbe oltre 100 mA per secondi, mentre con ESP-NOW la scheda si sveglia, invia la sua misura e torna a dormire in poche centinaia di millisecondi in tutto. In compenso ESP-NOW non arriva a internet; se vuoi portare i dati su internet o sul telefono, nella rete deve esserci un ESP32 collegato al Wi-Fi che faccia da gateway. Costruiremo questo schema più avanti nella guida.
ESP-NOW, Wi-Fi, Bluetooth o nRF24L01: quale scegliere?
| Metodo | Modulo extra | Router | Dimensione pacchetto | Portata tipica (campo aperto) | Ideale per |
|---|---|---|---|---|---|
| ESP-NOW | Non serve | Non serve | 250 byte | 100–200 m, di più in modalità LR | Comandi e dati dei sensori tra schede |
| Wi-Fi (HTTP, MQTT) | Non serve | Necessario | Praticamente illimitata | La copertura del router | Internet, pagine web, cloud |
| Bluetooth (BLE) | Non serve | Non serve | 20 byte di default | 10–20 m al chiuso | Collegamento con il telefono |
| nRF24L01 | Necessario (SPI) | Non serve | 32 byte | Qualche decina di metri, molto di più con la versione PA+LNA | Schede senza radio come Arduino Uno e Nano |
| LoRa (SX1278) | Necessario (SPI) | Non serve | Fino a 255 byte | Diversi chilometri | Distanze molto lunghe, dati poco frequenti |
Se devi parlare con il telefono, il Bluetooth è la scelta giusta; se ti serve internet, usa il Wi-Fi. Ma se hai più ESP32 che devono solo comunicare tra loro, ESP-NOW è spesso la strada più semplice: niente moduli extra, niente cavi, niente router e solo poche righe di codice. Per le schede senza radio come Arduino Uno è più adatto l'nRF24L01, per chilometri di portata i moduli LoRa. Per confrontare tutte le opzioni, leggi le soluzioni di comunicazione wireless per Arduino, e per collegarti al telefono c'è la nostra guida al Bluetooth dell'ESP32.
Quali schede supportano ESP-NOW?
| Scheda / chip | ESP-NOW | Nota |
|---|---|---|
| ESP32 (WROOM-32, DevKit) | Sì | Supporta anche la modalità a lungo raggio (LR) |
| ESP32-S2, S3, C3, C6 | Sì | Stesso codice Arduino, cambia solo la scheda selezionata |
| ESP32-C2 (ESP8684) | Sì | Niente modalità LR |
| ESP32-H2 | No | Non ha la radio Wi-Fi, quindi niente ESP-NOW |
| ESP8266 (NodeMCU, Wemos D1 Mini) | Sì | Libreria diversa (espnow.h), solo v1 e 250 byte |
| Raspberry Pi Pico W / Pico 2 W | No | Il chip Wi-Fi (CYW43439) non esegue ESP-NOW |
| Arduino Uno, Nano, Mega | No | Si può collegare a un ESP32 tramite porta seriale (UART) |
ESP-NOW funziona anche tra chip ESP diversi: un sensore ESP32-C3 può inviare pacchetti a un gateway ESP32 classico o a una scheda ESP8266. Le uniche condizioni sono che entrambe le parti siano sullo stesso canale Wi-Fi e che la struttura dei dati sia definita in modo identico su tutte e due. Gli esempi seguenti usano due schede ESP32 DevKit; puoi vedere quali pin della tua scheda sono sicuri nello strumento Pinout.
Passo 1: trovare l'indirizzo MAC dell'ESP32
In ESP-NOW ogni scheda trova l'altra tramite il suo indirizzo MAC di 6 byte; questo indirizzo viene scritto in ogni ESP32 in fabbrica e non cambia mai. Carica lo sketch qui sotto sulla scheda che userai come ricevitore e apri il Monitor seriale a 115200 baud. Lo sketch stampa l'indirizzo in forma leggibile e anche come riga di array da incollare direttamente nel codice dell'altra scheda. Negli esempi con due schede, fallo per entrambe e annota gli indirizzi.
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}Inviare dati con uno struct e il limite di 250 byte
ESP-NOW invia solo un array di byte e non sa cosa contiene. Il modo più pulito per inviare più valori insieme è raggrupparli in uno struct e mandarne i byte così come sono in memoria. Il ricevitore definisce lo stesso struct e ci copia sopra i byte ricevuti con memcpy. Perché funzioni, lo struct deve avere lo stesso ordine, gli stessi tipi e la stessa dimensione da entrambe le parti. Il compilatore può inserire byte di riempimento invisibili tra i campi per accedervi più in fretta: lo struct Paket degli esempi occupa normalmente 36 byte, ma con __attribute__((packed)) scende a esattamente 30 byte. Usare packed garantisce che ogni byte stia nello stesso punto tra un ESP32 e un ESP8266, o tra Arduino e MicroPython.
| Tipo | Dimensione | Nota |
|---|---|---|
| bool, uint8_t, int8_t, char | 1 byte | Flag, numero piccolo, singolo carattere |
| int16_t, uint16_t | 2 byte | −32768…32767 oppure 0…65535 |
| int, int32_t, uint32_t | 4 byte | int occupa 4 byte su ESP32 ed ESP8266, ma 2 byte su Arduino Uno |
| float | 4 byte | Circa 7 cifre di precisione |
| double | 8 byte | Usa float se non ti serve davvero |
| char mesaj[16] | 16 byte | Testo a lunghezza fissa; deve terminare con '\0' |
| String, char*, puntatore | Non si può inviare | Parte solo un indirizzo di memoria, che dall'altra parte non significa nulla |
In un pacchetto entrano al massimo 250 byte. Questo limite viene dalla prima versione di ESP-NOW (v1) e vale per tutte le schede, compreso l'ESP8266. Il core Arduino 3.2 e successivi, basati su ESP-IDF 5.4, possono inviare pacchetti fino a 1470 byte con ESP-NOW v2; però una scheda con v1 (un ESP8266 o un ESP32 con un core vecchio) tronca i pacchetti più lunghi di 250 byte o li scarta del tutto. Quindi in una rete mista la scelta più sicura è restare entro 250 byte. La riga static_assert nello sketch del mittente blocca la compilazione con un messaggio chiaro se lo struct supera i 250 byte. Se devi inviare dati più grandi, dividili in parti numerate e ricomponile in ordine sul ricevitore; per valori dei sensori, stati dei pulsanti o comandi bastano e avanzano poche decine di byte.
ESP-NOW in una direzione: lo sketch del mittente
- Assicurati che entrambi gli ESP32 siano selezionati come ESP32 Dev Module nell'Arduino IDE. La libreria ESP-NOW è inclusa nel pacchetto della scheda, non devi installare altro.
- Scrivi l'indirizzo MAC letto dalla scheda ricevente nell'array aliciMac del codice qui sotto.
- Collega un pulsante tra GPIO 4 e GND sul mittente; poiché usiamo INPUT_PULLUP, non serve una resistenza aggiuntiva. Trovi i dettagli in come collegare un pulsante ad Arduino.
- Carica lo sketch sul mittente e apri il Monitor seriale a 115200 baud. Finché il ricevitore non è in funzione, è normale vedere "Teslim edilemedi / failed" ogni due secondi.
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}Lo sketch avvia prima il Wi-Fi in modalità WIFI_STA; ESP-NOW non funziona senza la radio Wi-Fi, quindi esp_now_init() deve sempre venire dopo la riga WiFi.mode(). Poi il ricevitore viene aggiunto all'elenco come peer. È importante azzerare la struttura esp_now_peer_info_t con = {}; i valori casuali rimasti nei campi non azzerati causano l'errore "Peer interface is invalid". channel = 0 significa "usa il canale attuale della scheda". esp_now_send() mette il pacchetto in coda e ritorna subito; ESP_OK indica solo che il pacchetto è stato accettato per l'invio. Il risultato vero arriva qualche millisecondo dopo nella funzione gonderimSonucu(): ESP_NOW_SEND_SUCCESS significa che il ricevitore ha confermato il pacchetto a livello hardware. Il blocco #if all'inizio sceglie da solo la forma corretta della callback di invio, che cambia a seconda della versione del core; lo spieghiamo nella sezione sul core 3.x.
Lo sketch del ricevitore 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}Sul ricevitore i pacchetti arrivano alla funzione veriGeldi(). Questa funzione non gira nel tuo loop(), ma nel task Wi-Fi ad alta priorità, quindi al suo interno non dovresti usare Serial.print, delay o calcoli lunghi. Il metodo giusto è controllare che la dimensione del pacchetto corrisponda allo struct previsto, copiare i dati in una tua variabile con memcpy e avvisare loop() con un flag volatile. Nel core 3.x il parametro bilgi indica anche chi ha inviato il pacchetto (bilgi->src_addr) e l'intensità del segnale (bilgi->rx_ctrl->rssi, in dBm); queste informazioni valgono solo dentro la funzione, quindi copiale se ti servono dopo. Il ricevitore non deve aggiungere il mittente come peer: i pacchetti unicast non cifrati e i broadcast vengono accettati anche da schede che non sono nell'elenco.
Comunicazione bidirezionale: due ESP32 che si controllano a vicenda
Nella comunicazione bidirezionale ogni scheda è sia mittente sia ricevitore. Caricherai lo sketch qui sotto su entrambe le schede; l'unica differenza è che su ciascuna scrivi nell'array karsiMac l'indirizzo dell'altra. Quando premi il pulsante di una scheda, il LED dell'altra si accende o si spegne, e la scheda che ha cambiato il suo LED lo conferma con un messaggio di risposta. Il cablaggio è uguale su entrambe le schede:
| Componente | Pin ESP32 | Nota |
|---|---|---|
| Pulsante | GPIO 4 e GND | Usa il pull-up interno, nessuna resistenza aggiuntiva |
| LED | GPIO 2 | LED blu integrato sulla maggior parte delle DevKit; resistenza da 220 Ω per un LED esterno |
| Alimentazione | USB | Le schede possono essere alimentate da porte USB o alimentatori diversi |
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}Il campo tur dello struct del messaggio dice di che pacchetto si tratta: 1 è un comando, 2 una conferma. La scheda su cui si preme il pulsante invia lo stato desiderato per il LED dell'altra; la scheda che riceve il comando imposta il suo LED e rimanda lo stesso stato con tipo 2. Così il mittente riceve sia l'ACK hardware sia la conferma che "il LED è cambiato davvero". Il comando ricevuto viene di nuovo passato a loop() con flag volatile, e digitalWrite e la risposta avvengono lì. È più sicuro che chiamare esp_now_send() dentro la callback. L'attesa di 30 ms sul pulsante filtra i rimbalzi dei contatti. Collega un relè al posto del LED e lo stesso codice diventa un interruttore della luce wireless; per il cablaggio del relè leggi Cos'è un modulo relè?.
Più dispositivi: il limite di 20 peer e il broadcast
ESP-NOW supporta tutti e tre gli schemi: un mittente verso molti ricevitori (un telecomando verso cinque schede relè), molti mittenti verso un ricevitore (cinque sensori verso un solo gateway) e reti in cui tutti parlano con tutti. L'elenco dei peer di una scheda contiene al massimo 20 voci, e per impostazione predefinita solo 7 possono essere cifrate. Questo limite vale solo per le schede a cui invii. Un gateway non deve aggiungere i sensori che gli inviano pacchetti non cifrati, quindi può ascoltare più di 20 sensori. Se l'elenco è pieno, esp_now_add_peer() restituisce ESP_ERR_ESPNOW_FULL; allora puoi rimuovere con esp_now_del_peer() i peer che non ti servono più e aggiungerne di nuovi. Lo sketch qui sotto aggiunge tre ricevitori e l'indirizzo di broadcast all'elenco, poi invia sia uno per uno sia in broadcast:
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}Anche l'indirizzo di broadcast FF:FF:FF:FF:FF:FF va aggiunto come peer prima dell'invio. Un solo pacchetto broadcast raggiunge tutti i ricevitori ESP-NOW sullo stesso canale; non devi conoscere i loro indirizzi MAC, perciò è il metodo più facile da configurare. Il prezzo è che il broadcast non ha conferma di consegna, il risultato dell'invio sembra sempre riuscito e i pacchetti broadcast non possono essere cifrati. Con l'invio uno per uno, invece, ogni pacchetto riceve il suo ACK. Se passi NULL come indirizzo a esp_now_send(), il pacchetto viene inviato separatamente a ogni peer dell'elenco; se nell'elenco c'è anche l'indirizzo di broadcast, il pacchetto esce in più una volta come broadcast. Una soluzione pratica per molti progetti è far trovare le schede con un broadcast al primo avvio, poi memorizzare gli indirizzi MAC e passare all'unicast con conferma.
Portata e velocità di ESP-NOW
| Parametro | Valore tipico | Nota |
|---|---|---|
| Portata al chiuso | 20–40 m (1–3 muri) | I muri in cemento armato e gli oggetti metallici attenuano di più il segnale |
| Portata in campo aperto | 100–200 m | Con l'antenna PCB della scheda; aumenta se le schede sono più in alto da terra |
| Modalità LR (Long Range) | Fino a 1 km | Valore di Espressif in linea di vista; la velocità scende a 0,5 o 0,25 Mbps |
| Velocità dati predefinita | 1 Mbps | Una velocità maggiore significa una portata minore |
| Tempo in aria di un pacchetto da 250 byte | Circa 2,5 ms | Circa 3 ms per pacchetto con ACK e attese |
| Frequenza teorica dei pacchetti | ~300 pacchetti al secondo (~70 kB/s) | Con pacchetti da 250 byte; in pratica meno |
| Latenza di un comando | Pochi millisecondi | Nessuna connessione da stabilire né server |
Questi valori cambiano molto con l'ambiente, ma il calcolo è semplice. Alla velocità predefinita di 1 Mbps ogni byte resta in aria 8 microsecondi; 250 byte di dati con intestazioni e preambolo richiedono circa 2,5 ms. Con l'ACK del ricevitore e l'attesa che il canale si liberi servono circa 3 ms per pacchetto, quindi al massimo circa 300 pacchetti al secondo. È più che sufficiente per dati dei sensori e comandi, ma ESP-NOW non è adatto a trasmettere audio o video. Per aumentare la portata, metti le schede in alto, non coprire l'antenna con scatole metalliche, batterie o cavi, usa moduli con antenna esterna (come l'ESP32-WROOM-32U) e attiva su entrambi i lati la modalità a lungo raggio (LR) di Espressif con esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR). LR funziona solo tra due ESP32; un'interfaccia impostata solo su LR non può collegarsi ai router normali, e ESP8266 ed ESP32-C2 non supportano questa modalità. Quando provi la portata con il core 3.x, stampa bilgi->rx_ctrl->rssi sul ricevitore: −50 dBm è un segnale molto forte, −80 dBm è vicino al limite e intorno a −90 dBm iniziano le perdite di pacchetti.
Le novità del core 3.x di Arduino per ESP32
La maggior parte degli esempi ESP-NOW in rete è stata scritta per il core 2.x di Arduino per ESP32. Se hai installato la versione 3.x dal Gestore schede, quel codice non compila, perché con ESP-IDF 5 Espressif ha cambiato la forma delle funzioni di callback. Le modifiche sono arrivate in due fasi: il core 3.0 ha cambiato la callback di ricezione, il core 3.3, basato su ESP-IDF 5.5, quella di invio. Puoi vedere quale versione hai accanto al pacchetto esp32 in Strumenti > Scheda > Gestore schede.
| Argomento | Core 2.x | Core 3.0–3.2 | Core 3.3 e successivi |
|---|---|---|---|
| Callback di ricezione | (const uint8_t *mac, const uint8_t *veri, int uzunluk) | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | Come 3.0 |
| Indirizzo del mittente | mac | bilgi->src_addr | bilgi->src_addr |
| Callback di invio | (const uint8_t *mac, esp_now_send_status_t durum) | Come 2.x | (const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) |
| Pacchetto più grande | 250 byte | 250 byte; 1470 byte con v2 in 3.2 | 1470 byte con v2 |
| Impostare il canale | esp_wifi_set_channel() | Anche WiFi.setChannel() | Come 3.0 |
| Libreria a classi inclusa | Nessuna | ESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial) | ESP32_NOW.h con supporto v2 |
Compilando un vecchio codice con il 3.x vedrai, per la callback di ricezione, l'errore invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t'. La soluzione è rendere il primo parametro della funzione const esp_now_recv_info_t * e leggere l'indirizzo del mittente da bilgi->src_addr. Passando al core 3.3 compare un errore simile, questa volta per esp_now_send_cb_t; il primo parametro della callback di invio deve essere const esp_now_send_info_t *, e l'indirizzo del ricevitore si trova in bilgi->des_addr. Gli sketch di questa guida scelgono da soli la forma giusta con la macro ESP_IDF_VERSION, quindi compilano senza modifiche su 2.x, 3.0–3.2 e 3.3+. Il core 3.x ha portato anche una libreria a classi chiamata ESP32_NOW.h: scrivi onReceive() e onSent() in una classe derivata da ESP_NOW_Peer, e la classe ESP_NOW_Serial ti permette di usare un collegamento ESP-NOW proprio come Serial. Trovi i suoi esempi sotto ESP_NOW nel menu File > Esempi. Noi usiamo direttamente le funzioni di esp_now.h perché restano compatibili con l'ESP8266 e con il codice più vecchio.
ESP-NOW e Wi-Fi insieme: allineare il canale
L'ESP32 ha una sola radio a 2,4 GHz, e questa radio può lavorare su un solo canale alla volta. Quando la scheda si collega a un router, il canale lo decide il router, non tu; se il router è sul canale 6, anche ESP-NOW funziona sul canale 6. Perciò tutti i sensori che inviano pacchetti a un gateway collegato al Wi-Fi devono passare anch'essi al canale del router. Se i canali non coincidono, il mittente mostra "failed"; se hai aggiunto il peer con un canale diverso da zero, nel Monitor seriale compare anche l'errore "ESPNOW: Peer channel is not equal to the home channel, send fail!". Per un impianto senza problemi segui queste regole:
- Il gateway si collega prima al Wi-Fi, avvia ESP-NOW solo dopo e stampa nel Monitor seriale il canale del router con WiFi.channel().
- I sensori non si collegano al router; trovano il canale cercando il nome della rete (WiFi.scanNetworks() e WiFi.channel(i)) oppure usano un canale fisso nel codice. Per farlo non serve la password del Wi-Fi.
- Imposta il canale del router da "Automatico" a un canale fisso (1, 6 o 11). In modalità automatica, se il router cambia canale, i sensori perdono il gateway.
- Disattiva il risparmio energetico sul gateway con WiFi.setSleep(false). Per impostazione predefinita un ESP32 collegato a un router mette la radio in pausa a intervalli e può perdere i pacchetti ESP-NOW che arrivano in quel momento.
- Inserisci nei sensori l'indirizzo MAC STA del gateway; lo sketch del gateway qui sotto lo stampa nel Monitor seriale all'avvio.
Lo sketch del gateway qui sotto si collega alla tua rete di casa, memorizza le misure di un massimo di cinque sensori e le mostra in una semplice pagina web che puoi aprire dal telefono sulla stessa rete. Sostituisci nome della rete e password con i tuoi e, dopo il caricamento, apri nel browser del telefono l'indirizzo IP mostrato nel Monitor seriale:
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}Nella callback di ricezione il gateway si limita a mettere il pacchetto in una coda (queue) di FreeRTOS e ritorna subito. loop() svuota la coda e salva per ogni sensore l'ultima misura e l'ora di arrivo, e il web server mostra questa tabella. La coda garantisce che nessun pacchetto vada perso quando più sensori inviano nello stesso momento; è anche il modo pratico di seguire il consiglio di Espressif di non fare lavori lunghi nelle callback. La pagina si aggiorna da sola ogni 10 secondi, e la colonna "Son paket" (ultimo pacchetto) mostra subito se un sensore ha smesso di trasmettere (batteria scarica o fuori portata?). Se il gateway non riesce proprio a collegarsi al Wi-Fi, il problema non è ESP-NOW; segui i controlli di l'ESP32 non si collega al Wi-Fi. Se vuoi portare i dati in un sistema di domotica, puoi usare MQTT e Home Assistant al posto della pagina web.
Esempio di sensore remoto: DHT22, deep sleep ed ESP-NOW
Ora costruiamo il sensore a batteria che invia i dati al gateway. A ogni risveglio il sensore misura temperatura, umidità e tensione della batteria, invia un solo pacchetto ESP-NOW, aspetta la conferma di consegna e va in deep sleep per 60 secondi. Per temperatura e umidità usiamo un DHT22; devi installare la DHT sensor library di Adafruit dal Gestore librerie. Il funzionamento del sensore è spiegato nella nostra guida al sensore di temperatura e umidità DHT11; il cablaggio è lo stesso per il DHT22, e nel codice indichi solo DHT22 come tipo di sensore.




| Collegamento | Pin ESP32 | Nota |
|---|---|---|
| DHT22 VCC (+) | 3V3 | Funziona da 3,3 a 5 V |
| DHT22 DATA | GPIO 4 | Con il sensore nudo, resistenza da 10 kΩ tra DATA e 3V3 |
| DHT22 GND (−) | GND | Massa comune |
| Positivo della batteria | GPIO 34 (tramite il partitore) | Due resistenze da 100 kΩ; una batteria da 4,2 V arriva al pin come 2,1 V |
| Negativo della batteria | GND | Anche la resistenza inferiore del partitore va a 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}Il segreto di questo sketch sono le variabili marcate con RTC_DATA_ATTR. In deep sleep la memoria principale dell'ESP32 viene cancellata, ma queste variabili nella memoria RTC si conservano. Il sensore cerca il canale con una scansione solo al primo avvio (ci vogliono circa 2 secondi) e nei risvegli successivi usa direttamente quel canale; se un pacchetto non viene consegnato, azzera la variabile del canale e rifà la scansione al risveglio successivo. Così, anche se il router cambia canale, il sensore si riprende dopo un ciclo. La tensione della batteria viene letta già calibrata con analogReadMilliVolts() e moltiplicata per 2, perché il partitore la dimezza; puoi controllare i valori delle resistenze con lo strumento Partitore di tensione. GPIO 34 è un pin ADC1; questo è importante perché i pin ADC2 non possono fare letture analogiche con la radio Wi-Fi accesa. Per i dettagli sulle modalità di sospensione leggi la nostra guida ESP32 Deep Sleep.
Stimiamo a grandi linee la durata della batteria. Un risveglio senza scansione (avvio, misura e invio) dura circa 0,3 secondi con un assorbimento medio di circa 100 mA, cioè circa 0,008 mAh per risveglio. Con un risveglio al minuto si arriva a 1440 risvegli al giorno e circa 12 mAh in totale. Un modulo ESP32 nudo assorbe circa 10 µA in deep sleep; insieme alla corrente di standby del DHT22 e alle resistenze del partitore, il consumo in sospensione resta sotto i 2 mAh al giorno. Il sensore consuma quindi circa 14 mAh al giorno, e una cella 18650 da 2000 mAh dura 4–5 mesi. Per rifare il calcolo con i tuoi valori usa il calcolatore di durata della batteria.
ESP-NOW con l'ESP8266

Le schede ESP8266 possono usare ESP-NOW nella stessa rete delle ESP32, ma la loro libreria Arduino è diversa: si include espnow.h al posto di esp_now.h ed ESP8266WiFi.h al posto di WiFi.h, e le funzioni arrivano dall'SDK dell'ESP8266 anziché da ESP-IDF. Le differenze principali sono:
| Operazione | ESP32 (esp_now.h) | ESP8266 (espnow.h) |
|---|---|---|
| Inizializzazione | esp_now_init() == ESP_OK | esp_now_init() == 0 |
| Scelta del ruolo | Non serve | esp_now_set_self_role(ESP_NOW_ROLE_COMBO) |
| Aggiunta di un peer | esp_now_add_peer(&es) tramite uno struct | esp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, canale, NULL, 0) |
| Callback di ricezione | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | (uint8_t *mac, uint8_t *dati, uint8_t lunghezza) |
| Callback di invio | (..., esp_now_send_status_t durum) | (uint8_t *mac, uint8_t stato); 0 significa successo |
| Pacchetto più grande | 250 byte (1470 con v2) | 250 byte |
Sull'ESP8266 non dimenticare di aggiungere WiFi.disconnect() dopo WiFi.mode(WIFI_STA); all'avvio l'ESP8266 prova a ricollegarsi da solo all'ultimo router usato, il canale cambia e i pacchetti possono andare persi. Se definisci lo struct dei dati come packed su entrambi i lati, ESP32 ed ESP8266 leggono gli stessi byte nello stesso modo, perché sono entrambi processori a 32 bit little-endian. Per i pin e la configurazione dell'ESP8266 leggi la nostra guida ESP8266 NodeMCU.
ESP-NOW con MicroPython e il Raspberry Pi Pico
Se programmi l'ESP32 in MicroPython, per ESP-NOW esiste il modulo pronto espnow; è incluso nelle versioni di MicroPython per ESP32 ed ESP8266. Il codice qui sotto riceve lo struct Paket da 30 byte inviato dall'esempio del mittente Arduino e lo decodifica con struct.unpack(). Nella stringa di formato, < indica little-endian senza byte di riempimento; B è un numero da 1 byte, I un numero senza segno da 4 byte, f un float e 16s un testo da 16 byte. Il campo bool di Arduino si legge con B come 0 o 1. Carica il codice sulla scheda con Thonny e inserisci l'indirizzo MAC stampato nell'array aliciMac del mittente 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)Anche inviare un pacchetto da MicroPython richiede due righe: aggiungi il ricevitore con e.add_peer(mac) e invia lo stesso struct con e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")); così schede Arduino e MicroPython lavorano insieme nella stessa rete. Su una scheda MicroPython collegata al Wi-Fi, disattiva il risparmio energetico con sta.config(pm=sta.PM_NONE) per non perdere pacchetti. Se MicroPython è nuovo per te, comincia dalla nostra guida alle basi di MicroPython. La famiglia Raspberry Pi Pico, invece, non ha ESP-NOW: il Pico standard non ha alcuna radio, e il chip CYW43439 di Pico W e Pico 2 W gestisce solo Wi-Fi e Bluetooth standard. Per inserire un Pico in una rete ESP-NOW, collegagli un piccolo ESP32-C3 tramite comunicazione seriale UART e usalo come ponte, oppure fai dialogare il Pico W con il gateway via Wi-Fi; il Wi-Fi del Pico W è spiegato in Pico W con Wi-Fi e web server. Lo stesso metodo del ponte vale per Arduino Uno.
Cifrare i pacchetti ESP-NOW
Per impostazione predefinita i pacchetti ESP-NOW non sono cifrati; chiunque sullo stesso canale può catturarli nell'aria e inviare pacchetti falsi che imitano il tuo struct. Usa la crittografia per progetti come serrature o porte del garage. ESP-NOW usa CCMP, come il WPA2, e lavora con due chiavi: una PMK da 16 byte uguale su tutti i dispositivi (si imposta con esp_now_set_pmk()) e una LMK da 16 byte diversa per ogni peer. Quando aggiungi il peer imposti es.encrypt = true e copi la chiave in es.lmk; anche l'altra parte deve aggiungerti come peer cifrato con la stessa LMK. Per ricevere pacchetti cifrati, il ricevitore deve per forza aggiungere il mittente al suo elenco. I pacchetti broadcast non si possono cifrare e, per impostazione predefinita, i peer cifrati sono al massimo 7. La crittografia da sola non impedisce che un pacchetto catturato venga reinviato più tardi; aggiungere un contatore crescente ai comandi critici e rifiutare sul ricevitore i numeri vecchi è un'ottima protezione in più.
Idee di progetti con ESP-NOW
- Campanello wireless: Quando si preme il pulsante dell'ESP32 alla porta, suona un buzzer sulla scheda dentro casa; per far durare la batteria, usa il pulsante come pin che risveglia la scheda dal deep sleep.
- Temperatura in più stanze: Metti un sensore DHT22 o BME280 in ogni stanza, raccogli le misure su un gateway e confrontale in una pagina web.
- Irrigazione del giardino: Un sensore di umidità del terreno invia il comando "annaffia" alla scheda vicino alla pompa; per il cablaggio leggi sensore di umidità del terreno.
- Telecomando wireless per robot: Invia i valori di un joystick 20 volte al secondo; per la parte meccanica del robot guarda come costruire una macchina robot con Arduino.
- Allarme da giardino: Quando un sensore PIR in giardino rileva un movimento, il gateway invia una notifica tramite Telegram.
- Spettacolo di luci sincronizzato: Con un solo pacchetto broadcast porta contemporaneamente allo stesso colore le strisce WS2812B di più schede.
Errori comuni di ESP-NOW e soluzioni
| Sintomo / errore | Causa | Soluzione |
|---|---|---|
| "ESPNOW: Peer interface is invalid", il peer non si aggiunge | esp_now_peer_info_t usata senza azzerarla | Azzera la struttura con = {} o memset e imposta ifidx = WIFI_IF_STA |
| "failed" continuo | MAC sbagliato (indirizzo AP o errore di battitura), ricevitore spento o su un altro canale | Rileggi l'indirizzo MAC STA del ricevitore e controlla il canale di entrambe le schede |
| I pacchetti arrivano ma i valori non hanno senso | Lo struct è diverso sui due lati o contiene una String | Definisci lo struct nello stesso ordine e packed su entrambi i lati, usa un array char per il testo |
| "invalid conversion ... to 'esp_now_recv_cb_t'" | Codice scritto per il core 2.x compilato con il 3.x | Scrivi la callback di ricezione con const esp_now_recv_info_t * |
| "invalid conversion ... to 'esp_now_send_cb_t'" | Codice scritto per il core 3.0–3.2 compilato con il 3.3+ | Scrivi la callback di invio con const esp_now_send_info_t * oppure usa la macro ESP_IDF_VERSION |
| "Peer channel is not equal to the home channel, send fail!" | Il canale della scheda collegata al Wi-Fi è diverso da quello del peer | Aggiungi il peer con channel = 0 e porta i sensori sul canale del router |
| Il gateway collegato al Wi-Fi perde alcuni pacchetti | Risparmio energetico del Wi-Fi (modem sleep) | Usa WiFi.setSleep(false) |
| ESP_ERR_ESPNOW_NOT_INIT | esp_now_init() non è stata chiamata o è stata chiamata prima di avviare il Wi-Fi | Prima WiFi.mode(WIFI_STA), poi esp_now_init() |
| ESP_ERR_ESPNOW_FULL | L'elenco dei peer ha raggiunto 20 voci | Rimuovi i peer vecchi con esp_now_del_peer() o usa il broadcast |
| ESP_ERR_ESPNOW_NO_MEM | I pacchetti vengono inviati uno dopo l'altro prima che la coda di invio si svuoti | Aspetta il risultato dell'invio prima del pacchetto successivo |
| L'indirizzo MAC appare come 00:00:00:00:00:00 | Nel core 3.x WiFi.macAddress() è stata chiamata prima dell'avvio completo del Wi-Fi | Usa esp_wifi_get_mac() dopo WiFi.mode(WIFI_STA) |
ESP-NOW è il modo più rapido per far comunicare più ESP32 senza router, cavi o moduli aggiuntivi: leggi l'indirizzo MAC, definisci lo struct dei dati uguale su entrambi i lati, aggiungi il peer e invia. Quando lo usi insieme al Wi-Fi, ricorda l'unica regola: tutti devono stare sullo stesso canale. Nell'app BlueGrays puoi provare tutti i tasselli di questa guida, dal controllo di LED e pulsante con l'ESP32 alla misura della temperatura con il DHT22, fino alla connessione Wi-Fi, al web server e al deep sleep, passo dopo passo con schemi elettrici e codice pronto all'uso.
Domande frequenti
ESP-NOW ha bisogno di un router o di internet?
No. ESP-NOW usa la radio Wi-Fi delle schede ma non si collega a nessuna rete; le schede si inviano pacchetti direttamente ai rispettivi indirizzi MAC. Se vuoi portare i dati su internet, ti serve un ESP32 che faccia da gateway, collegato sia al Wi-Fi sia a ESP-NOW.
Qual è la portata di ESP-NOW?
Con schede ESP32 dotate di antenna integrata si ottengono di solito 20–40 metri al chiuso, a seconda dei muri, e circa 100–200 metri in campo aperto. Con moduli ad antenna esterna e la modalità LR (Long Range) attiva su entrambi i lati, secondo Espressif si può arrivare fino a 1 chilometro in linea di vista.
Quanti dispositivi si possono collegare con ESP-NOW?
L'elenco dei peer di una scheda può contenere fino a 20 dispositivi, di cui al massimo 7 cifrati per impostazione predefinita. Il limite riguarda i dispositivi a cui invii: un gateway può ascoltare più di 20 sensori che inviano senza crittografia, e un broadcast raggiunge con un solo pacchetto tutti i dispositivi sullo stesso canale.
Si possono usare ESP-NOW e Wi-Fi contemporaneamente?
Sì, ma poiché l'unica radio dell'ESP32 lavora su un canale alla volta, tutti i dispositivi ESP-NOW devono usare il canale del router. Disattiva il risparmio energetico sul gateway con WiFi.setSleep(false) e, sui sensori, trova il canale cercando il nome della rete oppure imposta il router su un canale fisso.
ESP32 ed ESP8266 possono comunicare con ESP-NOW?
Sì. Entrambi i chip supportano ESP-NOW e possono scambiarsi pacchetti; sull'ESP8266 cambiano solo la libreria (espnow.h) e la forma delle funzioni. Definisci lo struct dei dati identico e packed su entrambi i lati e mantieni i pacchetti sotto i 250 byte.
Quanti byte si possono inviare al massimo con ESP-NOW?
Con ESP-NOW v1 un pacchetto contiene al massimo 250 byte, e questo limite vale per tutte le schede, ESP8266 compreso. Le schede con ESP-IDF 5.4 o successivo (core Arduino 3.2+) possono inviare fino a 1470 byte con ESP-NOW v2, ma le schede con v1 non ricevono pacchetti più lunghi di 250 byte.
Progetti da realizzare dopo questo articolo
Vedi tutti i progetti
LED Lampeggiante con ESP32
Insegna l'uso di base delle uscite GPIO con l'ESP32. Otteniamo un effetto lampeggiante accendendo e spegnendo un LED dal pin digitale. Versione ESP32 del classico progetto blink su Arduino UNO; i comandi di base di blink nell'ambiente Arduino sono gli stessi, ma la disposizione dei pin e i livelli GPIO a 3,3 V dell'ESP32 sono diversi da quelli di Arduino UNO.

Controllo LED ESP32 con pulsante (Toggle)
Ogni volta che si preme il pulsante, lo stato del LED cambia: se è acceso si spegne, se è spento si accende. Classico schema toggle. Con il debounce puliamo il rumore meccanico del pulsante per garantire una singola commutazione con una sola pressione.

Comunicazione bidirezionale tra 2 Arduino
Due Arduino collegati con dei fili parlano tra loro: premi il pulsante di uno e si accende la luce dell'altro, premilo di nuovo e si spegne. Entrambe le schede hanno lo stesso programma, quindi ognuna

ESP32 DHT22 Temperatura e umidità (con display LCD)
Si tratta di un progetto che misura la temperatura e l'umidità dell'ambiente tramite il sensore DHT22 e le visualizza in tempo reale su un display LCD I2C 16x2. I dati vengono letti dal sensore utilizzando il protocollo one-wire tramite la libreria Adafruit DHT. Questo progetto costituisce la base per stazioni meteorologiche IoT, sistemi di automazione domestica intelligente o sistemi di monitoraggio ambientale nelle serre. La comunicazione con il DHT22 avviene tramite il pin GPIO 4 dell'ESP32.

Connessione Wi-Fi STA con ESP32 (con display LCD)
In questo progetto colleghiamo l'ESP32 alla rete Wi-Fi domestica in modalità Station (STA) in modo sicuro e tollerante agli errori. Separiamo SSID e password in un file secrets.h per impedire fughe su Git, impostiamo un meccanismo di riconnessione automatica e visualizziamo il livello del segnale RSSI e l'indirizzo IP su un display LCD I2C. Il LED interno funge da indicatore di stato.

Server web asincrono ESP32 (con indicazione dell'IP sul display LCD)
In questo progetto trasformiamo l'ESP32 in un server web asincrono che potrete controllare dal vostro telefono o computer collegato alla stessa rete Wi-Fi. Grazie alla libreria ESPAsyncWebServer, il server è in grado di elaborare più richieste contemporaneamente (a differenza del classico WebServer.h, che si blocca su una singola richiesta).

ESP32 Deep Sleep — Risparmio della batteria (OLED + contatore in RTC RAM)
La modalità Deep Sleep dell'ESP32 riduce il consumo di corrente a circa 10 µA spegnendo la CPU e la maggior parte dei dispositivi periferici ed è di fondamentale importanza per i dispositivi IoT alimentati a batteria. In questo progetto utilizziamo tre diverse fonti di risveglio: il timer (15 secondi), un pin esterno (EXT0 — pulsante GPIO 33) e il sensore tattile (T0/GPIO 4). Le variabili contrassegnate con RTC_DATA_ATTR vengono preservate durante la modalità Deep Sleep: il contatore di avvio,

Data Logger di Temperatura ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
Costruisci un data logger di temperatura con timestamp usando ESP32! Leggi temperatura/umidità dal DHT22 ogni minuto, ottieni l'ora reale dal DS1307 RTC e registra su scheda microSD in formato CSV. L'OLED mostra dal vivo temperatura (font grande), percentuale di umidità, timestamp e contatore di log. Il file /temp_log.csv sulla scheda SD si apre in Excel, Google Sheets o Python pandas per l'analisi — perfetto per il monitoraggio di serre, tracciamento della temperatura domestica, raccolta
Altri articoli
App BlueGrays
Costruisci questo circuito passo passo sul telefono
Schema, codice ed elenco componenti in un’unica schermata. Oltre 200 progetti Arduino, ESP32 e Pico da realizzare.
Inquadra con il telefono
Punta la fotocamera sul codice: si apre lo store del tuo telefono.
oppure apri lo store

