Guides · 24 min de lecture

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 ?.

Carte de développement ESP32 communiquant en ESP-NOW
ESP-NOW ne demande aucun module supplémentaire : la radio Wi-Fi intégrée à l'ESP32 permet aux cartes de communiquer directement, sans routeur.

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éristiqueValeur
Développé parEspressif Systems
Fréquence2,4 GHz (radio Wi-Fi, canaux 1–13)
Données par paquet250 octets (ESP-NOW v1) ; 1470 octets avec v2 à partir d'ESP-IDF 5.4
Pairs enregistrésJusqu'à 20 ; jusqu'à 7 chiffrés par défaut
Débit par défaut1 Mbit/s
Accusé de réceptionOui en unicast (ACK), non en diffusion
ChiffrementOptionnel, CCMP (AES-128) ; clés PMK et LMK de 16 octets chacune
Routeur / point d'accèsInutile
Puces compatiblesPuces 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éthodeModule en plusRouteurTaille de paquetPortée typique (champ libre)Idéal pour
ESP-NOWInutileInutile250 octets100–200 m, davantage en mode LRCommandes et données de capteurs entre cartes
Wi-Fi (HTTP, MQTT)InutileNécessairePratiquement illimitéeLa couverture du routeurInternet, pages web, cloud
Bluetooth (BLE)InutileInutile20 octets par défaut10–20 m en intérieurConnexion au téléphone
nRF24L01Nécessaire (SPI)Inutile32 octetsQuelques dizaines de mètres, bien plus en version PA+LNACartes sans radio comme l'Arduino Uno et le Nano
LoRa (SX1278)Nécessaire (SPI)InutileJusqu'à 255 octetsPlusieurs kilomètresTrè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 / puceESP-NOWRemarque
ESP32 (WROOM-32, DevKit)OuiGère aussi le mode longue portée (LR)
ESP32-S2, S3, C3, C6OuiMême code Arduino, seule la carte choisie change
ESP32-C2 (ESP8684)OuiPas de mode LR
ESP32-H2NonPas de radio Wi-Fi, donc pas d'ESP-NOW
ESP8266 (NodeMCU, Wemos D1 Mini)OuiBibliothèque différente (espnow.h), v1 et 250 octets uniquement
Raspberry Pi Pico W / Pico 2 WNonSa puce Wi-Fi (CYW43439) ne gère pas ESP-NOW
Arduino Uno, Nano, MegaNonPeut ê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.

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_wifi.h>
3
4void setup() {
5  Serial.begin(115200);
6  delay(1000);
7  WiFi.mode(WIFI_STA);                 // ESP-NOW istasyon arayüzünü kullanır / ESP-NOW uses the station interface
8
9  uint8_t mac[6];
10  if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) {
11    Serial.printf("STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
12                  mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
13    // Diğer karta kopyalanacak satır / line to paste into the other board's code
14    Serial.printf("uint8_t karsiMac[] = {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X};\n",
15                  mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
16  } else {
17    Serial.println("MAC okunamadi / could not read MAC");
18  }
19}
20
21void loop() {
22}
L'ESP32 possède deux adresses MAC Wi-Fi distinctes : station (STA) et point d'accès (AP). L'adresse AP est en général l'adresse STA avec le dernier octet augmenté de un. Tous les exemples de ce tutoriel font fonctionner les cartes en mode WIFI_STA, donc saisis toujours l'adresse STA sur l'autre carte ; utiliser l'adresse de la mauvaise interface est la cause la plus fréquente du message « failed ». Avec le core 3.x Arduino pour ESP32, WiFi.macAddress() peut renvoyer 00:00:00:00:00:00 s'il est appelé avant que le Wi-Fi soit complètement démarré ; l'appel esp_wifi_get_mac() du croquis ci-dessus n'a pas ce problème.

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.

TypeTailleRemarque
bool, uint8_t, int8_t, char1 octetDrapeau, petit nombre, caractère unique
int16_t, uint16_t2 octets−32768…32767 ou 0…65535
int, int32_t, uint32_t4 octetsint fait 4 octets sur ESP32 et ESP8266, mais 2 octets sur l'Arduino Uno
float4 octetsEnviron 7 chiffres de précision
double8 octetsPréfère float sauf vrai besoin
char mesaj[16]16 octetsTexte de longueur fixe ; doit se terminer par '\0'
String, char*, pointeurImpossible à envoyerSeule 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

  1. 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.
  2. Saisis l'adresse MAC lue sur la carte réceptrice dans le tableau aliciMac du code ci-dessous.
  3. 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.
  4. 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.
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Alıcının STA MAC adresi (mac_oku çıktısı) / receiver's STA MAC address (from the MAC sketch)
6uint8_t aliciMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
7
8const int BUTON_PIN = 4;               // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
9
10// İki kartta da birebir aynı olmalı / must be identical on both boards
11typedef struct __attribute__((packed)) {
12  uint8_t  id;                         // gönderen kart numarası / sender board number
13  uint32_t sayac;                      // paket sayacı / packet counter
14  float    sicaklik;                   // °C
15  float    nem;                        // % bağıl nem / % relative humidity
16  bool     buton;                      // buton basılı mı / is the button pressed
17  char     mesaj[16];                  // kısa metin, String değil / short text, not String
18} Paket;
19static_assert(sizeof(Paket) <= 250, "Paket 250 bayti asamaz / packet must not exceed 250 bytes");
20
21Paket paket;
22
23// Teslim sonucu: alıcı paketi aldıysa SUCCESS / delivery result: SUCCESS if the receiver got it
24#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
25void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) {   // çekirdek 3.3+ / core 3.3+
26#else
27void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) {                // çekirdek 2.x-3.2 / core 2.x-3.2
28#endif
29  Serial.println(durum == ESP_NOW_SEND_SUCCESS ? "Teslim edildi / delivered" : "Teslim edilemedi / failed");
30}
31
32void setup() {
33  Serial.begin(115200);
34  pinMode(BUTON_PIN, INPUT_PULLUP);
35  WiFi.mode(WIFI_STA);                 // önce Wi-Fi, sonra ESP-NOW / Wi-Fi first, then ESP-NOW
36
37  if (esp_now_init() != ESP_OK) {
38    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
39    return;
40  }
41  esp_now_register_send_cb(gonderimSonucu);
42
43  esp_now_peer_info_t es = {};         // tüm alanları sıfırla / zero every field
44  memcpy(es.peer_addr, aliciMac, 6);
45  es.channel = 0;                      // 0 = şu anki kanal / 0 = current channel
46  es.ifidx = WIFI_IF_STA;
47  es.encrypt = false;
48  if (esp_now_add_peer(&es) != ESP_OK) {
49    Serial.println("Es eklenemedi / failed to add peer");
50    return;
51  }
52  paket.id = 1;
53}
54
55void loop() {
56  paket.sayac++;
57  paket.sicaklik = 20.0 + random(0, 100) / 10.0;   // deneme değeri / test value
58  paket.nem = 40.0 + random(0, 200) / 10.0;
59  paket.buton = (digitalRead(BUTON_PIN) == LOW);
60  snprintf(paket.mesaj, sizeof(paket.mesaj), "Selam %lu", (unsigned long)paket.sayac);
61
62  esp_err_t sonuc = esp_now_send(aliciMac, (uint8_t *)&paket, sizeof(paket));
63  if (sonuc != ESP_OK) {
64    Serial.printf("Gonderme hatasi / send error: %s\n", esp_err_to_name(sonuc));
65  }
66  delay(2000);
67}

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

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

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.

