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

93 lines
3.4 KiB
C++

#pragma once
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
// Uses KLineTransport for UART, ring buffers, and bus management.
// Handles 5-baud slow init, fast init, request/response framing,
// and half-duplex echo clearing.
#include <Arduino.h>
#include "KLineTransport.h"
#include "driver/uart.h"
#ifndef KLINE_RESPONSE_TIMEOUT_MS
#define KLINE_RESPONSE_TIMEOUT_MS 2000
#endif
#ifndef KLINE_ECHO_TIMEOUT_MS
#define KLINE_ECHO_TIMEOUT_MS 100
#endif
class KLineObd2 {
public:
KLineObd2(KLineTransport& transport);
// ISO 9141 5-baud slow init — bit-bangs target address at 5 baud,
// then reads sync (0x55) and keyword bytes from ECU.
// Returns true if ECU responds with valid sync + keywords.
bool slowInit(uint8_t targetAddr = 0x33);
// ISO 14230 fast init — 25ms LOW + 25ms HIGH TiniPulse,
// then reads StartComm response.
bool fastInit();
// Send an OBD-II request. Frames the message and discards echo bytes.
// data[] should contain: [header, source, target, mode, pid, ...]
// For simple PID requests, use requestPid() instead.
void sendRequest(const uint8_t* data, uint8_t len);
// Read response from ECU. Blocks until response received or timeout.
// Returns number of bytes read, or 0 on timeout/checksum error.
uint8_t readResponse(uint8_t* buf, uint8_t maxLen,
uint16_t timeoutMs = KLINE_RESPONSE_TIMEOUT_MS);
// Convenience: send a mode/PID request and read the response.
// Returns response data length (excluding header/checksum), or 0 on failure.
uint8_t requestPid(uint8_t mode, uint8_t pid,
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; }
// Protocol info from init handshake
uint8_t keyword1() const { return _kw1; }
uint8_t keyword2() const { return _kw2; }
private:
// Bit-bang a single byte at 5 baud (200ms per bit) on TX pin
void sendByte5Baud(uint8_t data);
// Detach UART TX from GPIO pin (so we can bit-bang)
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
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;
bool _initialized;
uint8_t _kw1;
uint8_t _kw2;
uint8_t _testerAddr; // our address (0xF1 = external test equipment)
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
};