Pular para conteúdo

Contrato de Telemetria

Apêndice do Projeto conceitual de software. Define, sem ambiguidade, as mensagens trocadas entre o Micromouse, a ponte Bluetooth e o backend, para que as três partes possam ser implementadas e testadas separadamente (RNF-B13). Resolve as pendências "Formato das mensagens de telemetria" e "Detalhes da ponte Bluetooth" da arquitetura.

Versão do contrato: 1.

Fonte única dos exemplos: src/contrato/exemplos.jsonl. Os exemplos desta página são cópias desse arquivo, e os testes do backend e do firmware conferem que cada lado gera exatamente essas linhas (seção Implementações e testes).

Visão geral

Figura 1: Visão geral do contrato de telemetria

Visão geral do contrato de telemetria

Fonte: Wanjo Christopher Paraizo Escobar, 2026. · Fonte editável (Excalidraw)

  • O robô só envia telemetria. Ele recebe apenas os três comandos do contrato: interromper, largar e health_check (seção Comando da web para o robô, RF39, RNF06).
  • A ponte é "burra": não interpreta as mensagens. Ela lê cada linha da serial, anota a hora em que leu e repassa à API. Toda a validação fica no backend.

Enquadramento

  • Uma mensagem por linha, em JSON compacto (sem espaços), codificado em UTF-8 e terminado em \n.
  • Cada linha tem no máximo 256 bytes, contando o \n.
  • A ordem das chaves é a dos exemplos. O backend não depende da ordem, mas o firmware sempre gera nessa ordem, o que permite comparar byte a byte nos testes.

Campos comuns

Toda mensagem do robô começa com estes campos.

Tabela 1: Campos comuns a todas as mensagens

Campo Tipo Unidade / faixa Descrição
v inteiro sempre 1 Versão do contrato. O backend descarta qualquer outro valor (seção Versionamento).
boot inteiro 0 a 4 294 967 295 Contador de inicializações do robô, guardado na NVS e somado em 1 a cada vez que ele liga.
seq inteiro 0 a 4 294 967 295 Número da mensagem dentro do boot. Volta a 0 quando o robô liga e soma 1 a cada mensagem, de qualquer tipo.
t_ms inteiro ms, 0 a 4 294 967 295 Milissegundos desde que o robô ligou, no instante do envio.
tipo texto ver Mensagens do robô Define os demais campos.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

O par (boot, seq) identifica cada mensagem de forma única, mesmo depois de o robô reiniciar em uma retomada (RF38).

Mensagens do robô

As mensagens se dividem em dois grupos:

  • Amostra: a tel. Se uma se perder, a seguinte chega logo e a substitui. Não é reenviada.
  • Eventos: hc_item, hc_resultado, passo, falha e sucesso. Cada um registra algo que aconteceu uma vez e não pode se perder; por isso entram no buffer de reenvio (seção Reconexão).

tel: estado atual do robô

Enviada continuamente: 5 por segundo com o robô em movimento e 1 por segundo parado. Alimenta o painel ao vivo, serve de sinal de vida para o RF31 (10 s sem mensagem = link perdido) e cumpre o RF08 (célula, bateria e instante de envio).

{"v":1,"boot":7,"seq":42,"t_ms":11250,"tipo":"tel","estado":"running","x":0,"y":2,"rumo":"N","bat_mv":7810,"vel_mm_s":210,"eixo_longo":null}

Tabela 2: Campos da mensagem tel

Campo Tipo Unidade / faixa Descrição
estado texto health-check, running, success ou failed Fase da tentativa do ponto de vista do robô (RF16).
x, y inteiro célula, 0 a 11 Célula atual, no referencial do robô.
rumo texto N, S, L ou O Direção para a qual o robô aponta, no mesmo referencial. A web a usa para desenhar o robô.
bat_mv inteiro mV, 0 a 10 000 Tensão da bateria. O backend converte para % pela curva da fonte definida pela Energia (RF11).
vel_mm_s inteiro mm/s, 0 a 2 000 Velocidade medida pelos encoders (RF22).
eixo_longo texto ou nulo x, y ou null Eixo em que o labirinto 8x4 ou 12x4 se estende. null enquanto o robô não sabe, e sempre no 4x4.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

