Ratgeber · 20 Min. Lesezeit

ESP-NOW mit dem ESP32: Kabellose Kommunikation ohne Router

ESP-NOW mit dem ESP32: MAC-Adresse auslesen, struct bis 250 Byte senden, Einweg- und Zweiweg-Code, 20 Peers, Reichweite, Core 3.x und WLAN-Kanal im Griff.

ESP-NOW ist ein von Espressif entwickeltes Funkprotokoll, mit dem ESP32- und ESP8266-Boards Daten direkt untereinander austauschen, ganz ohne Router, Access Point oder Internet. Die Boards nutzen ihr WLAN-Funkmodul, melden sich aber in keinem Netzwerk an; jedes Gerät findet sein Gegenüber über die MAC-Adresse und überträgt kleine Pakete mit bis zu 250 Byte in wenigen Millisekunden. Weil kein Verbindungsaufbau, keine IP-Adresse und kein Passwort nötig sind, kann ein Tastendruck auf einem Board sofort ein Relais auf einem anderen schalten, und ein Sensor mit Akku kann aufwachen, seinen Messwert senden und gleich wieder schlafen gehen.

In dieser Anleitung richtest du ESP-NOW von Grund auf ein: Zuerst liest du die MAC-Adressen deiner Boards aus, dann sendest du mit Einweg- und Zweiweg-Beispielen verschiedene Werte wie Temperatur, Zähler und Text in einem einzigen Paket, zusammengefasst in einem struct. Danach lernst du das Limit von 20 Peers, Broadcast-Nachrichten, Reichweite und Geschwindigkeit, die mit dem ESP32 Arduino Core 3.x geänderten Funktionen und die Kanalabstimmung kennen, die einzige Regel für ESP-NOW und WLAN gleichzeitig. Im letzten Teil schickst du die Messwerte eines Funksensors im Tiefschlaf an ein mit dem WLAN verbundenes Gateway und siehst sie dir auf dem Handy an. Wenn du zum ersten Mal mit dem ESP32 arbeitest, erledige zuerst die Arduino-IDE-Einrichtung aus Was ist der ESP32?.

ESP32-Entwicklungsboard, das per ESP-NOW kommuniziert
Für ESP-NOW brauchst du kein Zusatzmodul: Das WLAN-Funkmodul im ESP32 lässt die Boards ohne Router direkt miteinander sprechen.

Was ist ESP-NOW und wie funktioniert es?

Bei normalem WLAN verbindet sich der ESP32 zuerst mit dem Router, prüft das Passwort, holt sich per DHCP eine IP-Adresse und kann erst dann Daten senden; das dauert meist 1–3 Sekunden. ESP-NOW überspringt all diese Schritte. Es packt deine Daten in ein kurzes WLAN-Paket, einen sogenannten herstellerspezifischen Action Frame aus dem Standard IEEE 802.11, und schickt es direkt an die MAC-Adresse des anderen Boards. Das empfangende Board antwortet auf Hardware-Ebene mit einer Bestätigung (ACK), und eine Callback-Funktion auf der Senderseite meldet dir, ob das Paket angekommen ist. Bleibt das ACK aus, wiederholt die WLAN-Schicht das Paket automatisch einige Male. Kurz gesagt funktioniert ESP-NOW wie ein unsichtbarer Briefkasten zwischen zwei Boards: Du schreibst die Adresse drauf, wirfst das Paket ein und erfährst, sobald die Gegenseite es erhalten hat.

EigenschaftWert
Entwickelt vonEspressif Systems
Frequenz2,4 GHz (WLAN-Funk, Kanäle 1–13)
Daten pro Paket250 Byte (ESP-NOW v1); 1470 Byte mit v2 ab ESP-IDF 5.4
Registrierte PeersHöchstens 20; standardmäßig höchstens 7 verschlüsselte
Standard-Datenrate1 Mbit/s
ZustellbestätigungBei Unicast ja (ACK), bei Broadcast nein
VerschlüsselungOptional, CCMP (AES-128); PMK- und LMK-Schlüssel mit je 16 Byte
Router / Access PointNicht nötig
Unterstützte ChipsESP32-Chips mit WLAN (ESP32, S2, S3, C2, C3, C6…) und ESP8266

Die eigentlichen Stärken von ESP-NOW sind schnelle Reaktion und geringer Energiebedarf. Drückst du einen Taster, leuchtet die LED auf der Gegenseite nach wenigen Millisekunden; per WLAN müsstest du dafür erst eine Verbindung aufbauen und dann eine Anfrage an einen Server schicken. Bei einem Sensor mit Akku ist der Unterschied noch größer: Beim Verbinden mit dem WLAN zieht das Funkmodul sekundenlang über 100 mA, mit ESP-NOW wacht das Board auf, sendet seinen Messwert und schläft insgesamt nach wenigen hundert Millisekunden wieder. Dafür kommt ESP-NOW nicht ins Internet; wenn deine Daten ins Internet oder aufs Handy sollen, braucht das Netz irgendwo einen mit dem WLAN verbundenen ESP32 als Gateway. Diesen Aufbau erstellen wir später in dieser Anleitung.

ESP-NOW, WLAN, Bluetooth oder nRF24L01?

MethodeZusatzmodulRouterPaketgrößeTypische Reichweite (Freifeld)Am besten für
ESP-NOWNicht nötigNicht nötig250 Byte100–200 m, im LR-Modus mehrBefehle und Sensordaten zwischen Boards
WLAN (HTTP, MQTT)Nicht nötigNötigPraktisch unbegrenztAbdeckung des RoutersInternet, Webseiten, Cloud
Bluetooth (BLE)Nicht nötigNicht nötigStandardmäßig 20 ByteInnen 10–20 mVerbindung mit dem Handy
nRF24L01Nötig (SPI)Nicht nötig32 ByteEinige zehn Meter, mit PA+LNA deutlich mehrBoards ohne Funk wie Arduino Uno und Nano
LoRa (SX1278)Nötig (SPI)Nicht nötigBis 255 ByteMehrere KilometerSehr große Entfernungen, seltene Daten

Willst du mit dem Handy sprechen, ist Bluetooth die richtige Wahl, fürs Internet WLAN. Hast du aber mehrere ESP32, die nur untereinander kommunizieren, ist ESP-NOW meist der einfachste Weg: kein Zusatzmodul, kein Kabel, kein Router und nur ein paar Zeilen Code. Für Boards ohne Funk wie den Arduino Uno eignet sich das nRF24L01 besser, für Reichweiten von Kilometern LoRa-Module. Alle Optionen im Vergleich findest du unter Funkoptionen für den Arduino, und für die Verbindung zum Handy gibt es unsere ESP32-Bluetooth-Anleitung.

