Evo: 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 ler e apagar códigos de erro (DTC) e gerar diagnósticos.
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.

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âmetros0C→ 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 RPM1A 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:
- O Evo envia o comando
ATSP0para o ELM327, que é responsável por detectar o protocolo automaticamente:
bluetoothManager.sendCommand("ATSP0").getOrThrow()
- 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
}
Fiz esse "fluxograma" no Excalidraw mostrando a conexão até as leituras:

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)