Rehberler · 17 dk okuma

ESP-NOW Nedir? ESP32 ile Modemsiz Kablosuz Haberleşme Rehberi

ESP-NOW ile ESP32'ler arasında modemsiz haberleşme: MAC adresi, tek ve çift yönlü kod, struct ile 250 bayt veri, 20 eş, menzil, core 3.x ve Wi-Fi kanal uyumu.

ESP-NOW, Espressif'in geliştirdiği ve ESP32 ile ESP8266 kartlarının modem, router ya da internet olmadan birbirine doğrudan veri göndermesini sağlayan kablosuz bir haberleşme protokolüdür. Kartlar Wi-Fi radyosunu kullanır ama hiçbir ağa bağlanmaz; her cihaz karşı tarafı MAC adresiyle tanır ve en fazla 250 baytlık küçük paketleri birkaç milisaniyede iletir. Bağlantı kurma, IP adresi alma ya da şifre girme aşaması olmadığı için bir karttaki butona basıldığı anda başka bir kartta röle çekebilir, pille çalışan bir sensörü uyandırıp ölçümünü gönderdikten hemen sonra yeniden uyutabilirsin.

Bu rehberde ESP-NOW'u sıfırdan kuracaksın: önce kartlarının MAC adresini öğrenecek, sonra tek yönlü ve çift yönlü örneklerle sıcaklık, sayaç ve metin gibi farklı verileri bir struct (yapı) içinde tek pakette göndereceksin. Ardından 20 eş sınırını, yayın (broadcast) mesajlarını, menzil ve hız değerlerini, ESP32 Arduino çekirdeği 3.x ile değişen fonksiyonları ve ESP-NOW ile Wi-Fi'yi aynı anda kullanmanın tek kuralı olan kanal uyumunu öğreneceksin. Son bölümde derin uykuda pille çalışan uzak bir sensörün ölçümlerini Wi-Fi'ye bağlı bir ağ geçidine gönderip telefondan izleyeceksin. ESP32'yi ilk kez kullanıyorsan önce ESP32 Nedir? yazısındaki Arduino IDE kurulumunu tamamla.

ESP-NOW ile haberleşen ESP32 geliştirme kartı
ESP-NOW için ek modül gerekmez: ESP32'nin içindeki Wi-Fi radyosu, kartların modem olmadan doğrudan konuşmasını sağlar.

ESP-NOW nedir, nasıl çalışır?

Normal Wi-Fi'de ESP32 önce modeme bağlanır, şifreyi doğrular, DHCP ile IP adresi alır ve ancak bundan sonra veri gönderebilir; bu süreç genellikle 1–3 saniye sürer. ESP-NOW bu adımların hepsini atlar. Veriyi IEEE 802.11 standardındaki üreticiye özel eylem çerçevesi (vendor-specific action frame) denen kısa bir Wi-Fi paketinin içine koyar ve doğrudan karşı kartın MAC adresine gönderir. Paketi alan kart, donanım seviyesinde bir onay (ACK) çerçevesiyle cevap verir; gönderen taraftaki geri çağırma (callback) fonksiyonu da paketin teslim edilip edilmediğini sana bildirir. Onay gelmezse Wi-Fi katmanı paketi birkaç kez kendiliğinden yeniden dener. Kısacası ESP-NOW, iki kart arasında görünmez bir posta kutusu gibi çalışır: adresi yazarsın, paketi atarsın, karşı taraf aldığında haberin olur.

ÖzellikDeğer
GeliştirenEspressif Systems
Frekans2,4 GHz (Wi-Fi radyosu, kanal 1–13)
Paket başına veri250 bayt (ESP-NOW v1); ESP-IDF 5.4 ve üzerinde v2 ile 1470 bayt
Kayıtlı eş sayısıEn fazla 20; şifreli eş varsayılan olarak en fazla 7
Varsayılan veri hızı1 Mbps
Teslim onayıTekli gönderimde var (ACK), yayında yok
Şifrelemeİsteğe bağlı, CCMP (AES-128); PMK ve LMK anahtarları 16'şar bayt
Modem / routerGerekmez
Destekleyen yongalarWi-Fi'li ESP32 yongaları (ESP32, S2, S3, C2, C3, C6…) ve ESP8266

ESP-NOW'un asıl avantajı hızlı tepki ve düşük enerjidir. Butona basıldığında karşı taraftaki LED'in yanma gecikmesi birkaç milisaniyedir; aynı işi Wi-Fi ile yapmak için önce bağlantı kurmak, sonra bir sunucuya istek atmak gerekir. Pille çalışan bir sensörde fark daha da büyüktür: Wi-Fi'ye bağlanırken radyo saniyelerce 100 mA'in üzerinde akım çekerken, ESP-NOW ile kart uyanıp ölçümünü gönderir ve toplamda birkaç yüz milisaniye içinde yeniden uyur. Buna karşılık ESP-NOW internete çıkmaz; veriyi internete ya da telefona taşımak istiyorsan ağın bir yerinde Wi-Fi'ye bağlı bir ağ geçidi (gateway) ESP32'si bulunmalıdır. Bu düzeni yazının ilerleyen bölümünde kuracağız.

ESP-NOW mu, Wi-Fi mı, Bluetooth mu, nRF24L01 mi?

YöntemEk modülModemPaket boyutuTipik menzil (açık alan)En uygun kullanım
ESP-NOWGerekmezGerekmez250 bayt100–200 m, LR modunda daha fazlaKartlar arası komut ve sensör verisi
Wi-Fi (HTTP, MQTT)GerekmezGerekirPratikte sınırsızModemin kapsama alanıİnternet, web sayfası, bulut
Bluetooth (BLE)GerekmezGerekmezVarsayılan 20 baytEv içinde 10–20 mTelefonla bağlantı
nRF24L01Gerekir (SPI)Gerekmez32 baytOnlarca metre, PA+LNA sürümüyle çok daha uzunArduino Uno ve Nano gibi kablosuz olmayan kartlar
LoRa (SX1278)Gerekir (SPI)Gerekmez255 bayta kadarBirkaç kilometreÇok uzak mesafe, seyrek veri

