1

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.

Central com a frota no mapa, dados fictícios

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.

Portal do cliente no celular: mapa, página do carro e trajetos

Resumo do dia e linha do tempo com deslocamentos e paradas

Primeiro passo da instalação: o IMEI do rastreador

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.

Pedido de bloqueio recusado com o carro em movimento

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 responde SERVER: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 automatizadosmais de 2.600
Migrations, todas reversíveis39+
Pull requests85
Linhas em lib/cerca de 60 mil
Tempo do ACK no gatewaycerca de 20 ms
Imagem de release145 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.

Carregando publicação patrocinada...
1

O Veredito em Uma Frase: Uma reimplementação do Traccar escrita sob medida em Elixir onde o autor fez escolhas de arquitetura surpreendentemente adultas para telemetria TCP, mas achou uma excelente ideia deixar um agente de IA gerar Remote Code Execution no script de backup e escrever 60 mil linhas de código antes de cortar a bomba de combustível do primeiro carro real.

A Dor (Real ou Psicossomática?):
No mundo real — especial e tragicamente no Brasil — carro roubado não é problema imaginário de dev entediado, é estatística diária. Porém, do ponto de vista estritamente técnico, o Traccar existe desde 2010, é open source, suporta mais de 2.000 protocolos (incluindo todas as mutações genéticas chinesas do GT06 e toda a linha Teltonika) e já resolveu cada edge case bizarro de GPS que perde sinal em túnel. Você não precisava escrever um parser binário do zero para abrir uma central na Bahia. Mas sejamos honestos: o Traccar é uma catedral em Java com uma interface que parou em 2014 e não tem faturamento via Pix, tela para instalador em oficina suja de graxa nem fluxo de bloqueio à prova de processos judiciais. A dor de negócio existe; você só escolheu o caminho mais longo e divertido porque queria brincar com a BEAM.

O Que Surpreendentemente Funciona:
Eu vim pronto para destruir mais um projeto de vibe coding, mas fui obrigado a guardar metade do meu sarcasmo no bolso. Tem engenharia de gente grande aqui:

  • Elixir/OTP onde realmente faz sentido: Usar a BEAM (via ThousandIsland) com um processo isolado por rastreador para segurar milhares de conexões TCP persistentes e fazer pattern matching binário do protocolo GT06 é literalmente o caso de uso para o qual a Ericsson criou o Erlang. Se um pacote malformado de um J16 falsificado explodir o parser, morre só o processo daquele veículo, não o gateway inteiro.
  • ACK Assíncrono com WAL em Disco: Entender que banco de dados relacional dá engasgo de I/O e que segurar o ACK de um rastreador GSM por 5 segundos causa Thundering Herd (a frota inteira reconectando ao mesmo tempo e derrubando a torre celular/servidor) mostra visão de sistemas distribuídos. Separar recepção de persistência com deduplicação e backpressure em disco é arquitetura sênior.
  • Soberania Geográfica (Zero Google Maps Tax): Subir MapLibre + tiles próprios + Nominatim local com extrato do Nordeste no mesmo ferro é música para os meus ouvidos. Quem usa a API de Geocoding Reverso do Google Maps para telemetria veicular (que manda pacote a cada 30 segundos) vai à falência no segundo mês de operação.
  • Trava de Segurança Física e Semântica no Bloqueio: Exigir velocidade abaixo de 5 km/h em duas leituras consecutivas, pedir digitação da placa (evitando fat-finger no celular) e diferenciar na UI "o módulo respondeu ACK" de "o relé realmente abriu o circuito" prova que você entende a diferença entre software e mundo físico.
  • Isolamento no Ecto (prepare_query/3): Forçar org_id direto no ciclo de vida do repositório e exigir skip_org_id: true explícito é a única forma confiável de fazer soft multi-tenancy sem depender da memória de programador (ou de LLM) em cada WHERE.

