Projeto
Projeto: mensageiro de pulso, dois relógios que conversam por ESP-NOW sem celular nem internet
Passo a passo de um par de comunicadores de pulso com a placa redonda ESP32-2424S012: frases rápidas por ESP-NOW, confirmação de entrega e só o botão BOOT.
Por Prof. Rodrigo Faustino · Atualizado em · 11 min de leitura
Métricas deste artigo
- Palavras
- 2.021
- Caracteres
- 11.476
- Tempo de leitura
- ~11 min
E se dois colegas pudessem trocar mensagens curtas pelo pulso, sem celular, sem chip, sem internet e sem roteador? Este projeto faz exatamente isso: dois relógios feitos com a placa redonda ESP32-2424S012 viram um par de comunicadores. Um aperto curto no botão escolhe uma frase — "Estou chegando", "Me espera", "Tudo certo?" —, um aperto longo envia, e o outro relógio acende a tela com a mensagem e devolve uma confirmação de entrega.
A conversa vai por ESP-NOW, o protocolo da Espressif que liga placas ESP32 diretamente, rádio a rádio. O artigo ESP-NOW na ESP32 explica o protocolo em detalhe; aqui ele vira um objeto de uso. É o projeto mais "inventivo" da série: ele lembra os pagers dos anos 1990 e os rádios de walkie-talkie, com a diferença de que o aluno escreve, linha por linha, o protocolo que faz a conversa funcionar.
Este artigo continua o relógio de pulso, que apresenta a placa redonda, a tela e o cuidado com a bateria. Os exemplos do simulador cobrem a parte que mais ensina — o pacote, a confirmação, o botão — e o código das placas fica no fim, para gravar quando os relógios estiverem nas mãos.
Por que frases prontas
Um relógio redondo de 1,28" não tem teclado, e digitar letra por letra com um botão só seria um castigo. A solução é a mesma dos pagers e de muitos rádios de equipe: uma lista de frases prontas, igual nas duas placas. O pacote não leva o texto, leva o número da frase. Isso tem três vantagens:
- o pacote fica pequeno — cinco bytes —, e o rádio passa menos tempo ligado, o que economiza bateria;
- não há problema com acentos ou tamanho de texto: a tela sabe desenhar cada frase da lista;
- a turma pode personalizar a lista — frases do time de robótica, da excursão, do grupo de estudo — e discutir quais mensagens realmente importam quando só cabem dez.
A limitação também é uma decisão de design interessante. Uma lista curta obriga a pensar em quem vai usar o mensageiro e em que situação: numa excursão, "me espera" e "onde você está?" valem mais que qualquer frase longa. Os pagers, que fizeram sucesso antes dos celulares, funcionavam com uma ideia muito parecida, e muita gente se comunicava bem com poucas mensagens combinadas. Pergunte à turma o que ela colocaria numa lista de dez frases para um dia de passeio da escola, e por quê.
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
Versão Community grátis · Windows
O que você vai precisar
- duas placas ESP32-2424S012, cada uma com a sua bateria (veja o relógio de pulso);
- o driver gc9a01py da tela, como no relógio;
- para testar sem as placas redondas: duas ESP32-C3 Super Mini (ficha) — o rádio é o mesmo chip, e o botão BOOT está no mesmo GPIO9. A mensagem aparece no Serial em vez da tela.
Passo 1: o pacote
O pacote tem quatro campos, empacotados com struct no formato '<BHBB' mais um byte de conferência:
- tipo (1 byte): 1 para mensagem, 2 para confirmação;
- seq (2 bytes): um número que cresce a cada mensagem enviada. Ele serve para o relógio que recebe perceber uma mensagem repetida — o que acontece quando a confirmação se perde e a outra placa reenvia;
- frase (1 byte): a posição da frase na lista;
- conferência (1 byte): a soma dos quatro primeiros bytes, cortada em 8 bits (
& 0xFF). O próprio ESP-NOW já confere os pacotes no rádio, mas um campo de conferência no protocolo é um ótimo jeito de ensinar a ideia — e protege contra erros do próprio programa, como mandar o pacote de outro projeto para este relógio.
O exemplo monta uma mensagem, mostra os bytes, desmonta de volta e testa um pacote estragado. Ele roda sem circuito.
import struct
FRASES = ['Estou chegando', 'Me espera', 'Tudo certo?', 'Sim', 'Não',
'Já saí', 'Atrasei 10 min', 'Me liga', 'Onde você está?', 'Chegou!']
MSG, OK = 1, 2
def montar(tipo, seq, frase=0):
corpo = struct.pack('<BHB', tipo, seq, frase)
return corpo + bytes([sum(corpo) & 0xFF])
def ler(pacote):
if len(pacote) != 5 or sum(pacote[:4]) & 0xFF != pacote[4]:
return None # tamanho errado ou conferência não bate
return struct.unpack('<BHB', pacote[:4])
p = montar(MSG, 17, 0)
print('pacote:', ' '.join('%02x' % b for b in p), '(%d bytes)' % len(p))
tipo, seq, frase = ler(p)
print('lido: tipo', tipo, 'seq', seq, 'frase:', FRASES[frase])
estragado = bytearray(p)
estragado[3] = 4 # um byte trocado no caminho
print('pacote estragado:', ler(bytes(estragado)))O < no formato manda o struct usar a ordem little-endian e não acrescentar bytes de alinhamento: as duas placas precisam concordar exatamente sobre a posição de cada byte. É o mesmo cuidado do artigo de ESP-NOW.
Passo 2: confirmação e nova tentativa
Rádio perde pacotes: alguém passou entre os dois, a outra pessoa está longe, há muito ruído. Um mensageiro sério não pode simplesmente torcer. O protocolo do projeto é:
- quem envia manda a mensagem e espera a confirmação (o tipo OK, com o mesmo seq) por um tempo;
- se ela não vier, reenvia a mesma mensagem, com o mesmo seq, até três vezes;
- quem recebe sempre responde OK — mesmo a uma mensagem repetida —, mas só mostra a mensagem se o seq for novo.
O item 3 é o pulo do gato: se a mensagem chegou e só a confirmação se perdeu, a nova tentativa chega como repetida. O relógio do Beto responde OK de novo, para acalmar o da Ana, mas não mostra "Estou chegando" duas vezes.
O exemplo simula as duas placas num programa só, com um "rádio" que perde pacotes escolhidos na lista PERDIDOS: a primeira mensagem perde a confirmação, e a segunda perde o primeiro envio.
import struct
def montar(tipo, seq, frase=0):
corpo = struct.pack('<BHB', tipo, seq, frase)
return corpo + bytes([sum(corpo) & 0xFF])
def ler(p):
if len(p) != 5 or sum(p[:4]) & 0xFF != p[4]:
return None
return struct.unpack('<BHB', p[:4])
FRASES = ['Estou chegando', 'Me espera', 'Tudo certo?']
PERDIDOS = {2, 5} # números dos pacotes que o "rádio" perde
contador = 0
def radio(pacote):
global contador
contador += 1
return None if contador in PERDIDOS else pacote
class Relogio:
def __init__(self, nome):
self.nome, self.ultimo_seq = nome, None
def receber(self, pacote):
tipo, seq, frase = ler(pacote)
if seq != self.ultimo_seq:
self.ultimo_seq = seq
print(' [%s] tela acende: "%s"' % (self.nome, FRASES[frase]))
else:
print(' [%s] repetida (seq %d): só confirma' % (self.nome, seq))
return montar(2, seq)
beto = Relogio('Beto')
def enviar(seq, frase, tentativas=3):
for t in range(1, tentativas + 1):
print('Ana envia seq %d, tentativa %d' % (seq, t))
chegou = radio(montar(1, seq, frase))
if chegou is None:
print(' (mensagem perdida)')
continue
resposta = radio(beto.receber(chegou))
if resposta and ler(resposta)[1] == seq:
print(' Ana recebe OK: entregue')
return True
print(' (confirmação perdida)')
print(' não deu: avisa na tela "sem resposta"')
return False
enviar(17, 0)
enviar(18, 1)Leia a saída com calma: na primeira mensagem, a confirmação se perde e o Beto recebe duas cópias, mas vê a frase uma vez só. Na segunda, o primeiro envio some no caminho, e a nova tentativa resolve. Mude o PERDIDOS para {1, 3, 5} e veja o que acontece quando o rádio perde tudo o que a Ana envia.
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
Plano Essencial grátis para sempre
Passo 3: um botão, dois comandos
A placa redonda tem um botão livre, o BOOT, no GPIO9, com resistor de pull-up: ele lê 1 solto e 0 apertado. Com um botão só, o truque é medir quanto tempo ele ficou apertado:
- curto (menos de 600 ms): passa para a próxima frase;
- longo (600 ms ou mais): envia a frase que está na tela.
Os 600 ms são uma escolha do projeto: curtos o bastante para não cansar, longos o bastante para não enviar sem querer. Para medir, o programa anota o ticks_ms() quando o botão desce e calcula a diferença quando ele sobe — e ignora apertos de poucos milissegundos, que são o repique do contato, explicado na lição debounce. O exemplo testa a regra com uma lista de durações de aperto.
FRASES = ['Estou chegando', 'Me espera', 'Tudo certo?', 'Sim', 'Não']
LONGO_MS = 600
REPIQUE_MS = 30
atual = 0
apertos = [120, 8, 150, 900, 90, 40, 700] # ms que o botão ficou apertado
for duracao in apertos:
if duracao < REPIQUE_MS:
print('%4d ms: repique, ignorado' % duracao)
elif duracao < LONGO_MS:
atual = (atual + 1) % len(FRASES)
print('%4d ms: curto -> tela mostra "%s"' % (duracao, FRASES[atual]))
else:
print('%4d ms: longo -> ENVIA "%s"' % (duracao, FRASES[atual]))O % len(FRASES) faz a lista dar a volta: depois da última frase, vem a primeira de novo.
Passo 4: emparelhar os dois relógios
O ESP-NOW endereça as placas pelo MAC, o número único do rádio de cada uma. Para emparelhar:
- grave em cada placa um programa curto que imprime o MAC:
network.WLAN(network.STA_IF).config('mac'), convertido para texto combinascii.hexlify, como no artigo de ESP-NOW; - anote os dois MACs e escreva em cada relógio o MAC do outro, numa constante
PARno começo do programa.
Uma alternativa mais elegante, que fica de desafio: um "modo de emparelhamento", em que os dois relógios mandam uma mensagem de broadcast e cada um guarda o MAC de quem respondeu num arquivo.
Passo 5: o programa da placa
O programa junta as partes: o rádio, o botão, a tela e o protocolo. Para ficar legível, ele mostra só o essencial — a parte da tela usa o mesmo driver e o mesmo cuidado de energia do relógio de pulso, e as funções mostrar() e luz() ficam a cargo de cada grupo. Este trecho é para gravar na placa: o simulador ainda não tem o ESP-NOW.
import network, espnow, struct
from machine import Pin
from time import ticks_ms, ticks_diff, sleep_ms
PAR = b'\xaa\xbb\xcc\xdd\xee\xff' # o MAC do OUTRO relógio (troque!)
FRASES = ['Estou chegando', 'Me espera', 'Tudo certo?', 'Sim', 'Não']
MSG, OK = 1, 2
sta = network.WLAN(network.STA_IF)
sta.active(True)
e = espnow.ESPNow()
e.active(True)
e.add_peer(PAR)
boot = Pin(9, Pin.IN, Pin.PULL_UP)
def montar(tipo, seq, frase=0):
corpo = struct.pack('<BHB', tipo, seq, frase)
return corpo + bytes([sum(corpo) & 0xFF])
def ler(p):
if not p or len(p) != 5 or sum(p[:4]) & 0xFF != p[4]:
return None
return struct.unpack('<BHB', p[:4])
def mostrar(texto): # desenhe na GC9A01 (veja o artigo do relógio)
print(texto)
def enviar(seq, frase):
for _ in range(3):
e.send(PAR, montar(MSG, seq, frase))
limite = ticks_ms()
while ticks_diff(ticks_ms(), limite) < 300: # espera o OK por 300 ms
mac, p = e.recv(50)
r = ler(p)
if r and r[0] == OK and r[1] == seq:
return True
return False
seq, atual, ultimo_recebido = 0, 0, None
apertou_em = None
mostrar(FRASES[atual])
while True:
mac, p = e.recv(20) # chegou algo?
r = ler(p)
if r and r[0] == MSG:
e.send(mac, montar(OK, r[1])) # sempre confirma
if r[1] != ultimo_recebido:
ultimo_recebido = r[1]
mostrar('Recebido: ' + FRASES[r[2]])
if boot.value() == 0 and apertou_em is None:
apertou_em = ticks_ms()
elif boot.value() == 1 and apertou_em is not None:
duracao = ticks_diff(ticks_ms(), apertou_em)
apertou_em = None
if 30 <= duracao < 600:
atual = (atual + 1) % len(FRASES)
mostrar(FRASES[atual])
elif duracao >= 600:
seq = (seq + 1) % 65536
mostrar('Entregue!' if enviar(seq, atual) else 'Sem resposta')
sleep_ms(10)Para testar com duas Super Mini antes das placas redondas: grave o mesmo programa nas duas (trocando o PAR), abra o Serial de cada uma e use o botão BOOT da própria Super Mini, que também está no GPIO9.
Um detalhe que a turma vai perceber: enquanto um relógio está enviando e esperando a confirmação, ele não confere o botão. Com 300 ms de espera e três tentativas, isso dura no máximo perto de um segundo — aceitável num mensageiro. Se o projeto crescer, o caminho é o aioespnow, que o artigo de ESP-NOW apresenta, para fazer as duas coisas ao mesmo tempo.
Bateria e alcance
Um comunicador precisa estar escutando para receber, e um rádio escutando gasta. O artigo baterias na ESP32 mostra a diferença entre o rádio ligado e a placa dormindo. Para um mensageiro de excursão, que precisa durar um dia, apague a tela sempre que possível e meça o consumo real. Uma versão mais econômica pode escutar em janelas: acordar a cada poucos segundos, escutar um pouco e dormir — o que exige que o remetente insista por mais tempo. É um ótimo problema de projeto para discutir em grupo, porque toda escolha troca bateria por rapidez.
O alcance depende da antena, dos obstáculos e do corpo de quem usa: a mão e o pulso absorvem sinal. O módulo da placa redonda usa antena externa (é a versão "U" do módulo), então a posição da antena dentro da caixa importa. Testem em grupo: até que distância a confirmação volta num corredor? E com uma parede no meio?
Quem mais escuta?
Uma pergunta que sempre aparece: outra pessoa com uma ESP32 consegue ler as mensagens? Sem cuidado nenhum, sim — o rádio é um meio compartilhado, e quem estiver no mesmo canal e souber o formato do pacote pode ouvir. O ESP-NOW tem uma resposta para isso: ele pode cifrar as mensagens trocadas entre pares cadastrados, com uma chave combinada nas duas placas. A documentação do módulo espnow mostra como, com o set_pmk() e a chave local de cada par no add_peer().
Vale levar a conversa para a turma: que tipo de mensagem poderia ser enviada sem cifra ("cheguei"), e que tipo nunca deveria ("a senha do armário é...")? Que outras informações o próprio pacote revela, mesmo cifrado — por exemplo, quando e quantas vezes duas pessoas conversaram? É uma introdução honesta a privacidade, com um objeto que eles mesmos construíram.
Roteiro sugerido para a turma
O projeto cabe bem em quatro encontros, em duplas — cada dupla com os seus dois relógios, ou com duas Super Mini:
- pacote: rodar o exemplo do passo 1, inventar a lista de frases da dupla e decidir quantas cabem num byte (a resposta surpreende: 256);
- protocolo: rodar o exemplo do passo 2, mudar o
PERDIDOSe explicar, por escrito, por que o seq existe; - placa: gravar o programa do passo 5, emparelhar os dois relógios e trocar as primeiras mensagens;
- teste de campo: medir o alcance em lugares diferentes da escola e registrar quantas mensagens precisaram de segunda tentativa.
No fim, cada dupla apresenta uma melhoria que faria no protocolo e por quê. As respostas costumam ir de "confirmação de leitura" a "mensagem de emergência que toca alto" — e todas são problemas reais de engenharia de comunicação.
Ideias para ir além
- Grupo: com o endereço de broadcast, uma mensagem chega a todos os relógios ao mesmo tempo. Que mudanças o protocolo precisa para saber quem confirmou?
- Vibração: um pequeno motor de vibração, com transistor, avisa sem barulho. Na placa redonda não há pinos livres, mas o conector da serial pode ser estudado para isso — depois de conferir quais GPIOs ele traz.
- Toque: nas versões com toque, deslizar o dedo pode trocar de frase e tocar duas vezes pode enviar.
- Frases pelo celular: um modo de configuração com servidor web para editar a lista de frases sem regravar o programa — lembrando que a lista precisa ser igual nos dois relógios.
O que este projeto ensina
Este projeto ensina, na prática, o que faz qualquer comunicação funcionar: um formato combinado (o pacote), uma forma de saber que a mensagem chegou (a confirmação), um jeito de lidar com perdas (as novas tentativas) e de evitar duplicatas (o seq). É, em miniatura, o que acontece dentro de toda mensagem de celular. Depois de construir o mensageiro, a turma nunca mais vai olhar para os "dois risquinhos" de entrega de um aplicativo de mensagens do mesmo jeito.
Para continuar
O botão e o repique estão na trilha Entradas digitais (botão e debounce); o tempo sem travar e os estados, na trilha Tempo sem travar (ticks e máquina de estados); as telas, na trilha Displays. O artigo ESP-NOW na ESP32 é a base do rádio.
Fontes
- Documentação do módulo
espnowdo MicroPython:ESPNow,add_peer,send,recve o broadcast. - Pinos e componentes da ESP32-2424S012: configuração da placa no projeto HomeDing, a conferir numa placa de verdade.
- Documentação do MicroPython:
struct.