Telefonla konuşacaksan Bluetooth, internete çıkacaksan Wi-Fi doğru seçimdir. Ama elinde birden fazla ESP32 varsa ve bunlar yalnızca birbirleriyle konuşacaksa ESP-NOW çoğu zaman en basit yoldur: ek modül, kablo ya da modem gerektirmez ve kodu birkaç satırdır. Arduino Uno gibi kablosuz bağlantısı olmayan kartlar için nRF24L01, kilometrelerce menzil için LoRa modülleri daha uygundur. Bütün seçenekleri yan yana görmek istersen Arduino kablosuz haberleşme seçenekleri yazısına, telefonla bağlantı için de ESP32 Bluetooth rehberine bakabilirsin.

Hangi kartlar ESP-NOW destekler?

Kart / yongaESP-NOWNot
ESP32 (WROOM-32, DevKit)VarUzun menzil (LR) modunu da destekler
ESP32-S2, S3, C3, C6VarArduino kodu aynıdır, yalnız kart seçimi değişir
ESP32-C2 (ESP8684)VarLR modu yok
ESP32-H2YokWi-Fi radyosu olmadığı için desteklemez
ESP8266 (NodeMCU, Wemos D1 Mini)VarFarklı kütüphane (espnow.h), yalnız v1 ve 250 bayt
Raspberry Pi Pico W / Pico 2 WYokWi-Fi yongası (CYW43439) ESP-NOW çalıştırmaz
Arduino Uno, Nano, MegaYokSeri port (UART) üzerinden bir ESP32'ye bağlanabilir

ESP-NOW farklı ESP türleri arasında da çalışır: bir ESP32-C3 sensörü, klasik bir ESP32 ağ geçidine ya da bir ESP8266 kartına paket gönderebilir. Tek şart, iki tarafın aynı Wi-Fi kanalında olması ve gönderilen veri yapısının iki tarafta da birebir aynı tanımlanmasıdır. Aşağıdaki örneklerde iki ESP32 DevKit kartı kullanıyoruz; kartının güvenle kullanılabilecek pinlerini Pinout aracında görebilirsin.

Adım 1: ESP32'nin MAC adresini öğrenmek

ESP-NOW'da her kart karşı tarafı 6 baytlık MAC adresiyle bulur; bu adres yonga üretilirken her ESP32'ye ayrı olarak yazılır ve değişmez. Aşağıdaki kodu alıcı olarak kullanacağın karta yükle ve Seri Monitör'ü 115200 baud hızında aç. Kod adresi hem okunabilir biçimde hem de diğer kartın koduna doğrudan yapıştırabileceğin bir dizi satırı olarak yazar. İki kartlı örneklerde bu işlemi iki kart için de yapıp adresleri bir kâğıda not et.

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}
ESP32'nin iki ayrı Wi-Fi MAC adresi vardır: istasyon (STA) ve erişim noktası (AP). AP adresi genellikle STA adresinin son baytının bir fazlasıdır. Bu yazıdaki örneklerin hepsi kartları WIFI_STA modunda çalıştırdığı için karşı karta her zaman STA adresini yazmalısın; yanlış arayüzün adresini yazmak "Teslim edilemedi" mesajının en sık sebebidir. ESP32 Arduino çekirdeği 3.x'te WiFi.macAddress() Wi-Fi tam başlamadan çağrılırsa 00:00:00:00:00:00 dönebilir; yukarıdaki koddaki esp_wifi_get_mac() bu sorunu yaşamaz.

struct ile veri göndermek ve 250 bayt sınırı

ESP-NOW yalnızca bir bayt dizisi gönderir, içinde ne olduğunu bilmez. Birkaç farklı değeri tek seferde göndermenin en temiz yolu, onları bir struct (yapı) içinde toplamak ve yapının bellekteki baytlarını olduğu gibi göndermektir. Alıcı aynı yapıyı tanımlar ve gelen baytları memcpy ile bu yapının üzerine kopyalar. Bunun çalışması için yapının iki tarafta aynı sırada, aynı türlerde ve aynı boyutta olması gerekir. Derleyici hızlı erişim için alanların arasına görünmez dolgu baytları ekleyebilir: aşağıdaki örneklerdeki Paket yapısı normalde 36 bayt yer kaplar, __attribute__((packed)) eklendiğinde ise tam 30 bayta iner. packed kullanmak, ESP32 ile ESP8266 arasında ya da Arduino ile MicroPython arasında her baytın aynı yerde durmasını garanti eder.

TürBoyutNot
bool, uint8_t, int8_t, char1 baytBayrak, küçük sayı, tek karakter
int16_t, uint16_t2 bayt−32768…32767 ya da 0…65535
int, int32_t, uint32_t4 baytESP32 ve ESP8266'da int 4 bayttır, Arduino Uno'da 2 bayt
float4 baytYaklaşık 7 basamak hassasiyet
double8 baytGerekmedikçe float kullan
char mesaj[16]16 baytSabit uzunluklu metin; sonunda '\0' bulunmalı
String, char*, işaretçiGönderilmezYalnız bellek adresi gider; karşı tarafta anlamsızdır

Bir pakete en fazla 250 bayt sığar. Bu sınır ESP-NOW'un ilk sürümünden (v1) gelir ve ESP8266 dâhil bütün kartlarda geçerlidir. ESP-IDF 5.4 tabanlı Arduino çekirdeği 3.2 ve sonrası ESP-NOW v2 ile 1470 bayta kadar paket gönderebilir; ancak v1 kullanan bir kart (ESP8266 ya da eski çekirdekli bir ESP32) 250 bayttan uzun paketi keser ya da hiç almaz. Bu yüzden karışık bir ağda 250 baytta kalmak en güvenlisidir. Verici kodundaki static_assert satırı, yapı 250 baytı geçerse derlemeyi anlaşılır bir mesajla durdurur. Daha büyük bir veriyi göndermen gerekiyorsa onu numaralı parçalara bölüp alıcıda sırayla birleştirmelisin; ama sensör verisi, buton durumu ya da komut için birkaç düzine bayt fazlasıyla yeter.

Tek yönlü ESP-NOW: verici kodu

  1. İki ESP32'nin de Arduino IDE'de ESP32 Dev Module olarak seçildiğinden emin ol. ESP-NOW kütüphanesi kart paketinin içindedir, ayrıca bir şey kurman gerekmez.
  2. Alıcı karttan okuduğun MAC adresini aşağıdaki kodda aliciMac dizisine yaz.
  3. Vericideki GPIO 4 ile GND arasına bir buton bağla; INPUT_PULLUP kullandığımız için ek dirence gerek yok. Ayrıntılar için Arduino'ya buton nasıl bağlanır? yazısına bakabilirsin.
  4. Kodu vericiye yükle ve Seri Monitör'ü 115200 baud hızında aç. Alıcı henüz çalışmıyorken her iki saniyede bir "Teslim edilemedi" yazısını görmen normaldir.
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}