Welche Boards unterstützen ESP-NOW?

Board / ChipESP-NOWHinweis
ESP32 (WROOM-32, DevKit)JaUnterstützt auch den Long-Range-Modus (LR)
ESP32-S2, S3, C3, C6JaGleicher Arduino-Code, nur die Boardauswahl ändert sich
ESP32-C2 (ESP8684)JaKein LR-Modus
ESP32-H2NeinKein WLAN-Funk, daher kein ESP-NOW
ESP8266 (NodeMCU, Wemos D1 Mini)JaAndere Bibliothek (espnow.h), nur v1 und 250 Byte
Raspberry Pi Pico W / Pico 2 WNeinDer WLAN-Chip (CYW43439) kann kein ESP-NOW
Arduino Uno, Nano, MegaNeinLässt sich per serieller Schnittstelle (UART) an einen ESP32 anbinden

ESP-NOW funktioniert auch zwischen verschiedenen ESP-Chips: Ein ESP32-C3-Sensor kann Pakete an ein klassisches ESP32-Gateway oder an ein ESP8266-Board schicken. Einzige Bedingungen: Beide Seiten sind auf demselben WLAN-Kanal, und die Datenstruktur ist auf beiden Seiten exakt gleich definiert. Die folgenden Beispiele nutzen zwei ESP32-DevKit-Boards; welche Pins deines Boards sicher nutzbar sind, zeigt dir das Werkzeug Pinout.

Schritt 1: Die MAC-Adresse des ESP32 auslesen

Bei ESP-NOW findet jedes Board sein Gegenüber über die 6 Byte lange MAC-Adresse; sie wird jedem ESP32 bei der Herstellung fest eingeschrieben und ändert sich nicht. Lade den folgenden Sketch auf das Board, das später empfangen soll, und öffne den Seriellen Monitor mit 115200 Baud. Er gibt die Adresse lesbar und zusätzlich als Array-Zeile aus, die du direkt in den Code des anderen Boards kopieren kannst. Für die Beispiele mit zwei Boards machst du das bei beiden und notierst dir die Adressen.

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}
Der ESP32 hat zwei getrennte WLAN-MAC-Adressen: Station (STA) und Access Point (AP). Die AP-Adresse ist meist die STA-Adresse, bei der das letzte Byte um eins erhöht ist. Alle Beispiele hier betreiben die Boards im Modus WIFI_STA, trage auf der Gegenseite also immer die STA-Adresse ein; die Adresse der falschen Schnittstelle ist der häufigste Grund für die Meldung "failed". Im ESP32 Arduino Core 3.x kann WiFi.macAddress() 00:00:00:00:00:00 liefern, wenn es aufgerufen wird, bevor das WLAN vollständig gestartet ist; der Aufruf esp_wifi_get_mac() im Sketch oben hat dieses Problem nicht.

Daten per struct senden und das Limit von 250 Byte

ESP-NOW sendet nur ein Byte-Array und weiß nicht, was darin steckt. Am saubersten verschickst du mehrere Werte auf einmal, indem du sie in einem struct bündelst und dessen Bytes genau so sendest, wie sie im Speicher liegen. Der Empfänger definiert dasselbe struct und kopiert die ankommenden Bytes mit memcpy hinein. Damit das klappt, muss das struct auf beiden Seiten dieselbe Reihenfolge, dieselben Typen und dieselbe Größe haben. Der Compiler kann für schnelleren Zugriff unsichtbare Füllbytes (Padding) zwischen die Felder setzen: Das struct Paket aus den folgenden Beispielen belegt normalerweise 36 Byte, mit __attribute__((packed)) genau 30 Byte. packed garantiert, dass jedes Byte zwischen ESP32 und ESP8266 oder zwischen Arduino und MicroPython an derselben Stelle liegt.

TypGrößeHinweis
bool, uint8_t, int8_t, char1 ByteFlag, kleine Zahl, einzelnes Zeichen
int16_t, uint16_t2 Byte−32768…32767 oder 0…65535
int, int32_t, uint32_t4 Byteint hat auf ESP32 und ESP8266 4 Byte, auf dem Arduino Uno 2 Byte
float4 ByteEtwa 7 Stellen Genauigkeit
double8 ByteNimm float, wenn du double nicht wirklich brauchst
char mesaj[16]16 ByteText fester Länge; muss mit '\0' enden
String, char*, ZeigerNicht sendbarEs geht nur eine Speicheradresse raus, die auf der Gegenseite nichts bedeutet

In ein Paket passen höchstens 250 Byte. Diese Grenze stammt aus der ersten ESP-NOW-Version (v1) und gilt für alle Boards, auch den ESP8266. Der auf ESP-IDF 5.4 basierende Arduino Core ab 3.2 kann mit ESP-NOW v2 Pakete bis 1470 Byte senden; ein Board mit v1 (ein ESP8266 oder ein ESP32 mit älterem Core) kürzt längere Pakete aber auf 250 Byte oder verwirft sie ganz. In einem gemischten Netz bleibst du deshalb am sichersten bei 250 Byte. Die Zeile static_assert im Sender-Sketch bricht das Kompilieren mit einer klaren Meldung ab, wenn das struct größer als 250 Byte wird. Musst du größere Daten verschicken, teile sie in nummerierte Stücke auf und setze sie beim Empfänger der Reihe nach zusammen; für Sensorwerte, Tasterzustände oder Befehle reichen ein paar Dutzend Byte aber locker.

ESP-NOW in eine Richtung: der Sender-Sketch

  1. Stelle sicher, dass beide ESP32 in der Arduino IDE als ESP32 Dev Module ausgewählt sind. Die ESP-NOW-Bibliothek ist im Boardpaket enthalten, du musst nichts weiter installieren.
  2. Trage die MAC-Adresse, die du vom Empfänger ausgelesen hast, im Code unten in das Array aliciMac ein.
  3. Schließe am Sender einen Taster zwischen GPIO 4 und GND an; weil wir INPUT_PULLUP nutzen, brauchst du keinen zusätzlichen Widerstand. Details findest du unter Taster am Arduino anschließen.
  4. Lade den Sketch auf den Sender und öffne den Seriellen Monitor mit 115200 Baud. Solange der Empfänger noch nicht läuft, ist es normal, alle zwei Sekunden "Teslim edilemedi / failed" zu sehen.
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}

