ESP-NOW avec l'ESP32 : communication sans fil sans routeur
ESP-NOW avec l'ESP32 : adresse MAC, envoi d'un struct jusqu'à 250 octets dans un ou deux sens, 20 pairs, portée, core 3.x et accord du canal Wi-Fi.
ESP-NOW est un protocole sans fil développé par Espressif qui permet aux cartes ESP32 et ESP8266 de s'envoyer des données directement, sans routeur, point d'accès ni connexion internet. Les cartes utilisent leur radio Wi-Fi mais ne rejoignent aucun réseau : chaque appareil trouve l'autre grâce à son adresse MAC et transmet de petits paquets de 250 octets au maximum en quelques millisecondes. Comme il n'y a ni connexion à établir, ni adresse IP à obtenir, ni mot de passe à saisir, un appui sur un bouton d'une carte peut actionner instantanément un relais sur une autre, et un capteur sur batterie peut se réveiller, envoyer sa mesure et se rendormir aussitôt.
Dans ce tutoriel, tu vas mettre en place ESP-NOW de zéro : d'abord tu liras l'adresse MAC de tes cartes, puis, avec des exemples dans un sens et dans les deux sens, tu enverras des valeurs différentes comme une température, un compteur et du texte dans un seul paquet grâce à un struct. Tu découvriras ensuite la limite de 20 pairs, les messages de diffusion (broadcast), la portée et la vitesse, les fonctions modifiées avec le core 3.x Arduino pour ESP32 et l'accord de canal, la seule règle pour utiliser ESP-NOW et le Wi-Fi en même temps. Dans la dernière partie, tu enverras les mesures d'un capteur distant sur batterie en sommeil profond à une passerelle connectée au Wi-Fi et tu les suivras depuis ton téléphone. Si c'est ton premier projet ESP32, termine d'abord l'installation de l'Arduino IDE décrite dans Qu'est-ce que l'ESP32 ?.