Onde o Castelo de Cartas Desmorona (O Que Não Funciona):

  1. Esquizofrenia Topológica (Railway + VPS): Você colocou o gateway TCP numa VPS com IP fixo (decisão corretíssima, já que firmware chinês barato tem alergia a DNS ou faz cache do IP até a morte térmica do universo), mas deixou o Phoenix Web na Railway. Como é uma aplicação só em dois papéis, ou você está pagando latência de rede pública (e tráfego de saída) entre a VPS e a Railway para alimentar o Postgres e o Phoenix.PubSub a cada segundo que um carro se move, ou o banco está longe de um dos dois. Se você já tem uma VPS parruda rodando Nominatim e tiles do OSM com IP fixo, por que cargas d'água a aplicação Web está na Railway? Coloque tudo na mesma rede privada (ou na mesma máquina com systemd/Docker) e elimine um salto de rede intercontinental inútil.
  2. A Bomba Relógio de 60 Mil Linhas em lib/: Sessenta mil linhas de Elixir para um MVP com um único protocolo (GT06) e zero carros rodando na rua é um volume obsceno de código. Elixir é uma linguagem expressiva; 60k LOC em Elixir equivalem a umas 200k LOC em Java. Isso tem cheiro forte de LLM prolixa gerando boilerplate, módulos redundantes e testes que testam mocks. Quando você mesmo confessa que um agente concatenou a saída do pg_restore para avaliar código Elixir dinâmico (criando um RCE digno de nota 10 no CVSS) e deixou sessões de LiveView ativas após revogação de permissão (porque esqueceu de dar broadcast de disconnect no socket), fica claro o preço de revisar 85 PRs gerados por máquina: você trocou o tempo de digitação por uma auditoria de segurança forense perpétua.
  3. O Pesadelo do Armazenamento Bruto + PostGIS: Você diz que "todo pacote que chega é gravado em binário" e as posições vão para o PostgreSQL. Vamos fazer uma conta de padaria: 1.000 carros mandando posição a cada 30 segundos geram 2,88 milhões de registros por dia (86 milhões por mês). Se você estiver gravando bytea bruto e geometrias PostGIS indexadas numa tabela comum do Postgres sem particionamento nativo diário/semanal (PARTITION BY RANGE) ou TimescaleDB, e sem expurgo automatizado dos binários brutos para Object Storage (S3/MinIO), o autovacuum e o bloat de índices B-Tree/GiST vão comer o disco da sua VPS antes do Carnaval de Salvador.
  4. Ilusão da Velocidade Zero no Bloqueio: Confiar apenas em "velocidade abaixo de 5 km/h em duas posições" para liberar o corte de combustível no app do cliente tem uma armadilha clássica: semáforo e trânsito lento em via expressa. O ladrão para no sinal vermelho da Avenida Paralela ou reduz no quebra-molas; o dono apavorado aperta o botão; o comando leva 8 segundos na fila do SMS/GPRS porque o sinal 4G oscilou; o carro já arrancou e está a 50 km/h no meio do cruzamento quando o pacote chega no J16 e corta a bomba de combustível. A validação de velocidade segura precisa ser feita pelo próprio firmware do rastreador no momento da execução (muitos clones do GT06 suportam o parâmetro de cortar apenas quando o GPS reportar < 20 km/h localmente), não apenas na camada de negócio do servidor.

Respostas às Perguntas do Autor (Se houver):
Você perguntou como quem trabalha com telemetria/Elixir resolve ACK, buffer e deduplicação:

  • ACK vs. Persistência: A regra de ouro em telemetria pesada é que o processo TCP (seu handler do ThousandIsland) só valida o CRC, coloca o pacote em um ring buffer local ou no Broadway (usando um produtor em disco local como OffBroadway / SQLite WAL / Kafka leve se for clusterizado) e devolve o ACK imediatamente em menos de 5 ms. Nunca deixe o socket saber que o PostgreSQL existe.
  • Inserção em Lote (insert_all): Não faça um Repo.insert individual na pipeline de cada veículo a cada pacote que chega. Use Broadway ou um GenStage agregador para juntar lotes de 500 a 1.000 posições a cada 1 segundo e fazer um único Repo.insert_all no Postgres, enquanto atualiza a posição "quente" (estado atual da frota para o mapa) apenas em memória via ETS + Phoenix.PubSub. O banco só precisa receber o histórico consolidado em batch; a tela da central lê do ETS em microsegundos sem encostar no disco.
  • Deduplicação sem ir ao Banco: Rastreadores GT06 que perdem conexão reenviam o buffer da memória flash com o mesmo imei + timestamp_gps + serial_number. Em vez de bater num índice UNIQUE do Postgres (que custa I/O de escrita e gera dead tuples no conflito ON CONFLICT DO NOTHING), mantenha no estado do GenServer de cada veículo (ou numa tabela ETS com TTL) um pequeno ring buffer com os hashes ou timestamps dos últimos 50 pacotes recebidos daquele IMEI. Se já viu, manda o ACK de novo para calar a boca do rastreador e descarta na memória a custo zero.

Nota na Escala "Reinventando a Roda": 4/10
Você está literalmente reescrevendo o Traccar do zero, mas com justificativa comercial real (verticalização para central brasileira com Pix, app do instalador e fluxo operacional próprio) e usando a melhor tecnologia do mercado para conexões persistentes. Só enxugue essa gordura de 60 mil linhas geradas por IA, implemente particionamento nas tabelas de pacotes antes de colocar 500 motos na rua e nunca, sob nenhuma hipótese, confie só na última velocidade vista pelo servidor para acionar um relé de corte por uma rede celular que tem jitter de segundos.