Pular para o conteúdo
IDE livre

ESP-NOW

ESP-NOW na ESP32: placas conversando sem roteador

Como duas ou mais ESP32 trocam mensagens direto pelo rádio com o espnow do MicroPython: MAC, peers, canal, broadcast, limite de bytes e mensagens com struct.

Por Prof. Rodrigo Faustino · Atualizado em · 14 min de leitura

Métricas deste artigo

Palavras
2.622
Caracteres
15.095
Tempo de leitura
~14 min

Nos artigos anteriores, a ESP32 conversou com o mundo pela rede Wi-Fi: ela se conecta ao roteador, ganha um endereço IP e fala com servidores HTTP e brokers MQTT. Esse caminho é ótimo quando existe um roteador e quando os dados precisam chegar à internet. Mas há muitos projetos em que isso é exagero — ou nem é possível: um sensor no fundo do quintal, onde o Wi-Fi da casa não chega; um controle remoto que liga um robô no meio da quadra; dez placas espalhadas numa feira de ciências, sem roteador nenhum por perto.

Para esses casos, a Espressif criou o ESP-NOW: um protocolo em que as placas trocam mensagens curtas direto pelo rádio, sem roteador, sem IP e sem servidor no meio. Cada placa é encontrada pelo seu endereço MAC, o número de série do rádio. O MicroPython da ESP32 traz o módulo espnow pronto no firmware, e é dele que este artigo trata: como ligar, como cadastrar as placas com quem se quer conversar, os limites de tamanho e de quantidade, o canal do rádio, a convivência com o Wi-Fi comum e como montar mensagens pequenas e confiáveis.

O que o ESP-NOW é (e o que não é)

O ESP-NOW usa o mesmo rádio de 2,4 GHz do Wi-Fi, mas não forma uma rede Wi-Fi. Não existe "conectar": uma placa simplesmente envia um pacote endereçado ao MAC da outra, e a outra, se estiver ouvindo no mesmo canal, recebe. Por isso ele é chamado de protocolo sem conexão. A comparação com o MQTT ajuda a ver a diferença:

Wi-Fi + MQTTESP-NOW
Precisa de roteador?simnão
Endereço de cada placaIP, dado pelo roteadorMAC, gravado no rádio
Servidor no meiobroker MQTTnenhum
Chega à internet?simnão, só a outras placas por perto
Tamanho da mensagemgrande (texto, JSON)pequeno (veja os limites)

O ESP-NOW não substitui o Wi-Fi com MQTT ou HTTP. Ele não leva dados para a nuvem, não tem tópicos, não guarda a última mensagem para quem chega depois. Ele resolve outra coisa: fazer placas próximas conversarem com pouca espera e sem infraestrutura. Muitos projetos usam os dois juntos: várias placas-sensor mandam leituras por ESP-NOW para uma placa "central", e só a central se conecta ao roteador para publicar tudo no MQTT.

Comparação de dois caminhos. À esquerda, uma ESP32 sensor manda dados ao roteador, que passa a um broker MQTT, que entrega a outra ESP32 painel. À direita, duas ESP32 identificadas pelo endereço MAC trocam mensagens direto com send(mac, msg) e uma confirmação; abaixo, uma ESP32 envia para o endereço de broadcast ff:ff:ff:ff:ff:ff e três outras recebem.
Com Wi-Fi e MQTT, toda mensagem passa pelo roteador e por um servidor. Com ESP-NOW, as placas falam direto, pelo endereço MAC — e o broadcast chega a todas que estão ouvindo.
FastMindAssistente de IA local

A IA que lembra da sua vida e mora no seu computador

  • Memória de longo prazo com a fonte de cada resposta
  • Funciona offline: seus dados ficam no seu PC
  • Lê PDFs, slides, fotos e gravações de reunião
Baixar grátis para Windows

Plano Essencial grátis para sempre

O endereço MAC: o "nome" de cada placa