Kod önce Wi-Fi'yi WIFI_STA modunda başlatır; ESP-NOW Wi-Fi radyosu olmadan çalışmaz, bu yüzden esp_now_init() her zaman WiFi.mode() satırından sonra gelmelidir. Ardından alıcı bir eş (peer) olarak listeye eklenir. esp_now_peer_info_t yapısını = {} ile sıfırlamak önemlidir; sıfırlanmayan alanlarda kalan rastgele değerler "Peer interface is invalid" hatasına yol açar. channel = 0, "kartın şu anki kanalını kullan" demektir. esp_now_send() paketi kuyruğa koyar ve hemen döner; ESP_OK dönmesi yalnızca paketin gönderilmek üzere kabul edildiğini gösterir. Asıl sonuç birkaç milisaniye sonra gonderimSonucu() fonksiyonuna gelir: ESP_NOW_SEND_SUCCESS, alıcının paketi donanım seviyesinde onayladığı anlamına gelir. Kodun başındaki #if bloğu, gönderim fonksiyonunun çekirdek sürümüne göre değişen biçimini kendisi seçer; bunu çekirdek 3.x bölümünde açıklıyoruz.

ESP-NOW alıcı kodu

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}

Alıcıda paketler veriGeldi() fonksiyonuna gelir. Bu fonksiyon senin loop() döngünde değil, Wi-Fi'nin yüksek öncelikli görevinde çalışır; bu yüzden içinde Serial.print, delay ya da uzun hesaplamalar yapmamalısın. Doğru yöntem, önce gelen paketin boyutunun beklenen yapıyla aynı olup olmadığını kontrol etmek, veriyi memcpy ile kendi değişkenine kopyalamak ve volatile bir bayrakla loop()'a haber vermektir. Çekirdek 3.x'te fonksiyona gelen bilgi parametresi paketin kimden geldiğini (bilgi->src_addr) ve sinyal gücünü (bilgi->rx_ctrl->rssi, dBm cinsinden) de taşır; bu bilgi yalnızca fonksiyonun içinde geçerlidir, sonradan kullanacaksan kopyalamalısın. Alıcının vericiyi eş listesine eklemesine gerek yoktur: şifresiz tekli paketler ve yayınlar listede olmayan kartlardan da kabul edilir.

"Teslim edildi" mesajı paketin alıcının Wi-Fi donanımına ulaştığını gösterir, alıcıdaki programın onu işlediğini değil. Karşı kart açık ve aynı kanaldaysa ESP-NOW'u hiç başlatmamış olsa bile onay döner. Önemli komutlarda yazılım seviyesinde ayrı bir onay mesajı kullan ve paketlere bir sayaç ekle: alıcı sayacın atladığını görürse kaç paketin kaybolduğunu anlar, aynı numara iki kez gelirse tekrarı yok sayar.

Çift yönlü haberleşme: iki ESP32 birbirini kontrol etsin

Çift yönlü haberleşmede her kart hem verici hem alıcıdır. Aşağıdaki kodu iki karta da yükleyeceksin; tek fark, her kartta karsiMac dizisine diğer kartın adresini yazmaktır. Bir karttaki butona bastığında diğer kartın LED'i yanar ya da söner; LED'i değiştiren kart da işi yaptığını bir onay mesajıyla geri bildirir. Bağlantılar iki kartta da aynıdır:

ElemanESP32 piniNot
ButonGPIO 4 ve GNDDahili pull-up kullanılır, ek direnç gerekmez
LEDGPIO 2Çoğu DevKit'te dahili mavi LED; harici LED için 220 Ω direnç
BeslemeUSBKartlar ayrı USB portlarından ya da adaptörlerden beslenebilir
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}

Mesaj yapısındaki tur alanı paketin ne olduğunu söyler: 1 komut, 2 onaydır. Butona basan kart, karşı tarafın LED'i için istediği durumu gönderir; komutu alan kart LED'i ayarlar ve aynı durumu tür 2 ile geri yollar. Böylece gönderen taraf hem donanım onayını hem de "LED gerçekten değişti" bilgisini alır. Gelen komut yine volatile bayraklarla loop()'a taşınır; digitalWrite ve cevap gönderme işi orada yapılır. Callback içinden esp_now_send() çağırmak yerine bu düzen daha güvenlidir. Butondaki 30 ms'lik bekleme, kontakların sıçramasını (bounce) filtreler. LED yerine bir röle bağlarsan aynı kodla kablosuz bir lamba anahtarı yapmış olursun; röle bağlantısı için Röle Modülü Nedir? yazısına bakabilirsin.

Birden çok cihaz: 20 eş sınırı ve yayın (broadcast)

ESP-NOW üç düzenin hepsini destekler: bir vericiden birçok alıcıya (bir kumandadan beş röle kartına), birçok vericiden tek alıcıya (beş sensörden tek ağ geçidine) ve herkesin herkesle konuştuğu ağlar. Bir kartın eş listesinde en fazla 20 kayıt olabilir; şifreli eşlerin sayısı ise varsayılan ayarlarla 7 ile sınırlıdır. Bu sınır yalnızca paket göndereceğin kartlar için geçerlidir. Bir ağ geçidi, kendisine şifresiz paket gönderen sensörleri listeye eklemek zorunda olmadığı için 20'den fazla sensörü dinleyebilir. Liste dolarsa esp_now_add_peer() ESP_ERR_ESPNOW_FULL döner; o zaman esp_now_del_peer() ile işi biten eşleri silip yenilerini ekleyebilirsin. Aşağıdaki kod üç alıcıyı ve yayın adresini listeye ekler, sonra hem tek tek hem de yayınla gönderir:

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}