Der Sketch startet zuerst das WLAN im Modus WIFI_STA; ohne WLAN-Funk funktioniert ESP-NOW nicht, deshalb muss esp_now_init() immer nach der Zeile WiFi.mode() stehen. Danach wird der Empfänger als Peer in die Liste eingetragen. Wichtig ist, das struct esp_now_peer_info_t mit = {} zu leeren; zufällige Restwerte in nicht gelöschten Feldern führen zum Fehler "Peer interface is invalid". channel = 0 bedeutet "nimm den aktuellen Kanal des Boards". esp_now_send() legt das Paket in eine Warteschlange und kehrt sofort zurück; ESP_OK heißt nur, dass das Paket zum Senden angenommen wurde. Das eigentliche Ergebnis kommt ein paar Millisekunden später in der Funktion gonderimSonucu() an: ESP_NOW_SEND_SUCCESS bedeutet, dass der Empfänger das Paket auf Hardware-Ebene bestätigt hat. Der #if-Block oben wählt automatisch die Form der Sende-Callback, die sich je nach Core-Version unterscheidet; das erklären wir im Abschnitt zu Core 3.x.

Der ESP-NOW-Empfänger-Sketch

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}

Beim Empfänger landen die Pakete in der Funktion veriGeldi(). Sie läuft nicht in deinem loop(), sondern im hochpriorisierten WLAN-Task; darin solltest du also kein Serial.print, kein delay und keine langen Berechnungen verwenden. Richtig ist: prüfen, ob die Paketgröße zum erwarteten struct passt, die Daten mit memcpy in eine eigene Variable kopieren und loop() über ein volatile-Flag Bescheid geben. In Core 3.x liefert der Parameter bilgi außerdem den Absender (bilgi->src_addr) und die Signalstärke (bilgi->rx_ctrl->rssi in dBm); diese Angaben gelten nur innerhalb der Funktion, kopiere sie also, wenn du sie später brauchst. Der Empfänger muss den Sender nicht als Peer eintragen: Unverschlüsselte Unicast-Pakete und Broadcasts werden auch von Boards angenommen, die nicht in der Liste stehen.

"Delivered" bedeutet, dass das Paket die WLAN-Hardware des Empfängers erreicht hat, nicht, dass das Programm dort es verarbeitet hat. Ist das andere Board eingeschaltet und auf demselben Kanal, kommt das ACK sogar dann zurück, wenn es ESP-NOW nie gestartet hat. Nutze für wichtige Befehle eine eigene Bestätigung auf Software-Ebene und gib deinen Paketen einen Zähler mit: Springt der Zähler beim Empfänger, weiß er, wie viele Pakete verloren gingen, und kommt dieselbe Nummer zweimal, ignoriert er die Wiederholung.

Zweiweg-Kommunikation: zwei ESP32 steuern sich gegenseitig

Bei der Zweiweg-Kommunikation ist jedes Board zugleich Sender und Empfänger. Du lädst den folgenden Sketch auf beide Boards; der einzige Unterschied: Auf jedem Board steht im Array karsiMac die Adresse des jeweils anderen. Drückst du den Taster an einem Board, geht die LED am anderen an oder aus, und das Board, das seine LED umgeschaltet hat, meldet das mit einer Bestätigung zurück. Die Verdrahtung ist bei beiden Boards gleich:

BauteilESP32-PinHinweis
TasterGPIO 4 und GNDNutzt den internen Pull-up, kein zusätzlicher Widerstand nötig
LEDGPIO 2Bei den meisten DevKits die blaue Onboard-LED; für eine externe LED ein 220-Ω-Widerstand
StromversorgungUSBDie Boards können an getrennten USB-Ports oder Netzteilen hängen
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}

Das Feld tur im Nachrichten-struct gibt an, was das Paket ist: 1 ist ein Befehl, 2 eine Bestätigung. Das Board, an dem der Taster gedrückt wird, schickt den gewünschten Zustand für die LED der Gegenseite; das Board, das den Befehl erhält, setzt seine LED und schickt denselben Zustand mit Typ 2 zurück. So bekommt der Sender sowohl das Hardware-ACK als auch die Rückmeldung "die LED hat sich wirklich geändert". Der eingehende Befehl wird wieder über volatile-Flags an loop() weitergereicht, und digitalWrite sowie die Antwort passieren dort. Das ist sicherer, als esp_now_send() direkt in der Callback aufzurufen. Die 30 ms Wartezeit am Taster filtern das Prellen der Kontakte heraus. Schließt du statt der LED ein Relais an, wird derselbe Code zum Funk-Lichtschalter; die Relais-Verdrahtung findest du unter Was ist ein Relaismodul?.

Mehrere Geräte: das Limit von 20 Peers und Broadcast

ESP-NOW unterstützt alle drei Aufbauten: ein Sender an viele Empfänger (eine Fernbedienung an fünf Relais-Boards), viele Sender an einen Empfänger (fünf Sensoren an ein Gateway) und Netze, in denen jeder mit jedem spricht. Die Peer-Liste eines Boards fasst höchstens 20 Einträge, und standardmäßig dürfen nur 7 davon verschlüsselt sein. Dieses Limit gilt nur für die Boards, an die du sendest. Ein Gateway muss Sensoren, die ihm unverschlüsselte Pakete schicken, nicht eintragen und kann daher mehr als 20 Sensoren empfangen. Ist die Liste voll, liefert esp_now_add_peer() ESP_ERR_ESPNOW_FULL; dann entfernst du nicht mehr benötigte Peers mit esp_now_del_peer() und trägst neue ein. Der folgende Sketch trägt drei Empfänger und die Broadcast-Adresse ein und sendet dann sowohl einzeln als auch per Broadcast:

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}

Auch die Broadcast-Adresse FF:FF:FF:FF:FF:FF muss vor dem Senden als Peer eingetragen werden. Ein einziges Broadcast-Paket erreicht alle ESP-NOW-Empfänger auf demselben Kanal; du musst ihre MAC-Adressen nicht kennen, deshalb ist das die am einfachsten einzurichtende Methode. Der Preis: Broadcasts haben keine Zustellbestätigung, das Sendeergebnis sieht immer erfolgreich aus, und Broadcast-Pakete lassen sich nicht verschlüsseln. Beim Einzelversand bekommt dagegen jedes Paket sein eigenes ACK. Übergibst du esp_now_send() als Adresse NULL, geht das Paket einzeln an jeden Peer der Liste; steht auch die Broadcast-Adresse in der Liste, geht es zusätzlich einmal als Broadcast raus. Für viele Projekte bewährt sich: Die Boards finden sich beim ersten Start per Broadcast, merken sich die MAC-Adressen und wechseln dann zum bestätigten Unicast.