Qu'est-ce qu'ESP-NOW et comment ça marche ?
Avec le Wi-Fi classique, l'ESP32 se connecte d'abord au routeur, vérifie le mot de passe, obtient une adresse IP par DHCP et ce n'est qu'ensuite qu'il peut envoyer des données ; cela prend en général 1 à 3 secondes. ESP-NOW saute toutes ces étapes. Il place tes données dans un court paquet Wi-Fi appelé trame d'action spécifique au fabricant (vendor-specific action frame) de la norme IEEE 802.11 et l'envoie directement à l'adresse MAC de l'autre carte. La carte qui le reçoit répond par une trame d'acquittement (ACK) au niveau matériel, et une fonction de rappel (callback) côté émetteur t'indique si le paquet a été remis. Si l'ACK n'arrive pas, la couche Wi-Fi renvoie automatiquement le paquet plusieurs fois. En bref, ESP-NOW fonctionne comme une boîte aux lettres invisible entre deux cartes : tu écris l'adresse, tu postes le paquet et tu es prévenu quand l'autre côté l'a reçu.
| Caractéristique | Valeur |
|---|---|
| Développé par | Espressif Systems |
| Fréquence | 2,4 GHz (radio Wi-Fi, canaux 1–13) |
| Données par paquet | 250 octets (ESP-NOW v1) ; 1470 octets avec v2 à partir d'ESP-IDF 5.4 |
| Pairs enregistrés | Jusqu'à 20 ; jusqu'à 7 chiffrés par défaut |
| Débit par défaut | 1 Mbit/s |
| Accusé de réception | Oui en unicast (ACK), non en diffusion |
| Chiffrement | Optionnel, CCMP (AES-128) ; clés PMK et LMK de 16 octets chacune |
| Routeur / point d'accès | Inutile |
| Puces compatibles | Puces ESP32 avec Wi-Fi (ESP32, S2, S3, C2, C3, C6…) et ESP8266 |
Les vrais atouts d'ESP-NOW sont la rapidité de réaction et la faible consommation. Quand tu appuies sur un bouton, la LED de l'autre côté s'allume en quelques millisecondes ; faire la même chose en Wi-Fi impose d'abord de se connecter, puis d'envoyer une requête à un serveur. Pour un capteur sur batterie, la différence est encore plus nette : pendant la connexion au Wi-Fi, la radio consomme plus de 100 mA pendant plusieurs secondes, alors qu'avec ESP-NOW la carte se réveille, envoie sa mesure et se rendort en quelques centaines de millisecondes au total. En contrepartie, ESP-NOW ne va pas sur internet ; pour amener tes données sur internet ou sur ton téléphone, il faut quelque part dans le réseau un ESP32 connecté au Wi-Fi qui joue le rôle de passerelle (gateway). Nous mettrons ce montage en place plus loin dans ce tutoriel.
ESP-NOW, Wi-Fi, Bluetooth ou nRF24L01 : que choisir ?
| Méthode | Module en plus | Routeur | Taille de paquet | Portée typique (champ libre) | Idéal pour |
|---|---|---|---|---|---|
| ESP-NOW | Inutile | Inutile | 250 octets | 100–200 m, davantage en mode LR | Commandes et données de capteurs entre cartes |
| Wi-Fi (HTTP, MQTT) | Inutile | Nécessaire | Pratiquement illimitée | La couverture du routeur | Internet, pages web, cloud |
| Bluetooth (BLE) | Inutile | Inutile | 20 octets par défaut | 10–20 m en intérieur | Connexion au téléphone |
| nRF24L01 | Nécessaire (SPI) | Inutile | 32 octets | Quelques dizaines de mètres, bien plus en version PA+LNA | Cartes sans radio comme l'Arduino Uno et le Nano |
| LoRa (SX1278) | Nécessaire (SPI) | Inutile | Jusqu'à 255 octets | Plusieurs kilomètres | Très longues distances, données peu fréquentes |
Pour dialoguer avec un téléphone, le Bluetooth est le bon choix ; pour aller sur internet, utilise le Wi-Fi. Mais si tu as plusieurs ESP32 qui doivent seulement se parler entre eux, ESP-NOW est souvent la solution la plus simple : pas de module en plus, pas de câble, pas de routeur et seulement quelques lignes de code. Pour les cartes sans radio comme l'Arduino Uno, le nRF24L01 convient mieux, et pour des kilomètres de portée, les modules LoRa. Pour comparer toutes les options, consulte les solutions de communication sans fil pour Arduino, et pour la connexion au téléphone, notre tutoriel Bluetooth sur l'ESP32.
Quelles cartes sont compatibles ESP-NOW ?
| Carte / puce | ESP-NOW | Remarque |
|---|---|---|
| ESP32 (WROOM-32, DevKit) | Oui | Gère aussi le mode longue portée (LR) |
| ESP32-S2, S3, C3, C6 | Oui | Même code Arduino, seule la carte choisie change |
| ESP32-C2 (ESP8684) | Oui | Pas de mode LR |
| ESP32-H2 | Non | Pas de radio Wi-Fi, donc pas d'ESP-NOW |
| ESP8266 (NodeMCU, Wemos D1 Mini) | Oui | Bibliothèque différente (espnow.h), v1 et 250 octets uniquement |
| Raspberry Pi Pico W / Pico 2 W | Non | Sa puce Wi-Fi (CYW43439) ne gère pas ESP-NOW |
| Arduino Uno, Nano, Mega | Non | Peut être relié à un ESP32 par liaison série (UART) |
ESP-NOW fonctionne aussi entre puces ESP différentes : un capteur ESP32-C3 peut envoyer des paquets à une passerelle ESP32 classique ou à une carte ESP8266. Les seules conditions sont que les deux côtés soient sur le même canal Wi-Fi et que la structure de données soit définie exactement de la même façon des deux côtés. Les exemples ci-dessous utilisent deux cartes ESP32 DevKit ; tu peux voir quelles broches de ta carte sont sûres dans l'outil Pinout.
Étape 1 : trouver l'adresse MAC de l'ESP32
Avec ESP-NOW, chaque carte trouve l'autre grâce à son adresse MAC de 6 octets ; cette adresse est inscrite dans chaque ESP32 en usine et ne change jamais. Téléverse le croquis ci-dessous sur la carte qui servira de récepteur et ouvre le moniteur série à 115200 bauds. Il affiche l'adresse sous forme lisible et aussi sous forme d'une ligne de tableau que tu peux coller directement dans le code de l'autre carte. Pour les exemples à deux cartes, fais-le pour les deux et note les adresses.
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}Envoyer des données avec un struct et la limite de 250 octets
ESP-NOW n'envoie qu'un tableau d'octets et ignore ce qu'il contient. La façon la plus propre d'envoyer plusieurs valeurs d'un coup est de les regrouper dans un struct et d'envoyer ses octets tels qu'ils sont en mémoire. Le récepteur définit le même struct et y copie les octets reçus avec memcpy. Pour que cela fonctionne, le struct doit avoir le même ordre, les mêmes types et la même taille des deux côtés. Le compilateur peut insérer des octets de remplissage invisibles entre les champs pour accélérer les accès : le struct Paket des exemples occupe normalement 36 octets, mais descend à exactement 30 octets avec __attribute__((packed)). Utiliser packed garantit que chaque octet est au même endroit entre un ESP32 et un ESP8266, ou entre Arduino et MicroPython.
| Type | Taille | Remarque |
|---|---|---|
| bool, uint8_t, int8_t, char | 1 octet | Drapeau, petit nombre, caractère unique |
| int16_t, uint16_t | 2 octets | −32768…32767 ou 0…65535 |
| int, int32_t, uint32_t | 4 octets | int fait 4 octets sur ESP32 et ESP8266, mais 2 octets sur l'Arduino Uno |
| float | 4 octets | Environ 7 chiffres de précision |
| double | 8 octets | Préfère float sauf vrai besoin |
| char mesaj[16] | 16 octets | Texte de longueur fixe ; doit se terminer par '\0' |
| String, char*, pointeur | Impossible à envoyer | Seule une adresse mémoire part, qui n'a aucun sens de l'autre côté |
Un paquet contient au maximum 250 octets. Cette limite vient de la première version d'ESP-NOW (v1) et s'applique à toutes les cartes, y compris l'ESP8266. Le core Arduino 3.2 et les suivants, basés sur ESP-IDF 5.4, peuvent envoyer des paquets jusqu'à 1470 octets avec ESP-NOW v2 ; mais une carte en v1 (un ESP8266 ou un ESP32 avec un ancien core) tronque les paquets de plus de 250 octets ou les ignore complètement. Dans un réseau mixte, rester sous 250 octets est donc le plus sûr. La ligne static_assert du croquis émetteur arrête la compilation avec un message clair si le struct dépasse 250 octets. Si tu dois envoyer des données plus volumineuses, découpe-les en morceaux numérotés et réassemble-les dans l'ordre côté récepteur ; pour des valeurs de capteurs, des états de boutons ou des commandes, quelques dizaines d'octets suffisent largement.
ESP-NOW dans un seul sens : le croquis émetteur
- Vérifie que les deux ESP32 sont sélectionnés comme ESP32 Dev Module dans l'Arduino IDE. La bibliothèque ESP-NOW fait partie du paquet de la carte, il n'y a rien d'autre à installer.
- Saisis l'adresse MAC lue sur la carte réceptrice dans le tableau aliciMac du code ci-dessous.
- Branche un bouton-poussoir entre GPIO 4 et GND sur l'émetteur ; comme nous utilisons INPUT_PULLUP, aucune résistance supplémentaire n'est nécessaire. Les détails sont dans comment brancher un bouton sur Arduino.
- Téléverse le croquis sur l'émetteur et ouvre le moniteur série à 115200 bauds. Tant que le récepteur ne tourne pas encore, il est normal de voir « Teslim edilemedi / failed » toutes les deux secondes.
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}Le croquis démarre d'abord le Wi-Fi en mode WIFI_STA ; ESP-NOW ne fonctionne pas sans la radio Wi-Fi, donc esp_now_init() doit toujours venir après la ligne WiFi.mode(). Ensuite, le récepteur est ajouté à la liste en tant que pair (peer). Il est important de remettre à zéro la structure esp_now_peer_info_t avec = {} ; des valeurs aléatoires restées dans des champs non effacés provoquent l'erreur « Peer interface is invalid ». channel = 0 signifie « utilise le canal actuel de la carte ». esp_now_send() place le paquet dans une file d'attente et rend la main immédiatement ; ESP_OK indique seulement que le paquet a été accepté pour l'envoi. Le vrai résultat arrive quelques millisecondes plus tard dans la fonction gonderimSonucu() : ESP_NOW_SEND_SUCCESS signifie que le récepteur a acquitté le paquet au niveau matériel. Le bloc #if du début choisit tout seul la bonne forme du callback d'envoi, qui dépend de la version du core ; nous l'expliquons dans la partie sur le core 3.x.
Le croquis récepteur 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}Côté récepteur, les paquets arrivent dans la fonction veriGeldi(). Elle ne s'exécute pas dans ton loop() mais dans la tâche Wi-Fi de haute priorité ; évite donc d'y mettre Serial.print, delay ou de longs calculs. La bonne méthode consiste à vérifier que la taille du paquet correspond au struct attendu, à copier les données dans ta propre variable avec memcpy et à prévenir loop() avec un drapeau volatile. Avec le core 3.x, le paramètre bilgi indique aussi l'expéditeur du paquet (bilgi->src_addr) et la force du signal (bilgi->rx_ctrl->rssi, en dBm) ; ces informations ne sont valables qu'à l'intérieur de la fonction, copie-les si tu en as besoin plus tard. Le récepteur n'a pas besoin d'ajouter l'émetteur comme pair : les paquets unicast non chiffrés et les diffusions sont acceptés même depuis des cartes absentes de la liste.
Communication bidirectionnelle : deux ESP32 qui se commandent mutuellement
En communication bidirectionnelle, chaque carte est à la fois émetteur et récepteur. Tu vas téléverser le croquis ci-dessous sur les deux cartes ; la seule différence est que, sur chacune, tu écris dans le tableau karsiMac l'adresse de l'autre. Quand tu appuies sur le bouton d'une carte, la LED de l'autre s'allume ou s'éteint, et la carte qui a changé sa LED le confirme par un message en retour. Le câblage est identique sur les deux cartes :
| Composant | Broche ESP32 | Remarque |
|---|---|---|
| Bouton | GPIO 4 et GND | Utilise la résistance de tirage interne, aucune résistance en plus |
| LED | GPIO 2 | LED bleue intégrée sur la plupart des DevKit ; résistance de 220 Ω pour une LED externe |
| Alimentation | USB | Les cartes peuvent être alimentées par des ports USB ou des chargeurs différents |
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}Le champ tur du struct de message indique la nature du paquet : 1 est une commande, 2 un acquittement. La carte dont on presse le bouton envoie l'état souhaité pour la LED de l'autre ; la carte qui reçoit la commande règle sa LED et renvoie le même état avec le type 2. L'émetteur reçoit ainsi à la fois l'ACK matériel et la confirmation que « la LED a vraiment changé ». La commande reçue est à nouveau transmise à loop() par des drapeaux volatile, et digitalWrite comme la réponse s'y font. C'est plus sûr que d'appeler esp_now_send() depuis le callback. L'attente de 30 ms sur le bouton filtre les rebonds des contacts. Branche un relais à la place de la LED et le même code devient un interrupteur d'éclairage sans fil ; pour le câblage du relais, consulte Qu'est-ce qu'un module relais ?.
Plusieurs appareils : la limite de 20 pairs et la diffusion (broadcast)
ESP-NOW gère les trois organisations : un émetteur vers plusieurs récepteurs (une télécommande vers cinq cartes relais), plusieurs émetteurs vers un récepteur (cinq capteurs vers une seule passerelle) et des réseaux où tout le monde parle à tout le monde. La liste de pairs d'une carte contient au maximum 20 entrées, et par défaut seulement 7 peuvent être chiffrées. Cette limite ne concerne que les cartes auxquelles tu envoies. Une passerelle n'a pas besoin d'ajouter les capteurs qui lui envoient des paquets non chiffrés, elle peut donc écouter plus de 20 capteurs. Si la liste est pleine, esp_now_add_peer() renvoie ESP_ERR_ESPNOW_FULL ; tu peux alors retirer avec esp_now_del_peer() les pairs dont tu n'as plus besoin et en ajouter de nouveaux. Le croquis ci-dessous ajoute trois récepteurs et l'adresse de diffusion à la liste, puis envoie à la fois un par un et en diffusion :
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}L'adresse de diffusion FF:FF:FF:FF:FF:FF doit elle aussi être ajoutée comme pair avant l'envoi. Un seul paquet de diffusion atteint tous les récepteurs ESP-NOW du même canal ; tu n'as pas besoin de connaître leurs adresses MAC, c'est donc la méthode la plus simple à mettre en place. En contrepartie, la diffusion n'a pas d'accusé de réception, le résultat d'envoi paraît toujours réussi et les paquets de diffusion ne peuvent pas être chiffrés. En envoi un par un, au contraire, chaque paquet reçoit son propre ACK. Si tu passes NULL comme adresse à esp_now_send(), le paquet est envoyé séparément à chaque pair de la liste ; si l'adresse de diffusion y figure aussi, le paquet part en plus une fois en diffusion. Une approche pratique pour beaucoup de projets : les cartes se trouvent par diffusion au premier démarrage, retiennent ensuite leurs adresses MAC et passent à l'unicast avec acquittement.
Portée et vitesse d'ESP-NOW
| Critère | Valeur typique | Remarque |
|---|---|---|
| Portée en intérieur | 20–40 m (1 à 3 murs) | Les murs en béton armé et les objets métalliques affaiblissent le plus le signal |
| Portée en champ libre | 100–200 m | Avec l'antenne PCB de la carte ; augmente quand les cartes sont placées plus haut |
| Mode LR (Long Range) | Jusqu'à 1 km | Valeur d'Espressif en vue directe ; le débit tombe à 0,5 ou 0,25 Mbit/s |
| Débit par défaut | 1 Mbit/s | Un débit plus élevé signifie une portée plus courte |
| Temps d'antenne d'un paquet de 250 octets | Environ 2,5 ms | Environ 3 ms par paquet avec l'ACK et les temps d'attente |
| Débit théorique de paquets | ~300 paquets par seconde (~70 ko/s) | Avec des paquets de 250 octets ; moins en pratique |
| Latence d'une commande | Quelques millisecondes | Ni connexion à établir ni serveur |
Ces valeurs varient beaucoup selon l'environnement, mais le calcul est simple. Au débit par défaut de 1 Mbit/s, chaque octet reste 8 microsecondes dans l'air ; 250 octets de données avec les en-têtes et le préambule prennent environ 2,5 ms. Avec l'ACK du récepteur et l'attente d'un canal libre, il faut environ 3 ms par paquet, soit au maximum environ 300 paquets par seconde. C'est largement suffisant pour des données de capteurs et des commandes, mais ESP-NOW ne convient pas pour diffuser du son ou de la vidéo. Pour gagner en portée, place les cartes en hauteur, ne couvre pas l'antenne avec un boîtier métallique, des piles ou des câbles, utilise des modules à antenne externe (comme l'ESP32-WROOM-32U) et active des deux côtés le mode longue portée (LR) d'Espressif avec esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR). LR ne fonctionne qu'entre deux ESP32 ; une interface réglée uniquement en LR ne peut pas se connecter à un routeur classique, et l'ESP8266 comme l'ESP32-C2 ne gèrent pas ce mode. Pour tester la portée avec le core 3.x, affiche bilgi->rx_ctrl->rssi sur le récepteur : −50 dBm est un signal très fort, −80 dBm est proche de la limite et les pertes de paquets commencent vers −90 dBm.
Les changements du core 3.x Arduino pour ESP32
La plupart des exemples ESP-NOW sur internet ont été écrits pour le core 2.x Arduino pour ESP32. Si tu as installé la version 3.x depuis le gestionnaire de cartes, ce code ne compile plus, car Espressif a modifié la forme des fonctions de rappel avec ESP-IDF 5. Les changements sont arrivés en deux étapes : le core 3.0 a modifié le callback de réception, et le core 3.3, basé sur ESP-IDF 5.5, celui d'envoi. Tu peux voir ta version à côté du paquet esp32 dans Outils > Type de carte > Gestionnaire de carte.
| Sujet | Core 2.x | Core 3.0–3.2 | Core 3.3 et plus |
|---|---|---|---|
| Callback de réception | (const uint8_t *mac, const uint8_t *veri, int uzunluk) | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | Comme 3.0 |
| Adresse de l'expéditeur | mac | bilgi->src_addr | bilgi->src_addr |
| Callback d'envoi | (const uint8_t *mac, esp_now_send_status_t durum) | Comme 2.x | (const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) |
| Plus gros paquet | 250 octets | 250 octets ; 1470 octets avec v2 en 3.2 | 1470 octets avec v2 |
| Réglage du canal | esp_wifi_set_channel() | Également WiFi.setChannel() | Comme 3.0 |
| Bibliothèque de classes fournie | Aucune | ESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial) | ESP32_NOW.h avec prise en charge de v2 |
En compilant un ancien code avec le 3.x, tu verras pour le callback de réception l'erreur invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t'. La solution : faire du premier paramètre de la fonction un const esp_now_recv_info_t * et lire l'adresse de l'expéditeur dans bilgi->src_addr. En passant au core 3.3, une erreur semblable apparaît, cette fois pour esp_now_send_cb_t ; le premier paramètre du callback d'envoi doit être const esp_now_send_info_t *, et l'adresse du récepteur se trouve dans bilgi->des_addr. Les croquis de ce tutoriel choisissent eux-mêmes la bonne forme grâce à la macro ESP_IDF_VERSION ; ils compilent donc sans modification en 2.x, 3.0–3.2 et 3.3+. Le core 3.x a aussi apporté une bibliothèque à base de classes appelée ESP32_NOW.h : tu écris onReceive() et onSent() dans une classe dérivée d'ESP_NOW_Peer, et la classe ESP_NOW_Serial te permet d'utiliser une liaison ESP-NOW comme Serial. Ses exemples se trouvent sous ESP_NOW dans le menu Fichier > Exemples. Nous utilisons directement les fonctions d'esp_now.h, car elles restent compatibles avec l'ESP8266 et l'ancien code.
ESP-NOW et Wi-Fi en même temps : l'accord de canal
L'ESP32 n'a qu'une seule radio 2,4 GHz, et cette radio ne peut travailler que sur un canal à la fois. Dès que la carte se connecte à un routeur, c'est le routeur qui choisit le canal, pas toi ; si le routeur est sur le canal 6, ESP-NOW fonctionne aussi sur le canal 6. Tous les capteurs qui envoient des paquets à une passerelle connectée au Wi-Fi doivent donc passer eux aussi sur le canal du routeur. Si les canaux ne correspondent pas, l'émetteur affiche « failed » ; si tu as ajouté le pair avec un canal différent de zéro, le moniteur série affiche en plus l'erreur « ESPNOW: Peer channel is not equal to the home channel, send fail! ». Pour un montage sans souci, suis ces règles :
- La passerelle se connecte d'abord au Wi-Fi, démarre ESP-NOW ensuite et affiche le canal du routeur dans le moniteur série avec WiFi.channel().
- Les capteurs ne se connectent pas au routeur ; ils trouvent le canal en cherchant le nom du réseau (WiFi.scanNetworks() et WiFi.channel(i)) ou utilisent un canal fixe dans le code. Ils n'ont pas besoin du mot de passe Wi-Fi pour cela.
- Passe le canal de ton routeur de « Automatique » à un canal fixe (1, 6 ou 11). En mode automatique, si le routeur change de canal, les capteurs perdent la passerelle.
- Désactive l'économie d'énergie sur la passerelle avec WiFi.setSleep(false). Par défaut, un ESP32 connecté à un routeur met sa radio en veille par intervalles et peut rater les paquets ESP-NOW qui arrivent à ce moment-là.
- Saisis dans les capteurs l'adresse MAC STA de la passerelle ; le croquis de passerelle ci-dessous l'affiche dans le moniteur série au démarrage.
Le croquis de passerelle ci-dessous se connecte à ton réseau domestique, conserve les mesures de cinq capteurs au maximum et les affiche sur une page web simple que tu peux ouvrir depuis un téléphone sur le même réseau. Remplace le nom du réseau et le mot de passe par les tiens, puis, après le téléversement, ouvre dans le navigateur de ton téléphone l'adresse IP affichée dans le moniteur série :
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}Dans le callback de réception, la passerelle se contente de placer le paquet dans une file (queue) FreeRTOS et rend la main aussitôt. loop() vide la file et enregistre pour chaque capteur la dernière mesure et l'heure d'arrivée, et le serveur web affiche ce tableau. La file garantit qu'aucun paquet n'est perdu quand plusieurs capteurs envoient au même moment ; c'est aussi la façon pratique de suivre le conseil d'Espressif de ne pas faire de longs traitements dans les callbacks. La page se rafraîchit toute seule toutes les 10 secondes, et la colonne « Son paket » (dernier paquet) montre tout de suite si un capteur s'est tu (batterie vide ou hors de portée ?). Si la passerelle n'arrive pas du tout à se connecter au Wi-Fi, le problème ne vient pas d'ESP-NOW ; applique les vérifications de l'ESP32 ne se connecte pas au Wi-Fi. Pour envoyer les données vers un système domotique, tu peux utiliser MQTT et Home Assistant à la place de la page web.
Exemple de capteur distant : DHT22, sommeil profond et ESP-NOW
Construisons maintenant le capteur sur batterie qui envoie ses données à la passerelle. À chaque réveil, il mesure la température, l'humidité et la tension de la batterie, envoie un seul paquet ESP-NOW, attend l'accusé de réception et passe en sommeil profond pendant 60 secondes. Pour la température et l'humidité, nous utilisons un DHT22 ; installe la DHT sensor library d'Adafruit depuis le gestionnaire de bibliothèques. Le fonctionnement du capteur est expliqué dans notre tutoriel sur le capteur de température et d'humidité DHT11 ; le câblage est identique pour le DHT22, et dans le code tu indiques simplement DHT22 comme type de capteur.