Cada interface de rede da ESP32 tem um endereço MAC de 6 bytes. É ele que o ESP-NOW usa para dizer "esta mensagem é para aquela placa". No MicroPython, o MAC sai do config('mac') da interface Wi-Fi. O exemplo abaixo roda no simulador: o rádio simulado tem um MAC fixo, e o programa mostra o endereço de duas formas — em texto, do jeito que se anota no caderno, e em bytes, do jeito que o espnow espera.

import network
import binascii

sta = network.WLAN(network.STA_IF)
sta.active(True)

mac = sta.config('mac')
print('MAC desta placa:', binascii.hexlify(mac, ':').decode())
print('do jeito que o espnow quer:', mac)
print('tamanho:', len(mac), 'bytes')

A linha dos bytes costuma assustar: aparece algo como b'$\n\xc4^\x1a\xb0'. O Python mostra como caractere todo byte que tem um caractere imprimível: 0x24 é o $, 0x0a é a quebra de linha \n, 0x5e é o ^. Os outros aparecem em hexadecimal, com \x. São os mesmos 6 bytes do texto com dois-pontos, só escritos de outro jeito.

Na placa de verdade, cada ESP32 tem o seu MAC, e o primeiro passo de qualquer projeto com ESP-NOW é descobrir e anotar o MAC de cada placa. Rode esse mesmo trecho em cada uma, pelo REPL, e guarde os endereços. Depois, no programa, o MAC entra como bytes: o endereço a4:cf:12:08:3c:51 vira b'\xa4\xcf\x12\x08\x3c\x51'. Um jeito prático de escrever é binascii.unhexlify('a4cf12083c51'), que transforma o texto sem os dois-pontos nos 6 bytes.

Ligando o ESP-NOW no MicroPython

O módulo espnow precisa de duas coisas antes de enviar ou receber:

  1. uma interface Wi-Fi ativa — a de estação (STA_IF) ou a de ponto de acesso (AP_IF). Ativa quer dizer active(True); ela não precisa estar conectada a roteador nenhum;
  2. o próprio ESP-NOW ativo, com ESPNow().active(True).

Toda chamada a espnow.ESPNow() devolve o mesmo objeto (é um singleton), então não há problema em chamá-la em mais de um lugar do programa. active(False) desliga o ESP-NOW e apaga a lista de placas cadastradas.

No simulador do PyPlaca, o espnow ainda não existe: import espnow dá um NotImplementedError explicando que o módulo existe na placa, mas não foi simulado. Por isso, os trechos com o rádio abaixo são para ler e levar à placa real; os exemplos que rodam no simulador tratam do que vem antes e depois do rádio — o MAC, o formato da mensagem e a lógica de confirmação.

A placa que envia

import network
import espnow
import binascii
import time

sta = network.WLAN(network.STA_IF)
sta.active(True)                      # o rádio precisa estar ligado
sta.disconnect()                      # não vamos usar roteador

e = espnow.ESPNow()
e.active(True)

receptor = binascii.unhexlify('a4cf12083c51')   # o MAC anotado da outra placa
e.add_peer(receptor)                  # cadastrar ANTES de enviar

contador = 0
while True:
    ok = e.send(receptor, 'ola %d' % contador)
    print('enviei', contador, 'confirmado' if ok else 'sem resposta')
    contador += 1
    time.sleep(1)

A placa que recebe

import network
import espnow

sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.disconnect()

e = espnow.ESPNow()
e.active(True)

while True:
    mac, msg = e.recv(1000)           # espera até 1 s
    if mac is None:
        continue                      # passou 1 s sem mensagem
    print('de', mac, ':', msg)

