Guias · 24 min de leitura

Tutorial de ESP-NOW: comunicação sem fio entre ESP32 sem roteador

ESP-NOW com o ESP32: descubra o endereço MAC, envie uma struct de até 250 bytes em uma ou duas direções, use até 20 peers, alcance, mudanças do core 3.x e canais Wi-Fi.

O ESP-NOW é um protocolo sem fio desenvolvido pela Espressif que permite que placas ESP32 e ESP8266 enviem dados diretamente umas às outras sem roteador, ponto de acesso ou conexão com a internet. As placas usam o rádio Wi-Fi, mas nunca entram em uma rede; cada dispositivo encontra o outro pelo endereço MAC e entrega pequenos pacotes de até 250 bytes em poucos milissegundos. Como não há conexão para estabelecer, endereço IP para obter nem senha para digitar, apertar um botão em uma placa pode acionar na hora um relé em outra placa, e um sensor alimentado por bateria pode acordar, enviar a leitura e voltar a dormir em seguida.

Neste guia você vai configurar o ESP-NOW do zero: primeiro vai ler os endereços MAC das suas placas e depois usar exemplos unidirecionais e bidirecionais para enviar valores diferentes, como temperatura, um contador e um texto, em um único pacote usando uma struct. Em seguida vai conhecer o limite de 20 peers (pares), as mensagens de broadcast, o alcance e a velocidade, as funções que mudaram com o core 3.x do ESP32 para Arduino e a combinação de canais, a única regra para usar ESP-NOW e Wi-Fi ao mesmo tempo. Na última parte você vai enviar as leituras de um sensor remoto alimentado por bateria, em deep sleep, para um gateway conectado ao Wi-Fi e acompanhá-las pelo celular. Se este é o seu primeiro projeto com ESP32, conclua antes a instalação da Arduino IDE em O que é o ESP32?.

Placa de desenvolvimento ESP32 se comunicando por ESP-NOW
O ESP-NOW não precisa de módulo extra: o rádio Wi-Fi dentro do ESP32 permite que as placas conversem diretamente, sem roteador.

O que é ESP-NOW e como funciona?

No Wi-Fi comum, o ESP32 primeiro se conecta ao roteador, confere a senha, obtém um endereço IP via DHCP e só então consegue enviar dados; isso costuma levar de 1 a 3 segundos. O ESP-NOW pula todas essas etapas. Ele coloca seus dados dentro de um pacote Wi-Fi curto, chamado vendor-specific action frame (quadro de ação específico do fabricante), do padrão IEEE 802.11 e o envia diretamente ao endereço MAC da outra placa. A placa receptora responde com um quadro de confirmação (ACK) em hardware, e uma função de callback no lado que envia informa se o pacote foi entregue. Se nenhum ACK chegar, a camada Wi-Fi tenta reenviar o pacote automaticamente algumas vezes. Resumindo, o ESP-NOW funciona como uma caixa de correio invisível entre duas placas: você escreve o endereço, coloca o pacote e recebe um aviso quando o outro lado o recebe.

CaracterísticaValor
Desenvolvido porEspressif Systems
Frequência2,4 GHz (rádio Wi-Fi, canais 1–13)
Dados por pacote250 bytes (ESP-NOW v1); 1470 bytes com a v2 no ESP-IDF 5.4 ou posterior
Peers registradosAté 20; por padrão, até 7 peers criptografados
Taxa de dados padrão1 Mbps
Confirmação de entregaSim no unicast (ACK), não no broadcast
CriptografiaOpcional, CCMP (AES-128); chaves PMK e LMK de 16 bytes cada
Roteador / ponto de acessoNão é necessário
Chips compatíveisChips ESP32 com Wi-Fi (ESP32, S2, S3, C2, C3, C6…) e ESP8266

As verdadeiras vantagens do ESP-NOW são a resposta rápida e o baixo consumo de energia. Quando você aperta um botão, o LED do outro lado acende em poucos milissegundos; fazer o mesmo por Wi-Fi exige conectar primeiro e depois enviar uma requisição a um servidor. A diferença é ainda maior em um sensor alimentado por bateria: enquanto se conecta ao Wi-Fi, o rádio consome mais de 100 mA durante segundos, ao passo que, com ESP-NOW, a placa acorda, envia a leitura e volta a dormir em poucas centenas de milissegundos no total. Por outro lado, o ESP-NOW não chega à internet; se você quer levar seus dados para a internet ou para o celular, precisa haver em algum ponto da rede um ESP32 gateway conectado ao Wi-Fi. Vamos montar essa estrutura mais adiante neste guia.

ESP-NOW x Wi-Fi x Bluetooth x nRF24L01

MétodoMódulo extraRoteadorTamanho do pacoteAlcance típico (área aberta)Ideal para
ESP-NOWNão é necessárioNão é necessário250 bytes100–200 m, mais no modo LRComandos e dados de sensores entre placas
Wi-Fi (HTTP, MQTT)Não é necessárioNecessárioPraticamente ilimitadoA cobertura do roteadorInternet, páginas web, nuvem
Bluetooth (BLE)Não é necessárioNão é necessário20 bytes por padrão10–20 m em ambiente internoConexão com o celular
nRF24L01Necessário (SPI)Não é necessário32 bytesDezenas de metros, muito mais com a versão PA+LNAPlacas sem comunicação sem fio, como o Arduino Uno e o Nano
LoRa (SX1278)Necessário (SPI)Não é necessárioAté 255 bytesVários quilômetrosDistâncias muito longas, dados pouco frequentes