| Connexion | Broche ESP32 | Remarque |
|---|---|---|
| DHT22 VCC (+) | 3V3 | Fonctionne de 3,3 à 5 V |
| DHT22 DATA | GPIO 4 | Sur un capteur nu, résistance de 10 kΩ entre DATA et 3V3 |
| DHT22 GND (−) | GND | Masse commune |
| Batterie + | GPIO 34 (via le pont diviseur) | Deux résistances de 100 kΩ ; une batterie de 4,2 V arrive sur la broche à 2,1 V |
| Batterie − | GND | La résistance basse du pont va aussi à 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}L'astuce de ce croquis, ce sont les variables marquées RTC_DATA_ATTR. En sommeil profond, la mémoire principale de l'ESP32 est effacée, mais ces variables placées en mémoire RTC sont conservées. Le capteur ne cherche le canal par balayage qu'au premier démarrage (environ 2 secondes) et l'utilise directement aux réveils suivants ; si un paquet n'est pas remis, il remet la variable du canal à zéro et refait un balayage au réveil suivant. Ainsi, même si le routeur change de canal, le capteur se rétablit au cycle suivant. La tension de la batterie est lue déjà étalonnée avec analogReadMilliVolts() puis multipliée par 2, car le pont diviseur la divise par deux ; tu peux vérifier les valeurs des résistances avec l'outil Pont diviseur de tension. GPIO 34 est une broche ADC1 ; c'est important, car les broches ADC2 ne peuvent pas faire de mesure analogique quand la radio Wi-Fi est active. Pour le détail des modes de veille, consulte notre tutoriel ESP32 Deep Sleep.
Estimons grossièrement l'autonomie. Un réveil sans balayage (démarrage, mesure et envoi) dure environ 0,3 seconde avec une consommation moyenne d'environ 100 mA, soit à peu près 0,008 mAh par réveil. Avec un réveil par minute, on obtient 1440 réveils par jour et environ 12 mAh au total. Un module ESP32 nu consomme environ 10 µA en sommeil profond ; avec le courant de veille du DHT22 et les résistances du pont, la consommation en sommeil reste sous 2 mAh par jour. Le capteur consomme donc environ 14 mAh par jour, et un accu 18650 de 2000 mAh tient 4 à 5 mois. Pour refaire le calcul avec tes propres valeurs, utilise le calculateur d'autonomie de batterie.
ESP-NOW avec l'ESP8266

