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.
459 lines
13 KiB
C++
459 lines
13 KiB
C++
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
|
|
// 5-baud slow init, fast init, request/response with echo clearing.
|
|
//
|
|
// Post-review hardening: proper pin detach (C1), timeout guards (C2),
|
|
// checksum validation (C3), echo verification (C4), RX flush (I1),
|
|
// structural frame parsing (I2/I3/I5), negative response handling.
|
|
|
|
#include "KLineObd2.h"
|
|
#include "Obd2Pids.h"
|
|
#include <string.h>
|
|
|
|
KLineObd2::KLineObd2(KLineTransport& transport)
|
|
: _transport(transport),
|
|
_initialized(false),
|
|
_kw1(0),
|
|
_kw2(0),
|
|
_testerAddr(obd2::TESTER_ADDR),
|
|
_ecuAddr(0),
|
|
_lastRequestMs(0) {}
|
|
|
|
// --- UART pin management for init sequences ---
|
|
|
|
void KLineObd2::detachUartTx() {
|
|
// Disconnect UART TX peripheral signal from the GPIO pin.
|
|
// pinMatrixOutDetach() routes the plain GPIO output register
|
|
// to the pin instead of the UART TX signal, so digitalWrite()
|
|
// has full control. Without this, UART and GPIO fight over the pin.
|
|
pinMatrixOutDetach(_transport.txPin(), false, false);
|
|
}
|
|
|
|
void KLineObd2::reattachUartTx() {
|
|
// Re-connect UART TX signal to the GPIO pin for normal serial I/O
|
|
uart_set_pin((uart_port_t)_transport.uartNum(),
|
|
_transport.txPin(), UART_PIN_NO_CHANGE,
|
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
}
|
|
|
|
// --- Timeout helper ---
|
|
|
|
uint16_t KLineObd2::remainingMs(unsigned long startMs, uint16_t timeoutMs) {
|
|
unsigned long elapsed = millis() - startMs;
|
|
return (elapsed < timeoutMs) ? (uint16_t)(timeoutMs - elapsed) : 0;
|
|
}
|
|
|
|
// --- 5-baud slow init (ISO 9141-2) ---
|
|
// Bit-bang the target address at 5 baud on the TX pin.
|
|
//
|
|
// Sequence:
|
|
// 1. TX pin LOW for 200ms (start bit at 5 baud)
|
|
// 2. 8 data bits of targetAddr, LSB first, 200ms each
|
|
// 3. TX pin HIGH for 200ms (stop bit)
|
|
// 4. Re-attach UART, flush RX, wait for ECU response
|
|
// 5. ECU sends: 0x55 (sync), keyword1, keyword2
|
|
// 6. Tester sends: inverted keyword2
|
|
// 7. ECU sends: inverted tester address (0xCC for 0x33)
|
|
|
|
bool KLineObd2::slowInit(uint8_t targetAddr) {
|
|
// Detach UART TX so we can bit-bang
|
|
detachUartTx();
|
|
|
|
// Configure TX pin as GPIO output, start HIGH (idle)
|
|
int8_t txp = _transport.txPin();
|
|
pinMode(txp, OUTPUT);
|
|
digitalWrite(txp, HIGH);
|
|
delay(300); // W0: idle before init (>300ms per ISO 9141)
|
|
|
|
// Bit-bang target address at 5 baud
|
|
sendByte5Baud(targetAddr);
|
|
|
|
// Re-attach UART to TX pin
|
|
reattachUartTx();
|
|
|
|
// Flush stale bytes — UART RX was receiving framing errors
|
|
// during the 2-second bit-bang period
|
|
_transport.flushRx();
|
|
|
|
// Wait for ECU sync byte (0x55) — W1: 20-300ms per ISO 9141
|
|
int sync = readByteTimeout(300);
|
|
if (sync != obd2::SYNC_BYTE) {
|
|
return false;
|
|
}
|
|
|
|
// Read keyword bytes — W2: 5-20ms between bytes
|
|
int kw1 = readByteTimeout(50);
|
|
if (kw1 < 0) return false;
|
|
_kw1 = (uint8_t)kw1;
|
|
|
|
int kw2 = readByteTimeout(50);
|
|
if (kw2 < 0) return false;
|
|
_kw2 = (uint8_t)kw2;
|
|
|
|
// W3: 25-50ms before tester responds
|
|
delay(30);
|
|
|
|
// Send inverted keyword2 back to ECU and verify echo
|
|
uint8_t invKw2 = ~_kw2;
|
|
_transport.serial()->write(invKw2);
|
|
_transport.serial()->flush();
|
|
if (!clearEcho(&invKw2, 1)) {
|
|
return false; // bus contention during handshake
|
|
}
|
|
|
|
// W4: ECU confirms by sending inverted target address
|
|
int ack = readByteTimeout(50);
|
|
if (ack < 0) return false;
|
|
|
|
// The ack should be the complement of the target address
|
|
if ((uint8_t)ack != (uint8_t)~targetAddr) {
|
|
return false;
|
|
}
|
|
|
|
_ecuAddr = targetAddr;
|
|
_initialized = true;
|
|
_lastRequestMs = millis();
|
|
return true;
|
|
}
|
|
|
|
// --- Fast init (ISO 14230-2, KWP2000) ---
|
|
// 25ms LOW + 25ms HIGH "TiniPulse" on TX, then StartComm request.
|
|
|
|
bool KLineObd2::fastInit() {
|
|
// Detach UART TX for the wake-up pulse
|
|
detachUartTx();
|
|
|
|
int8_t txp = _transport.txPin();
|
|
pinMode(txp, OUTPUT);
|
|
digitalWrite(txp, HIGH);
|
|
delay(300); // idle before pulse
|
|
|
|
// TiniPulse: 25ms LOW + 25ms HIGH
|
|
digitalWrite(txp, LOW);
|
|
delay(25);
|
|
digitalWrite(txp, HIGH);
|
|
delay(25);
|
|
|
|
// Re-attach UART and flush stale RX data
|
|
reattachUartTx();
|
|
_transport.flushRx();
|
|
|
|
// Build StartCommunication request (KWP2000 service 0x81)
|
|
uint8_t startComm[] = {
|
|
0xC1, // format: functional addressing, 1 data byte
|
|
obd2::FUNC_ADDR, // target: functional address
|
|
obd2::TESTER_ADDR, // source: tester
|
|
0x81 // StartCommunication service
|
|
};
|
|
uint8_t checksum = KLineTransport::checksumMod256(startComm, 4);
|
|
|
|
// Full message with checksum for echo verification
|
|
uint8_t fullMsg[5];
|
|
memcpy(fullMsg, startComm, 4);
|
|
fullMsg[4] = checksum;
|
|
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
_transport.serial()->write(fullMsg[i]);
|
|
}
|
|
_transport.serial()->flush();
|
|
|
|
// Verify echo (5 bytes: 4 data + 1 checksum)
|
|
if (!clearEcho(fullMsg, 5)) {
|
|
return false; // bus contention during fast init
|
|
}
|
|
|
|
// Read StartComm positive response
|
|
uint8_t resp[12];
|
|
uint8_t respLen = readResponse(resp, sizeof(resp), 300);
|
|
if (respLen < 4) return false;
|
|
|
|
// Parse response structurally using ISO 14230 format byte
|
|
uint8_t dLen = 0;
|
|
int8_t dataStart = parseFrameData(resp, respLen, &dLen);
|
|
if (dataStart < 0) return false;
|
|
if (dLen < 3) return false; // need service_id + kw1 + kw2
|
|
|
|
// Verify StartComm positive response (0x81 + 0x40 = 0xC1)
|
|
if (resp[dataStart] != 0xC1) return false;
|
|
|
|
_kw1 = resp[dataStart + 1];
|
|
_kw2 = resp[dataStart + 2];
|
|
|
|
_ecuAddr = obd2::FUNC_ADDR;
|
|
_initialized = true;
|
|
_lastRequestMs = millis();
|
|
return true;
|
|
}
|
|
|
|
// --- Request/response ---
|
|
|
|
void KLineObd2::sendRequest(const uint8_t* data, uint8_t len) {
|
|
if (len > KLINE_MAX_MSG) return;
|
|
|
|
// Build complete message with checksum for echo verification
|
|
uint8_t buf[KLINE_MAX_MSG + 1];
|
|
memcpy(buf, data, len);
|
|
buf[len] = KLineTransport::checksumMod256(data, len);
|
|
uint8_t totalLen = len + 1;
|
|
|
|
for (uint8_t i = 0; i < totalLen; i++) {
|
|
_transport.serial()->write(buf[i]);
|
|
}
|
|
_transport.serial()->flush();
|
|
|
|
// Verify echo (data bytes + checksum byte)
|
|
clearEcho(buf, totalLen);
|
|
|
|
_lastRequestMs = millis();
|
|
}
|
|
|
|
uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs) {
|
|
uint8_t count = 0;
|
|
unsigned long start = millis();
|
|
|
|
// Read first byte (format/header) to determine message length
|
|
int first = readByteTimeout(timeoutMs);
|
|
if (first < 0) return 0;
|
|
buf[count++] = (uint8_t)first;
|
|
|
|
// Determine expected length from format byte
|
|
uint8_t fmt = (uint8_t)first;
|
|
uint8_t addrMode = (fmt >> 6) & 0x03;
|
|
uint8_t dataLen = fmt & 0x3F;
|
|
|
|
// Determine address byte count from address mode
|
|
uint8_t addrBytes;
|
|
switch (addrMode) {
|
|
case 0: // no address information
|
|
addrBytes = 0;
|
|
break;
|
|
case 1: // CARB mode — unsupported, treat as 0 extra
|
|
addrBytes = 0;
|
|
break;
|
|
case 2: // physical addressing: target + source
|
|
case 3: // functional addressing: target + source
|
|
default:
|
|
addrBytes = 2;
|
|
break;
|
|
}
|
|
|
|
// Read address bytes if present
|
|
for (uint8_t i = 0; i < addrBytes && count < maxLen; i++) {
|
|
uint16_t rem = remainingMs(start, timeoutMs);
|
|
if (rem == 0) return count;
|
|
int b = readByteTimeout(rem);
|
|
if (b < 0) return count;
|
|
buf[count++] = (uint8_t)b;
|
|
}
|
|
|
|
// If dataLen == 0, next byte is the actual length
|
|
if (dataLen == 0 && count < maxLen) {
|
|
uint16_t rem = remainingMs(start, timeoutMs);
|
|
if (rem == 0) return count;
|
|
int lenByte = readByteTimeout(rem);
|
|
if (lenByte < 0) return count;
|
|
buf[count++] = (uint8_t)lenByte;
|
|
dataLen = (uint8_t)lenByte;
|
|
}
|
|
|
|
// Read data bytes + checksum
|
|
uint8_t expected = dataLen + 1; // +1 for checksum
|
|
for (uint8_t i = 0; i < expected; i++) {
|
|
if (count >= maxLen) {
|
|
#ifdef KLINE_DEBUG
|
|
Serial.println("KLINE: response buffer full");
|
|
#endif
|
|
return count; // truncated — can't validate checksum
|
|
}
|
|
uint16_t rem = remainingMs(start, timeoutMs);
|
|
if (rem == 0) return count;
|
|
int b = readByteTimeout(rem);
|
|
if (b < 0) return count;
|
|
buf[count++] = (uint8_t)b;
|
|
}
|
|
|
|
// Validate checksum (mod256 sum of all bytes except last)
|
|
if (count >= 2) {
|
|
uint8_t computed = KLineTransport::checksumMod256(buf, count - 1);
|
|
if (computed != buf[count - 1]) {
|
|
#ifdef KLINE_DEBUG
|
|
Serial.println("KLINE: checksum fail");
|
|
#endif
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
uint8_t KLineObd2::requestPid(uint8_t mode, uint8_t pid,
|
|
uint8_t* response, uint8_t maxLen) {
|
|
if (!_initialized) return 0;
|
|
|
|
// Send TesterPresent keepalive if idle too long (P3 timeout prevention)
|
|
if (_lastRequestMs > 0 && millis() - _lastRequestMs > P3_KEEPALIVE_MS) {
|
|
sendTesterPresent();
|
|
}
|
|
|
|
// ISO 14230 format: physical addressing, 2 data bytes
|
|
uint8_t req[] = {
|
|
(uint8_t)(0x82), // fmt: physical addressing, 2 data bytes
|
|
_ecuAddr, // target
|
|
_testerAddr, // source
|
|
mode, // service/mode
|
|
pid // PID
|
|
};
|
|
|
|
sendRequest(req, sizeof(req));
|
|
|
|
uint8_t raw[32];
|
|
uint8_t rawLen = readResponse(raw, sizeof(raw));
|
|
if (rawLen < 5) return 0; // minimum: fmt + target + source + response_mode + pid
|
|
|
|
// Parse response structurally
|
|
uint8_t dLen = 0;
|
|
int8_t dataStart = parseFrameData(raw, rawLen, &dLen);
|
|
if (dataStart < 0 || dLen < 2) return 0;
|
|
|
|
// Check for negative response (service 0x7F)
|
|
if (raw[dataStart] == 0x7F) {
|
|
#ifdef KLINE_DEBUG
|
|
if (dLen >= 3) {
|
|
Serial.print("KLINE: NRC 0x");
|
|
Serial.println(raw[dataStart + 2], HEX);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
// Verify positive response: service ID = request mode + 0x40
|
|
uint8_t respMode = mode + 0x40;
|
|
if (raw[dataStart] != respMode) return 0;
|
|
if (raw[dataStart + 1] != pid) return 0;
|
|
|
|
// Copy data bytes after service_id + pid
|
|
uint8_t copyLen = 0;
|
|
for (uint8_t i = 2; i < dLen && copyLen < maxLen; i++) {
|
|
response[copyLen++] = raw[dataStart + i];
|
|
}
|
|
return copyLen;
|
|
}
|
|
|
|
// --- TesterPresent keepalive ---
|
|
|
|
void KLineObd2::sendTesterPresent() {
|
|
if (!_initialized) return;
|
|
|
|
// ISO 14230: TesterPresent service (0x3E), no sub-function
|
|
uint8_t req[] = {
|
|
(uint8_t)(0x81), // fmt: physical addressing, 1 data byte
|
|
_ecuAddr,
|
|
_testerAddr,
|
|
0x3E // TesterPresent service
|
|
};
|
|
|
|
sendRequest(req, sizeof(req));
|
|
|
|
// Read and discard the positive response
|
|
uint8_t resp[8];
|
|
readResponse(resp, sizeof(resp), 500);
|
|
}
|
|
|
|
// --- Private helpers ---
|
|
|
|
void KLineObd2::sendByte5Baud(uint8_t data) {
|
|
int8_t txp = _transport.txPin();
|
|
|
|
// Start bit (LOW)
|
|
digitalWrite(txp, LOW);
|
|
delay(200);
|
|
|
|
// 8 data bits, LSB first
|
|
for (uint8_t i = 0; i < 8; i++) {
|
|
digitalWrite(txp, (data >> i) & 0x01 ? HIGH : LOW);
|
|
delay(200);
|
|
}
|
|
|
|
// Stop bit (HIGH)
|
|
digitalWrite(txp, HIGH);
|
|
delay(200);
|
|
}
|
|
|
|
bool KLineObd2::clearEcho(const uint8_t* expected, uint8_t count) {
|
|
bool match = true;
|
|
for (uint8_t i = 0; i < count; i++) {
|
|
int b = readByteTimeout(KLINE_ECHO_TIMEOUT_MS);
|
|
if (b < 0) {
|
|
#ifdef KLINE_DEBUG
|
|
Serial.print("KLINE: echo timeout byte ");
|
|
Serial.println(i);
|
|
#endif
|
|
match = false;
|
|
continue;
|
|
}
|
|
if ((uint8_t)b != expected[i]) {
|
|
#ifdef KLINE_DEBUG
|
|
Serial.print("KLINE: echo mismatch byte ");
|
|
Serial.print(i);
|
|
Serial.print(" exp=0x");
|
|
Serial.print(expected[i], HEX);
|
|
Serial.print(" got=0x");
|
|
Serial.println((uint8_t)b, HEX);
|
|
#endif
|
|
match = false;
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
int KLineObd2::readByteTimeout(uint16_t timeoutMs) {
|
|
if (timeoutMs == 0) return -1;
|
|
unsigned long start = millis();
|
|
while (millis() - start < timeoutMs) {
|
|
_transport.drainUart();
|
|
if (_transport.rxAvailable() > 0) {
|
|
return _transport.rxRead();
|
|
}
|
|
delayMicroseconds(100); // prevent busy-wait burning CPU
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int8_t KLineObd2::parseFrameData(const uint8_t* buf, uint8_t bufLen, uint8_t* dataLen) {
|
|
if (bufLen < 1) return -1;
|
|
|
|
uint8_t fmt = buf[0];
|
|
uint8_t addrMode = (fmt >> 6) & 0x03;
|
|
uint8_t inlineLen = fmt & 0x3F;
|
|
|
|
uint8_t headerSize = 1; // format byte
|
|
|
|
switch (addrMode) {
|
|
case 0: // no address bytes
|
|
break;
|
|
case 1: // CARB exception — not supported
|
|
#ifdef KLINE_DEBUG
|
|
Serial.println("KLINE: CARB mode unsupported");
|
|
#endif
|
|
return -1;
|
|
case 2: // physical addressing: target + source
|
|
case 3: // functional addressing: target + source
|
|
headerSize += 2;
|
|
break;
|
|
}
|
|
|
|
uint8_t len;
|
|
if (inlineLen > 0) {
|
|
len = inlineLen;
|
|
} else {
|
|
// Length in separate byte after address bytes
|
|
if (bufLen < headerSize + 1) return -1;
|
|
len = buf[headerSize];
|
|
headerSize += 1;
|
|
}
|
|
|
|
if (dataLen) *dataLen = len;
|
|
|
|
if (headerSize > bufLen) return -1;
|
|
return (int8_t)headerSize;
|
|
}
|