Se você precisa falar com um celular, o Bluetooth é a escolha certa; se precisa de internet, use Wi-Fi. Mas, se você tem várias placas ESP32 que só precisam conversar entre si, o ESP-NOW costuma ser o caminho mais simples: nenhum módulo extra, cabo ou roteador, e só algumas linhas de código. Para placas sem comunicação sem fio, como o Arduino Uno, o nRF24L01 é mais adequado e, para quilômetros de alcance, os módulos LoRa. Para comparar todas as opções lado a lado, veja Opções de comunicação sem fio para Arduino e, para conectar ao celular, confira nosso guia ESP32 Bluetooth.

Quais placas são compatíveis com ESP-NOW?

Placa / chipESP-NOWObservação
ESP32 (WROOM-32, DevKit)SimTambém suporta o modo Long Range (LR)
ESP32-S2, S3, C3, C6SimMesmo código Arduino, só muda a seleção da placa
ESP32-C2 (ESP8684)SimSem modo LR
ESP32-H2NãoNão tem rádio Wi-Fi, então não tem ESP-NOW
ESP8266 (NodeMCU, Wemos D1 Mini)SimBiblioteca diferente (espnow.h), apenas v1 e 250 bytes
Raspberry Pi Pico W / Pico 2 WNãoO chip Wi-Fi (CYW43439) não consegue rodar ESP-NOW
Arduino Uno, Nano, MegaNãoPode ser ligado a um ESP32 por uma porta serial (UART)

O ESP-NOW também funciona entre chips ESP diferentes: um sensor com ESP32-C3 pode enviar pacotes a um gateway com ESP32 clássico ou a uma placa ESP8266. Os únicos requisitos são que os dois lados estejam no mesmo canal Wi-Fi e que a estrutura de dados seja definida exatamente igual dos dois lados. Os exemplos abaixo usam duas placas ESP32 DevKit; você pode ver quais pinos da sua placa são seguros para uso na ferramenta Pinagem.

Passo 1: descubra o endereço MAC do ESP32

No ESP-NOW, cada placa encontra a outra pelo seu endereço MAC de 6 bytes; esse endereço é gravado em cada ESP32 na fábrica e nunca muda. Carregue o sketch abaixo na placa que você vai usar como receptora e abra o Monitor Serial a 115200 baud. Ele mostra o endereço tanto em formato legível quanto como uma linha de array que você pode colar direto no código da outra placa. Para os exemplos com duas placas, faça isso nas duas e anote os endereços.

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}
O ESP32 tem dois endereços MAC Wi-Fi separados: estação (STA) e ponto de acesso (AP). O endereço AP costuma ser o endereço STA com o último byte acrescido de um. Todos os exemplos deste guia rodam as placas no modo WIFI_STA, então sempre digite o endereço STA na outra placa; usar o endereço da interface errada é o motivo mais comum da mensagem “failed”. No core 3.x do ESP32 para Arduino, WiFi.macAddress() pode retornar 00:00:00:00:00:00 se for chamada antes de o Wi-Fi ter iniciado por completo; a chamada esp_wifi_get_mac() do sketch acima não tem esse problema.

Enviando dados com uma struct e o limite de 250 bytes

O ESP-NOW só envia um array de bytes e não faz ideia do que há dentro. A forma mais limpa de enviar vários valores diferentes de uma vez é agrupá-los em uma struct e enviar os bytes da struct exatamente como estão na memória. O receptor define a mesma struct e copia sobre ela os bytes recebidos com memcpy. Para isso funcionar, a struct precisa ter a mesma ordem, os mesmos tipos e o mesmo tamanho dos dois lados. O compilador pode inserir bytes de preenchimento (padding) invisíveis entre os campos para acelerar o acesso: a struct Paket dos exemplos abaixo normalmente ocupa 36 bytes, mas cai para exatamente 30 bytes quando você adiciona __attribute__((packed)). Usar packed garante que cada byte fique no mesmo lugar entre um ESP32 e um ESP8266, ou entre Arduino e MicroPython.

TipoTamanhoObservação
bool, uint8_t, int8_t, char1 byteFlag, número pequeno, caractere único
int16_t, uint16_t2 bytes−32768…32767 ou 0…65535
int, int32_t, uint32_t4 bytesint tem 4 bytes no ESP32 e no ESP8266, mas 2 bytes no Arduino Uno
float4 bytesCerca de 7 dígitos de precisão
double8 bytesUse float, a menos que realmente precise
char mesaj[16]16 bytesTexto de tamanho fixo; precisa terminar com '\0'
String, char*, ponteiroNão pode ser enviadoSó sai um endereço de memória, que não significa nada do outro lado

Um pacote comporta no máximo 250 bytes. Esse limite vem da primeira versão do ESP-NOW (v1) e vale para todas as placas, inclusive o ESP8266. O core 3.2 do Arduino ou posterior, baseado no ESP-IDF 5.4, consegue enviar pacotes de até 1470 bytes com o ESP-NOW v2; porém, uma placa rodando a v1 (um ESP8266 ou um ESP32 com core mais antigo) corta os pacotes maiores que 250 bytes ou os descarta por completo. Por isso, em uma rede mista, ficar dentro dos 250 bytes é a opção mais segura. A linha static_assert no sketch do transmissor interrompe a compilação com uma mensagem clara se a struct passar de 250 bytes. Se você precisa enviar dados maiores, divida-os em blocos numerados e remonte-os em ordem no receptor; para valores de sensores, estados de botões ou comandos, algumas dezenas de bytes são mais do que suficientes.