Les cartes ESP8266 peuvent utiliser ESP-NOW dans le même réseau que des ESP32, mais leur bibliothèque Arduino est différente : on inclut espnow.h au lieu d'esp_now.h et ESP8266WiFi.h au lieu de WiFi.h, et les fonctions viennent du SDK de l'ESP8266 et non d'ESP-IDF. Les principales différences :
| Opération | ESP32 (esp_now.h) | ESP8266 (espnow.h) |
|---|---|---|
| Initialisation | esp_now_init() == ESP_OK | esp_now_init() == 0 |
| Choix du rôle | Inutile | esp_now_set_self_role(ESP_NOW_ROLE_COMBO) |
| Ajout d'un pair | esp_now_add_peer(&es) via un struct | esp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, canal, NULL, 0) |
| Callback de réception | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | (uint8_t *mac, uint8_t *donnees, uint8_t longueur) |
| Callback d'envoi | (..., esp_now_send_status_t durum) | (uint8_t *mac, uint8_t statut) ; 0 signifie succès |
| Plus gros paquet | 250 octets (1470 avec v2) | 250 octets |
Sur l'ESP8266, n'oublie pas d'ajouter WiFi.disconnect() après WiFi.mode(WIFI_STA) ; au démarrage, l'ESP8266 essaie de se reconnecter tout seul au dernier routeur utilisé, le canal change et des paquets peuvent se perdre. Si tu définis le struct de données en packed des deux côtés, l'ESP32 et l'ESP8266 lisent les mêmes octets de la même façon, car ce sont tous deux des processeurs 32 bits little-endian. Pour les broches et l'installation de l'ESP8266, consulte notre tutoriel ESP8266 NodeMCU.
ESP-NOW avec MicroPython et le Raspberry Pi Pico
Si tu programmes l'ESP32 en MicroPython, il existe un module espnow tout prêt pour ESP-NOW ; il est inclus dans les versions de MicroPython pour ESP32 et ESP8266. Le code ci-dessous reçoit le struct Paket de 30 octets envoyé par l'exemple émetteur Arduino et le décode avec struct.unpack(). Dans la chaîne de format, < signifie little-endian sans octets de remplissage ; B est un nombre d'un octet, I un nombre non signé de 4 octets, f un float et 16s un texte de 16 octets. Le champ bool d'Arduino se lit avec B comme 0 ou 1. Téléverse le code sur la carte avec Thonny et saisis l'adresse MAC affichée dans le tableau aliciMac de l'émetteur 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)Envoyer un paquet depuis MicroPython tient aussi en deux lignes : ajoute le récepteur avec e.add_peer(mac) et envoie le même struct avec e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")) ; des cartes Arduino et MicroPython peuvent ainsi travailler ensemble sur le même réseau. Sur une carte MicroPython connectée au Wi-Fi, désactive l'économie d'énergie avec sta.config(pm=sta.PM_NONE) pour ne pas rater de paquets. Si tu découvres MicroPython, commence par nos bases de MicroPython. La famille Raspberry Pi Pico, en revanche, n'a pas d'ESP-NOW : le Pico standard n'a aucune radio, et la puce CYW43439 des Pico W et Pico 2 W ne gère que le Wi-Fi et le Bluetooth standard. Pour intégrer un Pico à un réseau ESP-NOW, relie-lui un petit ESP32-C3 par liaison série UART et utilise-le comme pont, ou fais dialoguer le Pico W avec la passerelle en Wi-Fi ; le Wi-Fi du Pico W est expliqué dans Pico W avec Wi-Fi et serveur web. La même méthode du pont fonctionne pour l'Arduino Uno.
Chiffrer les paquets ESP-NOW
Par défaut, les paquets ESP-NOW ne sont pas chiffrés ; n'importe qui sur le même canal peut les capter dans l'air et envoyer de faux paquets qui imitent ton struct. Utilise le chiffrement pour des projets comme une serrure ou une porte de garage. ESP-NOW utilise CCMP, comme le WPA2, et fonctionne avec deux clés : une PMK de 16 octets identique sur tous les appareils (réglée avec esp_now_set_pmk()) et une LMK de 16 octets propre à chaque pair. En ajoutant le pair, tu mets es.encrypt = true et tu copies la clé dans es.lmk ; l'autre côté doit aussi t'ajouter comme pair chiffré avec la même LMK. Pour recevoir des paquets chiffrés, le récepteur doit obligatoirement ajouter l'émetteur à sa liste. Les paquets de diffusion ne peuvent pas être chiffrés, et par défaut on ne peut avoir que 7 pairs chiffrés au maximum. Le chiffrement seul n'empêche pas qu'un paquet capté soit renvoyé plus tard ; ajouter un compteur croissant aux commandes critiques et refuser les anciens numéros côté récepteur est une bonne protection supplémentaire.
Idées de projets avec ESP-NOW
- Sonnette sans fil : Quand on appuie sur le bouton de l'ESP32 à la porte, un buzzer sonne sur la carte à l'intérieur ; pour l'autonomie, utilise le bouton comme broche de réveil du sommeil profond.
- Température de plusieurs pièces : Place un capteur DHT22 ou BME280 dans chaque pièce, regroupe les mesures sur une passerelle et compare-les sur une page web.
- Arrosage du jardin : Un capteur d'humidité du sol envoie la commande « arroser » à la carte placée près de la pompe ; pour le câblage, consulte capteur d'humidité du sol.
- Télécommande sans fil pour robot : Envoie les valeurs d'un joystick 20 fois par seconde ; pour la partie mécanique du robot, vois fabriquer une voiture robot Arduino.
- Alarme de jardin : Quand un capteur PIR du jardin détecte un mouvement, la passerelle envoie une notification via Telegram.
- Spectacle lumineux synchronisé : Avec un seul paquet de diffusion, fais passer en même temps à la même couleur les rubans WS2812B de plusieurs cartes.
Erreurs ESP-NOW fréquentes et solutions
| Symptôme / erreur | Cause | Solution |
|---|---|---|
| « ESPNOW: Peer interface is invalid », impossible d'ajouter le pair | esp_now_peer_info_t utilisée sans remise à zéro | Remets la structure à zéro avec = {} ou memset et écris ifidx = WIFI_IF_STA |
| « failed » en permanence | Mauvaise MAC (adresse AP ou faute de frappe), récepteur éteint ou sur un autre canal | Relis l'adresse MAC STA du récepteur et vérifie le canal des deux cartes |
| Les paquets arrivent mais les valeurs sont absurdes | Le struct diffère d'un côté à l'autre ou contient un String | Définis le struct dans le même ordre et en packed des deux côtés, utilise un tableau char pour le texte |
| « invalid conversion ... to 'esp_now_recv_cb_t' » | Code écrit pour le core 2.x compilé en 3.x | Écris le callback de réception avec const esp_now_recv_info_t * |
| « invalid conversion ... to 'esp_now_send_cb_t' » | Code écrit pour le core 3.0–3.2 compilé en 3.3+ | Écris le callback d'envoi avec const esp_now_send_info_t * ou utilise la macro ESP_IDF_VERSION |
| « Peer channel is not equal to the home channel, send fail! » | Le canal de la carte connectée au Wi-Fi diffère de celui du pair | Ajoute le pair avec channel = 0 et passe les capteurs sur le canal du routeur |
| La passerelle connectée au Wi-Fi rate des paquets | Économie d'énergie Wi-Fi (modem sleep) | Utilise WiFi.setSleep(false) |
| ESP_ERR_ESPNOW_NOT_INIT | esp_now_init() n'a pas été appelée ou l'a été avant le démarrage du Wi-Fi | D'abord WiFi.mode(WIFI_STA), puis esp_now_init() |
| ESP_ERR_ESPNOW_FULL | La liste de pairs a atteint 20 entrées | Retire d'anciens pairs avec esp_now_del_peer() ou utilise la diffusion |
| ESP_ERR_ESPNOW_NO_MEM | Les paquets s'enchaînent avant que la file d'envoi se vide | Attends le résultat d'envoi avant le paquet suivant |
| L'adresse MAC s'affiche 00:00:00:00:00:00 | Avec le core 3.x, WiFi.macAddress() a été appelée avant le démarrage complet du Wi-Fi | Utilise esp_wifi_get_mac() après WiFi.mode(WIFI_STA) |
ESP-NOW est le moyen le plus rapide de faire dialoguer plusieurs ESP32 sans routeur, câbles ni modules supplémentaires : lis l'adresse MAC, définis ton struct de données à l'identique des deux côtés, ajoute le pair et envoie. Quand tu l'utilises avec le Wi-Fi, retiens la seule règle : tout le monde doit être sur le même canal. Dans l'application BlueGrays, tu peux essayer toutes les briques de ce tutoriel, de la commande de LED et de bouton avec l'ESP32 à la mesure de température avec le DHT22, en passant par la connexion Wi-Fi, le serveur web et le sommeil profond, pas à pas avec schémas de câblage et code prêt à l'emploi.
Questions fréquentes
ESP-NOW a-t-il besoin d'un routeur ou d'internet ?
Non. ESP-NOW utilise la radio Wi-Fi des cartes mais ne rejoint aucun réseau ; les cartes s'envoient directement des paquets à leurs adresses MAC. Pour amener les données sur internet, il te faut un ESP32 servant de passerelle, connecté à la fois au Wi-Fi et à ESP-NOW.
Quelle est la portée d'ESP-NOW ?
Avec des cartes ESP32 à antenne intégrée, on obtient en général 20 à 40 mètres en intérieur selon les murs et environ 100 à 200 mètres en champ libre. Avec des modules à antenne externe et le mode LR (Long Range) activé des deux côtés, Espressif annonce jusqu'à 1 kilomètre en vue directe.
Combien d'appareils peut-on connecter avec ESP-NOW ?
La liste de pairs d'une carte peut contenir jusqu'à 20 appareils, dont 7 chiffrés au maximum par défaut. Cette limite concerne les appareils auxquels tu envoies : une passerelle peut écouter plus de 20 capteurs qui envoient sans chiffrement, et une diffusion atteint tous les appareils du même canal en un seul paquet.
Peut-on utiliser ESP-NOW et le Wi-Fi en même temps ?
Oui, mais comme l'unique radio de l'ESP32 ne travaille que sur un canal à la fois, tous les appareils ESP-NOW doivent utiliser le canal du routeur. Désactive l'économie d'énergie de la passerelle avec WiFi.setSleep(false) et, sur les capteurs, trouve le canal en cherchant le nom du réseau ou règle le routeur sur un canal fixe.
Un ESP32 et un ESP8266 peuvent-ils communiquer en ESP-NOW ?
Oui. Les deux puces gèrent ESP-NOW et peuvent s'envoyer des paquets ; sur l'ESP8266, seules la bibliothèque (espnow.h) et la forme des fonctions changent. Définis le struct de données à l'identique et en packed des deux côtés, et garde les paquets sous 250 octets.
Combien d'octets peut-on envoyer au maximum avec ESP-NOW ?
Avec ESP-NOW v1, un paquet contient au maximum 250 octets, et cette limite vaut pour toutes les cartes, ESP8266 compris. Les cartes sous ESP-IDF 5.4 ou plus récent (core Arduino 3.2+) peuvent envoyer jusqu'à 1470 octets avec ESP-NOW v2, mais les cartes en v1 ne reçoivent pas les paquets de plus de 250 octets.
Projets à réaliser après cet article
Voir tous les projets
Clignotement LED avec ESP32
Enseigne l'utilisation de base des sorties GPIO avec l'ESP32. Nous obtenons un effet de clignotement en allumant et éteignant une LED à partir de la broche numérique. Version ESP32 du projet blink classique sur Arduino UNO ; les commandes de base de blink dans l'environnement Arduino restent les mêmes, mais le brochage et les niveaux GPIO de 3,3 V de l'ESP32 diffèrent de ceux de l'Arduino UNO.