hc_item: um componente do health-check

Enviada uma vez por componente, assim que o robô termina de testá-lo (RF29). A web marca o checklist em tempo real e mostra qual componente reprovou.

{"v":1,"boot":7,"seq":0,"t_ms":412,"tipo":"hc_item","componente":"bateria","aprovado":true,"valor":7840}
{"v":1,"boot":7,"seq":5,"t_ms":1630,"tipo":"hc_item","componente":"motor_esquerdo","aprovado":true,"valor":null}

Tabela 3: Campos da mensagem hc_item

Campo Tipo Unidade / faixa Descrição
componente texto um dos 8 da lista fixa (Tabela 4) Componente verificado.
aprovado booleano true ou false Resultado da verificação.
valor inteiro ou nulo unidade conforme o componente (Tabela 4) Medida que justificou o resultado.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Tabela 4: Componentes do health-check e unidade do valor

Componente valor
bateria Tensão em mV.
tof_frontal, tof_esquerdo, tof_direito Distância lida, em mm.
encoder_esquerdo, encoder_direito Pulsos contados ao girar o motor no teste.
motor_esquerdo, motor_direito null: o motor é aprovado pelo próprio encoder.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

hc_resultado: fim do health-check

Enviada uma vez, depois do último hc_item. Traz o tipo de labirinto lido no DIP switch (RF27) e se a tentativa começa do zero ou continua o mapa gravado (RF38).

{"v":1,"boot":7,"seq":10,"t_ms":2380,"tipo":"hc_resultado","aprovado":true,"tipo_dip":"4x4","inicio":"nova"}

Tabela 5: Campos da mensagem hc_resultado

Campo Tipo Unidade / faixa Descrição
aprovado booleano true ou false Resultado geral. Se false, o robô não anda e o backend recusa abrir a tentativa (health_check_reprovado).
tipo_dip texto 4x4, 8x4, 12x4 ou invalido Tipo lido nas vias 1 e 2 do DIP switch. invalido (código 11) sempre vem com aprovado = false.
inicio texto nova ou retomada retomada quando o robô continua o mapa e a posição gravados na flash (RF42).

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

O backend compara o tipo_dip com o tipo informado em Nova execução. Se divergirem, o backend recusa abrir a tentativa (dip_divergente) e a web mostra um alerta (ver Tipo do labirinto e orientação).

passo: uma célula do trajeto

Enviada toda vez que o robô entra em uma célula, inclusive quando volta a uma célula já visitada; senão, o trajeto ficaria com buracos. Alimenta o desenho do mapa e do trajeto na web (RF09, RF37).

{"v":1,"boot":7,"seq":43,"t_ms":11400,"tipo":"passo","x":0,"y":3,"paredes":9}

Tabela 6: Campos da mensagem passo

Campo Tipo Unidade / faixa Descrição
x, y inteiro célula, 0 a 11 Célula em que o robô entrou.
paredes inteiro máscara, 0 a 15 Soma das paredes presentes: N = 1, S = 2, L = 4, O = 8.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

No exemplo, 9 = 1 + 8: paredes ao norte e a oeste.

falha: a tentativa parou sem chegar

Enviada quando o robô para por conta própria, quando confirma uma interrupção da web ou quando o operador toca no BOOT durante a corrida (RF41).

{"v":1,"boot":7,"seq":88,"t_ms":25120,"tipo":"falha","motivo":"falha_componente","origem":"automatica","x":2,"y":3,"componente":"tof_direito"}
{"v":1,"boot":8,"seq":57,"t_ms":19870,"tipo":"falha","motivo":null,"origem":"web","x":1,"y":2,"componente":null}
{"v":1,"boot":9,"seq":63,"t_ms":21040,"tipo":"falha","motivo":"encerrado_operador","origem":"boot","x":1,"y":3,"componente":null}