Yayın adresi FF:FF:FF:FF:FF:FF da gönderimden önce eş olarak eklenmelidir. Yayınla gönderilen tek bir paket aynı kanaldaki bütün ESP-NOW alıcılarına ulaşır; alıcıların MAC adresini bilmen gerekmez, bu yüzden kurulumu en kolay yöntemdir. Bedeli şudur: yayında teslim onayı yoktur, gönderim sonucu her zaman başarılı görünür ve yayın paketleri şifrelenemez. Tek tek gönderimde ise her paket için ayrı onay gelir. esp_now_send() fonksiyonuna adres olarak NULL verirsen paket listedeki bütün eşlere ayrı ayrı gönderilir; listede yayın adresi de varsa paket bir kez de yayın olarak çıkar. Birçok proje için pratik çözüm, kartların birbirini ilk açılışta yayınla bulması, sonra MAC adreslerini öğrenip tekli ve onaylı gönderime geçmesidir.

ESP-NOW menzili ve hızı

ÖlçütTipik değerNot
Ev içinde menzil20–40 m (1–3 duvar)Betonarme duvarlar ve metal eşyalar sinyali en çok zayıflatır
Açık alanda menzil100–200 mKart üstü PCB antenle; kartlar yerden yükseğe konunca artar
LR (Long Range) modu1 km'ye kadarEspressif'in görüş hattı değeri; hız 0,5 ya da 0,25 Mbps'e düşer
Varsayılan veri hızı1 MbpsDaha yüksek hız, daha kısa menzil demektir
250 baytlık paketin havada kalma süresiYaklaşık 2,5 msOnay ve bekleme süreleriyle paket başına yaklaşık 3 ms
Teorik paket hızıSaniyede ~300 paket (~70 kB/s)250 baytlık paketlerle; pratikte daha düşüktür
Komut gecikmesiBirkaç milisaniyeBağlantı kurma ve sunucu gerekmez

Bu değerler ortama göre çok değişir, ama hesabı kolaydır. Varsayılan 1 Mbps hızda her bayt havada 8 mikrosaniye kalır; 250 baytlık veri, başlıklar ve senkron dizisiyle birlikte yaklaşık 2,5 ms sürer. Alıcının onayı ve kanalın boşalmasını bekleme süresiyle paket başına yaklaşık 3 ms gerekir; yani saniyede en fazla 300 civarında paket gönderilebilir. Sensör verisi ve komutlar için bu fazlasıyla yeterlidir, ama ESP-NOW ses ya da görüntü aktarmak için uygun değildir. Menzili artırmak için kartları yerden yükseğe yerleştir, antenin üzerini metal kutu, pil ya da kablolarla kapatma, harici antenli modüller (örneğin ESP32-WROOM-32U) kullan ve iki tarafta da esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR) ile Espressif'in uzun menzil (LR) modunu aç. LR yalnızca iki ESP32 arasında çalışır; sadece LR'ye ayarlanan bir arayüz normal modemlere bağlanamaz, ESP8266 ve ESP32-C2 de bu modu desteklemez. Mesafe testi yaparken çekirdek 3.x'te alıcıdaki bilgi->rx_ctrl->rssi değerini yazdır: −50 dBm çok güçlü, −80 dBm sınıra yakın bir sinyaldir; −90 dBm civarında paket kayıpları başlar.

ESP32 Arduino core 3.x ile gelen değişiklikler

İnternetteki ESP-NOW örneklerinin çoğu ESP32 Arduino çekirdeği 2.x için yazıldı. Kart Yöneticisi'nden 3.x sürümünü kurduysan bu kodlar derlenmez, çünkü Espressif ESP-IDF 5 ile geri çağırma fonksiyonlarının biçimini değiştirdi. Değişiklikler iki aşamada geldi: çekirdek 3.0 alma fonksiyonunu, ESP-IDF 5.5 tabanlı çekirdek 3.3 ise gönderim fonksiyonunu değiştirdi. Hangi sürümü kullandığını Araçlar > Kart > Kart Yöneticisi'nde esp32 paketinin yanında görebilirsin.

KonuÇekirdek 2.xÇekirdek 3.0–3.2Çekirdek 3.3 ve üzeri
Alma fonksiyonu(const uint8_t *mac, const uint8_t *veri, int uzunluk)(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)3.0 ile aynı
Gönderenin adresimacbilgi->src_addrbilgi->src_addr
Gönderim fonksiyonu(const uint8_t *mac, esp_now_send_status_t durum)2.x ile aynı(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum)
En büyük paket250 bayt250 bayt; 3.2'de v2 ile 1470 baytv2 ile 1470 bayt
Kanal ayarıesp_wifi_set_channel()Ek olarak WiFi.setChannel()3.0 ile aynı
Hazır sınıf kütüphanesiYokESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial)ESP32_NOW.h, v2 desteğiyle

Eski bir kodu 3.x'te derlediğinde alma fonksiyonu için invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t' hatasını görürsün. Çözüm, fonksiyonun ilk parametresini const esp_now_recv_info_t * yapmak ve gönderenin adresine bilgi->src_addr ile ulaşmaktır. Çekirdek 3.3'e geçince benzer bir hata bu kez esp_now_send_cb_t için çıkar; gönderim fonksiyonunun ilk parametresi const esp_now_send_info_t * olmalıdır ve alıcının adresi bilgi->des_addr içindedir. Bu yazıdaki kodlar ESP_IDF_VERSION makrosuyla doğru biçimi kendileri seçer, bu yüzden 2.x, 3.0–3.2 ve 3.3+ çekirdeklerin hepsinde değiştirmeden derlenir. Çekirdek 3.x ayrıca ESP32_NOW.h adlı sınıf tabanlı bir kütüphane getirdi: ESP_NOW_Peer sınıfından türettiğin bir sınıfta onReceive() ve onSent() fonksiyonlarını yazarsın, ESP_NOW_Serial sınıfı ise ESP-NOW bağlantısını Serial gibi kullanmanı sağlar. Örneklerine Dosya > Örnekler menüsündeki ESP_NOW başlığından ulaşabilirsin. Biz ESP8266 ile ve eski kodlarla uyumlu kaldığı için doğrudan esp_now.h fonksiyonlarını kullanıyoruz.

ESP-NOW ve Wi-Fi aynı anda: kanal uyumu