ESP-NOW unidirecional: o sketch do transmissor

  1. Confira se as duas placas ESP32 estão selecionadas como ESP32 Dev Module na Arduino IDE. A biblioteca ESP-NOW faz parte do pacote da placa, então não há mais nada para instalar.
  2. Digite o endereço MAC que você leu da placa receptora no array aliciMac do código abaixo.
  3. Ligue um botão de pressão entre o GPIO 4 e o GND do transmissor; como usamos INPUT_PULLUP, não é preciso resistor extra. Para mais detalhes, veja Como ligar um botão no Arduino.
  4. Carregue o sketch no transmissor e abra o Monitor Serial a 115200 baud. Enquanto o receptor ainda não estiver rodando, é normal ver “Teslim edilemedi / failed” (não entregue) a cada dois segundos.
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}

O sketch primeiro inicia o Wi-Fi no modo WIFI_STA; o ESP-NOW não funciona sem o rádio Wi-Fi, então esp_now_init() sempre precisa vir depois da linha WiFi.mode(). Em seguida, o receptor é adicionado à lista como peer (par). Zerar a struct esp_now_peer_info_t com = {} é importante; valores aleatórios que sobram em campos não zerados causam o erro “Peer interface is invalid”. channel = 0 significa “use o canal atual da placa”. esp_now_send() coloca o pacote em uma fila e retorna na hora; ESP_OK só significa que o pacote foi aceito para envio. O resultado real chega alguns milissegundos depois na função gonderimSonucu(): ESP_NOW_SEND_SUCCESS significa que o receptor confirmou o pacote no nível do hardware. O bloco #if no topo escolhe a forma certa do callback de envio, que depende da versão do core; explicamos isso na seção sobre o core 3.x.

Sketch do receptor ESP-NOW

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

No receptor, os pacotes chegam na função veriGeldi(). Essa função não roda no seu loop(), e sim na tarefa de alta prioridade do Wi-Fi, então você não deve usar Serial.print, delay nem cálculos longos dentro dela. O certo é verificar se o tamanho do pacote corresponde à struct esperada, copiar os dados para a sua própria variável com memcpy e avisar o loop() por meio de uma flag volatile. No core 3.x, o parâmetro bilgi também informa quem enviou o pacote (bilgi->src_addr) e a intensidade do sinal (bilgi->rx_ctrl->rssi, em dBm); essas informações só valem dentro da função, então copie-as se precisar delas depois. O receptor não precisa adicionar o transmissor como peer: pacotes unicast sem criptografia e broadcasts são aceitos de placas que não estão na lista.

“Entregue” significa que o pacote chegou ao hardware Wi-Fi do receptor, não que o programa do receptor o processou. Se a outra placa estiver ligada e no mesmo canal, o ACK volta mesmo que ela nunca tenha iniciado o ESP-NOW. Para comandos importantes, use uma mensagem de confirmação separada, em nível de software, e adicione um contador aos seus pacotes: se o receptor vê o contador pular, sabe quantos pacotes se perderam e, se o mesmo número chega duas vezes, ignora a duplicata.

Comunicação bidirecional: duas placas ESP32 controlando uma à outra

Na comunicação bidirecional, cada placa é ao mesmo tempo transmissora e receptora. Você vai carregar o sketch abaixo nas duas placas; a única diferença é que em cada uma você coloca o endereço da outra placa no array karsiMac. Quando você aperta o botão em uma placa, o LED da outra liga ou desliga, e a placa que mudou o LED responde com uma mensagem de confirmação. A ligação é a mesma nas duas placas:

ComponentePino do ESP32Observação
BotãoGPIO 4 e GNDUsa o pull-up interno, sem resistor extra
LEDGPIO 2LED azul integrado na maioria dos DevKits; para um LED externo, use um resistor de 220 Ω
AlimentaçãoUSBAs placas podem ser alimentadas por portas USB ou fontes separadas
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}

O campo tur da struct de mensagem diz o que é o pacote: 1 é um comando, 2 é uma confirmação. A placa cujo botão foi apertado envia o estado que quer para o LED da outra placa; a placa que recebe o comando ajusta o LED e devolve o mesmo estado com o tipo 2. Assim, o transmissor recebe tanto o ACK de hardware quanto a confirmação de que “o LED realmente mudou”. O comando recebido é passado de novo ao loop() com flags volatile, e o digitalWrite e a resposta são feitos ali. Isso é mais seguro do que chamar esp_now_send() dentro do callback. A espera de 30 ms no botão filtra o repique dos contatos. Ligue um relé no lugar do LED e o mesmo código vira um interruptor de luz sem fio; para a ligação do relé, veja O que é um módulo relé?.

Vários dispositivos: o limite de 20 peers e o broadcast

O ESP-NOW aceita as três topologias: um transmissor para vários receptores (um controle para cinco placas de relé), vários transmissores para um receptor (cinco sensores para um único gateway) e redes em que todos falam com todos. A lista de peers de uma placa comporta no máximo 20 entradas e, por padrão, só 7 delas podem ser criptografadas. Esse limite vale apenas para as placas para as quais você envia. Um gateway não precisa adicionar os sensores que lhe enviam pacotes sem criptografia, então pode escutar mais de 20 sensores. Se a lista estiver cheia, esp_now_add_peer() retorna ESP_ERR_ESPNOW_FULL; nesse caso, você pode remover os peers que não usa mais com esp_now_del_peer() e adicionar novos. O sketch abaixo adiciona três receptores e o endereço de broadcast à lista e depois envia tanto um por um quanto em broadcast:

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

