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

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.
| Eigenschaft | Wert |
|---|---|
| Entwickelt von | Espressif Systems |
| Frequenz | 2,4 GHz (WLAN-Funk, Kanäle 1–13) |
| Daten pro Paket | 250 Byte (ESP-NOW v1); 1470 Byte mit v2 ab ESP-IDF 5.4 |
| Registrierte Peers | Höchstens 20; standardmäßig höchstens 7 verschlüsselte |
| Standard-Datenrate | 1 Mbit/s |
| Zustellbestätigung | Bei Unicast ja (ACK), bei Broadcast nein |
| Verschlüsselung | Optional, CCMP (AES-128); PMK- und LMK-Schlüssel mit je 16 Byte |
| Router / Access Point | Nicht nötig |
| Unterstützte Chips | ESP32-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?
| Methode | Zusatzmodul | Router | Paketgröße | Typische Reichweite (Freifeld) | Am besten für |
|---|---|---|---|---|---|
| ESP-NOW | Nicht nötig | Nicht nötig | 250 Byte | 100–200 m, im LR-Modus mehr | Befehle und Sensordaten zwischen Boards |
| WLAN (HTTP, MQTT) | Nicht nötig | Nötig | Praktisch unbegrenzt | Abdeckung des Routers | Internet, Webseiten, Cloud |
| Bluetooth (BLE) | Nicht nötig | Nicht nötig | Standardmäßig 20 Byte | Innen 10–20 m | Verbindung mit dem Handy |
| nRF24L01 | Nötig (SPI) | Nicht nötig | 32 Byte | Einige zehn Meter, mit PA+LNA deutlich mehr | Boards ohne Funk wie Arduino Uno und Nano |
| LoRa (SX1278) | Nötig (SPI) | Nicht nötig | Bis 255 Byte | Mehrere Kilometer | Sehr 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 / Chip | ESP-NOW | Hinweis |
|---|---|---|
| ESP32 (WROOM-32, DevKit) | Ja | Unterstützt auch den Long-Range-Modus (LR) |
| ESP32-S2, S3, C3, C6 | Ja | Gleicher Arduino-Code, nur die Boardauswahl ändert sich |
| ESP32-C2 (ESP8684) | Ja | Kein LR-Modus |
| ESP32-H2 | Nein | Kein WLAN-Funk, daher kein ESP-NOW |
| ESP8266 (NodeMCU, Wemos D1 Mini) | Ja | Andere Bibliothek (espnow.h), nur v1 und 250 Byte |
| Raspberry Pi Pico W / Pico 2 W | Nein | Der WLAN-Chip (CYW43439) kann kein ESP-NOW |
| Arduino Uno, Nano, Mega | Nein | Lä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.
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}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.
| Typ | Größe | Hinweis |
|---|---|---|
| bool, uint8_t, int8_t, char | 1 Byte | Flag, kleine Zahl, einzelnes Zeichen |
| int16_t, uint16_t | 2 Byte | −32768…32767 oder 0…65535 |
| int, int32_t, uint32_t | 4 Byte | int hat auf ESP32 und ESP8266 4 Byte, auf dem Arduino Uno 2 Byte |
| float | 4 Byte | Etwa 7 Stellen Genauigkeit |
| double | 8 Byte | Nimm float, wenn du double nicht wirklich brauchst |
| char mesaj[16] | 16 Byte | Text fester Länge; muss mit '\0' enden |
| String, char*, Zeiger | Nicht sendbar | Es 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
- 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.
- Trage die MAC-Adresse, die du vom Empfänger ausgelesen hast, im Code unten in das Array aliciMac ein.
- 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.
- 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.
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
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.
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:
| Bauteil | ESP32-Pin | Hinweis |
|---|---|---|
| Taster | GPIO 4 und GND | Nutzt den internen Pull-up, kein zusätzlicher Widerstand nötig |
| LED | GPIO 2 | Bei den meisten DevKits die blaue Onboard-LED; für eine externe LED ein 220-Ω-Widerstand |
| Stromversorgung | USB | Die Boards können an getrennten USB-Ports oder Netzteilen hängen |
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:
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
| Kennwert | Typischer Wert | Hinweis |
|---|---|---|
| Reichweite in Innenräumen | 20–40 m (1–3 Wände) | Stahlbetonwände und Metallgegenstände dämpfen das Signal am stärksten |
| Reichweite im Freien | 100–200 m | Mit der PCB-Antenne auf dem Board; steigt, wenn die Boards höher über dem Boden stehen |
| LR-Modus (Long Range) | Bis 1 km | Sichtlinienwert von Espressif; die Rate sinkt auf 0,5 oder 0,25 Mbit/s |
| Standard-Datenrate | 1 Mbit/s | Höhere Rate heißt geringere Reichweite |
| Sendedauer eines 250-Byte-Pakets | Etwa 2,5 ms | Mit 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 |
| Befehlslatenz | Wenige Millisekunden | Kein 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.
| Thema | Core 2.x | Core 3.0–3.2 | Core 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 |
| Absenderadresse | mac | bilgi->src_addr | bilgi->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 Paket | 250 Byte | 250 Byte; ab 3.2 mit v2 1470 Byte | 1470 Byte mit v2 |
| Kanal einstellen | esp_wifi_set_channel() | Zusätzlich WiFi.setChannel() | Wie 3.0 |
| Fertige Klassenbibliothek | Keine | ESP32_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:
- 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.
- 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.
- 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.
- 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.
- 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:
1#include <WiFi.h>
2#include <WebServer.h>
3#include <esp_now.h>
4#include <esp_idf_version.h>
5
6const char *ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char *sifre = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9// Sensör kodundakiyle birebir aynı / identical to the sensor sketch
10typedef struct __attribute__((packed)) {
11 uint8_t id; // sensör numarası 1-5 / sensor number 1-5
12 uint32_t sayac;
13 float sicaklik;
14 float nem;
15 float pil; // pil gerilimi (V) / battery voltage (V)
16} SensorPaketi;
17
18const int SENSOR_SAYISI = 5;
19SensorPaketi sonVeri[SENSOR_SAYISI + 1]; // 1..5 kullanılır / indexes 1..5 are used
20unsigned long sonZaman[SENSOR_SAYISI + 1]; // son paketin zamanı / time of the last packet
21QueueHandle_t kuyruk;
22WebServer server(80);
23
24#if ESP_IDF_VERSION_MAJOR >= 5
25void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) { // çekirdek 3.x / core 3.x
26#else
27void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) { // çekirdek 2.x / core 2.x
28#endif
29 if (uzunluk != sizeof(SensorPaketi)) return;
30 SensorPaketi p;
31 memcpy(&p, veri, sizeof(p));
32 xQueueSend(kuyruk, &p, 0); // kuyruğa at, Wi-Fi görevini bekletme / queue it, never block the Wi-Fi task
33}
34
35void anaSayfa() {
36 String html = "<meta name='viewport' content='width=device-width'>"
37 "<meta http-equiv='refresh' content='10'>"
38 "<h2>ESP-NOW sensorleri</h2><table border=1 cellpadding=6>"
39 "<tr><th>ID</th><th>Sicaklik</th><th>Nem</th><th>Pil</th><th>Son paket</th></tr>";
40 for (int i = 1; i <= SENSOR_SAYISI; i++) {
41 if (sonZaman[i] == 0) continue; // bu sensörden veri yok / no data from this sensor yet
42 html += "<tr><td>" + String(i) + "</td><td>" + String(sonVeri[i].sicaklik, 1) +
43 " °C</td><td>%" + String(sonVeri[i].nem, 0) + "</td><td>" + String(sonVeri[i].pil, 2) +
44 " V</td><td>" + String((millis() - sonZaman[i]) / 1000) + " sn once</td></tr>";
45 }
46 html += "</table>";
47 server.send(200, "text/html", html);
48}
49
50void setup() {
51 Serial.begin(115200);
52 kuyruk = xQueueCreate(10, sizeof(SensorPaketi));
53
54 WiFi.mode(WIFI_STA);
55 WiFi.begin(ssid, sifre);
56 while (WiFi.status() != WL_CONNECTED) delay(500);
57 WiFi.setSleep(false); // güç tasarrufu kapalı: paket kaçmaz / power save off: no missed packets
58
59 Serial.print("IP: ");
60 Serial.println(WiFi.localIP());
61 Serial.printf("Kanal / channel: %d\n", (int)WiFi.channel());
62 Serial.print("Gecit MAC / gateway MAC: ");
63 Serial.println(WiFi.macAddress()); // sensörlere bu adresi yaz / put this address in the sensors
64
65 if (esp_now_init() != ESP_OK) {
66 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
67 return;
68 }
69 esp_now_register_recv_cb(veriGeldi);
70
71 server.on("/", anaSayfa);
72 server.begin();
73}
74
75void loop() {
76 SensorPaketi p;
77 while (xQueueReceive(kuyruk, &p, 0) == pdTRUE) {
78 if (p.id >= 1 && p.id <= SENSOR_SAYISI) {
79 sonVeri[p.id] = p;
80 sonZaman[p.id] = millis();
81 Serial.printf("Sensor %u: %.1f C %%%.0f %.2f V\n", (unsigned)p.id, p.sicaklik, p.nem, p.pil);
82 }
83 }
84 server.handleClient();
85}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.




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

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:
| Aufgabe | ESP32 (esp_now.h) | ESP8266 (espnow.h) |
|---|---|---|
| Initialisieren | esp_now_init() == ESP_OK | esp_now_init() == 0 |
| Rolle festlegen | Nicht nötig | esp_now_set_self_role(ESP_NOW_ROLE_COMBO) |
| Peer hinzufügen | esp_now_add_peer(&es) über ein struct | esp_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 Paket | 250 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:
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 / Fehler | Ursache | Lösung |
|---|---|---|
| "ESPNOW: Peer interface is invalid", Peer lässt sich nicht eintragen | esp_now_peer_info_t wurde nicht geleert | Struct 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 Kanal | STA-MAC des Empfängers neu auslesen und die Kanäle beider Boards prüfen |
| Pakete kommen an, die Werte sind aber Unsinn | Das struct ist auf beiden Seiten verschieden oder enthält einen String | Struct 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 kompiliert | Empfangs-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+ kompiliert | Sende-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 ab | Peer mit channel = 0 eintragen und die Sensoren auf den Router-Kanal bringen |
| Das mit dem WLAN verbundene Gateway verpasst einige Pakete | WLAN-Energiesparen (Modem-Sleep) | WiFi.setSleep(false) verwenden |
| ESP_ERR_ESPNOW_NOT_INIT | esp_now_init() nicht aufgerufen oder vor dem WLAN-Start aufgerufen | Zuerst WiFi.mode(WIFI_STA), dann esp_now_init() |
| ESP_ERR_ESPNOW_FULL | Die Peer-Liste hat 20 Einträge erreicht | Alte Peers mit esp_now_del_peer() entfernen oder Broadcast nutzen |
| ESP_ERR_ESPNOW_NO_MEM | Pakete werden schneller gesendet, als sich die Sendewarteschlange leert | Vor dem nächsten Paket auf das Sendeergebnis warten |
| MAC-Adresse lautet 00:00:00:00:00:00 | Unter Core 3.x wurde WiFi.macAddress() vor dem vollständigen WLAN-Start aufgerufen | Nach WiFi.mode(WIFI_STA) esp_wifi_get_mac() verwenden |
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.
Projekte, die du danach bauen kannst
Alle Projekte ansehen
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.

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.

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

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.

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.

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

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

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
Weitere Artikel
BlueGrays-App
Diese Schaltung Schritt für Schritt auf dem Handy nachbauen
Schaltplan, Code und Teileliste auf einem Bildschirm. Über 200 Arduino-, ESP32- und Pico-Projekte zum Nachbauen.
Mit dem Handy scannen
Richte die Kamera auf den Code, dann öffnet sich der App-Store deines Handys.
oder im Store öffnen