Repare em três detalhes. Primeiro, quem envia precisa cadastrar o receptor com add_peer() antes do send() — na ESP32 isso é obrigatório. Quem recebe não precisa cadastrar ninguém para receber. Segundo, o recv() devolve um par [mac, msg]: o MAC de quem mandou e a mensagem em bytes. Terceiro, o argumento do recv() é o tempo máximo de espera em milissegundos. Sem argumento, o padrão do módulo é 300 000 ms — cinco minutos parado esperando. Se o tempo acabar sem mensagem, o recv() devolve (None, None). Com 0, ele volta na hora; com um número negativo, espera para sempre.

Esse tempo de espera é o mesmo assunto da trilha Tempo sem travar: um recv() sem limite deixa a placa surda para botões, sensores e tela. Prefira tempos curtos dentro do laço principal, ou o aioespnow, que aparece mais adiante.

send() e a confirmação

O send(mac, msg) tem um terceiro argumento, sync, que vem ligado. Com sync=True, a placa espera a confirmação de recebimento que o próprio rádio da outra placa devolve, e o send() retorna True se a confirmação chegou ou False se não chegou. Com sync=False, ele envia e volta na hora, sempre com True, sem saber se a mensagem chegou.

A confirmação do sync=True é do rádio, não do seu programa: ela diz que o pacote chegou à outra placa, não que o programa de lá o leu e entendeu. Se você precisa da segunda garantia — "o painel recebeu e acendeu o LED" —, o próprio programa receptor deve mandar uma resposta de volta, como na seção de mensagens confiáveis.

Os limites que valem decorar

O ESP-NOW foi feito para mensagens curtas e redes pequenas. Os números abaixo são da documentação do módulo espnow do MicroPython:

  • 20 placas cadastradas no total com add_peer(), das quais no máximo 6 com criptografia;
  • 250 bytes por mensagem na versão 1 do protocolo. A versão 2, que existe a partir do ESP-IDF 5.4, aceita até 1470 bytes. Na placa, a constante espnow.MAX_DATA_LEN diz o limite do firmware que está rodando. Se a sua rede mistura placas antigas (ou ESP8266) com novas, fique nos 250 bytes;
  • o endereço de broadcast b'\xff\xff\xff\xff\xff\xff' manda a mesma mensagem a todas as placas que ouvem no canal. Ele também precisa ser cadastrado com add_peer(), e o send() para o broadcast sempre devolve True, porque não há uma confirmação única para esperar.

E o alcance? Ele depende da antena da placa, de paredes, do corpo de quem está perto e de outros rádios na mesma faixa. Não existe um número que valha para todo projeto: meça no lugar de uso. Um teste simples é levar a placa que envia para longe, contando quantas mensagens o send() confirma a cada 100 tentativas.

FastTeacherPara professores

Da ementa ao semestre planejado

  • Plano de ensino e calendário montados sozinhos
  • Diário de aulas pronto para colar no SIGA
  • Slides e listas de exercícios com gabarito
Conhecer o FastTeacher

Versão Community grátis · Windows

Mensagens pequenas com struct

Como a mensagem é curta, vale pensar no formato. A tentação é mandar texto, como 't=23.45;u=61.2', ou JSON. Texto funciona, mas desperdiça espaço: cada dígito é um byte. O módulo struct empacota números diretamente em binário, num tamanho fixo e conhecido. A imagem abaixo compara a mesma leitura nos dois formatos.

A mesma leitura de sensor escrita como texto JSON, com 37 bytes, e empacotada com struct no formato <BHhH, com 7 bytes: 1 byte de tipo, 2 bytes de número de sequência, 2 bytes de temperatura vezes 100 e 2 bytes de umidade vezes 100, mostrados em hexadecimal. Uma barra compara os dois tamanhos com o limite de 250 bytes de uma mensagem ESP-NOW v1.
Empacotar com struct deixa a mensagem cinco vezes menor que o JSON. Os dois cabem no limite de 250 bytes do ESP-NOW v1, mas mensagens pequenas gastam menos rádio e menos bateria.

