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.
This commit is contained in:
parent
e639056ee8
commit
a2dcd6d58d
@ -1,8 +1,13 @@
|
|||||||
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
|
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
|
||||||
// 5-baud slow init, fast init, request/response with echo clearing.
|
// 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 "KLineObd2.h"
|
||||||
#include "Obd2Pids.h"
|
#include "Obd2Pids.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
KLineObd2::KLineObd2(KLineTransport& transport)
|
KLineObd2::KLineObd2(KLineTransport& transport)
|
||||||
: _transport(transport),
|
: _transport(transport),
|
||||||
@ -10,31 +15,51 @@ KLineObd2::KLineObd2(KLineTransport& transport)
|
|||||||
_kw1(0),
|
_kw1(0),
|
||||||
_kw2(0),
|
_kw2(0),
|
||||||
_testerAddr(obd2::TESTER_ADDR),
|
_testerAddr(obd2::TESTER_ADDR),
|
||||||
_ecuAddr(0) {}
|
_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) ---
|
// --- 5-baud slow init (ISO 9141-2) ---
|
||||||
// Bit-bang the target address at 5 baud on the TX pin.
|
// Bit-bang the target address at 5 baud on the TX pin.
|
||||||
// The UART peripheral must be detached from the TX pin during this sequence,
|
|
||||||
// then re-attached for normal 10400 baud communication.
|
|
||||||
//
|
//
|
||||||
// Sequence:
|
// Sequence:
|
||||||
// 1. TX pin LOW for 200ms (start bit at 5 baud)
|
// 1. TX pin LOW for 200ms (start bit at 5 baud)
|
||||||
// 2. 8 data bits of targetAddr, LSB first, 200ms each
|
// 2. 8 data bits of targetAddr, LSB first, 200ms each
|
||||||
// 3. TX pin HIGH for 200ms (stop bit)
|
// 3. TX pin HIGH for 200ms (stop bit)
|
||||||
// 4. Re-attach UART, wait for ECU response
|
// 4. Re-attach UART, flush RX, wait for ECU response
|
||||||
// 5. ECU sends: 0x55 (sync), keyword1, keyword2
|
// 5. ECU sends: 0x55 (sync), keyword1, keyword2
|
||||||
// 6. Tester sends: inverted keyword2
|
// 6. Tester sends: inverted keyword2
|
||||||
// 7. ECU sends: inverted tester address (0xCC for 0x33)
|
// 7. ECU sends: inverted tester address (0xCC for 0x33)
|
||||||
|
|
||||||
bool KLineObd2::slowInit(uint8_t targetAddr) {
|
bool KLineObd2::slowInit(uint8_t targetAddr) {
|
||||||
int8_t txp = _transport.txPin();
|
// Detach UART TX so we can bit-bang
|
||||||
uint8_t unum = _transport.uartNum();
|
detachUartTx();
|
||||||
|
|
||||||
// Detach UART from TX pin so we can bit-bang
|
|
||||||
uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
|
|
||||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
||||||
|
|
||||||
// Configure TX pin as GPIO output, start HIGH (idle)
|
// Configure TX pin as GPIO output, start HIGH (idle)
|
||||||
|
int8_t txp = _transport.txPin();
|
||||||
pinMode(txp, OUTPUT);
|
pinMode(txp, OUTPUT);
|
||||||
digitalWrite(txp, HIGH);
|
digitalWrite(txp, HIGH);
|
||||||
delay(300); // W0: idle before init (>300ms per ISO 9141)
|
delay(300); // W0: idle before init (>300ms per ISO 9141)
|
||||||
@ -43,8 +68,11 @@ bool KLineObd2::slowInit(uint8_t targetAddr) {
|
|||||||
sendByte5Baud(targetAddr);
|
sendByte5Baud(targetAddr);
|
||||||
|
|
||||||
// Re-attach UART to TX pin
|
// Re-attach UART to TX pin
|
||||||
uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE,
|
reattachUartTx();
|
||||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
||||||
|
// 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
|
// Wait for ECU sync byte (0x55) — W1: 20-300ms per ISO 9141
|
||||||
int sync = readByteTimeout(300);
|
int sync = readByteTimeout(300);
|
||||||
@ -64,13 +92,13 @@ bool KLineObd2::slowInit(uint8_t targetAddr) {
|
|||||||
// W3: 25-50ms before tester responds
|
// W3: 25-50ms before tester responds
|
||||||
delay(30);
|
delay(30);
|
||||||
|
|
||||||
// Send inverted keyword2 back to ECU
|
// Send inverted keyword2 back to ECU and verify echo
|
||||||
uint8_t invKw2 = ~_kw2;
|
uint8_t invKw2 = ~_kw2;
|
||||||
_transport.serial()->write(invKw2);
|
_transport.serial()->write(invKw2);
|
||||||
_transport.serial()->flush();
|
_transport.serial()->flush();
|
||||||
|
if (!clearEcho(&invKw2, 1)) {
|
||||||
// Clear our own echo (half-duplex bus)
|
return false; // bus contention during handshake
|
||||||
clearEcho(1);
|
}
|
||||||
|
|
||||||
// W4: ECU confirms by sending inverted target address
|
// W4: ECU confirms by sending inverted target address
|
||||||
int ack = readByteTimeout(50);
|
int ack = readByteTimeout(50);
|
||||||
@ -83,6 +111,7 @@ bool KLineObd2::slowInit(uint8_t targetAddr) {
|
|||||||
|
|
||||||
_ecuAddr = targetAddr;
|
_ecuAddr = targetAddr;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
_lastRequestMs = millis();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,13 +119,10 @@ bool KLineObd2::slowInit(uint8_t targetAddr) {
|
|||||||
// 25ms LOW + 25ms HIGH "TiniPulse" on TX, then StartComm request.
|
// 25ms LOW + 25ms HIGH "TiniPulse" on TX, then StartComm request.
|
||||||
|
|
||||||
bool KLineObd2::fastInit() {
|
bool KLineObd2::fastInit() {
|
||||||
|
// Detach UART TX for the wake-up pulse
|
||||||
|
detachUartTx();
|
||||||
|
|
||||||
int8_t txp = _transport.txPin();
|
int8_t txp = _transport.txPin();
|
||||||
uint8_t unum = _transport.uartNum();
|
|
||||||
|
|
||||||
// Detach UART from TX pin for the wake-up pulse
|
|
||||||
uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
|
|
||||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
||||||
|
|
||||||
pinMode(txp, OUTPUT);
|
pinMode(txp, OUTPUT);
|
||||||
digitalWrite(txp, HIGH);
|
digitalWrite(txp, HIGH);
|
||||||
delay(300); // idle before pulse
|
delay(300); // idle before pulse
|
||||||
@ -107,12 +133,11 @@ bool KLineObd2::fastInit() {
|
|||||||
digitalWrite(txp, HIGH);
|
digitalWrite(txp, HIGH);
|
||||||
delay(25);
|
delay(25);
|
||||||
|
|
||||||
// Re-attach UART
|
// Re-attach UART and flush stale RX data
|
||||||
uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE,
|
reattachUartTx();
|
||||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
_transport.flushRx();
|
||||||
|
|
||||||
// Send StartCommunication request (KWP2000 service 0x81)
|
// Build StartCommunication request (KWP2000 service 0x81)
|
||||||
// Format: [fmt+len, target, source, service_id]
|
|
||||||
uint8_t startComm[] = {
|
uint8_t startComm[] = {
|
||||||
0xC1, // format: functional addressing, 1 data byte
|
0xC1, // format: functional addressing, 1 data byte
|
||||||
obd2::FUNC_ADDR, // target: functional address
|
obd2::FUNC_ADDR, // target: functional address
|
||||||
@ -121,56 +146,64 @@ bool KLineObd2::fastInit() {
|
|||||||
};
|
};
|
||||||
uint8_t checksum = KLineTransport::checksumMod256(startComm, 4);
|
uint8_t checksum = KLineTransport::checksumMod256(startComm, 4);
|
||||||
|
|
||||||
for (uint8_t i = 0; i < 4; i++) {
|
// Full message with checksum for echo verification
|
||||||
_transport.serial()->write(startComm[i]);
|
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()->write(checksum);
|
|
||||||
_transport.serial()->flush();
|
_transport.serial()->flush();
|
||||||
|
|
||||||
// Clear echo (5 bytes: 4 data + 1 checksum)
|
// Verify echo (5 bytes: 4 data + 1 checksum)
|
||||||
clearEcho(5);
|
if (!clearEcho(fullMsg, 5)) {
|
||||||
|
return false; // bus contention during fast init
|
||||||
|
}
|
||||||
|
|
||||||
// Read StartComm positive response
|
// Read StartComm positive response
|
||||||
// Expected: [fmt+len, source, target, 0xC1, kw1, kw2, checksum]
|
uint8_t resp[12];
|
||||||
uint8_t resp[7];
|
|
||||||
uint8_t respLen = readResponse(resp, sizeof(resp), 300);
|
uint8_t respLen = readResponse(resp, sizeof(resp), 300);
|
||||||
if (respLen < 4) return false;
|
if (respLen < 4) return false;
|
||||||
|
|
||||||
// Positive response service ID is request + 0x40
|
// Parse response structurally using ISO 14230 format byte
|
||||||
// Find the service byte — it's after header bytes
|
uint8_t dLen = 0;
|
||||||
// For physical addressing response: [fmt, target, source, 0xC1, kw1, kw2, chk]
|
int8_t dataStart = parseFrameData(resp, respLen, &dLen);
|
||||||
bool foundResponse = false;
|
if (dataStart < 0) return false;
|
||||||
for (uint8_t i = 0; i < respLen; i++) {
|
if (dLen < 3) return false; // need service_id + kw1 + kw2
|
||||||
if (resp[i] == 0xC1) { // StartComm positive response
|
|
||||||
if (i + 2 < respLen) {
|
|
||||||
_kw1 = resp[i + 1];
|
|
||||||
_kw2 = resp[i + 2];
|
|
||||||
foundResponse = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundResponse) return false;
|
// 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;
|
_ecuAddr = obd2::FUNC_ADDR;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
_lastRequestMs = millis();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Request/response ---
|
// --- Request/response ---
|
||||||
|
|
||||||
void KLineObd2::sendRequest(const uint8_t* data, uint8_t len) {
|
void KLineObd2::sendRequest(const uint8_t* data, uint8_t len) {
|
||||||
uint8_t checksum = KLineTransport::checksumMod256(data, len);
|
if (len > KLINE_MAX_MSG) return;
|
||||||
|
|
||||||
for (uint8_t i = 0; i < len; i++) {
|
// Build complete message with checksum for echo verification
|
||||||
_transport.serial()->write(data[i]);
|
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()->write(checksum);
|
|
||||||
_transport.serial()->flush();
|
_transport.serial()->flush();
|
||||||
|
|
||||||
// Clear echo: data bytes + checksum byte
|
// Verify echo (data bytes + checksum byte)
|
||||||
clearEcho(len + 1);
|
clearEcho(buf, totalLen);
|
||||||
|
|
||||||
|
_lastRequestMs = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs) {
|
uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs) {
|
||||||
@ -183,37 +216,72 @@ uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs
|
|||||||
buf[count++] = (uint8_t)first;
|
buf[count++] = (uint8_t)first;
|
||||||
|
|
||||||
// Determine expected length from format byte
|
// Determine expected length from format byte
|
||||||
// Bits 6-7: address mode (00=no addr, 10=phys, 11=func)
|
|
||||||
// Bits 0-5: data length (0 = length in separate byte)
|
|
||||||
uint8_t fmt = (uint8_t)first;
|
uint8_t fmt = (uint8_t)first;
|
||||||
uint8_t addrBytes = (fmt & 0x80) ? 2 : 0; // target + source if addressing present
|
uint8_t addrMode = (fmt >> 6) & 0x03;
|
||||||
uint8_t dataLen = fmt & 0x3F;
|
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
|
// Read address bytes if present
|
||||||
for (uint8_t i = 0; i < addrBytes && count < maxLen; i++) {
|
for (uint8_t i = 0; i < addrBytes && count < maxLen; i++) {
|
||||||
int b = readByteTimeout(timeoutMs - (millis() - start));
|
uint16_t rem = remainingMs(start, timeoutMs);
|
||||||
|
if (rem == 0) return count;
|
||||||
|
int b = readByteTimeout(rem);
|
||||||
if (b < 0) return count;
|
if (b < 0) return count;
|
||||||
buf[count++] = (uint8_t)b;
|
buf[count++] = (uint8_t)b;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If dataLen == 0, next byte is the actual length
|
// If dataLen == 0, next byte is the actual length
|
||||||
if (dataLen == 0 && count < maxLen) {
|
if (dataLen == 0 && count < maxLen) {
|
||||||
int lenByte = readByteTimeout(timeoutMs - (millis() - start));
|
uint16_t rem = remainingMs(start, timeoutMs);
|
||||||
|
if (rem == 0) return count;
|
||||||
|
int lenByte = readByteTimeout(rem);
|
||||||
if (lenByte < 0) return count;
|
if (lenByte < 0) return count;
|
||||||
buf[count++] = (uint8_t)lenByte;
|
buf[count++] = (uint8_t)lenByte;
|
||||||
dataLen = (uint8_t)lenByte;
|
dataLen = (uint8_t)lenByte;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read data bytes + checksum
|
// Read data bytes + checksum
|
||||||
uint8_t remaining = dataLen + 1; // +1 for checksum
|
uint8_t expected = dataLen + 1; // +1 for checksum
|
||||||
for (uint8_t i = 0; i < remaining && count < maxLen; i++) {
|
for (uint8_t i = 0; i < expected; i++) {
|
||||||
uint16_t elapsed = millis() - start;
|
if (count >= maxLen) {
|
||||||
if (elapsed >= timeoutMs) return count;
|
#ifdef KLINE_DEBUG
|
||||||
int b = readByteTimeout(timeoutMs - elapsed);
|
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;
|
if (b < 0) return count;
|
||||||
buf[count++] = (uint8_t)b;
|
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;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +289,11 @@ uint8_t KLineObd2::requestPid(uint8_t mode, uint8_t pid,
|
|||||||
uint8_t* response, uint8_t maxLen) {
|
uint8_t* response, uint8_t maxLen) {
|
||||||
if (!_initialized) return 0;
|
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
|
// ISO 14230 format: physical addressing, 2 data bytes
|
||||||
uint8_t req[] = {
|
uint8_t req[] = {
|
||||||
(uint8_t)(0x82), // fmt: physical addressing, 2 data bytes
|
(uint8_t)(0x82), // fmt: physical addressing, 2 data bytes
|
||||||
@ -232,27 +305,57 @@ uint8_t KLineObd2::requestPid(uint8_t mode, uint8_t pid,
|
|||||||
|
|
||||||
sendRequest(req, sizeof(req));
|
sendRequest(req, sizeof(req));
|
||||||
|
|
||||||
uint8_t raw[16];
|
uint8_t raw[32];
|
||||||
uint8_t rawLen = readResponse(raw, sizeof(raw));
|
uint8_t rawLen = readResponse(raw, sizeof(raw));
|
||||||
if (rawLen < 5) return 0; // minimum: fmt + target + source + response_mode + pid
|
if (rawLen < 5) return 0; // minimum: fmt + target + source + response_mode + pid
|
||||||
|
|
||||||
// Find response data — skip header, copy from response_mode onward
|
// Parse response structurally
|
||||||
// Response mode = request mode + 0x40
|
uint8_t dLen = 0;
|
||||||
uint8_t respMode = mode + 0x40;
|
int8_t dataStart = parseFrameData(raw, rawLen, &dLen);
|
||||||
for (uint8_t i = 0; i < rawLen; i++) {
|
if (dataStart < 0 || dLen < 2) return 0;
|
||||||
if (raw[i] == respMode && (i + 1) < rawLen && raw[i + 1] == pid) {
|
|
||||||
// Copy data bytes after mode+pid, excluding checksum
|
// Check for negative response (service 0x7F)
|
||||||
uint8_t dataStart = i + 2;
|
if (raw[dataStart] == 0x7F) {
|
||||||
uint8_t dataEnd = rawLen - 1; // exclude checksum
|
#ifdef KLINE_DEBUG
|
||||||
uint8_t dataLen = 0;
|
if (dLen >= 3) {
|
||||||
for (uint8_t j = dataStart; j < dataEnd && dataLen < maxLen; j++) {
|
Serial.print("KLINE: NRC 0x");
|
||||||
response[dataLen++] = raw[j];
|
Serial.println(raw[dataStart + 2], HEX);
|
||||||
}
|
|
||||||
return dataLen;
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ---
|
// --- Private helpers ---
|
||||||
@ -275,13 +378,35 @@ void KLineObd2::sendByte5Baud(uint8_t data) {
|
|||||||
delay(200);
|
delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KLineObd2::clearEcho(uint8_t count) {
|
bool KLineObd2::clearEcho(const uint8_t* expected, uint8_t count) {
|
||||||
|
bool match = true;
|
||||||
for (uint8_t i = 0; i < count; i++) {
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
readByteTimeout(KLINE_ECHO_TIMEOUT_MS);
|
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) {
|
int KLineObd2::readByteTimeout(uint16_t timeoutMs) {
|
||||||
|
if (timeoutMs == 0) return -1;
|
||||||
unsigned long start = millis();
|
unsigned long start = millis();
|
||||||
while (millis() - start < timeoutMs) {
|
while (millis() - start < timeoutMs) {
|
||||||
_transport.drainUart();
|
_transport.drainUart();
|
||||||
@ -292,3 +417,42 @@ int KLineObd2::readByteTimeout(uint16_t timeoutMs) {
|
|||||||
}
|
}
|
||||||
return -1;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public:
|
|||||||
void sendRequest(const uint8_t* data, uint8_t len);
|
void sendRequest(const uint8_t* data, uint8_t len);
|
||||||
|
|
||||||
// Read response from ECU. Blocks until response received or timeout.
|
// Read response from ECU. Blocks until response received or timeout.
|
||||||
// Returns number of bytes read, or 0 on timeout.
|
// Returns number of bytes read, or 0 on timeout/checksum error.
|
||||||
uint8_t readResponse(uint8_t* buf, uint8_t maxLen,
|
uint8_t readResponse(uint8_t* buf, uint8_t maxLen,
|
||||||
uint16_t timeoutMs = KLINE_RESPONSE_TIMEOUT_MS);
|
uint16_t timeoutMs = KLINE_RESPONSE_TIMEOUT_MS);
|
||||||
|
|
||||||
@ -43,6 +43,12 @@ public:
|
|||||||
uint8_t requestPid(uint8_t mode, uint8_t pid,
|
uint8_t requestPid(uint8_t mode, uint8_t pid,
|
||||||
uint8_t* response, uint8_t maxLen);
|
uint8_t* response, uint8_t maxLen);
|
||||||
|
|
||||||
|
// Send TesterPresent (service 0x3E) to keep the diagnostic session alive.
|
||||||
|
void sendTesterPresent();
|
||||||
|
|
||||||
|
// Reset initialized state (for re-init after session loss)
|
||||||
|
void reset() { _initialized = false; }
|
||||||
|
|
||||||
bool isInitialized() const { return _initialized; }
|
bool isInitialized() const { return _initialized; }
|
||||||
|
|
||||||
// Protocol info from init handshake
|
// Protocol info from init handshake
|
||||||
@ -53,16 +59,34 @@ private:
|
|||||||
// Bit-bang a single byte at 5 baud (200ms per bit) on TX pin
|
// Bit-bang a single byte at 5 baud (200ms per bit) on TX pin
|
||||||
void sendByte5Baud(uint8_t data);
|
void sendByte5Baud(uint8_t data);
|
||||||
|
|
||||||
// Discard echo bytes from half-duplex bus
|
// Detach UART TX from GPIO pin (so we can bit-bang)
|
||||||
void clearEcho(uint8_t count);
|
void detachUartTx();
|
||||||
|
|
||||||
|
// Re-attach UART TX to GPIO pin (after bit-bang init)
|
||||||
|
void reattachUartTx();
|
||||||
|
|
||||||
|
// Discard echo bytes from half-duplex bus, verify they match expected.
|
||||||
|
// Returns true if all echo bytes matched, false on bus contention.
|
||||||
|
bool clearEcho(const uint8_t* expected, uint8_t count);
|
||||||
|
|
||||||
// Read a single byte with timeout
|
// Read a single byte with timeout
|
||||||
int readByteTimeout(uint16_t timeoutMs);
|
int readByteTimeout(uint16_t timeoutMs);
|
||||||
|
|
||||||
|
// Parse ISO 14230 frame structure. Returns index of first data byte
|
||||||
|
// (service ID position), or -1 on parse error.
|
||||||
|
// Sets *dataLen to the number of data bytes.
|
||||||
|
int8_t parseFrameData(const uint8_t* buf, uint8_t bufLen, uint8_t* dataLen);
|
||||||
|
|
||||||
|
// Remaining timeout helper — clamps to 0 if already expired
|
||||||
|
static uint16_t remainingMs(unsigned long startMs, uint16_t timeoutMs);
|
||||||
|
|
||||||
KLineTransport& _transport;
|
KLineTransport& _transport;
|
||||||
bool _initialized;
|
bool _initialized;
|
||||||
uint8_t _kw1;
|
uint8_t _kw1;
|
||||||
uint8_t _kw2;
|
uint8_t _kw2;
|
||||||
uint8_t _testerAddr; // our address (0xF1 = external test equipment)
|
uint8_t _testerAddr; // our address (0xF1 = external test equipment)
|
||||||
uint8_t _ecuAddr; // ECU address from init response
|
uint8_t _ecuAddr; // ECU address from init response
|
||||||
|
unsigned long _lastRequestMs; // for P3 keepalive timing
|
||||||
|
|
||||||
|
static constexpr uint16_t P3_KEEPALIVE_MS = 4000; // send TesterPresent before P3max
|
||||||
};
|
};
|
||||||
|
|||||||
@ -112,6 +112,13 @@ int KLineTransport::rxRead() {
|
|||||||
return _rxRing.read();
|
return _rxRing.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void KLineTransport::flushRx() {
|
||||||
|
uart_flush_input((uart_port_t)_uartNum);
|
||||||
|
while (_rxRing.available() > 0) {
|
||||||
|
_rxRing.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- LED control ---
|
// --- LED control ---
|
||||||
|
|
||||||
void KLineTransport::ledOn() {
|
void KLineTransport::ledOn() {
|
||||||
|
|||||||
@ -73,6 +73,9 @@ public:
|
|||||||
void rxRemove(int n);
|
void rxRemove(int n);
|
||||||
int rxRead();
|
int rxRead();
|
||||||
|
|
||||||
|
// Flush both UART FIFO and RX ring buffer (discard all pending data)
|
||||||
|
void flushRx();
|
||||||
|
|
||||||
// Bus idle state
|
// Bus idle state
|
||||||
bool clearToSend() const { return _clearToSend; }
|
bool clearToSend() const { return _clearToSend; }
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,39 @@ KLineObd2* scanner = nullptr;
|
|||||||
|
|
||||||
static unsigned long lastPollMs = 0;
|
static unsigned long lastPollMs = 0;
|
||||||
static const unsigned long POLL_INTERVAL_MS = 500;
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@ -44,31 +77,13 @@ void setup() {
|
|||||||
KLINE_UART_NUM, config);
|
KLINE_UART_NUM, config);
|
||||||
|
|
||||||
scanner = new KLineObd2(transport);
|
scanner = new KLineObd2(transport);
|
||||||
|
if (!scanner) {
|
||||||
|
Serial.println("ERROR: allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try slow init first (ISO 9141-2)
|
if (!tryInit()) {
|
||||||
Serial.print("Slow init (0x");
|
Serial.println("No ECU response. Check wiring and key position.");
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
Serial.println("failed");
|
|
||||||
Serial.println("No ECU response. Check wiring and key position.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println();
|
Serial.println();
|
||||||
@ -80,15 +95,28 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
if (!scanner || !scanner->isInitialized()) {
|
if (!scanner) {
|
||||||
delay(1000);
|
delay(1000);
|
||||||
return;
|
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;
|
if (millis() - lastPollMs < POLL_INTERVAL_MS) return;
|
||||||
lastPollMs = millis();
|
lastPollMs = millis();
|
||||||
|
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
|
uint8_t successes = 0;
|
||||||
|
|
||||||
// RPM (2 data bytes)
|
// RPM (2 data bytes)
|
||||||
uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data));
|
uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data));
|
||||||
@ -96,6 +124,7 @@ void loop() {
|
|||||||
float rpm = obd2::decodeRpm(data[0], data[1]);
|
float rpm = obd2::decodeRpm(data[0], data[1]);
|
||||||
Serial.print(rpm, 0);
|
Serial.print(rpm, 0);
|
||||||
Serial.print(" rpm");
|
Serial.print(" rpm");
|
||||||
|
successes++;
|
||||||
} else {
|
} else {
|
||||||
Serial.print("--- ");
|
Serial.print("--- ");
|
||||||
}
|
}
|
||||||
@ -106,6 +135,7 @@ void loop() {
|
|||||||
if (len >= 1) {
|
if (len >= 1) {
|
||||||
Serial.print(obd2::decodeSpeed(data[0]));
|
Serial.print(obd2::decodeSpeed(data[0]));
|
||||||
Serial.print(" km/h");
|
Serial.print(" km/h");
|
||||||
|
successes++;
|
||||||
} else {
|
} else {
|
||||||
Serial.print("--- ");
|
Serial.print("--- ");
|
||||||
}
|
}
|
||||||
@ -116,6 +146,7 @@ void loop() {
|
|||||||
if (len >= 1) {
|
if (len >= 1) {
|
||||||
Serial.print(obd2::decodeCoolantTemp(data[0]));
|
Serial.print(obd2::decodeCoolantTemp(data[0]));
|
||||||
Serial.print(" C");
|
Serial.print(" C");
|
||||||
|
successes++;
|
||||||
} else {
|
} else {
|
||||||
Serial.print("--- ");
|
Serial.print("--- ");
|
||||||
}
|
}
|
||||||
@ -126,6 +157,7 @@ void loop() {
|
|||||||
if (len >= 1) {
|
if (len >= 1) {
|
||||||
Serial.print(obd2::decodeThrottle(data[0]), 1);
|
Serial.print(obd2::decodeThrottle(data[0]), 1);
|
||||||
Serial.print(" %");
|
Serial.print(" %");
|
||||||
|
successes++;
|
||||||
} else {
|
} else {
|
||||||
Serial.print("--- ");
|
Serial.print("--- ");
|
||||||
}
|
}
|
||||||
@ -136,9 +168,22 @@ void loop() {
|
|||||||
if (len >= 2) {
|
if (len >= 2) {
|
||||||
Serial.print(obd2::decodeControlVoltage(data[0], data[1]), 1);
|
Serial.print(obd2::decodeControlVoltage(data[0], data[1]), 1);
|
||||||
Serial.print(" V");
|
Serial.print(" V");
|
||||||
|
successes++;
|
||||||
} else {
|
} else {
|
||||||
Serial.print("--- ");
|
Serial.print("--- ");
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user