Reichweite und Geschwindigkeit von ESP-NOW

KennwertTypischer WertHinweis
Reichweite in Innenräumen20–40 m (1–3 Wände)Stahlbetonwände und Metallgegenstände dämpfen das Signal am stärksten
Reichweite im Freien100–200 mMit der PCB-Antenne auf dem Board; steigt, wenn die Boards höher über dem Boden stehen
LR-Modus (Long Range)Bis 1 kmSichtlinienwert von Espressif; die Rate sinkt auf 0,5 oder 0,25 Mbit/s
Standard-Datenrate1 Mbit/sHöhere Rate heißt geringere Reichweite
Sendedauer eines 250-Byte-PaketsEtwa 2,5 msMit ACK und Wartezeiten etwa 3 ms pro Paket
Theoretische Paketrate~300 Pakete pro Sekunde (~70 kB/s)Mit 250-Byte-Paketen; in der Praxis weniger
BefehlslatenzWenige MillisekundenKein Verbindungsaufbau und kein Server nötig

Diese Werte hängen stark von der Umgebung ab, die Rechnung ist aber einfach. Bei den standardmäßigen 1 Mbit/s ist jedes Byte 8 Mikrosekunden in der Luft; 250 Byte Daten mit Headern und Präambel brauchen etwa 2,5 ms. Mit dem ACK des Empfängers und dem Warten auf einen freien Kanal kommst du auf etwa 3 ms pro Paket, also höchstens rund 300 Pakete pro Sekunde. Für Sensordaten und Befehle ist das mehr als genug, für Audio- oder Videostreams ist ESP-NOW aber nicht geeignet. Für mehr Reichweite stellst du die Boards möglichst hoch, deckst die Antenne nicht mit Metallgehäuse, Akkus oder Kabeln ab, nutzt Module mit externer Antenne (etwa den ESP32-WROOM-32U) und schaltest auf beiden Seiten mit esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR) den Long-Range-Modus (LR) von Espressif ein. LR funktioniert nur zwischen zwei ESP32; eine nur auf LR gestellte Schnittstelle kann sich mit normalen Routern nicht verbinden, und ESP8266 sowie ESP32-C2 beherrschen diesen Modus nicht. Beim Reichweitentest mit Core 3.x gibst du am Empfänger bilgi->rx_ctrl->rssi aus: −50 dBm ist ein sehr starkes Signal, −80 dBm liegt nahe an der Grenze, und um −90 dBm beginnen Paketverluste.

Änderungen im ESP32 Arduino Core 3.x

Die meisten ESP-NOW-Beispiele im Netz wurden für den ESP32 Arduino Core 2.x geschrieben. Hast du über den Boardverwalter Version 3.x installiert, kompiliert dieser Code nicht mehr, weil Espressif mit ESP-IDF 5 die Form der Callback-Funktionen geändert hat. Die Änderungen kamen in zwei Schritten: Core 3.0 hat die Empfangs-Callback geändert, der auf ESP-IDF 5.5 basierende Core 3.3 die Sende-Callback. Welche Version du hast, siehst du unter Werkzeuge > Board > Boardverwalter neben dem Paket esp32.

ThemaCore 2.xCore 3.0–3.2Core 3.3 und neuer
Empfangs-Callback(const uint8_t *mac, const uint8_t *veri, int uzunluk)(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)Wie 3.0
Absenderadressemacbilgi->src_addrbilgi->src_addr
Sende-Callback(const uint8_t *mac, esp_now_send_status_t durum)Wie 2.x(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum)
Größtes Paket250 Byte250 Byte; ab 3.2 mit v2 1470 Byte1470 Byte mit v2
Kanal einstellenesp_wifi_set_channel()Zusätzlich WiFi.setChannel()Wie 3.0
Fertige KlassenbibliothekKeineESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial)ESP32_NOW.h mit v2-Unterstützung

Kompilierst du alten Code mit 3.x, bekommst du für die Empfangs-Callback den Fehler invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t'. Die Lösung: Mach den ersten Parameter der Funktion zu const esp_now_recv_info_t * und lies die Absenderadresse aus bilgi->src_addr. Beim Wechsel auf Core 3.3 taucht ein ähnlicher Fehler auf, diesmal für esp_now_send_cb_t; der erste Parameter der Sende-Callback muss const esp_now_send_info_t * sein, und die Empfängeradresse steht in bilgi->des_addr. Die Sketches in dieser Anleitung wählen die richtige Form mit dem Makro ESP_IDF_VERSION selbst aus und kompilieren daher unverändert unter 2.x, 3.0–3.2 und 3.3+. Core 3.x hat außerdem eine klassenbasierte Bibliothek namens ESP32_NOW.h eingeführt: Du schreibst onReceive() und onSent() in einer von ESP_NOW_Peer abgeleiteten Klasse, und mit der Klasse ESP_NOW_Serial nutzt du eine ESP-NOW-Verbindung wie Serial. Ihre Beispiele findest du unter ESP_NOW im Menü Datei > Beispiele. Wir verwenden direkt die Funktionen aus esp_now.h, weil sie mit dem ESP8266 und älterem Code kompatibel bleiben.

ESP-NOW und WLAN gleichzeitig: den Kanal abstimmen