O formato '<BHhH' quer dizer: < little-endian (a ordem de bytes da ESP32), B um byte sem sinal, H dois bytes sem sinal e h dois bytes com sinal (a temperatura pode ficar negativa). Em vez de mandar 23.45 como número de ponto flutuante (4 bytes), o programa manda 2345, a temperatura vezes 100, num inteiro de 2 bytes, e o receptor divide por 100. Esse truque, chamado de ponto fixo, mantém duas casas decimais com metade do espaço.

O exemplo roda no simulador: ele empacota uma leitura, mostra os bytes e desempacota de volta, conferindo o tamanho antes — uma mensagem com tamanho errado é descartada em vez de virar números absurdos.

import struct

FORMATO = '<BHhH'        # tipo, sequência, temperatura x100, umidade x100
TAMANHO = struct.calcsize(FORMATO)
TIPO_LEITURA = 1

def empacotar(seq, temperatura, umidade):
    return struct.pack(FORMATO, TIPO_LEITURA, seq, round(temperatura * 100), round(umidade * 100))

def desempacotar(msg):
    if len(msg) != TAMANHO:
        return None                       # mensagem de outro formato: ignora
    tipo, seq, t, u = struct.unpack(FORMATO, msg)
    return {'tipo': tipo, 'seq': seq, 'temperatura': t / 100, 'umidade': u / 100}

msg = empacotar(12, 23.45, 61.2)
print('tamanho:', len(msg), 'bytes')
print('bytes:', msg)
print('de volta:', desempacotar(msg))
print('mensagem curta demais:', desempacotar(b'\x01\x02'))

O primeiro byte, o tipo, parece desperdício, mas é o que permite crescer o projeto: tipo 1 é leitura de sensor, tipo 2 pode ser um comando ("acende o LED"), tipo 3 uma confirmação. O receptor olha o primeiro byte e escolhe como desempacotar o resto.

Canal: as duas placas precisam ouvir o mesmo

O rádio de 2,4 GHz é dividido em canais, e uma placa só escuta um canal por vez. Para o ESP-NOW funcionar, quem envia e quem recebe precisam estar no mesmo canal. Quando nenhuma das placas está conectada a um roteador, as duas ficam no canal padrão e isso costuma "simplesmente funcionar".

O problema aparece quando uma das placas também usa o Wi-Fi comum, por exemplo a central que recebe por ESP-NOW e publica no MQTT. Ao se conectar ao roteador, o rádio dela muda para o canal do roteador, e as placas-sensor precisam usar esse mesmo canal. A documentação do MicroPython lembra disso: o canal do add_peer() (argumento channel) com valor 0 usa o canal atual da interface; qualquer outro valor precisa ser igual ao canal configurado na interface. Nas placas que não se conectam a roteador, o canal pode ser escolhido com sta.config(channel=...) — use o mesmo número do roteador da central.

Há mais duas armadilhas na placa que mistura Wi-Fi e ESP-NOW, ambas descritas na documentação do módulo:

  • ao se conectar a um roteador, a ESP32 liga automaticamente a economia de energia do Wi-Fi, e com ela o rádio "cochila" entre os avisos do roteador e perde mensagens ESP-NOW. A correção é desligar a economia com sta.config(pm=sta.PM_NONE);
  • quando a conexão com o roteador cai, a placa sai procurando o roteador em todos os canais, e as mensagens que chegam nesse meio-tempo se perdem. sta.config(reconnects=0) desliga essa busca automática; o programa reconecta quando quiser.

Mensagens confiáveis: sequência e confirmação

Rádio perde pacotes. Em vez de fingir que não, o programa deve perceber e reagir. Duas ideias simples resolvem quase tudo:

  1. número de sequência: cada mensagem leva um contador que sobe de 1 em 1. Se o receptor recebe a 7 depois da 5, sabe que a 6 se perdeu. Se recebe a 7 duas vezes, sabe que é repetição e não processa de novo;
  2. reenvio com limite: se o send() volta False, o emissor tenta de novo algumas vezes, com uma pequena pausa, e depois desiste e registra a falha — em vez de travar tentando para sempre.