ESP32'de tek bir 2,4 GHz radyo vardır ve bu radyo aynı anda yalnızca tek bir kanalda çalışabilir. Kart bir modeme bağlandığında kanalı artık sen değil modem belirler; modem 6. kanaldaysa ESP-NOW da 6. kanalda çalışır. Bu yüzden Wi-Fi'ye bağlı bir ağ geçidine paket gönderecek bütün sensörlerin de modemin kanalına geçmesi gerekir. Kanallar uyuşmazsa vericide "Teslim edilemedi" görürsün; eşi sıfırdan farklı bir kanalla eklediysen Seri Monitör'de "ESPNOW: Peer channel is not equal to the home channel, send fail!" hatası da çıkar. Sorunsuz bir kurulum için şu kurallara uy:

  1. Ağ geçidi önce Wi-Fi'ye bağlanır, ESP-NOW'u ondan sonra başlatır ve WiFi.channel() ile modemin kanalını Seri Monitör'e yazar.
  2. Sensörler modeme bağlanmaz; ağ adını tarayarak kanalı bulur (WiFi.scanNetworks() ve WiFi.channel(i)) ya da kanalı kodda sabit olarak kullanır. Bunun için Wi-Fi şifresini bilmeleri gerekmez.
  3. Modemin kanal ayarını "Otomatik" yerine sabit bir kanala (1, 6 ya da 11) çek. Otomatik modda modem kanal değiştirirse sensörler ağ geçidini kaybeder.
  4. Ağ geçidinde WiFi.setSleep(false) ile güç tasarrufunu kapat. Modeme bağlı bir ESP32 radyosunu varsayılan olarak aralıklarla uyutur ve bu sırada gelen ESP-NOW paketlerini kaçırabilir.
  5. Sensörlere ağ geçidinin STA MAC adresini yaz; aşağıdaki ağ geçidi kodu bu adresi açılışta Seri Monitör'e yazar.

Aşağıdaki ağ geçidi kodu ev ağına bağlanır, beş sensöre kadar gelen ölçümleri saklar ve aynı ağa bağlı telefonundan açabileceğin basit bir web sayfasında gösterir. Ağ adını ve şifreni kendi bilgilerinle değiştir, kodu yükledikten sonra Seri Monitör'de görünen IP adresini telefonunun tarayıcısında aç:

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}

Ağ geçidi, alma fonksiyonunda paketi yalnızca bir FreeRTOS kuyruğuna (queue) atar ve hemen çıkar. loop() kuyruğu boşaltıp her sensörün son ölçümünü ve geliş zamanını kaydeder, web sunucusu da bu tabloyu gösterir. Kuyruk, aynı anda birkaç sensörden paket geldiğinde hiçbirinin kaybolmamasını sağlar; Espressif'in "callback içinde uzun iş yapma" önerisinin pratik uygulaması da budur. Sayfa her 10 saniyede bir kendini yeniler ve "Son paket" sütunu bir sensörün susup susmadığını (pili mi bitti, menzil dışında mı) hemen gösterir. Ağ geçidi Wi-Fi'ye hiç bağlanamıyorsa sorun ESP-NOW'da değildir; ESP32 Wi-Fi'ye bağlanmıyor yazısındaki kontrolleri uygula. Verileri bir ev otomasyonu sistemine aktarmak istersen web sayfası yerine MQTT ve Home Assistant kullanabilirsin.

Uzak sensör örneği: DHT22, derin uyku ve ESP-NOW

Şimdi ağ geçidine veri gönderen, pille çalışan sensörü yapalım. Sensör her uyanışta sıcaklık, nem ve pil gerilimini ölçer, tek bir ESP-NOW paketi gönderir, teslim onayını bekler ve 60 saniyeliğine derin uykuya geçer. Sıcaklık ve nem için DHT22 kullanıyoruz; Kütüphane Yöneticisi'nden Adafruit DHT sensor library kütüphanesini kurman gerekir. Sensörün çalışma mantığını DHT11 sıcaklık ve nem sensörü yazısında bulabilirsin; bağlantı DHT22 için de aynıdır, kodda yalnızca sensör türü DHT22 olarak yazılır.

ESP32 DevKit
ESP32 DevKit
DHT22 (AM2302)
DHT22 (AM2302)
18650 Li-Ion pil
18650 Li-Ion pil
TP4056 şarj modülü
TP4056 şarj modülü
BağlantıESP32 piniNot
DHT22 VCC (+)3V33,3–5 V ile çalışır
DHT22 DATAGPIO 4Çıplak sensörde DATA ile 3V3 arasına 10 kΩ direnç
DHT22 GND (−)GNDOrtak toprak
Pil + ucuGPIO 34 (bölücü üzerinden)İki adet 100 kΩ direnç; 4,2 V pil pine 2,1 V olarak gelir
Pil − ucuGNDBölücünün alt direnci de GND'ye bağlanır
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}

Kodun püf noktası RTC_DATA_ATTR ile işaretlenmiş değişkenlerdir. Derin uykuda ESP32'nin ana belleği silinir, ama RTC belleğindeki bu değişkenler korunur. Sensör kanalı yalnızca ilk açılışta taramayla bulur (bu yaklaşık 2 saniye sürer) ve sonraki uyanışlarda doğrudan o kanalı kullanır; paket teslim edilemezse kanal değişkenini sıfırlar ve bir sonraki uyanışta yeniden tarar. Böylece modem kanal değiştirse bile sensör bir döngü sonra kendini toparlar. Pil gerilimi analogReadMilliVolts() ile kalibre edilmiş olarak okunur ve bölücü gerilimi yarıya indirdiği için 2 ile çarpılır; direnç değerlerini Gerilim bölücü aracında kontrol edebilirsin. GPIO 34 bir ADC1 pinidir; Wi-Fi radyosu açıkken ADC2 pinleri analog okuma yapamadığı için bu seçim önemlidir. Uyku modlarının ayrıntıları için ESP32 Deep Sleep yazısına bakabilirsin.

Pil ömrünü kabaca hesaplayalım. Taramasız bir uyanış (açılış, ölçüm ve gönderim) yaklaşık 0,3 saniye sürer ve bu sırada ortalama 100 mA çekilir; bu, uyanış başına yaklaşık 0,008 mAh eder. Dakikada bir uyanışla günde 1440 uyanış olur ve toplam yaklaşık 12 mAh harcanır. Çıplak bir ESP32 modülü uykuda 10 µA civarında akım çeker; DHT22'nin bekleme akımı ve bölücü dirençlerle birlikte uyku tüketimi günde 2 mAh'yi geçmez. Yani günde yaklaşık 14 mAh harcanır ve 2000 mAh'lik bir 18650 pil 4–5 ay dayanır. Kendi değerlerinle hesap yapmak için Pil ömrü hesaplayıcı aracını kullanabilirsin.

