Library dir: lib/AutoWire/ -> lib/KLine/ Site title, docs, CLAUDE.md, platformio.ini all updated. All 4 firmware environments build clean.
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);
|
|
}
|
|
}
|