O exemplo abaixo roda no simulador sem rádio nenhum: uma função faz o papel do ar, com uma lista fixa de pacotes que "se perdem", para o resultado ser sempre o mesmo. Ele mostra o emissor reenviando e o receptor contando perdas e ignorando repetições.

# o "ar" entre as placas: perde algumas tentativas, sempre as mesmas, para o teste ser repetível
PERDIDAS = {2, 3, 7}
tentativa = 0

def ar(mensagem, receptor):
    global tentativa
    tentativa += 1
    if tentativa in PERDIDAS:
        return False              # o rádio não confirmou
    receptor(mensagem)
    return True

ultima = -1
perdidas = 0

def receptor(msg):
    global ultima, perdidas
    seq = msg['seq']
    if seq == ultima:
        print('  repetida, ignorei a', seq)
        return
    if seq > ultima + 1:
        perdidas += seq - ultima - 1
    ultima = seq
    print('  recebi a', seq, '->', msg['valor'])

def enviar(msg, tentativas=3):
    for n in range(tentativas):
        if ar(msg, receptor):
            return True
        if n < tentativas - 1:
            print('  sem confirmação, tentando de novo (%d)' % (n + 2))
    return False

for seq in range(5):
    print('enviando', seq)
    if not enviar({'seq': seq, 'valor': seq * 10}):
        print('  desisti da', seq)

print('mensagens perdidas no receptor:', perdidas)

Com PERDIDAS = {2, 3, 7}, a mensagem 1 precisa de três tentativas e chega na última. A mensagem 4 perde a primeira tentativa e chega na segunda. Nenhuma desiste, e o receptor conta zero perdas. Experimente acrescentar 4 ao conjunto: as três tentativas da mensagem 1 se perdem, o emissor desiste dela, e o receptor percebe o buraco quando a 2 chega e conta uma perda. É exatamente o comportamento que se quer na placa: o programa sabe o que aconteceu.

Broadcast e descoberta de placas

Anotar o MAC de cada placa à mão funciona para duas ou três. Para mais, dá para fazer as placas se descobrirem: a central manda periodicamente uma mensagem "estou aqui" para o broadcast; cada sensor que recebe anota o MAC de quem mandou (o recv() entrega esse MAC), cadastra a central com add_peer() e, a partir daí, manda as leituras direto para ela, com confirmação.

import espnow

BROADCAST = b'\xff\xff\xff\xff\xff\xff'
e = espnow.ESPNow()
e.active(True)
e.add_peer(BROADCAST)
e.send(BROADCAST, b'\x03central')     # tipo 3: "sou a central"

Duas regras da documentação valem aqui: mensagens para o broadcast não podem ser criptografadas, e o send() para o broadcast sempre devolve True — ele não serve para saber se alguém ouviu.

Criptografia: PMK e LMK

Por padrão, as mensagens ESP-NOW vão abertas: qualquer rádio no mesmo canal pode ouvi-las. Para projetos em que isso importa (um controle de portão, por exemplo), o ESP-NOW cifra mensagens entre placas cadastradas com duas chaves de 16 bytes:

  • a PMK (chave primária), definida com e.set_pmk(...), a mesma em todas as placas da rede;
  • a LMK (chave local), passada no add_peer(mac, lmk=...) de cada par de placas.

As duas placas precisam se cadastrar mutuamente com a mesma PMK e a mesma LMK. Lembre do limite: no máximo seis placas cadastradas com criptografia. E guarde as chaves fora do código que você compartilha — o mesmo cuidado que se tem com a senha do Wi-Fi.

ESP-NOW sem travar: aioespnow

O firmware da ESP32 traz também o aioespnow, a versão do ESP-NOW para o asyncio. Ele tem os mesmos métodos do espnow e mais as versões assíncronas: arecv(), airecv() e asend(). Com ele, receber mensagens vira uma tarefa entre outras — piscar um LED, ler um botão, atualizar uma tela —, sem que uma espere pela outra.

