i-k-bus-board/firmware/lib/KLine/KLineTransport.cpp
Ryan Malloy 2aaa6e32a5 Rename library from AutoWire to K-Line
Library dir: lib/AutoWire/ -> lib/KLine/
Site title, docs, CLAUDE.md, platformio.ini all updated.
All 4 firmware environments build clean.
2026-02-13 08:31:34 -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);
}
}