Tabela 7: Campos da mensagem falha

Campo Tipo Unidade / faixa Descrição
motivo texto ou nulo collision, stuck, falha_componente, low_battery, encerrado_operador ou null Motivo da parada (RF15). Depende da origem (Tabela 8).
origem texto automatica, web ou boot Quem causou a parada.
x, y inteiro célula, 0 a 11 Célula em que o robô parou; ponto de partida da retomada.
componente texto ou nulo um dos 8 da Tabela 4, ou null Preenchido só quando motivo = falha_componente.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Tabela 8: Combinações válidas de origem e motivo

origem motivo Como o backend registra
automatica collision, stuck, falha_componente ou low_battery Cria a FALHA com o motivo e a origem automatica.
web null Só confirma a interrupção. A FALHA já foi criada pelo modal Encerrar tentativa, com o motivo escolhido; o backend atualiza a célula com o x, y recebidos, porque é onde o robô de fato parou.
boot encerrado_operador Cria a FALHA com motivo e origem encerrado_operador (RF41).

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Os motivos time_exceeded e link_lost são decididos só pelo backend (RF31, RF32) e nunca vêm do robô. O out_of_track só é escolhido pelo operador no modal. Um health-check reprovado vai no hc_resultado, sem falha separada.

sucesso: o robô chegou ao objetivo

Enviada uma vez, quando o robô entra em uma célula de objetivo do tipo selecionado no DIP switch (RF26).

{"v":1,"boot":11,"seq":131,"t_ms":48200,"tipo":"sucesso","x":3,"y":3}

Tabela 9: Campos da mensagem sucesso

Campo Tipo Unidade / faixa Descrição
x, y inteiro célula, 0 a 11 Célula de objetivo alcançada.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Referencial e paredes

As coordenadas estão no referencial do robô, descrito em Tipo do labirinto e orientação:

  • A largada é a célula (0, 0). O eixo y aponta para a saída do beco da largada.
  • O sinal de x é fixado na primeira abertura lateral. Antes disso o robô só anda em y, com x = 0.
  • Com isso, x e y estão sempre entre 0 e 11.

A máscara de paredes e o rumo usam direções absolutas nesse referencial: N = +y, S = −y, L = +x e O = −x. Enquanto o sinal de x não está fixado, as células visitadas têm parede dos dois lados, então L e O não são ambíguos. O firmware só envia o passo da célula da primeira abertura lateral depois de fixar o sinal.

Desenho na web. No 8x4 e no 12x4, o robô não sabe de antemão se o lado longo do labirinto fica em x ou em y. Enquanto eixo_longo for null, a web desenha só o canto 4x4 junto à largada e expande a grade quando ele passa a x ou y. O robô define o eixo_longo ao entrar na primeira célula com x > 3 ou y > 3.

Taxas

Tabela 10: Taxas de envio

Mensagem Taxa
tel 5 por segundo em movimento; 1 por segundo parado.
Eventos Na hora em que acontecem (um passo a cada célula).
Reenvio depois de uma reconexão No máximo 20 por segundo, além da tel.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Em regime normal, o total fica abaixo das 10 mensagens por segundo do RNF-B11. A tel a 1 Hz com o robô parado mantém o sinal de vida, sem uma mensagem heartbeat separada.

Comando da web para o robô

São três comandos. Todos seguem a mesma regra: o backend devolve o comando em toda resposta ao POST /telemetria até o robô mostrar que o atendeu. Assim, um comando não se perde se uma resposta cair, e repeti-lo não tem efeito colateral.

Interrupção do encerramento manual (RF30, RF39):

{"v":1,"cmd":"interromper"}
  • Vai nas respostas até a falha com origem = web que confirma a parada, ou até abrir uma nova tentativa.
  • O robô para, envia a falha de confirmação e ignora o comando se já estiver parado, apenas repetindo a confirmação.
  • Com o robô em movimento, a tel a 5 Hz faz o comando chegar em até cerca de 200 ms. Sem link, o comando chega na reconexão; a parada sem rede continua sendo o botão BOOT (RF41).