« Delivered » (remis) signifie que le paquet a atteint le matériel Wi-Fi du récepteur, pas que le programme du récepteur l'a traité. Si l'autre carte est alimentée et sur le même canal, l'ACK revient même si elle n'a jamais démarré ESP-NOW. Pour les commandes importantes, utilise un message d'acquittement logiciel séparé et ajoute un compteur à tes paquets : si le récepteur voit le compteur sauter, il sait combien de paquets ont été perdus, et si le même numéro arrive deux fois, il ignore le doublon.

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 :

ComposantBroche ESP32Remarque
BoutonGPIO 4 et GNDUtilise la résistance de tirage interne, aucune résistance en plus
LEDGPIO 2LED bleue intégrée sur la plupart des DevKit ; résistance de 220 Ω pour une LED externe
AlimentationUSBLes cartes peuvent être alimentées par des ports USB ou des chargeurs différents
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Bu kodu iki karta da yükle; yalnız karşı kartın MAC adresini değiştir
6// Upload to both boards; change only the other board's MAC address
7uint8_t karsiMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
8
9const int BUTON_PIN = 4;               // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
10const int LED_PIN = 2;                 // dahili LED / onboard LED
11
12typedef struct __attribute__((packed)) {
13  uint8_t tur;                         // 1 = komut, 2 = onay / 1 = command, 2 = acknowledgement
14  bool    ledAcik;                     // istenen ya da uygulanan durum / requested or applied state
15} Mesaj;
16
17volatile bool komutGeldi = false;
18volatile bool onayGeldi = false;
19volatile bool gelenLed = false;
20bool karsiLed = false;                 // karşı karttaki LED için son isteğimiz / our last request for the other LED
21int sonButon = HIGH;
22
23#if ESP_IDF_VERSION_MAJOR >= 5
24void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) {   // çekirdek 3.x / core 3.x
25#else
26void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) {                // çekirdek 2.x / core 2.x
27#endif
28  if (uzunluk != sizeof(Mesaj)) return;
29  Mesaj m;
30  memcpy(&m, veri, sizeof(m));
31  if (m.tur == 1) {
32    gelenLed = m.ledAcik;
33    komutGeldi = true;
34  } else if (m.tur == 2) {
35    onayGeldi = true;
36  }
37}
38
39void gonder(uint8_t tur, bool ledAcik) {
40  Mesaj m = {tur, ledAcik};
41  esp_now_send(karsiMac, (uint8_t *)&m, sizeof(m));
42}
43
44void setup() {
45  Serial.begin(115200);
46  pinMode(BUTON_PIN, INPUT_PULLUP);
47  pinMode(LED_PIN, OUTPUT);
48  WiFi.mode(WIFI_STA);
49
50  if (esp_now_init() != ESP_OK) {
51    Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
52    return;
53  }
54  esp_now_register_recv_cb(veriGeldi);
55
56  esp_now_peer_info_t es = {};
57  memcpy(es.peer_addr, karsiMac, 6);
58  es.ifidx = WIFI_IF_STA;              // kanal 0, şifresiz / channel 0, unencrypted
59  esp_now_add_peer(&es);
60}
61
62void loop() {
63  // 1) Butona basılınca karşı kartın LED'ini değiştir / on press, toggle the other board's LED
64  int buton = digitalRead(BUTON_PIN);
65  if (buton != sonButon) {
66    delay(30);                         // kontak sıçramasını bekle / wait out contact bounce
67    buton = digitalRead(BUTON_PIN);
68    if (sonButon == HIGH && buton == LOW) {
69      karsiLed = !karsiLed;
70      gonder(1, karsiLed);
71      Serial.println(karsiLed ? "Komut: AC / command: ON" : "Komut: KAPAT / command: OFF");
72    }
73    sonButon = buton;
74  }
75
76  // 2) Gelen komutu uygula ve onay gönder / apply an incoming command and reply
77  if (komutGeldi) {
78    komutGeldi = false;
79    digitalWrite(LED_PIN, gelenLed ? HIGH : LOW);
80    gonder(2, gelenLed);
81  }
82
83  // 3) Karşı karttan gelen onay / acknowledgement from the other board
84  if (onayGeldi) {
85    onayGeldi = false;
86    Serial.println("Karsi kart uyguladi / the other board applied it");
87  }
88}

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 :

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

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èreValeur typiqueRemarque
Portée en intérieur20–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 libre100–200 mAvec l'antenne PCB de la carte ; augmente quand les cartes sont placées plus haut
Mode LR (Long Range)Jusqu'à 1 kmValeur d'Espressif en vue directe ; le débit tombe à 0,5 ou 0,25 Mbit/s
Débit par défaut1 Mbit/sUn débit plus élevé signifie une portée plus courte
Temps d'antenne d'un paquet de 250 octetsEnviron 2,5 msEnviron 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 commandeQuelques millisecondesNi 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.