Der ESP32 hat nur ein 2,4-GHz-Funkmodul, und das kann immer nur auf einem Kanal arbeiten. Sobald sich das Board mit einem Router verbindet, bestimmt der Router den Kanal, nicht du; funkt der Router auf Kanal 6, läuft auch ESP-NOW auf Kanal 6. Deshalb müssen alle Sensoren, die an ein mit dem WLAN verbundenes Gateway senden, ebenfalls auf den Kanal des Routers wechseln. Passen die Kanäle nicht, zeigt der Sender "failed"; hast du den Peer mit einem Kanal ungleich null eingetragen, erscheint im Seriellen Monitor zusätzlich der Fehler "ESPNOW: Peer channel is not equal to the home channel, send fail!". Halte dich für einen störungsfreien Aufbau an diese Regeln:

  1. Das Gateway verbindet sich zuerst mit dem WLAN, startet erst danach ESP-NOW und gibt den Kanal des Routers mit WiFi.channel() im Seriellen Monitor aus.
  2. Die Sensoren verbinden sich nicht mit dem Router; sie finden den Kanal, indem sie nach dem Netzwerknamen scannen (WiFi.scanNetworks() und WiFi.channel(i)), oder nutzen einen fest eingetragenen Kanal. Das WLAN-Passwort brauchen sie dafür nicht.
  3. Stelle den Kanal deines Routers von "Automatisch" auf einen festen Kanal (1, 6 oder 11). Im Automatikmodus verlieren die Sensoren das Gateway, wenn der Router den Kanal wechselt.
  4. Schalte am Gateway das Energiesparen mit WiFi.setSleep(false) ab. Ein mit dem Router verbundener ESP32 legt sein Funkmodul standardmäßig in Abständen schlafen und kann in dieser Zeit eintreffende ESP-NOW-Pakete verpassen.
  5. Trage in den Sensoren die STA-MAC-Adresse des Gateways ein; der folgende Gateway-Sketch gibt sie beim Start im Seriellen Monitor aus.

Der folgende Gateway-Sketch verbindet sich mit deinem Heimnetz, speichert die Messwerte von bis zu fünf Sensoren und zeigt sie auf einer einfachen Webseite, die du vom Handy im selben Netz öffnen kannst. Ersetze Netzwerkname und Passwort durch deine eigenen Daten und öffne nach dem Hochladen die im Seriellen Monitor angezeigte IP-Adresse im Browser deines Handys:

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}

In der Empfangs-Callback legt das Gateway das Paket nur in eine FreeRTOS-Queue (Warteschlange) und kehrt sofort zurück. loop() leert die Queue und speichert für jeden Sensor den letzten Messwert und die Ankunftszeit, der Webserver zeigt diese Tabelle an. Die Queue sorgt dafür, dass kein Paket verloren geht, wenn mehrere Sensoren gleichzeitig senden; so setzt du auch Espressifs Rat um, in Callbacks keine langen Aufgaben zu erledigen. Die Seite lädt sich alle 10 Sekunden neu, und die Spalte "Son paket" (letztes Paket) zeigt sofort, ob ein Sensor verstummt ist (Akku leer, außer Reichweite?). Verbindet sich das Gateway gar nicht erst mit dem WLAN, liegt das Problem nicht bei ESP-NOW; geh die Prüfungen aus ESP32 verbindet sich nicht mit dem WLAN durch. Willst du die Daten in eine Hausautomation einspeisen, kannst du statt der Webseite MQTT und Home Assistant verwenden.

Beispiel Funksensor: DHT22, Tiefschlaf und ESP-NOW

Jetzt bauen wir den akkubetriebenen Sensor, der seine Daten ans Gateway schickt. Bei jedem Aufwachen misst er Temperatur, Luftfeuchtigkeit und Akkuspannung, sendet ein einziges ESP-NOW-Paket, wartet auf das Zustell-ACK und geht für 60 Sekunden in den Tiefschlaf. Für Temperatur und Feuchte nutzen wir einen DHT22; dafür installierst du über den Bibliotheksverwalter die DHT sensor library von Adafruit. Wie der Sensor funktioniert, steht in unserer Anleitung zum DHT11 Temperatur- und Feuchtigkeitssensor; die Verdrahtung ist beim DHT22 gleich, im Code gibst du lediglich DHT22 als Sensortyp an.

ESP32 DevKit
ESP32 DevKit
DHT22 (AM2302)
DHT22 (AM2302)
18650 Li-Ion pil
18650 Li-Ion pil
TP4056 Lademodul
TP4056 Lademodul
VerbindungESP32-PinHinweis
DHT22 VCC (+)3V3Arbeitet mit 3,3–5 V
DHT22 DATAGPIO 4Beim nackten Sensor einen 10-kΩ-Widerstand zwischen DATA und 3V3
DHT22 GND (−)GNDGemeinsame Masse
Akku +GPIO 34 (über den Spannungsteiler)Zwei 100-kΩ-Widerstände; ein 4,2-V-Akku kommt am Pin mit 2,1 V an
Akku −GNDDer untere Widerstand des Teilers geht ebenfalls an 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}

Der Kniff dieses Sketches sind die mit RTC_DATA_ATTR markierten Variablen. Im Tiefschlaf wird der Hauptspeicher des ESP32 gelöscht, diese Variablen im RTC-Speicher bleiben aber erhalten. Der Sensor sucht den Kanal nur beim ersten Start per Scan (das dauert etwa 2 Sekunden) und nutzt ihn bei den folgenden Aufwachvorgängen direkt; lässt sich ein Paket nicht zustellen, setzt er die Kanalvariable zurück und scannt beim nächsten Aufwachen neu. So fängt sich der Sensor nach einem Zyklus wieder, selbst wenn der Router den Kanal wechselt. Die Akkuspannung wird mit analogReadMilliVolts() bereits kalibriert gelesen und mit 2 multipliziert, weil der Teiler sie halbiert; die Widerstandswerte kannst du mit dem Werkzeug Spannungsteiler prüfen. GPIO 34 ist ein ADC1-Pin; das ist wichtig, weil ADC2-Pins bei eingeschaltetem WLAN-Funk keine Analogwerte lesen können. Die Einzelheiten der Schlafmodi findest du in unserer Anleitung ESP32 Deep Sleep.

Schätzen wir die Akkulaufzeit grob ab. Ein Aufwachen ohne Scan (Start, Messung und Senden) dauert etwa 0,3 Sekunden bei durchschnittlich etwa 100 mA, das sind rund 0,008 mAh pro Aufwachen. Bei einem Aufwachen pro Minute ergeben sich 1440 Aufwachvorgänge am Tag und insgesamt etwa 12 mAh. Ein nacktes ESP32-Modul braucht im Tiefschlaf rund 10 µA; zusammen mit dem Ruhestrom des DHT22 und den Teilerwiderständen bleibt der Schlafverbrauch unter 2 mAh am Tag. Der Sensor verbraucht also etwa 14 mAh pro Tag, und eine 18650-Zelle mit 2000 mAh hält 4–5 Monate. Mit eigenen Werten rechnest du im Akkulaufzeit-Rechner.

