Evo: Como desenvolvi um scanner automotivo com IA

Um resumo de como criei um app para ler parâmetros automotivos em tempo real via OBD2, com IA integrada, capaz de conversar com o carro, ler e apagar códigos de erro (DTC) e gerar diagnósticos.

IA · OBD2 · ELM327

Evo

O Evo é um aplicativo que se conecta ao carro por meio de scanners OBD2 Bluetooth e transforma dados técnicos em informações fáceis de entender. O app possui um painel totalmente personalizável, onde é possível escolher quais dados do carro serão exibidos e de que forma eles aparecem na tela.

O principal destaque do Evo é o chat de IA integrado. O app permite “conversar” com o carro, fazendo perguntas que são respondidas com base na análise dos dados em tempo real. A IA interpreta sensores, leituras e comportamentos do carro para entregar respostas claras e úteis.

O Evo também é capaz de gerar diagnósticos, utilizando tanto os parâmetros do carro quanto os códigos de erro (DTC), ajudando a entender os problemas atuais do carro.

O app já foi publicado na Google Play e está disponível no site: evoscanner.com

Evo Scanner

Ideia

Meses atrás, descobri a existência do ELM327, um aparelho barato capaz de ler dados do carro através do OBD2, usado por muitas pessoas principalmente para ler e apagar códigos de erro. Testando alguns apps que se conectam a esse aparelho, como o Torque, Car Scanner e outros, senti que, além de pouco intuitivos, eles não aproveitavam todo o potencial daqueles dados lidos, principalmente agora, que temos IAs capazes de analisar tudo isso e ajudar o usuário a entender o que cada informação significa.

Decidi então comprar um simulador de ECU simples no Aliexpress e começar a testar o ELM327 a fundo. O simulador é o da foto abaixo: ele consegue simular alguns parâmetros em 7 protocolos diferentes, além de simular até quatro códigos de erro.

Simulador de ECU

Comprei também dois ELM327: um mais simples, e outro com placa dupla e chip PIC18F25K80. A ideia é testar na prática a diferença entre eles, já que geralmente dizem que o modelo com placa dupla tem mais qualidade.

Diferenças entre as versões

Ainda não fiz testes mais detalhados, mas os principais problemas que notei na versão mais simples do ELM327 foram:

  • Problemas ao conectar via Bluetooth: As vezes preciso até reiniciar o simulador
  • Travamento do simulador ao apagar códigos de erro: No início achei até que o problema estava no meu código, mas testando com a versão placa dupla esse problema não acontece
  • Menos estabilidade na conexão: Em algumas situações, a conexão parece se perder de forma aleatória

Ouvi dizer também que a versão com placa dupla responde mais rápido aos PIDs, mas não testei ainda, por enquanto deixei um delay seguro entre as leituras, para garantir que funcione com a versão mais simples.

Entendendo a comunicação OBD2

A comunicação OBD2 não me pareceu estranha, a experiência que tive na comunicação entre pacotes em um jogo chamado Shaiya ajudou bastante, é quase a mesma coisa: é uma comunicação baseada em pacotes hexadecimais. Basicamente, nessa página do Wikipédia temos toda a informação que precisamos.

O app vai enviar via Bluetooth para o ELM327 algo como (Número do modo) (PID), no caso do RPM é: 01 0C

Explicação:

  • 01 → modo 01 (show current data) responsável por retornar informações dos parâmetros
  • 0C → PID do RPM

O ELM327 vai responder assim: 41 0C 1A F8 (na prática é um pouco diferente disso, vou explicar melhor na parte sobre protocolos)

Explicação:

  • 41 → resposta do modo 01 (0x40 + 0x01)
  • 0C → PID do RPM
  • 1A F8 → os dois bytes com o valor do RPM

A formula para entender os valores 1A F8 é explicada na própria página do Wikipédia: ((A * 256) + B) / 4 sendo A = 1A e B = F8. Em Kotlin eu implementei assim:

"010C" -> {
    if (hexValue.length >= 4) {
        val a = hexValue.substring(0, 2).toIntOrNull(16) ?: 0
        val b = hexValue.substring(2, 4).toIntOrNull(16) ?: 0
        ((a * 256) + b) / 4f
    } else 0f
}

O resultado disso é: 1726 RPM

Protocolos

Quando comecei a desenvolver o Evo e a estudar a comunicação entre o scanner e o carro, fiquei confuso sobre o que exatamente significava "OBD2". Depois descobri que o OBD2 não é um protocolo em si, mas sim um padrão que suporta vários protocolos diferentes. Cada carro utiliza um deles, variando conforme o fabricante e a região. Depois de aprender o básico de como funciona os protocolos mais comuns, implementei 6 variações diferentes deles (10 se contarmos as diferentes configurações de CAN):

  • SAE J1850 PWM (América do Norte)
  • SAE J1850 VPW (América do Norte)
  • ISO 9141-2 (Europa / Ásia)
  • ISO 14230 KWP (Europa - mais moderno)
  • ISO 15765-4 CAN (2008+ mais comum atualmente)
  • SAE J1939 CAN (pesados/comerciais)

Para detectar qual protocolo é usado pelo carro, fiz assim:

  1. O Evo envia o comando ATSP0 para o ELM327, que é responsável por detectar o protocolo automaticamente:
bluetoothManager.sendCommand("ATSP0").getOrThrow()
  1. Se falhar, o Evo tenta se conectar a cada um individualmente nessa ordem (começando pelos ISO 15765-4 mais comuns):