O endereço de broadcast FF:FF:FF:FF:FF:FF também precisa ser adicionado como peer antes de você enviar para ele. Um único pacote de broadcast chega a todos os receptores ESP-NOW no mesmo canal; você não precisa saber os endereços MAC dos receptores, o que o torna o método mais fácil de configurar. O preço é que o broadcast não tem confirmação de entrega, o resultado do envio sempre parece bem-sucedido e os pacotes de broadcast não podem ser criptografados. No envio um por um, cada pacote recebe o próprio ACK. Se você passar NULL como endereço para esp_now_send(), o pacote é enviado separadamente para cada peer da lista; se o endereço de broadcast também estiver na lista, o pacote ainda sai uma vez em broadcast. Uma abordagem prática para muitos projetos é deixar as placas se encontrarem com um broadcast na primeira inicialização, depois aprender os endereços MAC e passar para unicast com confirmação.

Alcance e velocidade do ESP-NOW

MétricaValor típicoObservação
Alcance em ambiente interno20–40 m (1–3 paredes)Paredes de concreto armado e objetos metálicos são os que mais enfraquecem o sinal
Alcance em área aberta100–200 mCom a antena de PCB integrada; aumenta quando as placas ficam mais altas em relação ao chão
Modo LR (Long Range)Até 1 kmValor da Espressif com linha de visada; a taxa cai para 0,5 ou 0,25 Mbps
Taxa de dados padrão1 MbpsTaxa maior significa alcance menor
Tempo no ar de um pacote de 250 bytesCerca de 2,5 msCerca de 3 ms por pacote, incluindo ACK e tempos de espera
Taxa teórica de pacotes~300 pacotes por segundo (~70 kB/s)Com pacotes de 250 bytes; menor na prática
Latência de comandoPoucos milissegundosNão exige estabelecer conexão nem servidor

Esses valores variam muito com o ambiente, mas a conta é simples. Na taxa padrão de 1 Mbps, cada byte fica 8 microssegundos no ar; 250 bytes de dados com cabeçalhos e o preâmbulo de sincronização levam cerca de 2,5 ms. Com o ACK do receptor e o tempo de espera por um canal livre, você precisa de cerca de 3 ms por pacote, então dá para enviar no máximo uns 300 pacotes por segundo. Isso é mais do que suficiente para dados de sensores e comandos, mas o ESP-NOW não serve para transmitir áudio ou vídeo. Para aumentar o alcance, coloque as placas bem acima do chão, não cubra a antena com caixa metálica, baterias ou cabos, use módulos com antena externa (como o ESP32-WROOM-32U) e ative o modo Long Range (LR) da Espressif nos dois lados com esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR). O LR só funciona entre duas placas ESP32; uma interface configurada só em LR não consegue se conectar a roteadores comuns, e o ESP8266 e o ESP32-C2 não suportam esse modo. Ao testar o alcance no core 3.x, imprima bilgi->rx_ctrl->rssi no receptor: −50 dBm é um sinal muito forte, −80 dBm está perto do limite e a perda de pacotes começa por volta de −90 dBm.

Mudanças no core 3.x do ESP32 para Arduino

A maioria dos exemplos de ESP-NOW na internet foi escrita para o core 2.x do ESP32 para Arduino. Se você instalou a versão 3.x pelo Gerenciador de Placas, esse código não compila, porque a Espressif mudou a forma das funções de callback com o ESP-IDF 5. As mudanças vieram em duas etapas: o core 3.0 mudou o callback de recepção, e o core 3.3, baseado no ESP-IDF 5.5, mudou o callback de envio. Você pode ver qual versão tem ao lado do pacote esp32 em Ferramentas > Placa > Gerenciador de Placas.

AssuntoCore 2.xCore 3.0–3.2Core 3.3 ou posterior
Callback de recepção(const uint8_t *mac, const uint8_t *veri, int uzunluk) (assinatura antiga)(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) (assinatura nova)Igual à 3.0
Endereço do transmissormacbilgi->src_addrbilgi->src_addr
Callback de envio(const uint8_t *mac, esp_now_send_status_t durum) (assinatura antiga)Igual à 2.x(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) (assinatura nova)
Maior pacote250 bytes250 bytes; 1470 bytes com a v2 na 3.21470 bytes com a v2
Definir o canalesp_wifi_set_channel()Também WiFi.setChannel()Igual à 3.0
Biblioteca de classes prontaNenhumaESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial)ESP32_NOW.h com suporte à v2

Ao compilar código antigo no 3.x, você verá o erro invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t' no callback de recepção. A solução é fazer o primeiro parâmetro da função ser const esp_now_recv_info_t * e ler o endereço do transmissor em bilgi->src_addr. Ao passar para o core 3.3, aparece um erro parecido, desta vez para esp_now_send_cb_t; o primeiro parâmetro do callback de envio precisa ser const esp_now_send_info_t *, e o endereço do receptor fica em bilgi->des_addr. Os sketches deste guia usam a macro ESP_IDF_VERSION para escolher sozinhos a forma certa, então compilam sem alterações no 2.x, no 3.0–3.2 e no 3.3+. O core 3.x também trouxe uma biblioteca baseada em classes chamada ESP32_NOW.h: você escreve onReceive() e onSent() em uma classe derivada de ESP_NOW_Peer, e a classe ESP_NOW_Serial permite usar um link ESP-NOW exatamente como a Serial. Os exemplos dela ficam em ESP_NOW, no menu Arquivo > Exemplos. Nós usamos diretamente as funções do esp_now.h porque elas continuam compatíveis com o ESP8266 e com código mais antigo.

ESP-NOW e Wi-Fi ao mesmo tempo: combinando o canal