Largada, dada pelo operador em Nova execução ou Retomar:

{"v":1,"cmd":"largar"}
  • Vai nas respostas até a primeira tel com estado = running ou o primeiro passo.
  • O robô só parte com o último health-check aprovado; enquanto espera, manda tel com estado = health-check a 1 Hz.

Novo health-check, pedido pelo operador (botão Pedir novo health-check), por exemplo depois de um reprovado, de trocar o DIP ou de passar a validade de 7 minutos:

{"v":1,"cmd":"health_check"}
  • Vai nas respostas até o primeiro hc_item ou hc_resultado, ou até uma largada.
  • O robô só atende parado, fora de uma tentativa; o backend recusa o pedido (409) com uma tentativa em running.
  • O robô roda o health-check completo e envia os hc_item e o hc_resultado, como faz ao ligar.

Ponte e API

A ponte roda no notebook, fora do Docker, e conversa com a API por um único endpoint.

POST /telemetria

{"linha":"{\"v\":1,\"boot\":7,\"seq\":42,...}","recebido_em":"2026-10-05T14:03:21.512-03:00"}
  • linha: a linha lida da serial, sem o \n, sem nenhuma alteração.
  • recebido_em: instante em que a ponte leu a linha, com fuso horário. É o ponto mais próximo do rádio, então o RNF-B04 mede o backend sem o atraso da ponte.

Resposta

{"comandos":[{"v":1,"cmd":"interromper"}]}

A lista vem vazia ({"comandos":[]}) quando não há comando. A ponte escreve cada comando na serial como JSON compacto, terminado em \n.

API fora do ar. Se o POST falhar, a ponte guarda as linhas numa fila em memória e as reenvia em ordem quando a API voltar, com no máximo 100 linhas (10 s a 10 mensagens por segundo). Com a fila cheia, a ponte descarta primeiro as linhas não classificáveis (que não são um objeto JSON com tipo), depois a tel mais antiga e, por último, o evento mais antigo. Esse é o único ponto em que ela olha o conteúdo da linha, e só o tipo, sem alterá-la. O robô continua a corrida normalmente (RNF-B07).

Validação no backend

O backend valida cada linha com o schema Pydantic do contrato. Uma linha é descartada e registrada só em log, sem interromper o serviço (RNF-B09), quando:

  • passa de 256 bytes ou não é JSON;
  • tem v diferente de 1 ou tipo desconhecido;
  • falta um campo, um campo tem o tipo errado ou um valor sai da faixa das tabelas;
  • origem, motivo e componente da falha não combinam (Tabela 8);
  • a célula está fora dos limites do labirinto da execução. Por exemplo, num 4x4, x ou y maior que 3.

Campos extras desconhecidos são ignorados, sem descartar a mensagem.

Mensagem sem tentativa aberta. Se não houver tentativa em running (por exemplo, o robô foi ligado antes de Nova execução, ou a mensagem chegou atrasada depois de um encerramento), a mensagem é descartada e registrada só em log. A exceção é a falha com origem = web, que confirma a interrupção da tentativa recém-encerrada.

Reconexão e mensagens repetidas

Regra para quedas de até 10 s (RNF-B08). A partir de 10 s sem mensagem, vale o RF31 (link_lost).

  1. No firmware: todo evento entra num buffer circular de 64 eventos no momento em que é gerado, em ordem de seq. Em 10 s de queda, o pior caso é de cerca de 15 passo e alguns outros eventos, então o buffer não enche. Se encher, o evento mais antigo é descartado.
  2. Ao reconectar: o robô reenvia o buffer inteiro, do mais antigo para o mais novo, a no máximo 20 por segundo. Eventos novos entram no fim do buffer e saem depois dos antigos, então os eventos sempre chegam em ordem de seq. A tel continua saindo normalmente e não é reenviada.
  3. No backend: para cada boot, guarda o maior seq de evento já aceito. Um evento com seq menor ou igual a esse é repetido e é descartado. A tel não passa por essa regra.