Diese Rechnung gilt für ein nacktes Modul oder stromsparende Boards. Auf einem gewöhnlichen ESP32 DevKit ziehen Spannungsregler, USB-Seriell-Chip und Power-LED selbst im Schlaf einige mA und drücken die Laufzeit auf wenige Wochen. Für langlebige Sensoren nimmst du einen Regler mit niedrigem Ruhestrom (etwa den HT7333), entfernst die Power-LED oder wählst ein fertiges Low-Power-Board. Verlängerst du das Messintervall von 60 Sekunden auf 5 Minuten, verlängert sich die Akkulaufzeit auf mehr als das Dreifache. Zum sicheren Laden der 18650-Zelle eignet sich ein TP4056-Modul mit Schutzschaltung.

ESP-NOW mit dem ESP8266

Wemos D1 Mini mit ESP8266
ESP8266-Boards wie der Wemos D1 Mini unterstützen ebenfalls ESP-NOW, ihre Arduino-Bibliothek und die Funktionssignaturen unterscheiden sich aber vom ESP32.

ESP8266-Boards können im selben Netz wie ESP32-Boards ESP-NOW nutzen, ihre Arduino-Bibliothek ist aber eine andere: Statt esp_now.h bindest du espnow.h ein, statt WiFi.h ESP8266WiFi.h, und die Funktionen stammen aus dem ESP8266-SDK statt aus ESP-IDF. Die wichtigsten Unterschiede:

AufgabeESP32 (esp_now.h)ESP8266 (espnow.h)
Initialisierenesp_now_init() == ESP_OKesp_now_init() == 0
Rolle festlegenNicht nötigesp_now_set_self_role(ESP_NOW_ROLE_COMBO)
Peer hinzufügenesp_now_add_peer(&es) über ein structesp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, kanal, NULL, 0)
Empfangs-Callback(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)(uint8_t *mac, uint8_t *daten, uint8_t laenge)
Sende-Callback(..., esp_now_send_status_t durum)(uint8_t *mac, uint8_t status); 0 bedeutet Erfolg
Größtes Paket250 Byte (1470 mit v2)250 Byte

Vergiss beim ESP8266 nicht, nach WiFi.mode(WIFI_STA) ein WiFi.disconnect() einzufügen; der ESP8266 versucht beim Start, sich von selbst mit dem zuletzt genutzten Router zu verbinden, dabei wechselt der Kanal und Pakete können verloren gehen. Definierst du das Daten-struct auf beiden Seiten als packed, lesen ESP32 und ESP8266 dieselben Bytes gleich, denn beide sind 32-Bit-Prozessoren mit Little-Endian-Byte-Reihenfolge. Pins und Einrichtung des ESP8266 findest du in unserer Anleitung ESP8266 NodeMCU.

ESP-NOW mit MicroPython und der Raspberry Pi Pico

Programmierst du den ESP32 in MicroPython, gibt es für ESP-NOW das fertige Modul espnow; es ist in den MicroPython-Builds für ESP32 und ESP8266 enthalten. Der folgende Code empfängt das 30 Byte große struct Paket aus dem Arduino-Sender-Beispiel und entschlüsselt es mit struct.unpack(). Im Formatstring steht < für Little-Endian ohne Füllbytes; B ist eine 1-Byte-Zahl, I eine vorzeichenlose 4-Byte-Zahl, f ein float und 16s ein 16 Byte langer Text. Das bool-Feld aus Arduino wird mit B als 0 oder 1 gelesen. Lade den Code mit Thonny aufs Board und trage die ausgegebene MAC-Adresse im Array aliciMac des Arduino-Senders ein:

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)

Auch das Senden aus MicroPython sind nur zwei Zeilen: Mit e.add_peer(mac) trägst du den Empfänger ein, mit e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")) schickst du dasselbe struct; so arbeiten Arduino- und MicroPython-Boards im selben Netz zusammen. Auf einem MicroPython-Board, das mit dem WLAN verbunden ist, schaltest du mit sta.config(pm=sta.PM_NONE) das Energiesparen ab, damit keine Pakete verloren gehen. Wenn MicroPython neu für dich ist, beginne mit unseren MicroPython-Grundlagen. Die Raspberry Pi Pico-Familie hat dagegen kein ESP-NOW: Der normale Pico hat überhaupt keinen Funk, und der CYW43439-Chip auf Pico W und Pico 2 W beherrscht nur normales WLAN und Bluetooth. Um einen Pico in ein ESP-NOW-Netz einzubinden, schließt du einen kleinen ESP32-C3 per serieller UART-Kommunikation an und nutzt ihn als Brücke, oder du lässt den Pico W per WLAN mit dem Gateway sprechen; WLAN auf dem Pico W erklären wir in Pico W mit WLAN und Webserver. Dieselbe Brückenlösung funktioniert auch für den Arduino Uno.

ESP-NOW-Pakete verschlüsseln

Standardmäßig sind ESP-NOW-Pakete unverschlüsselt; jeder auf demselben Kanal kann sie mitschneiden und gefälschte Pakete senden, die dein struct nachahmen. Für Projekte wie Türschlösser oder Garagentore solltest du verschlüsseln. ESP-NOW nutzt wie WPA2 das Verfahren CCMP und arbeitet mit zwei Schlüsseln: einem 16 Byte langen PMK, der auf allen Geräten gleich ist (gesetzt mit esp_now_set_pmk()), und einem eigenen 16-Byte-LMK pro Peer. Beim Eintragen des Peers setzt du es.encrypt = true und kopierst den Schlüssel nach es.lmk; die Gegenseite muss dich ebenfalls mit demselben LMK als verschlüsselten Peer eintragen. Um verschlüsselte Pakete zu empfangen, muss der Empfänger den Sender in seine Liste aufnehmen. Broadcast-Pakete lassen sich nicht verschlüsseln, und standardmäßig sind höchstens 7 verschlüsselte Peers möglich. Verschlüsselung allein verhindert nicht, dass ein mitgeschnittenes Paket später erneut gesendet wird; ein steigender Zähler in kritischen Befehlen, bei dem der Empfänger alte Nummern ablehnt, ist ein guter zusätzlicher Schutz.