O ESP32 tem um único rádio de 2,4 GHz, e esse rádio só consegue trabalhar em um canal por vez. Quando a placa se conecta a um roteador, quem decide o canal é o roteador, não você; se o roteador está no canal 6, o ESP-NOW também roda no canal 6. Por isso, todo sensor que envia pacotes a um gateway conectado ao Wi-Fi também precisa mudar para o canal do roteador. Se os canais não coincidirem, o transmissor mostra “failed”; se você adicionou o peer com um canal diferente de zero, o Monitor Serial também mostra o erro “ESPNOW: Peer channel is not equal to the home channel, send fail!”. Siga estas regras para uma montagem sem problemas:

  1. O gateway se conecta primeiro ao Wi-Fi, inicia o ESP-NOW depois disso e mostra o canal do roteador no Monitor Serial com WiFi.channel().
  2. Os sensores não se conectam ao roteador; eles descobrem o canal procurando o nome da rede (WiFi.scanNetworks() e WiFi.channel(i)) ou usam um canal fixo no código. Para isso, não precisam da senha do Wi-Fi.
  3. Mude o canal do seu roteador de “Automático” para um canal fixo (1, 6 ou 11). No modo automático, se o roteador mudar de canal, os sensores perdem o gateway.
  4. Desative a economia de energia no gateway com WiFi.setSleep(false). Por padrão, um ESP32 conectado a um roteador coloca o rádio para dormir em intervalos e pode perder os pacotes ESP-NOW que chegam nesse tempo.
  5. Digite o endereço MAC STA do gateway nos sensores; o sketch do gateway abaixo mostra esse endereço no Monitor Serial ao iniciar.

O sketch do gateway abaixo se conecta à sua rede doméstica, guarda as leituras de até cinco sensores e as mostra em uma página web simples que você pode abrir em um celular na mesma rede. Troque o nome da rede e a senha pelos seus e, depois de carregar, abra no navegador do celular o endereço IP mostrado no Monitor Serial:

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}

No callback de recepção, o gateway só coloca o pacote em uma fila (queue) do FreeRTOS e retorna na hora. O loop() esvazia a fila e guarda a leitura mais recente e o horário de chegada de cada sensor, e o servidor web mostra essa tabela. A fila garante que nenhum pacote se perca quando vários sensores enviam no mesmo instante; ela também é a forma prática de seguir a recomendação da Espressif de não fazer trabalho demorado dentro de callbacks. A página se atualiza sozinha a cada 10 segundos, e a coluna “Son paket” (último pacote) mostra na hora se um sensor ficou em silêncio (bateria descarregada ou fora de alcance?). Se o gateway não consegue se conectar ao Wi-Fi de jeito nenhum, o problema não é o ESP-NOW; passe pelas verificações de ESP32 não conecta ao Wi-Fi. Se você quer levar os dados para um sistema de automação residencial, pode usar MQTT e Home Assistant no lugar da página web.

Exemplo de sensor remoto: DHT22, deep sleep e ESP-NOW

Agora vamos montar o sensor alimentado por bateria que envia dados ao gateway. Cada vez que acorda, o sensor mede a temperatura, a umidade e a tensão da bateria, envia um único pacote ESP-NOW, espera o ACK de entrega e entra em deep sleep por 60 segundos. Usamos um DHT22 para temperatura e umidade; você precisa instalar a DHT sensor library da Adafruit pelo Gerenciador de Bibliotecas. O funcionamento do sensor está no nosso guia Sensor de temperatura e umidade DHT11; a ligação é a mesma para o DHT22, e no código você só define o tipo do sensor como DHT22.

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

O segredo deste sketch são as variáveis marcadas com RTC_DATA_ATTR. No deep sleep, a memória principal do ESP32 é apagada, mas essas variáveis na memória RTC sobrevivem. O sensor só descobre o canal fazendo a varredura na primeira inicialização (o que leva cerca de 2 segundos) e usa esse canal diretamente nos despertares seguintes; se um pacote não puder ser entregue, ele zera a variável do canal e faz a varredura de novo no próximo despertar. Assim, mesmo que o roteador mude de canal, o sensor se recupera depois de um ciclo. A tensão da bateria é lida já calibrada com analogReadMilliVolts() e multiplicada por 2, porque o divisor a reduz à metade; você pode conferir os valores dos resistores na ferramenta Divisor de tensão. O GPIO 34 é um pino do ADC1; isso importa porque os pinos do ADC2 não conseguem fazer leituras analógicas enquanto o rádio Wi-Fi está ligado. Para os detalhes dos modos de sono, veja nosso guia ESP32 Deep Sleep.

Vamos estimar por alto a autonomia da bateria. Um despertar sem varredura (inicialização, medição e envio) leva cerca de 0,3 segundo e consome em média uns 100 mA, o que dá aproximadamente 0,008 mAh por despertar. Acordar uma vez por minuto dá 1440 despertares por dia, num total de cerca de 12 mAh. Um módulo ESP32 avulso consome em torno de 10 µA em deep sleep; somando a corrente em espera do DHT22 e os resistores do divisor, o consumo dormindo fica abaixo de 2 mAh por dia. Ou seja, o sensor gasta cerca de 14 mAh por dia, e uma célula 18650 de 2000 mAh dura de 4 a 5 meses. Para fazer as contas com os seus valores, use a calculadora Autonomia da bateria.

