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.
258 lines
6.8 KiB
C++
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);
|
|
}
|
|
}
|