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.

IAScanner automotivoOBD2ELM327

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
}

Fiz esse "fluxograma" no Excalidraw mostrando a conexão até as leituras:

Protocolos

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)