Bu hesap çıplak modül ya da düşük tüketimli kartlar içindir. Sıradan bir ESP32 DevKit kartında regülatör, USB-seri yonga ve güç LED'i uykuda bile birkaç mA çeker ve pil ömrünü birkaç haftaya indirir. Uzun ömürlü sensörlerde boşta akımı düşük bir regülatör (örneğin HT7333) kullan, güç LED'ini sök ya da hazır düşük tüketimli kartları tercih et. Ölçüm aralığını 60 saniyeden 5 dakikaya çıkarmak pil ömrünü üç katından fazla uzatır. 18650 pili güvenle şarj etmek için korumalı bir TP4056 modülü kullanabilirsin.

ESP8266 ile ESP-NOW

Wemos D1 Mini ESP8266 kartı
Wemos D1 Mini gibi ESP8266 kartları da ESP-NOW destekler; ancak Arduino kütüphanesi ve fonksiyon biçimleri ESP32'den farklıdır.

ESP8266 kartları ESP32 ile aynı ağda ESP-NOW kullanabilir, ama Arduino'daki kütüphanesi farklıdır: esp_now.h yerine espnow.h, WiFi.h yerine ESP8266WiFi.h eklenir ve fonksiyonlar ESP-IDF'den değil ESP8266 SDK'sından gelir. En önemli farklar şunlardır:

İşlemESP32 (esp_now.h)ESP8266 (espnow.h)
Başlatmaesp_now_init() == ESP_OKesp_now_init() == 0
Rol seçimiGerekmezesp_now_set_self_role(ESP_NOW_ROLE_COMBO)
Eş eklemeesp_now_add_peer(&es) ile yapı üzerindenesp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, kanal, NULL, 0)
Alma fonksiyonu(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk)(uint8_t *mac, uint8_t *veri, uint8_t uzunluk)
Gönderim fonksiyonu(..., esp_now_send_status_t durum)(uint8_t *mac, uint8_t durum); 0 başarılı demektir
En büyük paket250 bayt (v2 ile 1470)250 bayt

ESP8266'da WiFi.mode(WIFI_STA) satırından sonra WiFi.disconnect() eklemeyi unutma; ESP8266 açılışta son bağlandığı modeme kendiliğinden bağlanmaya çalışır, bu sırada kanal değişir ve paketler kaybolabilir. Veri yapısını iki tarafta da packed olarak tanımlarsan ESP32 ile ESP8266 aynı baytları aynı şekilde okur, çünkü ikisi de 32 bit ve küçük bayt önce (little-endian) çalışan işlemcilerdir. ESP8266'nın pinleri ve kurulumu için ESP8266 NodeMCU yazısına bakabilirsin.

MicroPython ile ESP-NOW ve Raspberry Pi Pico

ESP32'yi MicroPython ile programlıyorsan ESP-NOW için hazır bir espnow modülü vardır; ESP32 ve ESP8266 için hazırlanan MicroPython sürümlerine dâhildir. Aşağıdaki kod, Arduino'daki verici örneğinin gönderdiği 30 baytlık Paket yapısını alır ve struct.unpack() ile çözer. Biçim dizesindeki < işareti dolgu baytı olmadan, küçük bayt önce okumayı; B 1 baytlık sayıyı, I 4 baytlık işaretsiz sayıyı, f float'u, 16s ise 16 baytlık metni temsil eder. Arduino'daki bool alan da B ile 0 ya da 1 olarak okunur. Kodu Thonny ile karta yükle ve ekrana yazdığı MAC adresini Arduino vericisindeki aliciMac dizisine yaz:

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)

MicroPython'dan paket göndermek de iki satırdır: e.add_peer(mac) ile alıcıyı ekler, e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")) ile aynı yapıyı gönderirsin; böylece Arduino ve MicroPython kartlar aynı ağda birlikte çalışır. Wi-Fi'ye bağlı bir MicroPython kartında paket kaçırmamak için sta.config(pm=sta.PM_NONE) ile güç tasarrufunu kapat. MicroPython'a yeniysen MicroPython temelleri yazısından başlayabilirsin. Raspberry Pi Pico ailesinde ise ESP-NOW yoktur: standart Pico'da kablosuz bağlantı bulunmaz, Pico W ve Pico 2 W'deki CYW43439 yongası ise yalnızca standart Wi-Fi ve Bluetooth çalıştırır. Pico'yu bir ESP-NOW ağına katmak istiyorsan küçük bir ESP32-C3'ü UART seri haberleşme ile Pico'ya bağlayıp köprü olarak kullanabilir ya da Pico W'yi Wi-Fi üzerinden ağ geçidiyle konuşturabilirsin; Pico W'nin Wi-Fi kullanımı Pico W ile Wi-Fi ve web sunucu yazısında anlatılıyor. Aynı köprü yöntemi Arduino Uno için de geçerlidir.

ESP-NOW paketlerini şifrelemek

Varsayılan olarak ESP-NOW paketleri şifresizdir; aynı kanaldaki herkes paketleri havada yakalayabilir ve senin yapını taklit eden sahte paketler gönderebilir. Kapı kilidi ya da garaj kapısı gibi projelerde şifreleme kullan. ESP-NOW, WPA2'deki gibi CCMP yöntemini kullanır ve iki anahtarla çalışır: bütün cihazlarda aynı olan 16 baytlık PMK (esp_now_set_pmk() ile ayarlanır) ve her eş için ayrı 16 baytlık LMK. Eşi eklerken es.encrypt = true yapar ve anahtarı es.lmk alanına kopyalarsın; karşı taraf da seni aynı LMK ile şifreli eş olarak eklemelidir. Şifreli paketleri alabilmek için alıcının vericiyi listeye eklemesi zorunludur. Yayın paketleri şifrelenemez ve şifreli eş sayısı varsayılan ayarlarla 7 ile sınırlıdır. Şifreleme, yakalanan bir paketin sonradan tekrar gönderilmesini tek başına engellemez; kritik komutlara artan bir sayaç ekleyip alıcıda eski numaraları reddetmek iyi bir ek önlemdir.