Essa estimativa vale para um módulo avulso ou placas de baixo consumo. Em um ESP32 DevKit comum, o regulador, o chip USB-serial e o LED de alimentação consomem alguns mA mesmo dormindo e reduzem a autonomia da bateria para poucas semanas. Para sensores de longa duração, use um regulador com baixa corrente quiescente (como o HT7333), remova o LED de alimentação ou escolha uma placa pronta de baixo consumo. Aumentar o intervalo de medição de 60 segundos para 5 minutos mais do que triplica a autonomia da bateria. Para carregar a célula 18650 com segurança, você pode usar um módulo TP4056 com proteção.

ESP-NOW com o ESP8266

Placa Wemos D1 Mini com ESP8266
Placas ESP8266 como a Wemos D1 Mini também suportam ESP-NOW, mas a biblioteca para Arduino e as assinaturas das funções são diferentes das do ESP32.

Placas ESP8266 podem usar ESP-NOW na mesma rede que placas ESP32, mas a biblioteca para Arduino é diferente: você inclui espnow.h no lugar de esp_now.h e ESP8266WiFi.h no lugar de WiFi.h, e as funções vêm do SDK do ESP8266, e não do ESP-IDF. As principais diferenças são:

TarefaESP32 (esp_now.h)ESP8266 (espnow.h)
Inicializaresp_now_init() == ESP_OKesp_now_init() == 0
PapelNão é necessárioesp_now_set_self_role(ESP_NOW_ROLE_COMBO)
Adicionar um peeresp_now_add_peer(&es) com uma structesp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, channel, NULL, 0)
Callback de recepção(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) (assinatura)(uint8_t *mac, uint8_t *data, uint8_t len) (assinatura)
Callback de envio(..., esp_now_send_status_t durum)(uint8_t *mac, uint8_t status); 0 significa sucesso
Maior pacote250 bytes (1470 com a v2)250 bytes

No ESP8266, não esqueça de adicionar WiFi.disconnect() depois de WiFi.mode(WIFI_STA); ao iniciar, o ESP8266 tenta se reconectar sozinho ao último roteador que usou, o que muda o canal e pode causar perda de pacotes. Se você definir a struct de dados como packed nos dois lados, o ESP32 e o ESP8266 leem os mesmos bytes da mesma forma, já que os dois são processadores de 32 bits little-endian. Para os pinos e a configuração do ESP8266, veja nosso guia ESP8266 NodeMCU.

ESP-NOW com MicroPython e o Raspberry Pi Pico

Se você programa o ESP32 em MicroPython, existe um módulo espnow pronto para ESP-NOW; ele já vem nas builds do MicroPython para ESP32 e ESP8266. O código abaixo recebe a struct Paket de 30 bytes enviada pelo exemplo de transmissor em Arduino e a decodifica com struct.unpack(). Na string de formato, < significa little-endian sem padding; B é um número de 1 byte, I um número sem sinal de 4 bytes, f um float e 16s um texto de 16 bytes. O campo bool do Arduino é lido como 0 ou 1 com B. Carregue o código na placa com o Thonny e digite o endereço MAC que ele mostra no array aliciMac do transmissor em Arduino:

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

Enviar um pacote pelo MicroPython também leva duas linhas: adicione o receptor com e.add_peer(mac) e envie a mesma struct com e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")), assim placas com Arduino e com MicroPython podem trabalhar juntas na mesma rede. Em uma placa com MicroPython conectada ao Wi-Fi, desative a economia de energia com sta.config(pm=sta.PM_NONE) para não perder pacotes. Se você está começando no MicroPython, comece pelo nosso guia Fundamentos de MicroPython. Já a família Raspberry Pi Pico não tem ESP-NOW: o Pico padrão não tem nenhuma comunicação sem fio, e o chip CYW43439 do Pico W e do Pico 2 W só roda Wi-Fi e Bluetooth padrão. Para colocar um Pico em uma rede ESP-NOW, ligue a ele um pequeno ESP32-C3 por comunicação serial UART e use-o como ponte, ou deixe o Pico W conversar com o gateway por Wi-Fi; o Wi-Fi no Pico W é explicado em Pico W: Wi-Fi e servidor web. A mesma abordagem de ponte funciona para o Arduino Uno.

Criptografando pacotes ESP-NOW

Por padrão, os pacotes ESP-NOW não são criptografados; qualquer pessoa no mesmo canal pode capturá-los pelo ar e enviar pacotes falsos que imitam a sua struct. Use criptografia em projetos como fechaduras de porta ou portões de garagem. O ESP-NOW usa CCMP, como no WPA2, e trabalha com duas chaves: uma PMK de 16 bytes, igual em todos os dispositivos (definida com esp_now_set_pmk()), e uma LMK de 16 bytes separada para cada peer. Ao adicionar o peer, você define es.encrypt = true e copia a chave em es.lmk; o outro lado também precisa adicionar você como peer criptografado com a mesma LMK. Para receber pacotes criptografados, o receptor precisa adicionar o transmissor à lista. Pacotes de broadcast não podem ser criptografados e, por padrão, você pode ter no máximo 7 peers criptografados. A criptografia sozinha não impede que um pacote capturado seja reenviado depois; adicionar um contador crescente aos comandos críticos e rejeitar números antigos no receptor é uma boa proteção extra.

