Fiz uma plataforma de rastreamento veicular em Elixir, e o protocolo do rastreador foi a parte fácil
Desde setembro estou construindo o dalivim, o sistema de uma central de rastreamento que vai começar a operar na Bahia. A central compra os rastreadores, instala nos carros e motos dos clientes e cobra mensalidade. O sistema recebe as posições, mostra no mapa, guarda o histórico, manda alerta e envia o comando de bloqueio quando o carro é roubado.
Quero contar como ficou, o que eu decidi e por quê, e principalmente os bugs que me pegaram.

O que tem pronto
A central vê a frota inteira no mapa, cadastra cliente, veículo, rastreador e chip, recebe alertas (ignição, velocidade, SOS, corte de energia, rastreador offline, cerca) e manda comandos. A cobrança é por Pix.
O cliente acessa pelo celular. É um site que dá para instalar na tela inicial, então ninguém precisa baixar app de loja. Ele vê onde o carro está, com o endereço escrito, os trajetos do dia com as paradas, as faturas, e pode bloquear o próprio carro. Também consegue gerar um link de localização para mandar para alguém, que expira sozinho depois de algumas horas.
O instalador tem uma tela própria, feita para celular, que ele usa dentro da garagem: escolhe o veículo, vincula o IMEI do rastreador, manda a configuração por SMS e espera o primeiro pacote chegar.



Stack
Elixir 1.20, Phoenix 1.8 com LiveView, PostgreSQL com PostGIS, Oban para jobs e Thousand Island para o servidor TCP que conversa com os rastreadores. O mapa é MapLibre com tiles do OpenStreetMap servidos por mim, e o endereço por extenso vem de um Nominatim rodando no mesmo servidor, com o extrato do Nordeste. Não uso Google Maps em lugar nenhum.
É uma aplicação Phoenix só. Ela sobe em dois papéis: a parte web e o gateway que recebe os rastreadores.
Como um pacote chega até o mapa
rastreador (J16 4G)
| TCP, porta 5023, protocolo GT06
v
gateway: um processo Elixir por rastreador
1. separa os pacotes do fluxo TCP e confere o CRC
2. responde o ACK
3. grava o pacote bruto
4. decodifica a posição
v
pipeline por veículo: filtros, estado atual, viagens e paradas, alertas
|-> PostgreSQL
|-> PubSub -> LiveView -> navegador
Alguns pontos desse caminho deram mais trabalho do que parece.
O ACK sai antes de gravar no banco. O rastreador GT06 espera a confirmação por uns 5 segundos e, se ela não vem, derruba a conexão e reconecta. Se eu esperasse o banco para responder, uma lentidão no Postgres faria a frota inteira cair e voltar ao mesmo tempo. O lado ruim é que eu confirmo dado que ainda não está salvo. Para cobrir isso existe um buffer que escreve em disco quando o banco falha e uma chave de deduplicação para quando o rastreador reenvia o mesmo pacote.
Todo pacote que chega é gravado em binário, inclusive os que eu não consigo decodificar. Quando o rastreador faz algo estranho, é com isso que eu descubro o que ele mandou de verdade.
O decoder do GT06 é uma função pura: recebe um binário e devolve um struct, sem tocar em socket ou banco. Por isso consigo testar o protocolo inteiro com arquivos .hex de pacotes reais.
A posição usa a hora que vem no pacote, nunca a hora em que ele chegou. Rastreador que fica sem sinal guarda as posições e manda tudo de uma vez depois. Pacote com mais de 10 minutos de atraso entra no histórico, mas não gera alerta. Receber "ignição ligada" de duas horas atrás só confunde.
Por que o gateway não fica na Railway
A parte web roda na Railway, com deploy pelo GitHub. O gateway precisa ficar numa VPS com IP fixo, e o motivo é bem prático: o endereço do servidor fica gravado dentro de cada rastreador, configurado por SMS na instalação. Se o IP mudar, tenho que mandar SMS de novo para todos os carros. O proxy TCP da Railway sorteia a porta e o IP não é meu. Uso ele na bancada, mas não em produção.
Bloqueio remoto
O bloqueio é o principal motivo de alguém contratar rastreamento, e também a função que mais pode machucar alguém. Cortar o motor de um carro a 80 km/h numa BR causa acidente.
Então o pedido de bloqueio só passa com o veículo parado: velocidade abaixo de 5 km/h em pelo menos duas posições recentes. O cliente não tem como forçar. A central pode forçar, mas precisa confirmar. A confirmação é digitar a placa do carro, que é mais difícil de fazer por engano do que apertar um botão e não exige mais uma senha. Tudo fica registrado em audit_logs, com quem pediu, quando, a velocidade e a posição.
Outro detalhe: a tela diferencia "o rastreador respondeu" de "o motor foi cortado". O ACK do rastreador só mostra que o comando chegou. Se o relé foi mal instalado, o rastreador responde do mesmo jeito.
No print abaixo, o cliente tenta bloquear o carro andando a 45 km/h. A tela mostra a velocidade e a hora da leitura, explica que só um administrador pode forçar o bloqueio com o carro em movimento e deixa o botão de confirmar desativado.