ESP-NOW proje fikirleri

  • Kablosuz kapı zili: Kapıdaki ESP32 butona basılınca evin içindeki kartta buzzer çalsın; pil ömrü için butonu derin uykudan uyandıran pin olarak kullan.
  • Çok odalı sıcaklık takibi: Her odaya bir DHT22 ya da BME280 sensörü koy, ölçümleri tek ağ geçidinde topla ve web sayfasında karşılaştır.
  • Bahçe sulama: Toprak nemini ölçen sensör, pompanın yanındaki karta "sula" komutu göndersin; sensör bağlantısı için Toprak Nem Sensörü yazısına bak.
  • Kablosuz robot kumandası: Joystick değerlerini saniyede 20 kez gönder; robotun mekanik kısmı için Arduino Robot Araba Yapımı yazısına göz at.
  • Bahçe alarmı: Bahçedeki PIR sensörü hareket algılayınca ağ geçidi Telegram üzerinden bildirim göndersin.
  • Eş zamanlı ışık gösterisi: Tek bir yayın paketiyle birden fazla kartın WS2812B şeritlerini aynı anda aynı renge geçir.

ESP-NOW sık hataları ve çözümleri

Belirti / hataNedeniÇözüm
"ESPNOW: Peer interface is invalid", eş eklenemiyoresp_now_peer_info_t sıfırlanmadan kullanıldıYapıyı = {} ya da memset ile sıfırla, ifidx = WIFI_IF_STA yaz
Sürekli "Teslim edilemedi"Yanlış MAC (AP adresi ya da yazım hatası), alıcı kapalı ya da başka kanaldaAlıcının STA MAC adresini yeniden oku, iki kartın kanalını kontrol et
Paket geliyor ama değerler anlamsızİki taraftaki struct farklı ya da içinde String varYapıyı iki tarafta aynı sırayla ve packed tanımla, metin için char dizisi kullan
"invalid conversion ... to 'esp_now_recv_cb_t'"Çekirdek 2.x için yazılmış kod 3.x'te derleniyorAlma fonksiyonunu const esp_now_recv_info_t * ile yaz
"invalid conversion ... to 'esp_now_send_cb_t'"Çekirdek 3.0–3.2 için yazılmış kod 3.3+'ta derleniyorGönderim fonksiyonunu const esp_now_send_info_t * ile yaz ya da ESP_IDF_VERSION makrosunu kullan
"Peer channel is not equal to the home channel, send fail!"Wi-Fi'ye bağlı kartın kanalı ile eşin kanalı farklıEşi channel = 0 ile ekle ve sensörleri modemin kanalına geçir
Wi-Fi'ye bağlı ağ geçidi bazı paketleri kaçırıyorWi-Fi güç tasarrufu (modem uykusu)WiFi.setSleep(false) kullan
ESP_ERR_ESPNOW_NOT_INITesp_now_init() çağrılmadı ya da Wi-Fi başlatılmadan çağrıldıÖnce WiFi.mode(WIFI_STA), sonra esp_now_init()
ESP_ERR_ESPNOW_FULLEş listesi 20 kayda ulaştıesp_now_del_peer() ile eski eşleri sil ya da yayın kullan
ESP_ERR_ESPNOW_NO_MEMPaketler, gönderim kuyruğu boşalmadan art arda gönderiliyorBir sonraki paketten önce gönderim sonucunu bekle
MAC adresi 00:00:00:00:00:00 görünüyorÇekirdek 3.x'te Wi-Fi tam başlamadan WiFi.macAddress() çağrıldıWiFi.mode(WIFI_STA) sonrasında esp_wifi_get_mac() kullan
Sorun ararken en basit adımdan başla: iki kartı yan yana koy ve önce yayın adresine gönder. Yayın paketi geliyorsa radyo, kanal ve veri yapısı doğrudur; sorun MAC adresindedir. Hiçbir şey gelmiyorsa Araçlar > Core Debug Level menüsünden Debug seç; çekirdeğin ayrıntılı kayıtları Seri Monitör'de görünür. Kart sık sık yeniden başlıyorsa sorun büyük ihtimalle güç kaynağındadır; radyo açılırken çekilen ani akımla ilgili çözümler ESP32 Brownout ve Guru Meditation yazısında.

ESP-NOW, birden fazla ESP32'yi modem, kablo ya da ek modül olmadan konuşturmanın en hızlı yoludur: MAC adresini öğren, veri yapını iki tarafta aynı tanımla, eşi ekle ve gönder. Wi-Fi ile birlikte kullanırken tek kuralı unutma: herkes aynı kanalda olmalı. BlueGrays uygulamasında ESP32 ile LED ve buton kontrolü, DHT22 ile sıcaklık ölçümü, Wi-Fi bağlantısı, web sunucusu ve derin uyku gibi bu yazıdaki parçaların hepsini adım adım devre şeması ve hazır koduyla deneyebilirsin.

Sıkça sorulan sorular

ESP-NOW için modem ya da internet gerekir mi?

Hayır. ESP-NOW kartların Wi-Fi radyosunu kullanır ama hiçbir ağa bağlanmaz; kartlar birbirine MAC adresleriyle doğrudan paket gönderir. İnternete veri taşımak istersen ağdaki bir ESP32'yi hem Wi-Fi'ye hem ESP-NOW'a bağlı bir ağ geçidi olarak kullanman gerekir.

ESP-NOW menzili ne kadar?

Kart üstü antenli ESP32 kartlarla ev içinde duvarlara bağlı olarak genellikle 20–40 metre, açık alanda 100–200 metre civarında menzil elde edilir. Harici antenli modüllerle ve iki tarafta da açılan LR (Long Range) moduyla Espressif'e göre görüş hattında 1 kilometreye kadar ulaşılabilir.

ESP-NOW ile kaç cihaz bağlanabilir?

Bir kartın eş listesine en fazla 20 cihaz eklenebilir; bunların şifreli olanları varsayılan ayarlarla en fazla 7 tanedir. Bu sınır paket gönderilecek cihazlar içindir: bir ağ geçidi şifresiz paket gönderen 20'den fazla sensörü dinleyebilir, yayın (broadcast) ile de aynı kanaldaki bütün cihazlara tek paketle ulaşılır.

ESP-NOW ve Wi-Fi aynı anda kullanılabilir mi?

Evet, ama ESP32'nin tek radyosu aynı anda tek kanalda çalıştığı için bütün ESP-NOW cihazları modemin kanalını kullanmalıdır. Ağ geçidinde WiFi.setSleep(false) ile güç tasarrufunu kapat, sensörlerde de kanalı ağ adını tarayarak bul ya da modemi sabit bir kanala ayarla.