Ideias de projetos com ESP-NOW

  • Campainha sem fio: quando o botão do ESP32 na porta é apertado, um buzzer toca na placa dentro de casa; para poupar a bateria, use o botão como o pino que acorda a placa do deep sleep.
  • Monitoramento de temperatura em vários cômodos: coloque um sensor DHT22 ou BME280 em cada cômodo, reúna as leituras em um gateway e compare-as em uma página web.
  • Irrigação do jardim: um sensor de umidade do solo envia um comando “regar agora” para a placa ao lado da bomba; para a ligação, veja Sensor de umidade do solo.
  • Controle remoto sem fio para robô: envie os valores do joystick 20 vezes por segundo; para a parte mecânica do robô, veja Como fazer um carrinho robô com Arduino.
  • Alarme de jardim: quando um sensor PIR no jardim detecta movimento, o gateway envia uma notificação pelo Telegram.
  • Show de luzes sincronizado: mude as fitas WS2812B de várias placas para a mesma cor ao mesmo tempo com um único pacote de broadcast.

Erros comuns de ESP-NOW e como resolver

Sintoma / erroCausaSolução
“ESPNOW: Peer interface is invalid” (interface do peer inválida), o peer não é adicionadoesp_now_peer_info_t usada sem ser zeradaZere a struct com = {} ou memset e defina ifidx = WIFI_IF_STA
Mensagens “failed” constantesMAC errado (endereço AP ou erro de digitação), receptor desligado ou em outro canalLeia de novo o endereço MAC STA do receptor e confira os canais das duas placas
Os pacotes chegam, mas os valores são lixoA struct é diferente entre os dois lados ou contém uma StringDefina a struct como packed, na mesma ordem, nos dois lados e use um array de char para texto
“invalid conversion ... to 'esp_now_recv_cb_t'” (conversão inválida)Código escrito para o core 2.x compilado no 3.xEscreva o callback de recepção com const esp_now_recv_info_t *
“invalid conversion ... to 'esp_now_send_cb_t'” (conversão inválida)Código escrito para o core 3.0–3.2 compilado no 3.3+Escreva o callback de envio com const esp_now_send_info_t * ou use a macro ESP_IDF_VERSION
“Peer channel is not equal to the home channel, send fail!” (canal diferente)O canal da placa conectada ao Wi-Fi é diferente do canal do peerAdicione o peer com channel = 0 e passe os sensores para o canal do roteador
O gateway conectado ao Wi-Fi perde alguns pacotesEconomia de energia do Wi-Fi (modem sleep)Use WiFi.setSleep(false)
ESP_ERR_ESPNOW_NOT_INITesp_now_init() não foi chamada, ou foi chamada antes de o Wi-Fi iniciarPrimeiro WiFi.mode(WIFI_STA), depois esp_now_init()
ESP_ERR_ESPNOW_FULLA lista de peers chegou a 20 entradasRemova peers antigos com esp_now_del_peer() ou use broadcast
ESP_ERR_ESPNOW_NO_MEMOs pacotes são enviados um atrás do outro antes de a fila de envio esvaziarEspere o resultado do envio antes de enviar o próximo pacote
O endereço MAC aparece como 00:00:00:00:00:00No core 3.x, WiFi.macAddress() foi chamada antes de o Wi-Fi iniciar por completoUse esp_wifi_get_mac() depois de WiFi.mode(WIFI_STA)
Ao procurar o problema, comece pelo passo mais simples: coloque as duas placas lado a lado e envie primeiro para o endereço de broadcast. Se o pacote de broadcast chegar, o rádio, o canal e a struct de dados estão certos, e o problema é o endereço MAC. Se nada chegar, escolha Debug em Ferramentas > Core Debug Level; os logs detalhados do core vão aparecer no Monitor Serial. Se a placa fica reiniciando, o problema mais provável é a alimentação; as soluções para o pico de corrente quando o rádio liga estão no nosso guia ESP32: brownout e Guru Meditation.

O ESP-NOW é o jeito mais rápido de fazer várias placas ESP32 conversarem sem roteador, cabos ou módulos extras: leia o endereço MAC, defina sua struct de dados igual nos dois lados, adicione o peer e envie. Quando usá-lo junto com o Wi-Fi, lembre-se da única regra: todos precisam estar no mesmo canal. No app BlueGrays você pode experimentar, passo a passo, com esquemas do circuito e código pronto para usar, cada peça deste guia, incluindo o controle de LED e botão com o ESP32, a medição de temperatura com o DHT22, a conexão Wi-Fi, um servidor web e o deep sleep.

Perguntas frequentes

O ESP-NOW precisa de roteador ou internet?

Não. O ESP-NOW usa o rádio Wi-Fi das placas, mas não entra em nenhuma rede; as placas enviam pacotes diretamente aos endereços MAC umas das outras. Se você quer levar os dados para a internet, precisa de um ESP32 funcionando como gateway, conectado tanto ao Wi-Fi quanto ao ESP-NOW.

Qual é o alcance do ESP-NOW?

Com placas ESP32 de antena integrada, você normalmente consegue de 20 a 40 metros em ambiente interno, dependendo das paredes, e cerca de 100 a 200 metros em área aberta. Com módulos de antena externa e o modo LR (Long Range) ativado nos dois lados, a Espressif diz que é possível chegar a 1 quilômetro com linha de visada.

Quantos dispositivos podem se conectar com ESP-NOW?

A lista de peers de uma placa comporta até 20 dispositivos e, por padrão, no máximo 7 deles podem ser criptografados. Esse limite vale para os dispositivos para os quais você envia: um gateway pode escutar mais de 20 sensores que enviam pacotes sem criptografia, e um broadcast chega a todos os dispositivos no mesmo canal com um único pacote.

Dá para usar ESP-NOW e Wi-Fi ao mesmo tempo?

Sim, mas, como o único rádio do ESP32 trabalha em um canal por vez, todo dispositivo ESP-NOW precisa usar o canal do roteador. Desative a economia de energia no gateway com WiFi.setSleep(false) e, nos sensores, descubra o canal procurando o nome da rede ou configure o roteador com um canal fixo.