val protocols = listOf(
    "6" to "ISO 15765-4 CAN (11 bit ID, 500 kbaud)",
    "7" to "ISO 15765-4 CAN (29 bit ID, 500 kbaud)",
    "8" to "ISO 15765-4 CAN (11 bit ID, 250 kbaud)",
    "9" to "ISO 15765-4 CAN (29 bit ID, 250 kbaud)",
    "A" to "SAE J1939 CAN (29 bit ID, 250 kbaud)",
    "3" to "ISO 9141-2",
    "4" to "ISO 14230-4 KWP (5 baud init)",
    "5" to "ISO 14230-4 KWP (fast init)",
    "1" to "SAE J1850 PWM (41.6 kbaud)",
    "2" to "SAE J1850 VPW (10.4 kbaud)"
)

// Testamos um por um                                                                                                                                              
for ((protocolCode, protocolName) in protocols) {

    // Envia o comando ATSP para selecionar o protocolo                                                                                              
    bluetoothManager.sendCommand("ATSP$protocolCode").getOrThrow()

    // Espera mais tempo para protocolos lentos                                                                                                   
    if (protocolCode in listOf("3", "4", "5")) {
        delay(1500)                                                                                                            
    }

    // Envia 0100 para testar, se funcionar, o carro deve retornar os PIDs suportados                                                                                                           
    val testResponse = bluetoothManager.sendCommand("0100").getOrNull()

    // Verifica se respondeu 
    if (testResponse?.contains("41") == true) {
        // Respondeu. Protocolo encontrado                                                                                                     
        break                                                                                                                                   
    }

    // Não respondeu, continua o teste no próximo protocolo
}

Diferenças na leitura dos protocolos

Para explicar a diferença na leitura de acordo com cada protocolo, vou usar novamente o exemplo da leitura do RPM, mas dessa vez vou incluir todos os bytes da resposta do ELM327 que ocultei na explicação sobre a comunicação OBD2 acima.

CAN (ISO 15765-4)
É o protocolo mais moderno e rápido. Ele transmite as mensagens em frames fixos de 8 bytes e usa um sistema de CRC interno, dispensando o uso de checksum no final da mensagem. A comunicação é full duplex, ou seja, o carro pode responder enquanto o app continua enviando requisições.

Envia: 01 0C
Recebe: 7E8 06 41 0C 1A 5D 00 00 00

7E8 → header
06 → tamanho dos dados úteis (quantidade de bytes após a respota 41)
41 → resposta do modo 01 (0x40 + 0x01)
0C → PID do RPM
1A 5D → os dois bytes com o valor do RPM (big-endian)
00 00 00 → bytes de preenchimento

ISO 9141-2
O ISO 9141-2 é usado em veículos europeus e asiáticos mais antigos. A comunicação é serial, half-duplex, mais lenta e com um checksum no final.

Envia: 01 0C
Recebe: 48 6B 10 41 0C 1A 5D 87

48 6B 10 → header
41 → resposta do modo 01 (0x40 + 0x01)
0C → PID do RPM
1A 5D → os dois bytes com o valor do RPM (big-endian)
87 → checksum

O checksum é a soma de todos os bytes anteriores (mod 256):

0x48 + 0x6B + 0x10 + 0x41 + 0x0C + 0x1A + 0x5D = 0x187
0x187 (391 dec) mod 256 = 0x87 (135 dec)

KWP2000 (ISO 14230)
O KWP2000 é uma evolução do ISO 9141, usado em muitos carros europeus entre 2000 e 2010. Também usa checksum e, pelo menos nos testes feitos no meu simulador, a única diferença pro ISO 9141 é o header.

Envia: 01 0C
Recebe: 84 F1 11 41 0C 1A 53 40

84 F1 11 → header
41 → resposta do modo 01 (0x40 + 0x01)
0C → PID do RPM
1A 53 → os dois bytes com o valor do RPM (big-endian)
40 → checksum

Códigos de erro (DTC)

DTC (Diagnostic Trouble Codes) são códigos de erro que a ECU armazena quando detecta problemas no carro.

Formato de um DTC (exemplo P0302):
P = Prefixo (tipo de sistema)
0 = Segundo dígito (0-3)
3 = Terceiro dígito (0-F em hex)
02 = Dígitos específicos (00-FF em hex)

Prefixos:
P = Powertrain (motor/transmissão)
C = Chassis (suspensão, freios, direção)
B = Body (airbag, cintos, portas)
U = Network (comunicação entre módulos)

Existem 3 tipos de códigos DTC: Os confirmados, os pendentes e os permanentes, dessa forma:

Tipo Comando enviado Resposta esperada Significado
Confirmado 03 43 Codigos que dispararam o MIL (check engine)
Pendente 07 47 Codigos que falharam uma vez mas não confirmaram
Permanente 0A 4A Codigos que não podem ser limpos pelo motorista

Na prática, os mais úteis são os códigos confirmados e permanentes, sendo os pendentes não muito confiáveis para diagnósticos.

DTC Confirmados (os mais importantes no dia a dia)

  • A falha aconteceu mais de uma vez
  • A ECU já “aceitou” que o problema é real
  • Normalmente acende a luz da injeção (MIL)
  • São os códigos que você usa para diagnosticar e consertar Se tem DTC confirmado, existe defeito ativo ou recorrente.

DTC Pendentes (tipo um aviso antecipado)

  • A falha ocorreu uma única vez
  • A ECU ainda está “observando”
  • Pode desaparecer sozinho se não se repetir
  • Não é confiável para diagnóstico isolado

Útil para:

  • Flagras de falha intermitente
  • Ver algo que pode virar problema

DTC Permanentes (importantes especialmente para inspeção/emissões)

  • Registrados quando a falha afeta emissões
  • Não somem com scanner ou reset de bateria
  • Só são apagados quando:
    • O problema é resolvido de verdade
    • O carro completa ciclos de condução válidos

Indicam que:

  • O carro já teve um problema sério
  • A ECU ainda não confirmou que está tudo OK

Formato da resposta bruta