Projektideen mit ESP-NOW

  • Funk-Türklingel: Drückt jemand den Taster am ESP32 an der Tür, ertönt im Haus ein Summer am anderen Board; für eine lange Akkulaufzeit nutzt du den Taster als Pin, der das Board aus dem Tiefschlaf weckt.
  • Temperatur in mehreren Räumen: Stelle in jeden Raum einen DHT22- oder BME280-Sensor, sammle die Werte auf einem Gateway und vergleiche sie auf einer Webseite.
  • Gartenbewässerung: Ein Bodenfeuchtesensor schickt dem Board an der Pumpe den Befehl "gießen"; die Verdrahtung findest du unter Bodenfeuchtesensor.
  • Funkfernbedienung für Roboter: Sende die Werte eines Joysticks 20-mal pro Sekunde; für die Mechanik des Roboters schau in Arduino-Roboterauto bauen.
  • Gartenalarm: Erkennt ein PIR-Sensor im Garten eine Bewegung, schickt das Gateway eine Nachricht über Telegram.
  • Synchrone Lichtshow: Schalte die WS2812B-Streifen mehrerer Boards mit einem einzigen Broadcast-Paket gleichzeitig auf dieselbe Farbe.

Häufige ESP-NOW-Fehler und Lösungen

Symptom / FehlerUrsacheLösung
"ESPNOW: Peer interface is invalid", Peer lässt sich nicht eintragenesp_now_peer_info_t wurde nicht geleertStruct mit = {} oder memset leeren und ifidx = WIFI_IF_STA setzen
Ständig "failed"Falsche MAC (AP-Adresse oder Tippfehler), Empfänger aus oder auf anderem KanalSTA-MAC des Empfängers neu auslesen und die Kanäle beider Boards prüfen
Pakete kommen an, die Werte sind aber UnsinnDas struct ist auf beiden Seiten verschieden oder enthält einen StringStruct auf beiden Seiten in gleicher Reihenfolge und packed definieren, für Text ein char-Array nutzen
"invalid conversion ... to 'esp_now_recv_cb_t'"Für Core 2.x geschriebener Code wird mit 3.x kompiliertEmpfangs-Callback mit const esp_now_recv_info_t * schreiben
"invalid conversion ... to 'esp_now_send_cb_t'"Für Core 3.0–3.2 geschriebener Code wird mit 3.3+ kompiliertSende-Callback mit const esp_now_send_info_t * schreiben oder das Makro ESP_IDF_VERSION nutzen
"Peer channel is not equal to the home channel, send fail!"Der Kanal des mit dem WLAN verbundenen Boards weicht vom Kanal des Peers abPeer mit channel = 0 eintragen und die Sensoren auf den Router-Kanal bringen
Das mit dem WLAN verbundene Gateway verpasst einige PaketeWLAN-Energiesparen (Modem-Sleep)WiFi.setSleep(false) verwenden
ESP_ERR_ESPNOW_NOT_INITesp_now_init() nicht aufgerufen oder vor dem WLAN-Start aufgerufenZuerst WiFi.mode(WIFI_STA), dann esp_now_init()
ESP_ERR_ESPNOW_FULLDie Peer-Liste hat 20 Einträge erreichtAlte Peers mit esp_now_del_peer() entfernen oder Broadcast nutzen
ESP_ERR_ESPNOW_NO_MEMPakete werden schneller gesendet, als sich die Sendewarteschlange leertVor dem nächsten Paket auf das Sendeergebnis warten
MAC-Adresse lautet 00:00:00:00:00:00Unter Core 3.x wurde WiFi.macAddress() vor dem vollständigen WLAN-Start aufgerufenNach WiFi.mode(WIFI_STA) esp_wifi_get_mac() verwenden
Fang bei der Fehlersuche mit dem einfachsten Schritt an: Leg die beiden Boards nebeneinander und sende zuerst an die Broadcast-Adresse. Kommt das Broadcast-Paket an, stimmen Funk, Kanal und Datenstruktur, und das Problem liegt bei der MAC-Adresse. Kommt gar nichts an, wähle unter Werkzeuge > Core Debug Level die Stufe Debug; dann erscheinen die ausführlichen Protokolle des Cores im Seriellen Monitor. Startet das Board ständig neu, liegt es höchstwahrscheinlich an der Stromversorgung; Lösungen für die Stromspitze beim Einschalten des Funks findest du in ESP32 Brownout und Guru Meditation.

ESP-NOW ist der schnellste Weg, mehrere ESP32 ohne Router, Kabel oder Zusatzmodule miteinander sprechen zu lassen: MAC-Adresse auslesen, das Daten-struct auf beiden Seiten gleich definieren, Peer eintragen und senden. Wenn du es zusammen mit WLAN nutzt, denk an die eine Regel: Alle müssen auf demselben Kanal sein. In der BlueGrays-App kannst du alle Bausteine dieser Anleitung, von LED- und Tastersteuerung mit dem ESP32 über Temperaturmessung mit dem DHT22, WLAN-Verbindung und Webserver bis zum Tiefschlaf, Schritt für Schritt mit Schaltplänen und fertigem Code ausprobieren.

Häufige Fragen

Braucht ESP-NOW einen Router oder Internet?

Nein. ESP-NOW nutzt zwar das WLAN-Funkmodul der Boards, meldet sich aber in keinem Netzwerk an; die Boards schicken ihre Pakete direkt an die MAC-Adresse des anderen. Sollen die Daten ins Internet, brauchst du einen ESP32 als Gateway, der sowohl mit dem WLAN als auch mit ESP-NOW verbunden ist.

Wie groß ist die Reichweite von ESP-NOW?

Mit ESP32-Boards mit Onboard-Antenne erreichst du in Innenräumen je nach Wänden meist 20–40 Meter und im Freien etwa 100–200 Meter. Mit Modulen mit externer Antenne und auf beiden Seiten aktiviertem LR-Modus (Long Range) sind laut Espressif bei Sichtverbindung bis zu 1 Kilometer möglich.

Wie viele Geräte kann man mit ESP-NOW verbinden?

Die Peer-Liste eines Boards fasst bis zu 20 Geräte, davon standardmäßig höchstens 7 verschlüsselte. Das Limit gilt für die Geräte, an die du sendest: Ein Gateway kann mehr als 20 Sensoren empfangen, die unverschlüsselt senden, und ein Broadcast erreicht mit einem einzigen Paket alle Geräte auf demselben Kanal.

Kann man ESP-NOW und WLAN gleichzeitig nutzen?

Ja, aber weil das einzige Funkmodul des ESP32 immer nur auf einem Kanal arbeitet, müssen alle ESP-NOW-Geräte den Kanal des Routers verwenden. Schalte am Gateway das Energiesparen mit WiFi.setSleep(false) ab und lass die Sensoren den Kanal per Scan nach dem Netzwerknamen finden oder stelle den Router auf einen festen Kanal.

Können ESP32 und ESP8266 per ESP-NOW kommunizieren?