Um ESP32 e um ESP8266 conseguem se comunicar com ESP-NOW?

Sim. Os dois chips suportam ESP-NOW e podem enviar pacotes um ao outro; no ESP8266, só a biblioteca (espnow.h) e as assinaturas das funções são diferentes. Defina a struct de dados como packed e idêntica nos dois lados e mantenha os pacotes abaixo de 250 bytes.

Qual é o tamanho máximo de dados no ESP-NOW?

Com o ESP-NOW v1, um pacote comporta no máximo 250 bytes, e esse limite vale para todas as placas, inclusive o ESP8266. Placas com ESP-IDF 5.4 ou posterior (Arduino core 3.2+) conseguem enviar até 1470 bytes com o ESP-NOW v2, mas placas que usam a v1 não conseguem receber pacotes maiores que 250 bytes.

Ver todos os projetos
LED piscando com ESP32
#103Iniciante

LED piscando com ESP32

Ensina o uso básico de uma saída GPIO no ESP32. Fazemos um LED piscar ligando e desligando um pino digital. É a versão para ESP32 do clássico projeto blink do Arduino UNO: os comandos básicos de blink no ambiente Arduino são os mesmos, mas a disposição dos pinos e os níveis de 3,3 V dos GPIOs do ESP32 diferem dos do Arduino UNO.

4.7(37)
99
Controle de LED com botão no ESP32 (alternância)
#105Iniciante

Controle de LED com botão no ESP32 (alternância)

Cada vez que o botão é pressionado, o estado do LED muda: se estiver aceso, apaga; se estiver apagado, acende. É o padrão clássico de alternância. Com debounce, eliminamos o ruído mecânico dos contatos do botão para garantir uma única mudança a cada pressionamento.

4.6(39)
57
Comunicação bidirecional entre 2 Arduinos
#58Intermediário
Premium

Comunicação bidirecional entre 2 Arduinos

Dois Arduinos se conectam por fios e conversam entre si: ao pressionar o botão de um, a luz do outro acende; ao pressionar novamente, ela apaga. As duas placas executam o mesmo programa, então ambas e

4.5(20)
26
Temperatura e umidade com ESP32 e DHT22 (com display LCD)
#117Intermediário

Temperatura e umidade com ESP32 e DHT22 (com display LCD)

Este projeto mede a temperatura e a umidade do ambiente com um sensor DHT22 e exibe os dados em tempo real em um LCD I2C 16x2. Os dados são lidos do sensor pelo protocolo de um fio (one-wire) com a biblioteca Adafruit DHT. Este projeto serve de base para estações meteorológicas IoT, automação residencial ou sistemas de monitoramento ambiental em estufas. A comunicação com o DHT22 é feita pelo pino GPIO 4 do ESP32.

4.8(38)
56
Conexão Wi-Fi STA com ESP32 e display LCD
#127Avançado

Conexão Wi-Fi STA com ESP32 e display LCD

Neste projeto, conectamos o ESP32 à rede Wi-Fi de casa no modo estação (STA), de forma segura e tolerante a falhas. Guardamos o SSID e a senha em um arquivo secrets.h separado para evitar que sejam expostos no Git, configuramos um mecanismo de reconexão automática e exibimos o nível de sinal RSSI e o endereço IP em um display LCD I2C. O LED integrado funciona como indicador de status. É um gerenciador de conexão pronto para uso em produção.

4.4(21)
34
Servidor web assíncrono com ESP32 (IP no LCD)
#129Avançado

Servidor web assíncrono com ESP32 (IP no LCD)

Neste projeto, transformamos o ESP32 em um servidor web assíncrono que você pode controlar pelo celular ou computador na mesma rede Wi-Fi. Com a biblioteca ESPAsyncWebServer, o servidor processa várias requisições ao mesmo tempo (ao contrário do WebServer.h tradicional, que bloqueia a execução ao processar uma requisição). Você poderá abrir o endereço IP do ESP32 no navegador e clicar nos botões ON/OFF (as rotas /on e /off) para ligar e desligar o LED conectado ao GPIO 5.

4.8(16)
26
ESP32 em sono profundo — Economia de bateria (OLED + contador na RAM RTC)
#135Avançado
Premium

ESP32 em sono profundo — Economia de bateria (OLED + contador na RAM RTC)

O modo de sono profundo do ESP32 reduz o consumo de corrente para ~10 µA ao desligar a CPU e a maioria dos periféricos, sendo essencial para dispositivos IoT alimentados por bateria. Neste projeto, usamos três fontes de despertar: um temporizador (15 segundos), um pino externo (EXT0 — botão no GPIO 33) e toque (T0/GPIO 4). As variáveis marcadas com RTC_DATA_ATTR são preservadas durante o sono profundo, mantendo os contadores de inicializações e de despertares por temporizador e botão.

4.3(10)
16
Registrador de temperatura com ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
#144Avançado
Premium

Registrador de temperatura com ESP32 (DHT22 + DS1307 RTC + microSD + OLED)

Monte um registrador de temperatura com data e hora usando ESP32! Leia a temperatura e a umidade do DHT22 a cada minuto, obtenha a hora real do DS1307 RTC e grave os dados em CSV no cartão microSD. O OLED exibe a temperatura atual em letras grandes, a porcentagem de umidade, a data e a hora e o contador de registros. O arquivo /temp_log.csv no cartão SD pode ser aberto no Excel, Google Sheets ou pandas do Python para análise — ideal para monitorar estufas, acompanhar a temperatura de casa ou…

4.7(7)
12