Não há confirmação (ack) do backend para o robô: reenviar o buffer inteiro custa no máximo 64 linhas por reconexão e mantém o downlink restrito aos três comandos do contrato (RNF06).

Tempo e horário de Brasília

  • O relógio oficial é o do backend. O recebido_em é gravado como TIMESTAMPTZ: o PostgreSQL guarda em UTC e a web mostra em America/Sao_Paulo. Os tempos de tentativa e de execução (RF13, RF32) usam esse relógio.
  • O robô não tem relógio de parede nem recebe comando de sincronia. O t_ms mede o instante de envio no relógio interno do robô.
  • O backend converte o t_ms em hora real e grava em enviado_em (RF08). Na primeira mensagem aceita de cada boot, ele calcula uma âncora = recebido_em − t_ms, fixada só depois de a mensagem ser gravada; em cada mensagem seguinte do mesmo boot, enviado_em = âncora + t_ms. O erro é a latência do Bluetooth na primeira mensagem, da ordem de dezenas de milissegundos, e os intervalos entre mensagens do mesmo boot saem exatos. Assim, o enviado_em também é exibido em horário de Brasília.
  • A interrupção é gerada pelo próprio backend, então o horário dela é exato.

Versionamento

O campo v protege contra firmware e backend em versões diferentes do contrato, por exemplo um robô gravado com firmware antigo.

Tabela 11: Quando mudar a versão

Mudança Exemplo Versão
Acrescentar um campo opcional temp_c na tel Continua v = 1. Quem não conhece o campo o ignora.
Mudar ou remover um campo, ou mudar uma unidade bat_mv → bat em % Passa a v = 2, com firmware e backend mudando juntos, num PR aprovado pelos dois.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

O backend aceita só v = 1 e descarta as outras versões com registro em log, o que deixa o erro visível em vez de gravar dados errados.

Onde cada mensagem é gravada

Tabela 12: Mensagens e tabelas do banco

Mensagem Tabela do DER Observação
tel LEITURA_TELEMETRIA Uma linha por tel. bat_mv → bateria (em mV); vel_mm_s → velocidade; t_ms → enviado_em (seção Tempo). O boot e o seq só servem à deduplicação, em memória, e não são gravados.
hc_item HEALTH_CHECK_ITEM valor → valor_lido.
hc_resultado TENTATIVA tipo_dip e inicio → tipo_dip e tipo_inicio; muda o status para running ou failed.
passo PASSO_TRAJETO e PAREDE_CELULA Na mesma transação; paredes → paredes_mask e as quatro colunas de parede.
falha FALHA e TENTATIVA Conforme a Tabela 8.
sucesso TENTATIVA Status success e métricas finais.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Implementações e testes

Tabela 13: Arquivos do contrato

Arquivo Conteúdo
src/contrato/exemplos.jsonl Um exemplo real de cada tipo de mensagem. Fonte única dos exemplos desta página.
src/backend/contrato/telemetria.py Schema Pydantic, leitura e escrita das linhas, deduplicação, conversão do t_ms em enviado_em e modelos do POST /telemetria.
src/backend/tests/test_contrato_telemetria.py Lê cada exemplo e o gera de volta byte a byte; testa as linhas inválidas e a deduplicação.
src/firmware/lib/contrato/telemetria.h e .c Estruturas e serializador em C, com snprintf e sem alocação dinâmica.
src/firmware/lib/contrato/teste/teste_telemetria.c Monta as mesmas mensagens e compara byte a byte com o arquivo de exemplos.

Fonte: Wanjo Christopher Paraizo Escobar, 2026.

Para rodar os testes:

cd src/backend && pip install -e ".[dev]" && pytest   # backend (Python 3.11+)
make -C src/firmware/lib/contrato teste   # firmware, compilado com gcc no PC

Qualquer mudança no contrato começa pelo exemplos.jsonl. Se o schema Pydantic ou o serializador em C não acompanharem, o teste correspondente falha.