ESP32 Contrôle de LED avec bouton (Toggle)
Chaque fois que l'on appuie sur le bouton, la LED change d'état : si elle est allumée, elle s'éteint, si elle est éteinte, elle s'allume. Modèle classique de basculement. Avec le debounce, nous nettoyons le bruit mécanique du bouton-poussoir pour garantir un seul basculement avec une seule pression.

Communication bidirectionnelle entre 2 Arduino
Deux Arduino reliés par des fils se parlent : appuie sur le bouton de l'un et la lumière de l'autre s'allume, appuie encore et elle s'éteint. Les deux cartes ont le même programme, donc chacune envoie

ESP32 DHT22 Température et humidité (avec écran LCD)
Il s'agit d'un projet permettant de mesurer la température ambiante et le taux d'humidité à l'aide d'un capteur DHT22 et d'afficher ces données en temps réel sur un écran LCD I2C 16x2. Les données du capteur sont lues à l'aide du protocole one-wire grâce à la bibliothèque Adafruit DHT. Ce projet constitue la base des stations météo IoT, de la domotique ou des systèmes de surveillance de l'environnement dans les serres.

Connexion Wi-Fi STA ESP32 (avec écran LCD)
Dans ce projet, nous connectons l'ESP32 au réseau Wi-Fi domestique en mode Station (STA) de manière sécurisée et tolérante aux pannes. Nous isolons SSID/mot de passe dans un fichier secrets.h pour empêcher leur fuite sur Git, mettons en place une reconnexion automatique, et affichons le niveau RSSI et l'adresse IP sur un écran LCD I2C. La LED intégrée sert d'indicateur d'état.