Ja. Beide Chips unterstützen ESP-NOW und können sich gegenseitig Pakete schicken; beim ESP8266 unterscheiden sich nur die Bibliothek (espnow.h) und die Funktionssignaturen. Definiere das Daten-struct auf beiden Seiten identisch und als packed und halte die Pakete unter 250 Byte.

Wie viele Byte kann man mit ESP-NOW maximal senden?

Mit ESP-NOW v1 passen höchstens 250 Byte in ein Paket, und diese Grenze gilt für alle Boards einschließlich ESP8266. Boards mit ESP-IDF 5.4 oder neuer (Arduino Core 3.2+) können mit ESP-NOW v2 bis zu 1470 Byte senden, Boards mit v1 empfangen aber keine Pakete über 250 Byte.

Alle Projekte ansehen
LED Blinken mit ESP32
#103Anfänger

LED Blinken mit ESP32

Lehrt die grundlegende Verwendung von GPIO-Ausgängen mit ESP32. Wir erreichen einen Blinkeffekt, indem wir eine LED über einen digitalen Pin ein- und ausschalten. ESP32-Version des klassischen Blink-Projekts auf Arduino UNO; die grundlegenden Blink-Befehle in der Arduino-Umgebung bleiben gleich, aber die Pinbelegung und die GPIO-Pegel von 3,3 V des ESP32 unterscheiden sich von denen des Arduino UNO.

4.7(37)
99
ESP32 LED Steuerung mit Taster (Toggle)
#105Anfänger

ESP32 LED Steuerung mit Taster (Toggle)

Jedes Mal, wenn die Taste gedrückt wird, ändert sich der Zustand der LED: ist sie an, schaltet sie aus, ist sie aus, schaltet sie ein. Klassisches Toggle-Muster. Mit der Entprellung wird das mechanische Geräusch des Tasters beseitigt, um ein einmaliges Umschalten mit einem einzigen Druck zu gewährleisten.

4.6(39)
57
Bidirektionale Kommunikation zwischen 2 Arduinos
#58Mittel
Premium

Bidirektionale Kommunikation zwischen 2 Arduinos

Zwei Arduinos werden per Kabel verbunden und unterhalten sich: Drückst du den Knopf am einen, geht am anderen das Licht an, drückst du noch einmal, geht es aus. Auf beiden Platinen läuft dasselbe Prog

4.5(20)
26
ESP32 DHT22 Temperatur und Luftfeuchtigkeit (mit LCD-Display)
#117Mittel

ESP32 DHT22 Temperatur und Luftfeuchtigkeit (mit LCD-Display)

Bei diesem Projekt werden die Umgebungstemperatur und die Luftfeuchtigkeit mit einem DHT22-Sensor gemessen und in Echtzeit auf einem 16x2 I2C-LCD angezeigt. Mithilfe der Adafruit-DHT-Bibliothek werden die Daten über das One-Wire-Protokoll vom Sensor ausgelesen. Dieses Projekt bildet die Grundlage für IoT-Wetterstationen, Smart-Home-Automatisierung oder Umgebungsüberwachungssysteme in Gewächshäusern. Die Kommunikation mit dem DHT22 erfolgt über den GPIO-4-Pin des ESP32.

4.8(38)
56
ESP32-WLAN-STA-Verbindung (mit LCD-Display)
#127Fortgeschritten

ESP32-WLAN-STA-Verbindung (mit LCD-Display)

In diesem Projekt verbinden wir den ESP32 im Station-Modus (STA) sicher und fehlertolerant mit dem heimischen WLAN. Wir lagern SSID und Passwort in eine separate secrets.h-Datei aus, um Leaks zu Git zu verhindern, richten einen Auto-Reconnect-Mechanismus ein und zeigen RSSI-Signalpegel und IP-Adresse auf einem I2C-LCD-Display an. Die interne LED dient als Statusanzeige. Ein Verbindungsmanager auf Produktionsniveau.

4.4(21)
34
ESP32 Async-Webserver (mit IP-Anzeige auf dem LCD)
#129Fortgeschritten

ESP32 Async-Webserver (mit IP-Anzeige auf dem LCD)

In diesem Projekt verwandeln wir den ESP32 in einen asynchronen Webserver, den Sie über Ihr Smartphone oder Ihren Computer im selben WLAN-Netzwerk steuern können. Dank der ESPAsyncWebServer-Bibliothek kann der Server mehrere Anfragen gleichzeitig bearbeiten (im Gegensatz zum klassischen WebServer.h, der bei einer einzigen Anfrage blockiert). Sie können über den Browser die IP-Adresse des ESP32 öffnen und durch Drücken der ON/OFF-Schaltflächen (Routen /on und /off) die an GPIO 5 anges

4.8(16)
26
ESP32 Deep Sleep – Batteriesparmodus (OLED + RTC-RAM-Zähler)
#135Fortgeschritten
Premium

ESP32 Deep Sleep – Batteriesparmodus (OLED + RTC-RAM-Zähler)

Der Deep-Sleep-Modus des ESP32 senkt den Stromverbrauch auf etwa 10 µA, indem er die CPU und die meisten Peripheriegeräte abschaltet, und ist für batteriebetriebene IoT-Geräte von entscheidender Bedeutung. In diesem Projekt verwenden wir drei verschiedene Weckquellen: den Timer (15 s), einen externen Pin (EXT0 – Taste GPIO 33) und die Berührungserkennung (T0/GPIO 4). Mit RTC_DATA_ATTR markierte Variablen bleiben während des Deep-Sleep-Modus erhalten – der Boot-Zähler, der Timer-Wake-Zähler und

4.3(10)
16
ESP32 Temperatur-Datenlogger (DHT22 + DS1307 RTC + microSD + OLED)
#144Fortgeschritten
Premium

ESP32 Temperatur-Datenlogger (DHT22 + DS1307 RTC + microSD + OLED)

Baue einen zeitgestempelten Temperaturlogger mit ESP32! Lese Temperatur/Feuchtigkeit vom DHT22 jede Minute, hole die aktuelle Zeit vom DS1307 RTC und protokolliere auf microSD-Karte als CSV. Das OLED zeigt live Temperatur (große Schrift), Luftfeuchtigkeit, Zeitstempel und Logzähler. Die /temp_log.csv-Datei auf der SD-Karte öffnet sich in Excel, Google Sheets oder Python pandas zur Analyse — perfekt für Gewächshausüberwachung, Raumtemperaturverfolgung, Laboraufzeichnung, Lebensmittellageru

4.7(7)
12