i-k-bus-board/firmware/lib/AutoWire/KLineTransport.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

258 lines
6.8 KiB
C++

// Hardware transport for single-wire automotive buses (K-line family)
// Extracted from IbusEsp32 — all ISR/timer/ring-buffer logic lives here.
// Protocol handlers (IbusHandler, KLineObd2) read/write through this layer.
#include "KLineTransport.h"
#include "driver/uart.h"
KLineTransport::KLineTransport()
: _serial(nullptr),
_ledPin(-1),
_txPin(-1),
_uartNum(1),
_rxRing(KLINE_RX_BUFFER_SIZE),
_txRing(KLINE_TX_BUFFER_SIZE),
_idleTimer(nullptr),
_lastRxTransitionUs(0),
_clearToSend(false),
_isTransmitting(false),
_lastTxMs(0) {}
KLineTransport::~KLineTransport() {
if (_idleTimer) {
esp_timer_stop(_idleTimer);
esp_timer_delete(_idleTimer);
}
}
void KLineTransport::begin(HardwareSerial& serial, int8_t rxPin, int8_t txPin,
int8_t ledPin, uint8_t uartNum,
const KLineConfig& config) {
_serial = &serial;
_ledPin = ledPin;
_txPin = txPin;
_uartNum = uartNum;
_config = config;
if (_ledPin >= 0) {
pinMode(_ledPin, OUTPUT);
digitalWrite(_ledPin, LOW);
}
_serial->begin(_config.baud, _config.framing, rxPin, txPin);
if (_config.txInvert) {
uart_set_line_inverse((uart_port_t)uartNum, UART_SIGNAL_TXD_INV);
}
if (_config.idleTimeoutUs > 0) {
setupBusIdleDetection(rxPin);
} else {
// Master/slave mode — always clear to send (no contention)
_clearToSend = true;
}
}
// --- Bus idle detection ---
// On every RX pin transition (bus activity), record timestamp and clear CTS.
// Periodic timer checks whether bus has been quiet for >= idleTimeoutUs.
void KLineTransport::setupBusIdleDetection(int8_t rxPin) {
_lastRxTransitionUs = (uint32_t)esp_timer_get_time();
_clearToSend = false;
pinMode(rxPin, INPUT);
attachInterruptArg(digitalPinToInterrupt(rxPin), onRxPinChange, this, CHANGE);
esp_timer_create_args_t args = {};
args.callback = onIdleTimerCallback;
args.arg = this;
args.name = "kline_idle";
esp_timer_create(&args, &_idleTimer);
esp_timer_start_periodic(_idleTimer, _config.idleCheckUs);
}
void IRAM_ATTR KLineTransport::onRxPinChange(void* arg) {
KLineTransport* self = static_cast<KLineTransport*>(arg);
if (self->_isTransmitting) return;
self->_lastRxTransitionUs = (uint32_t)esp_timer_get_time();
self->_clearToSend = false;
}
void KLineTransport::onIdleTimerCallback(void* arg) {
KLineTransport* self = static_cast<KLineTransport*>(arg);
if (self->_clearToSend || self->_isTransmitting) return;
uint32_t elapsed = (uint32_t)esp_timer_get_time() - self->_lastRxTransitionUs;
if (elapsed >= self->_config.idleTimeoutUs) {
self->_clearToSend = true;
}
}
// --- UART drain + RX ring accessors ---
void KLineTransport::drainUart() {
while (_serial->available()) {
_rxRing.write(_serial->read());
}
}
int KLineTransport::rxAvailable() {
return _rxRing.available();
}
int KLineTransport::rxPeek(int n) {
return _rxRing.peek(n);
}
void KLineTransport::rxRemove(int n) {
_rxRing.remove(n);
}
int KLineTransport::rxRead() {
return _rxRing.read();
}
void KLineTransport::flushRx() {
uart_flush_input((uart_port_t)_uartNum);
while (_rxRing.available() > 0) {
_rxRing.read();
}
}
// --- LED control ---
void KLineTransport::ledOn() {
if (_ledPin >= 0) digitalWrite(_ledPin, HIGH);
}
void KLineTransport::ledOff() {
if (_ledPin >= 0) digitalWrite(_ledPin, LOW);
}
// --- Transmit ---
void KLineTransport::write(const uint8_t* message, uint8_t size) {
uint8_t totalNeeded = size + 2; // length prefix + message + checksum
int freeSpace = _txRing.capacity() - _txRing.available();
if (freeSpace < totalNeeded) {
#ifdef KLINE_DEBUG
Serial.println("TX DROP: buffer full");
#endif
return;
}
uint8_t checksum = calculateChecksum(message, size);
_txRing.write(size + 1); // total bytes: message + checksum
for (uint8_t i = 0; i < size; i++) {
_txRing.write(message[i]);
}
_txRing.write(checksum);
}
void KLineTransport::sendRaw(const uint8_t* data, uint8_t len) {
_isTransmitting = true;
for (uint8_t i = 0; i < len; i++) {
_serial->write(data[i]);
}
_serial->flush();
delayMicroseconds(200);
if (_config.idleTimeoutUs > 0) {
_lastRxTransitionUs = (uint32_t)esp_timer_get_time();
_clearToSend = false;
}
__asm__ __volatile__("" ::: "memory");
_isTransmitting = false;
}
void KLineTransport::run() {
trySend();
}
void KLineTransport::trySend() {
if (!_clearToSend) return;
if (_txRing.available() <= 0) return;
if (millis() - _lastTxMs < _config.packetGapMs) return;
sendNextPacket();
_lastTxMs = millis();
}
void KLineTransport::sendNextPacket() {
if (_txRing.available() <= 0) return;
int packetLen = _txRing.read();
if (packetLen <= 0 || packetLen > KLINE_MAX_MSG) {
#ifdef KLINE_DEBUG
Serial.print("TX CORRUPT len=");
Serial.println(packetLen);
#endif
while (_txRing.available() > 0) _txRing.read();
return;
}
if (_txRing.available() < packetLen) {
#ifdef KLINE_DEBUG
Serial.println("TX UNDERRUN");
#endif
while (_txRing.available() > 0) _txRing.read();
return;
}
_isTransmitting = true;
for (int i = 0; i < packetLen; i++) {
int raw = _txRing.read();
if (raw < 0) {
#ifdef KLINE_DEBUG
Serial.println("TX ABORT: underrun");
#endif
if (_config.idleTimeoutUs > 0) {
_lastRxTransitionUs = (uint32_t)esp_timer_get_time();
_clearToSend = false;
}
__asm__ __volatile__("" ::: "memory");
_isTransmitting = false;
return;
}
_serial->write((uint8_t)raw);
}
_serial->flush();
delayMicroseconds(200);
if (_config.idleTimeoutUs > 0) {
_lastRxTransitionUs = (uint32_t)esp_timer_get_time();
_clearToSend = false;
}
__asm__ __volatile__("" ::: "memory");
_isTransmitting = false;
}
// --- Checksum utilities ---
uint8_t KLineTransport::checksumXor(const uint8_t* data, uint8_t length) {
uint8_t checksum = 0;
for (uint8_t i = 0; i < length; i++) {
checksum ^= data[i];
}
return checksum;
}
uint8_t KLineTransport::checksumMod256(const uint8_t* data, uint8_t length) {
uint8_t checksum = 0;
for (uint8_t i = 0; i < length; i++) {
checksum += data[i];
}
return checksum;
}
uint8_t KLineTransport::calculateChecksum(const uint8_t* data, uint8_t length) const {
switch (_config.checksumType) {
case CHECKSUM_MOD256:
return checksumMod256(data, length);
case CHECKSUM_XOR:
default:
return checksumXor(data, length);
}
}