Serveur Web asynchrone ESP32 (avec affichage de l'adresse IP sur l'écran LCD)
Dans ce projet, nous transformons l'ESP32 en un serveur web asynchrone que vous pouvez contrôler depuis votre téléphone ou votre ordinateur connecté au même réseau Wi-Fi. Grâce à la bibliothèque ESPAsyncWebServer, le serveur peut traiter plusieurs requêtes simultanément (contrairement au classique WebServer.h qui se bloque sur une seule requête). Depuis votre navigateur, vous pourrez vous connecter à l'adresse IP de l'ESP32, puis allumer et éteindre la LED co

Mode veille profonde de l'ESP32 — Économie de batterie (OLED + compteur RAM RTC)
Le mode « Deep Sleep » de l'ESP32 réduit la consommation de courant à environ 10 µA en désactivant le processeur et la plupart des périphériques, ce qui revêt une importance cruciale pour les appareils IoT fonctionnant sur batterie. Dans ce projet, nous utilisons trois sources de réveil différentes : une minuterie (timer, 15 s), une broche externe (EXT0 — bouton GPIO 33) et un capteur tactile (T0/GPIO 4). Les variables marquées avec RTC_DATA_ATTR sont conservées pendant le mode Deep Sleep — le

Enregistreur de Température ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
Construis un enregistreur de température horodaté avec ESP32 ! Lis la température/humidité du DHT22 chaque minute, obtiens l'heure réelle du DS1307 RTC et enregistre sur carte microSD au format CSV. L'OLED affiche en direct la température (grande police), le pourcentage d'humidité, l'horodatage et le compteur. Le fichier /temp_log.csv sur la carte SD s'ouvre dans Excel, Google Sheets ou Python pandas pour analyse — parfait pour surveillance de serre, suivi de température à domicile, c
Autres articles
Application BlueGrays
Réalise ce montage pas à pas sur ton téléphone
Schéma, code et liste des composants sur un seul écran. Plus de 200 projets Arduino, ESP32 et Pico à réaliser.
Scanne avec ton téléphone
Pointe l’appareil photo vers le code : la boutique d’applications de ton téléphone s’ouvre.
ou ouvre la boutique

