// 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(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(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(); } // --- 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); } }