Multi-tenant desde o começo
Hoje existe uma central só, mas toda tabela de domínio tem org_id. O Repo tem um prepare_query/3 que recusa qualquer query sem org_id, a não ser que ela declare skip_org_id: true de forma explícita. A hierarquia é organização, cliente e veículo, com papéis separados para administrador, operador, instalador e cliente. Achar vazamento de dado entre clientes depois que eles já existem sai muito mais caro.
Um rastreador falso para testar
Testar com rastreador de verdade é lento. Precisa de chip, de carro e de alguém dirigindo. Então escrevi um simulador que fala GT06 por TCP de verdade:
mix dalivim.sim.drive --imei 752533678900242 --loop
Ele faz login, espera o ACK, percorre a orla de Salvador da Barra até o Jardim dos Namorados, para, manda heartbeat e responde comandos, inclusive o de bloqueio. Do lado do servidor não tem diferença nenhuma entre ele e um J16.
No print da central lá em cima, o carro verde no canto de baixo, perto da Barra, é o simulador rodando.
O hardware real
O rastreador escolhido foi o J16 4G, da família GT06 e homologado na Anatel. GT06 é um nome que cobre muita coisa: o Traccar reconhece 16 variantes, e não achei documentação oficial do J16. Algumas coisas que só descobri na bancada:
- Mando
SERVER,1,<ip>,<porta>,0#, onde o modo 1 seria domínio, e ele respondeSERVER:0,.... O firmware grava como IP de qualquer jeito. - Com o fuso configurado em
GMT,W,0,0#, a hora do pacote chega mais ou menos 1 segundo antes da hora de recepção, o que é um bom sinal. - A tela do instalador travou em três pontos na primeira instalação de verdade. Nenhum teste tinha pegado. Corrigi no mesmo dia.
- Um bloqueio enviado pela central foi respondido pelo rastreador. O teste do relé cortando o motor num carro ainda vai acontecer.
Tudo que só o hardware pode confirmar está marcado como [VALIDAR] na documentação e tem um teste que registra a suposição. Quando o aparelho mostrar outra coisa, sei exatamente qual teste e qual código mudam.
Os bugs que os testes não pegavam
O projeto tem mais de 2.600 testes, Credo no modo estrito, Dialyzer sem erro e toda migration é revertida e aplicada de novo num banco descartável na CI. Mesmo assim, quando fiz rodadas de revisão com o objetivo de quebrar o que já estava aprovado, todas acharam problema sério. Alguns:
- Um script de backup montava código Elixir concatenando texto, e um dos pedaços era a saída do
pg_restore. Quem controlasse um arquivo de dump conseguia rodar código no servidor de produção. Não resolvi com um filtro melhor. Parei de gerar código a partir de texto. - O
/readyz, que libera o tráfego no deploy blue/green, nunca passava quando havia fila configurada. O ambiente de teste desliga as filas, então nenhum teste via isso. Só apareceu quando subi a aplicação. - Se dois administradores revogassem o acesso um do outro ao mesmo tempo, a organização ficava sem administrador. Reproduzi 60 vezes em 60 tentativas. A primeira correção resolveu só metade do problema.
- Uma aba aberta de alguém que já tinha perdido o acesso continuava conseguindo fazer ações de administrador.
Depois disso passei a conferir cada correção quebrando o código de propósito e vendo se o teste falha. Se o teste continua verde com o bug, ele não serve. Achei 18 testes assim, que passavam sem proteger nada.
Sobre usar IA
Escrevi boa parte do código com agentes de IA, com mais de 20 documentos de especificação em docs/ e um DECISOES.md que decide qualquer conflito entre eles. Cada commit e cada PR citam o ID do requisito que implementam (GT06-04, ING-10 etc.).
Algumas coisas que funcionaram: dar a cada agente uma lista de arquivos que ele pode mexer e outra que ele não pode; fazer a revisão num worktree separado, procurando o que o autor não testou; e só aceitar um bug reportado se vier com a entrada e a saída errada.
E uma coisa que aprendi rápido: conferir o que o agente diz que fez. Mais de uma vez um agente disse que o format e o credo estavam limpos e não estavam. Conferir é rodar um comando.
Números
| Testes automatizados | mais de 2.600 |
| Migrations, todas reversíveis | 39+ |
| Pull requests | 85 |
Linhas em lib/ | cerca de 60 mil |
| Tempo do ACK no gateway | cerca de 20 ms |
| Imagem de release | 145 MB |
Próximos passos
Agora falta testar o relé num carro, começar um piloto acompanhado com os primeiros clientes e, depois, adicionar o Teltonika (FMC920 e FMC130) como segundo protocolo.
Se você trabalha com telemetria, IoT ou Elixir em produção, queria saber como vocês resolvem ACK, buffer e deduplicação. E se quiser que eu detalhe alguma parte, como o framing do GT06, o cálculo de viagens ou o mapa auto-hospedado, deixa nos comentários.
Funcionamento
Se você quer ver o sistema funcionando postei um vídeo no Linkedin mostrando o sistema funcionando. Clica aqui.
Fonte: https://lnkd.in/p/eAfEPYZZ