i-k-bus-board/firmware/src/obd2_scanner.cpp
Ryan Malloy a2dcd6d58d Harden OBD-II K-line handler (code review findings)
Fix UART pin detach: pinMatrixOutDetach() instead of no-op
uart_set_pin(ALL_NO_CHANGE). Fix timeout underflow in
readResponse() with remainingMs() helper. Add checksum
validation on received frames. Echo verification in clearEcho()
detects bus contention. Flush RX buffers after init sequences.
Structural ISO 14230 frame parsing replaces byte-scanning.
TesterPresent keepalive prevents P3 session timeout. Scanner
re-initializes after consecutive failures.
2026-02-13 06:02:25 -07:00

190 lines
5.3 KiB
C++

// OBD-II K-line Scanner — ESP32 + transistor/optocoupler interface
// Polls common PIDs (RPM, speed, coolant temp) and prints to serial.
//
// Hardware: Transistor circuit (Tucker k-line-board) or PC817 optocoupler.
// Set KLINE_TX_INVERT=1 in build flags for optocoupler, 0 for transistor.
#include <Arduino.h>
#include "config.h"
#include "KLineTransport.h"
#include "KLineObd2.h"
#include "Obd2Pids.h"
KLineTransport transport;
KLineObd2* scanner = nullptr;
static unsigned long lastPollMs = 0;
static const unsigned long POLL_INTERVAL_MS = 500;
static uint8_t consecutiveFailures = 0;
static const uint8_t MAX_FAILURES_BEFORE_REINIT = 5;
// Attempt ECU initialization (slow init, then fast init fallback)
static bool tryInit() {
Serial.print("Slow init (0x");
Serial.print(KLINE_INIT_ADDR, HEX);
Serial.print(")... ");
if (scanner->slowInit(KLINE_INIT_ADDR)) {
Serial.println("OK");
Serial.print("Keywords: ");
Serial.print(scanner->keyword1(), HEX);
Serial.print(" ");
Serial.println(scanner->keyword2(), HEX);
return true;
}
Serial.println("failed");
Serial.print("Fast init... ");
if (scanner->fastInit()) {
Serial.println("OK");
Serial.print("Keywords: ");
Serial.print(scanner->keyword1(), HEX);
Serial.print(" ");
Serial.println(scanner->keyword2(), HEX);
return true;
}
Serial.println("failed");
return false;
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("=== OBD-II K-line Scanner ===");
Serial.print("Bus UART: ");
Serial.print(KLINE_BAUD);
Serial.println(" 8N1");
Serial.print("RX pin: GPIO ");
Serial.print(KLINE_RX_PIN);
Serial.print(" TX pin: GPIO ");
Serial.println(KLINE_TX_PIN);
Serial.println();
KLineConfig config;
config.baud = KLINE_BAUD;
config.framing = KLINE_FRAMING;
config.idleTimeoutUs = 0; // master/slave — no idle detection needed
config.idleCheckUs = 0;
config.packetGapMs = 50; // P3: inter-message timing
config.txInvert = KLINE_TX_INVERT;
config.checksumType = CHECKSUM_MOD256;
transport.begin(Serial1, KLINE_RX_PIN, KLINE_TX_PIN, KLINE_LED_PIN,
KLINE_UART_NUM, config);
scanner = new KLineObd2(transport);
if (!scanner) {
Serial.println("ERROR: allocation failed");
return;
}
if (!tryInit()) {
Serial.println("No ECU response. Check wiring and key position.");
}
Serial.println();
if (scanner->isInitialized()) {
Serial.println("Polling PIDs...");
Serial.println("RPM | Speed | Coolant | Throttle | Voltage");
Serial.println("---------+---------+---------+----------+--------");
}
}
void loop() {
if (!scanner) {
delay(1000);
return;
}
// Re-init after consecutive failures (session lost)
if (!scanner->isInitialized()) {
delay(3000);
Serial.println("Retrying init...");
scanner->reset();
if (tryInit()) {
consecutiveFailures = 0;
Serial.println("Polling PIDs...");
}
return;
}
if (millis() - lastPollMs < POLL_INTERVAL_MS) return;
lastPollMs = millis();
uint8_t data[4];
uint8_t successes = 0;
// RPM (2 data bytes)
uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data));
if (len >= 2) {
float rpm = obd2::decodeRpm(data[0], data[1]);
Serial.print(rpm, 0);
Serial.print(" rpm");
successes++;
} else {
Serial.print("--- ");
}
Serial.print(" | ");
// Speed (1 data byte)
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_SPEED, data, sizeof(data));
if (len >= 1) {
Serial.print(obd2::decodeSpeed(data[0]));
Serial.print(" km/h");
successes++;
} else {
Serial.print("--- ");
}
Serial.print(" | ");
// Coolant temp (1 data byte)
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_COOLANT_TEMP, data, sizeof(data));
if (len >= 1) {
Serial.print(obd2::decodeCoolantTemp(data[0]));
Serial.print(" C");
successes++;
} else {
Serial.print("--- ");
}
Serial.print(" | ");
// Throttle (1 data byte)
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_THROTTLE, data, sizeof(data));
if (len >= 1) {
Serial.print(obd2::decodeThrottle(data[0]), 1);
Serial.print(" %");
successes++;
} else {
Serial.print("--- ");
}
Serial.print(" | ");
// Control module voltage (2 data bytes)
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_CONTROL_VOLTAGE, data, sizeof(data));
if (len >= 2) {
Serial.print(obd2::decodeControlVoltage(data[0], data[1]), 1);
Serial.print(" V");
successes++;
} else {
Serial.print("--- ");
}
Serial.println();
// Track consecutive total failures for session loss detection
if (successes == 0) {
consecutiveFailures++;
if (consecutiveFailures >= MAX_FAILURES_BEFORE_REINIT) {
Serial.println("Session lost — will re-initialize.");
scanner->reset();
consecutiveFailures = 0;
}
} else {
consecutiveFailures = 0;
}
}