import asyncio
import aioespnow
import network

sta = network.WLAN(network.STA_IF)
sta.active(True)
e = aioespnow.AIOESPNow()
e.active(True)

async def ouvir():
    async for mac, msg in e:
        print('de', mac, ':', msg)

async def piscar():
    from machine import Pin
    led = Pin(2, Pin.OUT)
    while True:
        led.value(not led.value())
        await asyncio.sleep_ms(500)

async def main():
    await asyncio.gather(ouvir(), piscar())

asyncio.run(main())

O async for mac, msg in e é a forma mais limpa de escrever o receptor: cada mensagem que chega entra no laço, e enquanto nada chega as outras tarefas continuam rodando.

ESP-NOW e bateria

Uma das maiores vantagens do ESP-NOW aparece em projetos com bateria. Conectar a um roteador leva tempo: procurar a rede, autenticar, pedir um IP. Com ESP-NOW, a placa acorda, liga o rádio, manda uma mensagem curta e volta a dormir — o rádio, que é a parte que mais consome, fica ligado por muito menos tempo.

Duas observações da documentação do MicroPython para esse uso: antes de machine.lightsleep() ou machine.deepsleep(), desative as interfaces Wi-Fi com active(False); e lembre que, dormindo, a placa não recebe nada — o sensor que dorme pode mandar, mas não pode ser chamado. O artigo sobre baterias e autonomia trata do sono da ESP32 em detalhe.

Quando escolher ESP-NOW

Um resumo prático para decidir:

  • escolha ESP-NOW quando as placas estão perto umas das outras, as mensagens são curtas, não há roteador (ou não se quer depender dele) e a resposta precisa ser rápida: controles remotos, robôs, redes de sensores num mesmo ambiente, jogos entre placas;
  • escolha Wi-Fi com MQTT ou HTTP quando os dados precisam chegar à internet, a um painel no celular ou a um banco de dados, ou quando as placas estão em lugares diferentes;
  • combine os dois quando há muitos sensores e um ponto com internet: os sensores falam ESP-NOW com uma central, e a central publica no MQTT. Só lembre do canal: todos no canal do roteador da central.

Para continuar

Para praticar o que vem antes do rádio, a trilha Wi-Fi e HTTP mostra a interface WLAN, o active(True) e os estados da conexão, e a trilha MQTT é a comparação natural com o ESP-NOW: mensagens por tópicos, passando por um broker. A lição várias tarefas ajuda a montar o laço principal que recebe mensagens sem parar o resto do programa.

O artigo Wi-Fi, HTTP e MQTT na ESP32 cobre a outra metade da comunicação, e o servidor web na placa mostra como a ESP32 pode receber pedidos de um navegador. Para levar o projeto à bancada, a ficha da ESP32 DevKit V1 e a ficha da ESP32-C3 Super Mini trazem os pinos e os cuidados de cada placa.

Fontes

Para praticar no PyPlaca

Trilhas

Lições

Fichas de componentes

FastTeacherPara professores

Da ementa ao semestre planejado

  • Plano de ensino e calendário montados sozinhos
  • Diário de aulas pronto para colar no SIGA
  • Slides e listas de exercícios com gabarito
Conhecer o FastTeacher

Versão Community grátis · Windows

FastMindAssistente de IA local

A IA que lembra da sua vida e mora no seu computador

  • Memória de longo prazo com a fonte de cada resposta
  • Funciona offline: seus dados ficam no seu PC
  • Lê PDFs, slides, fotos e gravações de reunião
Baixar grátis para Windows

Plano Essencial grátis para sempre

FastTeacherPara professores

Da ementa ao semestre planejado

  • Plano de ensino e calendário montados sozinhos
  • Diário de aulas pronto para colar no SIGA
  • Slides e listas de exercícios com gabarito
Conhecer o FastTeacher

Versão Community grátis · Windows