SujetCore 2.xCore 3.0–3.2Core 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éditeurmacbilgi->src_addrbilgi->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 paquet250 octets250 octets ; 1470 octets avec v2 en 3.21470 octets avec v2
Réglage du canalesp_wifi_set_channel()Également WiFi.setChannel()Comme 3.0
Bibliothèque de classes fournieAucuneESP32_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 :

  1. 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().
  2. 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.
  3. 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.
  4. 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à.
  5. 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 :

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

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.

ESP32 DevKit
ESP32 DevKit
DHT22 (AM2302)
DHT22 (AM2302)
18650 Li-Ion pil
18650 Li-Ion pil
Module de charge TP4056
Module de charge TP4056
ConnexionBroche ESP32Remarque
DHT22 VCC (+)3V3Fonctionne de 3,3 à 5 V
DHT22 DATAGPIO 4Sur un capteur nu, résistance de 10 kΩ entre DATA et 3V3
DHT22 GND (−)GNDMasse 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 −GNDLa résistance basse du pont va aussi à GND
C++ (Arduino IDE)
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_wifi.h>
4#include <esp_idf_version.h>
5#include <DHT.h>
6
7const char *AG_ADI = "AgAdi";          // ağ geçidinin bağlandığı Wi-Fi (şifre gerekmez) / Wi-Fi the gateway uses (no password needed)
8uint8_t gecitMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};   // ağ geçidinin STA MAC adresi / gateway STA MAC
9const uint8_t SENSOR_ID = 1;           // her sensöre farklı numara / a different number for each sensor
10const uint64_t UYKU_US = 60ULL * 1000000ULL;   // 60 saniye uyku / sleep for 60 seconds
11
12#define DHT_PIN 4
13#define PIL_PIN 34                     // ADC1 pini, 100k + 100k bölücü / ADC1 pin, 100k + 100k divider
14DHT dht(DHT_PIN, DHT22);
15
16typedef struct __attribute__((packed)) {
17  uint8_t  id;
18  uint32_t sayac;
19  float    sicaklik;
20  float    nem;
21  float    pil;
22} SensorPaketi;
23
24RTC_DATA_ATTR uint8_t kanal = 0;       // derin uykuda silinmez / kept during deep sleep
25RTC_DATA_ATTR uint32_t paketSayaci = 0;
26volatile bool sonucGeldi = false;
27volatile bool teslimEdildi = false;
28
29#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
30void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) {   // çekirdek 3.3+ / core 3.3+
31#else
32void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) {                // çekirdek 2.x-3.2 / core 2.x-3.2
33#endif
34  teslimEdildi = (durum == ESP_NOW_SEND_SUCCESS);
35  sonucGeldi = true;
36}
37
38uint8_t kanalBul(const char *ag) {     // ağın kanalını tarayarak bul / find the network's channel by scanning
39  int n = WiFi.scanNetworks();
40  for (int i = 0; i < n; i++) {
41    if (WiFi.SSID(i) == ag) return WiFi.channel(i);
42  }
43  return 1;                            // bulunamazsa kanal 1 / channel 1 if not found
44}
45
46void uyu() {
47  esp_sleep_enable_timer_wakeup(UYKU_US);
48  esp_deep_sleep_start();
49}
50
51void setup() {
52  Serial.begin(115200);
53  dht.begin();
54  WiFi.mode(WIFI_STA);
55
56  if (kanal == 0) kanal = kanalBul(AG_ADI);   // yalnız ilk açılışta ya da hatadan sonra tara / scan only at first boot or after a failure
57  esp_wifi_set_promiscuous(true);
58  esp_wifi_set_channel(kanal, WIFI_SECOND_CHAN_NONE);
59  esp_wifi_set_promiscuous(false);
60
61  if (esp_now_init() != ESP_OK) uyu();
62  esp_now_register_send_cb(gonderimSonucu);
63
64  esp_now_peer_info_t es = {};
65  memcpy(es.peer_addr, gecitMac, 6);
66  es.channel = kanal;
67  es.ifidx = WIFI_IF_STA;
68  esp_now_add_peer(&es);
69
70  SensorPaketi p;
71  p.id = SENSOR_ID;
72  p.sayac = ++paketSayaci;
73  p.sicaklik = dht.readTemperature();
74  p.nem = dht.readHumidity();
75  p.pil = analogReadMilliVolts(PIL_PIN) * 2 / 1000.0;   // bölücü yarıya indirir / the divider halves it
76
77  esp_now_send(gecitMac, (uint8_t *)&p, sizeof(p));
78  unsigned long baslangic = millis();
79  while (!sonucGeldi && millis() - baslangic < 200) delay(1);   // en fazla 200 ms bekle / wait up to 200 ms
80
81  Serial.printf("Kanal %u, %.1f C, %%%.0f, %.2f V, %s\n", (unsigned)kanal, p.sicaklik, p.nem, p.pil,
82                teslimEdildi ? "teslim edildi / delivered" : "teslim edilemedi / failed");
83  if (!teslimEdildi) kanal = 0;        // teslim edilemedi: sonraki uyanışta yeniden tara / not delivered: rescan next wake
84  Serial.flush();
85  uyu();
86}
87
88void loop() {
89}

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.