ESP32 ile ESP8266 ESP-NOW ile haberleşebilir mi?

Evet. İki yonga da ESP-NOW destekler ve birbirine paket gönderebilir; ESP8266'da yalnızca kütüphane (espnow.h) ve fonksiyon biçimleri farklıdır. Veri yapısını iki tarafta packed olarak aynı tanımla ve paketleri 250 baytın altında tut.

ESP-NOW ile en fazla kaç bayt gönderilebilir?

ESP-NOW v1 ile bir pakete en fazla 250 bayt sığar ve bu sınır ESP8266 dâhil bütün kartlarda geçerlidir. ESP-IDF 5.4 ve üzerini kullanan kartlar (Arduino çekirdeği 3.2+) ESP-NOW v2 ile 1470 bayta kadar gönderebilir, ancak v1 kullanan kartlar 250 bayttan uzun paketleri alamaz.

Tüm projeleri gör
ESP32 ile LED Yakma
#103Başlangıç

ESP32 ile LED Yakma

ESP32 ile temel GPIO çıkışı kullanımını öğretir. Bir LED'i dijital pinden açıp kapatarak yanıp sönme efekti elde ederiz. Arduino UNO'daki klasik blink projesinin ESP32 versiyonu; Arduino ortamındaki temel blink komutları aynıdır, ancak ESP32'nin pin yerleşimi ve 3,3 V GPIO seviyeleri Arduino UNO'dan farklıdır.

4.7(37)
99
ESP32 Buton ile LED Kontrol (Toggle)
#105Başlangıç

ESP32 Buton ile LED Kontrol (Toggle)

Butona her basıldığında LED'in durumu değişir: açıksa kapanır, kapalıysa açılır. Klasik toggle (geçiş) pattern'i. Debounce (sıçrama önleme) ile butonun mekanik gürültüsünü temizleyerek tek basışta tek değişim sağlarız.

4.6(39)
57
2 Arduino Çift Yönlü Haberleşme
#58Orta
Premium

2 Arduino Çift Yönlü Haberleşme

İki Arduino kabloyla birbirine bağlanıp konuşur: birinin butonuna basınca öbürünün ışığı yanar, yeniden basınca söner. İki kartta da aynı program çalışır, yani ikisi de hem haber gönderir hem haber al

4.5(20)
26
ESP32 DHT22 Sıcaklık ve Nem (LCD Ekranlı)
#117Orta

ESP32 DHT22 Sıcaklık ve Nem (LCD Ekranlı)

DHT22 sensörü ile ortam sıcaklığını ve nem oranını ölçüp 16x2 I2C LCD ekranda gerçek zamanlı olarak gösteren bir projedir. Adafruit DHT kütüphanesi kullanılarak tek kablo (one-wire) protokolü ile sensörden veri okunur. Bu proje, IoT hava istasyonu, akıllı ev otomasyonu veya seralarda ortam takip sistemlerinin temelini oluşturur. ESP32'nin GPIO 4 pini üzerinden DHT22 ile haberleşme sağlanır.

4.8(38)
56
ESP32 Wi-Fi STA Bağlantısı (LCD Ekranlı)
#127İleri

ESP32 Wi-Fi STA Bağlantısı (LCD Ekranlı)

Bu projede ESP32'yi Station (STA) modunda evdeki Wi-Fi ağına güvenli ve hata toleranslı şekilde bağlıyoruz. SSID/şifreyi secrets.h dosyasına ayırarak Git'e sızmasını önlüyor, otomatik yeniden bağlanma mekanizması kuruyor, RSSI sinyal seviyesini ve IP adresini I2C LCD ekranda gösteriyoruz. Dahili LED durum göstergesi olarak çalışır. Üretim seviyesi bir bağlantı yöneticisidir.

4.4(21)
34
ESP32 Async Web Server (LCD'de IP Göstergeli)
#129İleri

ESP32 Async Web Server (LCD'de IP Göstergeli)

Bu projede ESP32'yi aynı Wi-Fi ağındaki telefonunuzdan veya bilgisayarınızdan kontrol edebileceğiniz asenkron bir web sunucusuna dönüştürüyoruz. ESPAsyncWebServer kütüphanesi sayesinde sunucu birden fazla isteği aynı anda işleyebiliyor (klasik WebServer.h gibi tek istekte bloklamıyor). Tarayıcıdan ESP32'nin IP adresine bağlanıp ON/OFF butonlarına basarak (/on ve /off adresleri) GPIO 5'e bağlı LED'i açıp kapatabileceksiniz.

4.8(16)
26
ESP32 Deep Sleep — Pil Tasarrufu (OLED + RTC RAM Sayaç)
#135İleri
Premium

ESP32 Deep Sleep — Pil Tasarrufu (OLED + RTC RAM Sayaç)

ESP32'nin Deep Sleep modu, CPU ve çoğu çevre birimini kapatarak akım tüketimini ~10µA seviyesine düşürür ve pille çalışan IoT cihazları için kritik öneme sahiptir. Bu projede üç farklı uyandırma kaynağını kullanıyoruz: zamanlayıcı (timer, 15sn), harici pin (EXT0 — buton GPIO 33) ve touch (T0/GPIO 4). RTC_DATA_ATTR ile işaretlenen değişkenler deep sleep boyunca korunur — boot sayacı, timer wake sayacı ve buton wake sayacı kalıcı olarak izlenir. SSD1306 OLED ekranda her uyanışta wake reason, say

4.3(10)
16
ESP32 Sıcaklık Loglayıcı (DHT22 + DS1307 RTC + microSD + OLED)
#144İleri
Premium

ESP32 Sıcaklık Loglayıcı (DHT22 + DS1307 RTC + microSD + OLED)

ESP32 ile zamanlı sıcaklık ve nem kayıt sistemi kur! Bu projede DHT22 sensörü ile dakikada bir sıcaklık/nem okuyup DS1307 RTC modülünden aldığın gerçek zaman etiketiyle microSD karta CSV formatında kaydediyoruz. OLED ekranda büyük font ile son sıcaklık değeri, nem yüzdesi, timestamp ve toplam kayıt sayısı canlı gösterilir. SD kart üzerindeki /temp_log.csv dosyası Excel/Google Sheets/Python pandas ile açılıp analiz edilebilir — sera monitörü, ev/oda sıcaklık takibi, laboratuvar veri toplama,

4.7(7)
12