Ce calcul vaut pour un module nu ou des cartes basse consommation. Sur un ESP32 DevKit ordinaire, le régulateur, la puce USB-série et la LED d'alimentation consomment plusieurs mA même en veille et réduisent l'autonomie à quelques semaines. Pour des capteurs durables, utilise un régulateur à faible courant de repos (comme le HT7333), retire la LED d'alimentation ou choisis une carte basse consommation toute prête. Passer l'intervalle de mesure de 60 secondes à 5 minutes multiplie l'autonomie par plus de trois. Pour charger l'accu 18650 en toute sécurité, tu peux utiliser un module TP4056 avec protection.

ESP-NOW avec l'ESP8266

Carte Wemos D1 Mini à ESP8266
Les cartes ESP8266 comme la Wemos D1 Mini gèrent aussi ESP-NOW, mais leur bibliothèque Arduino et la forme de leurs fonctions diffèrent de l'ESP32.

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érationESP32 (esp_now.h)ESP8266 (espnow.h)
Initialisationesp_now_init() == ESP_OKesp_now_init() == 0
Choix du rôleInutileesp_now_set_self_role(ESP_NOW_ROLE_COMBO)
Ajout d'un pairesp_now_add_peer(&es) via un structesp_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 paquet250 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 :

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

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 / erreurCauseSolution
« ESPNOW: Peer interface is invalid », impossible d'ajouter le pairesp_now_peer_info_t utilisée sans remise à zéroRemets la structure à zéro avec = {} ou memset et écris ifidx = WIFI_IF_STA
« failed » en permanenceMauvaise MAC (adresse AP ou faute de frappe), récepteur éteint ou sur un autre canalRelis l'adresse MAC STA du récepteur et vérifie le canal des deux cartes
Les paquets arrivent mais les valeurs sont absurdesLe struct diffère d'un côté à l'autre ou contient un StringDé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 pairAjoute 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_INITesp_now_init() n'a pas été appelée ou l'a été avant le démarrage du Wi-FiD'abord WiFi.mode(WIFI_STA), puis esp_now_init()
ESP_ERR_ESPNOW_FULLLa liste de pairs a atteint 20 entréesRetire d'anciens pairs avec esp_now_del_peer() ou utilise la diffusion
ESP_ERR_ESPNOW_NO_MEMLes paquets s'enchaînent avant que la file d'envoi se videAttends le résultat d'envoi avant le paquet suivant
L'adresse MAC s'affiche 00:00:00:00:00:00Avec le core 3.x, WiFi.macAddress() a été appelée avant le démarrage complet du Wi-FiUtilise esp_wifi_get_mac() après WiFi.mode(WIFI_STA)
Pour chercher une panne, commence par le plus simple : pose les deux cartes côte à côte et envoie d'abord à l'adresse de diffusion. Si le paquet de diffusion arrive, la radio, le canal et la structure de données sont bons, et le problème vient de l'adresse MAC. Si rien n'arrive, choisis Debug dans Outils > Core Debug Level ; les journaux détaillés du core apparaîtront dans le moniteur série. Si la carte redémarre sans cesse, le problème vient très probablement de l'alimentation ; les solutions au pic de courant à l'allumage de la radio sont dans ESP32 Brownout et Guru Meditation.

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.

Voir tous les projets
Clignotement LED avec ESP32
#103Débutant

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.

4.7(37)
99
ESP32 Contrôle de LED avec bouton (Toggle)
#105Débutant

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.

4.6(39)
57
Communication bidirectionnelle entre 2 Arduino
#58Intermédiaire
Premium

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

4.5(20)
26
ESP32 DHT22 Température et humidité (avec écran LCD)
#117Intermédiaire

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.

4.8(38)
56
Connexion Wi-Fi STA ESP32 (avec écran LCD)
#127Avancé

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.

4.4(21)
34
Serveur Web asynchrone ESP32 (avec affichage de l'adresse IP sur l'écran LCD)
#129Avancé

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

4.8(16)
26
Mode veille profonde de l'ESP32 — Économie de batterie (OLED + compteur RAM RTC)
#135Avancé
Premium

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

4.3(10)
16
Enregistreur de Température ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
#144Avancé
Premium

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

4.7(7)
12