diff --git a/firmware/include/config.h b/firmware/include/config.h index 93d8076..0693af4 100644 --- a/firmware/include/config.h +++ b/firmware/include/config.h @@ -58,3 +58,39 @@ // Maximum raw message size (source + length + up to 36 body bytes) #define IBUS_MAX_MSG 40 + +// =================================================================== +// OBD-II K-line configuration (ISO 9141 / ISO 14230) +// =================================================================== + +// --- UART pin assignments (defaults to same as IBUS — shared hardware) --- +#ifndef KLINE_RX_PIN +#define KLINE_RX_PIN IBUS_RX_PIN +#endif +#ifndef KLINE_TX_PIN +#define KLINE_TX_PIN IBUS_TX_PIN +#endif +#ifndef KLINE_LED_PIN +#define KLINE_LED_PIN IBUS_LED_PIN +#endif +#ifndef KLINE_UART_NUM +#define KLINE_UART_NUM IBUS_UART_NUM +#endif + +// --- Protocol constants --- +#ifndef KLINE_BAUD +#define KLINE_BAUD 10400 +#endif +#ifndef KLINE_FRAMING +#define KLINE_FRAMING SERIAL_8N1 +#endif + +// TX inversion: 1 for optocoupler (PC817), 0 for transistor circuit +#ifndef KLINE_TX_INVERT +#define KLINE_TX_INVERT 0 +#endif + +// ISO 9141 default ECU address for 5-baud slow init +#ifndef KLINE_INIT_ADDR +#define KLINE_INIT_ADDR 0x33 +#endif diff --git a/firmware/lib/AutoWire/KLineObd2.cpp b/firmware/lib/AutoWire/KLineObd2.cpp new file mode 100644 index 0000000..3dc54ba --- /dev/null +++ b/firmware/lib/AutoWire/KLineObd2.cpp @@ -0,0 +1,294 @@ +// OBD-II K-line protocol handler (ISO 9141 / ISO 14230) +// 5-baud slow init, fast init, request/response with echo clearing. + +#include "KLineObd2.h" +#include "Obd2Pids.h" + +KLineObd2::KLineObd2(KLineTransport& transport) + : _transport(transport), + _initialized(false), + _kw1(0), + _kw2(0), + _testerAddr(obd2::TESTER_ADDR), + _ecuAddr(0) {} + +// --- 5-baud slow init (ISO 9141-2) --- +// Bit-bang the target address at 5 baud on the TX pin. +// The UART peripheral must be detached from the TX pin during this sequence, +// then re-attached for normal 10400 baud communication. +// +// Sequence: +// 1. TX pin LOW for 200ms (start bit at 5 baud) +// 2. 8 data bits of targetAddr, LSB first, 200ms each +// 3. TX pin HIGH for 200ms (stop bit) +// 4. Re-attach UART, wait for ECU response +// 5. ECU sends: 0x55 (sync), keyword1, keyword2 +// 6. Tester sends: inverted keyword2 +// 7. ECU sends: inverted tester address (0xCC for 0x33) + +bool KLineObd2::slowInit(uint8_t targetAddr) { + int8_t txp = _transport.txPin(); + uint8_t unum = _transport.uartNum(); + + // Detach UART from TX pin so we can bit-bang + uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + + // Configure TX pin as GPIO output, start HIGH (idle) + pinMode(txp, OUTPUT); + digitalWrite(txp, HIGH); + delay(300); // W0: idle before init (>300ms per ISO 9141) + + // Bit-bang target address at 5 baud + sendByte5Baud(targetAddr); + + // Re-attach UART to TX pin + uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + + // Wait for ECU sync byte (0x55) — W1: 20-300ms per ISO 9141 + int sync = readByteTimeout(300); + if (sync != obd2::SYNC_BYTE) { + return false; + } + + // Read keyword bytes — W2: 5-20ms between bytes + int kw1 = readByteTimeout(50); + if (kw1 < 0) return false; + _kw1 = (uint8_t)kw1; + + int kw2 = readByteTimeout(50); + if (kw2 < 0) return false; + _kw2 = (uint8_t)kw2; + + // W3: 25-50ms before tester responds + delay(30); + + // Send inverted keyword2 back to ECU + uint8_t invKw2 = ~_kw2; + _transport.serial()->write(invKw2); + _transport.serial()->flush(); + + // Clear our own echo (half-duplex bus) + clearEcho(1); + + // W4: ECU confirms by sending inverted target address + int ack = readByteTimeout(50); + if (ack < 0) return false; + + // The ack should be the complement of the target address + if ((uint8_t)ack != (uint8_t)~targetAddr) { + return false; + } + + _ecuAddr = targetAddr; + _initialized = true; + return true; +} + +// --- Fast init (ISO 14230-2, KWP2000) --- +// 25ms LOW + 25ms HIGH "TiniPulse" on TX, then StartComm request. + +bool KLineObd2::fastInit() { + int8_t txp = _transport.txPin(); + uint8_t unum = _transport.uartNum(); + + // Detach UART from TX pin for the wake-up pulse + uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + + pinMode(txp, OUTPUT); + digitalWrite(txp, HIGH); + delay(300); // idle before pulse + + // TiniPulse: 25ms LOW + 25ms HIGH + digitalWrite(txp, LOW); + delay(25); + digitalWrite(txp, HIGH); + delay(25); + + // Re-attach UART + uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + + // Send StartCommunication request (KWP2000 service 0x81) + // Format: [fmt+len, target, source, service_id] + uint8_t startComm[] = { + 0xC1, // format: functional addressing, 1 data byte + obd2::FUNC_ADDR, // target: functional address + obd2::TESTER_ADDR, // source: tester + 0x81 // StartCommunication service + }; + uint8_t checksum = KLineTransport::checksumMod256(startComm, 4); + + for (uint8_t i = 0; i < 4; i++) { + _transport.serial()->write(startComm[i]); + } + _transport.serial()->write(checksum); + _transport.serial()->flush(); + + // Clear echo (5 bytes: 4 data + 1 checksum) + clearEcho(5); + + // Read StartComm positive response + // Expected: [fmt+len, source, target, 0xC1, kw1, kw2, checksum] + uint8_t resp[7]; + uint8_t respLen = readResponse(resp, sizeof(resp), 300); + if (respLen < 4) return false; + + // Positive response service ID is request + 0x40 + // Find the service byte — it's after header bytes + // For physical addressing response: [fmt, target, source, 0xC1, kw1, kw2, chk] + bool foundResponse = false; + for (uint8_t i = 0; i < respLen; i++) { + if (resp[i] == 0xC1) { // StartComm positive response + if (i + 2 < respLen) { + _kw1 = resp[i + 1]; + _kw2 = resp[i + 2]; + foundResponse = true; + } + break; + } + } + + if (!foundResponse) return false; + + _ecuAddr = obd2::FUNC_ADDR; + _initialized = true; + return true; +} + +// --- Request/response --- + +void KLineObd2::sendRequest(const uint8_t* data, uint8_t len) { + uint8_t checksum = KLineTransport::checksumMod256(data, len); + + for (uint8_t i = 0; i < len; i++) { + _transport.serial()->write(data[i]); + } + _transport.serial()->write(checksum); + _transport.serial()->flush(); + + // Clear echo: data bytes + checksum byte + clearEcho(len + 1); +} + +uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs) { + uint8_t count = 0; + unsigned long start = millis(); + + // Read first byte (format/header) to determine message length + int first = readByteTimeout(timeoutMs); + if (first < 0) return 0; + buf[count++] = (uint8_t)first; + + // Determine expected length from format byte + // Bits 6-7: address mode (00=no addr, 10=phys, 11=func) + // Bits 0-5: data length (0 = length in separate byte) + uint8_t fmt = (uint8_t)first; + uint8_t addrBytes = (fmt & 0x80) ? 2 : 0; // target + source if addressing present + uint8_t dataLen = fmt & 0x3F; + + // Read address bytes if present + for (uint8_t i = 0; i < addrBytes && count < maxLen; i++) { + int b = readByteTimeout(timeoutMs - (millis() - start)); + if (b < 0) return count; + buf[count++] = (uint8_t)b; + } + + // If dataLen == 0, next byte is the actual length + if (dataLen == 0 && count < maxLen) { + int lenByte = readByteTimeout(timeoutMs - (millis() - start)); + if (lenByte < 0) return count; + buf[count++] = (uint8_t)lenByte; + dataLen = (uint8_t)lenByte; + } + + // Read data bytes + checksum + uint8_t remaining = dataLen + 1; // +1 for checksum + for (uint8_t i = 0; i < remaining && count < maxLen; i++) { + uint16_t elapsed = millis() - start; + if (elapsed >= timeoutMs) return count; + int b = readByteTimeout(timeoutMs - elapsed); + if (b < 0) return count; + buf[count++] = (uint8_t)b; + } + + return count; +} + +uint8_t KLineObd2::requestPid(uint8_t mode, uint8_t pid, + uint8_t* response, uint8_t maxLen) { + if (!_initialized) return 0; + + // ISO 14230 format: physical addressing, 2 data bytes + uint8_t req[] = { + (uint8_t)(0x82), // fmt: physical addressing, 2 data bytes + _ecuAddr, // target + _testerAddr, // source + mode, // service/mode + pid // PID + }; + + sendRequest(req, sizeof(req)); + + uint8_t raw[16]; + uint8_t rawLen = readResponse(raw, sizeof(raw)); + if (rawLen < 5) return 0; // minimum: fmt + target + source + response_mode + pid + + // Find response data — skip header, copy from response_mode onward + // Response mode = request mode + 0x40 + uint8_t respMode = mode + 0x40; + for (uint8_t i = 0; i < rawLen; i++) { + if (raw[i] == respMode && (i + 1) < rawLen && raw[i + 1] == pid) { + // Copy data bytes after mode+pid, excluding checksum + uint8_t dataStart = i + 2; + uint8_t dataEnd = rawLen - 1; // exclude checksum + uint8_t dataLen = 0; + for (uint8_t j = dataStart; j < dataEnd && dataLen < maxLen; j++) { + response[dataLen++] = raw[j]; + } + return dataLen; + } + } + + return 0; +} + +// --- Private helpers --- + +void KLineObd2::sendByte5Baud(uint8_t data) { + int8_t txp = _transport.txPin(); + + // Start bit (LOW) + digitalWrite(txp, LOW); + delay(200); + + // 8 data bits, LSB first + for (uint8_t i = 0; i < 8; i++) { + digitalWrite(txp, (data >> i) & 0x01 ? HIGH : LOW); + delay(200); + } + + // Stop bit (HIGH) + digitalWrite(txp, HIGH); + delay(200); +} + +void KLineObd2::clearEcho(uint8_t count) { + for (uint8_t i = 0; i < count; i++) { + readByteTimeout(KLINE_ECHO_TIMEOUT_MS); + } +} + +int KLineObd2::readByteTimeout(uint16_t timeoutMs) { + unsigned long start = millis(); + while (millis() - start < timeoutMs) { + _transport.drainUart(); + if (_transport.rxAvailable() > 0) { + return _transport.rxRead(); + } + delayMicroseconds(100); // prevent busy-wait burning CPU + } + return -1; +} diff --git a/firmware/lib/AutoWire/KLineObd2.h b/firmware/lib/AutoWire/KLineObd2.h new file mode 100644 index 0000000..0b1f192 --- /dev/null +++ b/firmware/lib/AutoWire/KLineObd2.h @@ -0,0 +1,68 @@ +#pragma once +// OBD-II K-line protocol handler (ISO 9141 / ISO 14230) +// Uses KLineTransport for UART, ring buffers, and bus management. +// Handles 5-baud slow init, fast init, request/response framing, +// and half-duplex echo clearing. + +#include +#include "KLineTransport.h" +#include "driver/uart.h" + +#ifndef KLINE_RESPONSE_TIMEOUT_MS +#define KLINE_RESPONSE_TIMEOUT_MS 2000 +#endif +#ifndef KLINE_ECHO_TIMEOUT_MS +#define KLINE_ECHO_TIMEOUT_MS 100 +#endif + +class KLineObd2 { +public: + KLineObd2(KLineTransport& transport); + + // ISO 9141 5-baud slow init — bit-bangs target address at 5 baud, + // then reads sync (0x55) and keyword bytes from ECU. + // Returns true if ECU responds with valid sync + keywords. + bool slowInit(uint8_t targetAddr = 0x33); + + // ISO 14230 fast init — 25ms LOW + 25ms HIGH TiniPulse, + // then reads StartComm response. + bool fastInit(); + + // Send an OBD-II request. Frames the message and discards echo bytes. + // data[] should contain: [header, source, target, mode, pid, ...] + // For simple PID requests, use requestPid() instead. + void sendRequest(const uint8_t* data, uint8_t len); + + // Read response from ECU. Blocks until response received or timeout. + // Returns number of bytes read, or 0 on timeout. + uint8_t readResponse(uint8_t* buf, uint8_t maxLen, + uint16_t timeoutMs = KLINE_RESPONSE_TIMEOUT_MS); + + // Convenience: send a mode/PID request and read the response. + // Returns response data length (excluding header/checksum), or 0 on failure. + uint8_t requestPid(uint8_t mode, uint8_t pid, + uint8_t* response, uint8_t maxLen); + + bool isInitialized() const { return _initialized; } + + // Protocol info from init handshake + uint8_t keyword1() const { return _kw1; } + uint8_t keyword2() const { return _kw2; } + +private: + // Bit-bang a single byte at 5 baud (200ms per bit) on TX pin + void sendByte5Baud(uint8_t data); + + // Discard echo bytes from half-duplex bus + void clearEcho(uint8_t count); + + // Read a single byte with timeout + int readByteTimeout(uint16_t timeoutMs); + + KLineTransport& _transport; + bool _initialized; + uint8_t _kw1; + uint8_t _kw2; + uint8_t _testerAddr; // our address (0xF1 = external test equipment) + uint8_t _ecuAddr; // ECU address from init response +}; diff --git a/firmware/lib/AutoWire/Obd2Pids.h b/firmware/lib/AutoWire/Obd2Pids.h new file mode 100644 index 0000000..f101a54 --- /dev/null +++ b/firmware/lib/AutoWire/Obd2Pids.h @@ -0,0 +1,141 @@ +#pragma once +// OBD-II PID constants and decode helpers (SAE J1979) +// Covers the most common Mode 01 (current data) PIDs. + +#include + +namespace obd2 { + +// --- Diagnostic modes --- +constexpr uint8_t MODE_CURRENT = 0x01; // current data +constexpr uint8_t MODE_FREEZE = 0x02; // freeze frame +constexpr uint8_t MODE_DTC = 0x03; // stored DTCs +constexpr uint8_t MODE_CLEAR_DTC = 0x04; // clear DTCs +constexpr uint8_t MODE_O2_TEST = 0x05; // O2 sensor monitoring +constexpr uint8_t MODE_TEST_RESULTS = 0x06; // on-board monitoring test results +constexpr uint8_t MODE_PENDING_DTC = 0x07; // pending DTCs +constexpr uint8_t MODE_CONTROL = 0x08; // control of on-board system +constexpr uint8_t MODE_VEHICLE_INFO = 0x09; // vehicle information (VIN, etc.) +constexpr uint8_t MODE_PERMANENT_DTC = 0x0A; // permanent DTCs + +// --- Mode 01 PIDs --- +constexpr uint8_t PID_SUPPORTED_01_20 = 0x00; // supported PIDs [01-20] +constexpr uint8_t PID_DTC_STATUS = 0x01; // monitor status since DTCs cleared +constexpr uint8_t PID_FUEL_STATUS = 0x03; // fuel system status +constexpr uint8_t PID_ENGINE_LOAD = 0x04; // calculated engine load (%) +constexpr uint8_t PID_COOLANT_TEMP = 0x05; // engine coolant temperature (C) +constexpr uint8_t PID_SHORT_FUEL_TRIM_1 = 0x06; // short term fuel trim bank 1 (%) +constexpr uint8_t PID_LONG_FUEL_TRIM_1 = 0x07; // long term fuel trim bank 1 (%) +constexpr uint8_t PID_INTAKE_PRESSURE = 0x0B; // intake manifold pressure (kPa) +constexpr uint8_t PID_RPM = 0x0C; // engine RPM +constexpr uint8_t PID_SPEED = 0x0D; // vehicle speed (km/h) +constexpr uint8_t PID_TIMING_ADVANCE = 0x0E; // timing advance (degrees BTDC) +constexpr uint8_t PID_INTAKE_TEMP = 0x0F; // intake air temperature (C) +constexpr uint8_t PID_MAF_RATE = 0x10; // MAF air flow rate (g/s) +constexpr uint8_t PID_THROTTLE = 0x11; // throttle position (%) +constexpr uint8_t PID_OBD_STANDARD = 0x1C; // OBD standards compliance +constexpr uint8_t PID_RUN_TIME = 0x1F; // run time since engine start (s) +constexpr uint8_t PID_SUPPORTED_21_40 = 0x20; // supported PIDs [21-40] +constexpr uint8_t PID_FUEL_LEVEL = 0x2F; // fuel tank level input (%) +constexpr uint8_t PID_BARO_PRESSURE = 0x33; // barometric pressure (kPa) +constexpr uint8_t PID_SUPPORTED_41_60 = 0x40; // supported PIDs [41-60] +constexpr uint8_t PID_CONTROL_VOLTAGE = 0x42; // control module voltage (V) +constexpr uint8_t PID_FUEL_TYPE = 0x51; // fuel type +constexpr uint8_t PID_OIL_TEMP = 0x5C; // engine oil temperature (C) +constexpr uint8_t PID_FUEL_RATE = 0x5E; // engine fuel rate (L/h) + +// --- K-line protocol constants --- +constexpr uint8_t SYNC_BYTE = 0x55; // ECU sync response +constexpr uint8_t DEFAULT_TARGET = 0x33; // ISO 9141 default ECU address +constexpr uint8_t TESTER_ADDR = 0xF1; // external test equipment +constexpr uint8_t FUNC_ADDR = 0x33; // functional addressing + +// --- ISO 14230 (KWP2000) header formats --- +constexpr uint8_t KWP_FMT_NO_ADDR = 0x00; // no address info +constexpr uint8_t KWP_FMT_PHYS_ADDR = 0x80; // physical addressing +constexpr uint8_t KWP_FMT_FUNC_ADDR = 0xC0; // functional addressing + +// --- Decode helpers --- +// All formulas from SAE J1979 / ISO 15031-5 + +// PID 0x04: Calculated engine load (0-100%) +inline float decodeEngineLoad(uint8_t a) { + return a * (100.0f / 255.0f); +} + +// PID 0x05: Engine coolant temperature (-40 to 215 C) +inline int16_t decodeCoolantTemp(uint8_t a) { + return (int16_t)a - 40; +} + +// PID 0x06/0x07: Short/long term fuel trim (-100 to 99.2%) +inline float decodeFuelTrim(uint8_t a) { + return (a / 1.28f) - 100.0f; +} + +// PID 0x0B: Intake manifold pressure (0-255 kPa) +inline uint8_t decodeIntakePressure(uint8_t a) { + return a; +} + +// PID 0x0C: Engine RPM (0-16383.75 RPM) — 2 bytes +inline float decodeRpm(uint8_t a, uint8_t b) { + return ((a * 256.0f) + b) / 4.0f; +} + +// PID 0x0D: Vehicle speed (0-255 km/h) +inline uint8_t decodeSpeed(uint8_t a) { + return a; +} + +// PID 0x0E: Timing advance (-64 to 63.5 degrees BTDC) +inline float decodeTimingAdvance(uint8_t a) { + return (a / 2.0f) - 64.0f; +} + +// PID 0x0F: Intake air temperature (-40 to 215 C) +inline int16_t decodeIntakeTemp(uint8_t a) { + return (int16_t)a - 40; +} + +// PID 0x10: MAF air flow rate (0-655.35 g/s) — 2 bytes +inline float decodeMafRate(uint8_t a, uint8_t b) { + return ((a * 256.0f) + b) / 100.0f; +} + +// PID 0x11: Throttle position (0-100%) +inline float decodeThrottle(uint8_t a) { + return a * (100.0f / 255.0f); +} + +// PID 0x1F: Run time since engine start (0-65535 s) — 2 bytes +inline uint16_t decodeRunTime(uint8_t a, uint8_t b) { + return (uint16_t)(a * 256) + b; +} + +// PID 0x2F: Fuel tank level input (0-100%) +inline float decodeFuelLevel(uint8_t a) { + return a * (100.0f / 255.0f); +} + +// PID 0x33: Barometric pressure (0-255 kPa) +inline uint8_t decodeBaroPressure(uint8_t a) { + return a; +} + +// PID 0x42: Control module voltage (0-65.535 V) — 2 bytes +inline float decodeControlVoltage(uint8_t a, uint8_t b) { + return ((a * 256.0f) + b) / 1000.0f; +} + +// PID 0x5C: Engine oil temperature (-40 to 210 C) +inline int16_t decodeOilTemp(uint8_t a) { + return (int16_t)a - 40; +} + +// PID 0x5E: Engine fuel rate (0-3212.75 L/h) — 2 bytes +inline float decodeFuelRate(uint8_t a, uint8_t b) { + return ((a * 256.0f) + b) / 20.0f; +} + +} // namespace obd2 diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 4fddf18..2dfb206 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -1,4 +1,5 @@ -; BMW I/K-Bus Interface — ESP32 + PC817 Optocoupler +; AutoWire — Multi-protocol automotive bus interface (ESP32) +; BMW I/K-Bus (optocoupler) + OBD-II K-line (transistor/optocoupler) ; Based on muki01/I-K_Bus (MIT), ported for ESP32 with R2=220 fix [platformio] @@ -17,10 +18,15 @@ build_flags = -DIBUS_IDLE_TIMEOUT_US=1500 -DIBUS_PACKET_GAP_MS=10 +; =================================================================== +; BMW I/K-Bus sniffer environments (main.cpp) +; =================================================================== + ; --- ESP32 (classic, dual-core) --- ; GPIO 16/17 are UART2 defaults, free on most devkits [env:esp32dev] board = esp32dev +build_src_filter = + build_flags = ${env.build_flags} -DIBUS_RX_PIN=16 @@ -32,6 +38,7 @@ build_flags = ; GPIO 4/5 are general-purpose; GPIO 8 is onboard LED on most C3 devkits [env:esp32-c3] board = esp32-c3-devkitm-1 +build_src_filter = + build_flags = ${env.build_flags} -DIBUS_RX_PIN=4 @@ -43,9 +50,28 @@ build_flags = ; GPIO 15/16 free on S3-DevKitM; GPIO 48 is RGB LED [env:esp32-s3] board = esp32-s3-devkitm-1 +build_src_filter = + build_flags = ${env.build_flags} -DIBUS_RX_PIN=15 -DIBUS_TX_PIN=16 -DIBUS_LED_PIN=48 -DIBUS_UART_NUM=1 + +; =================================================================== +; OBD-II K-line scanner environment (obd2_scanner.cpp) +; =================================================================== + +; Uses same ESP32 devkit — different sketch and protocol config. +; KLINE_TX_INVERT=0 for transistor circuit (Tucker k-line-board). +; Set to 1 if using PC817 optocoupler interface. +[env:obd2-scanner] +board = esp32dev +build_src_filter = + +build_flags = + ${env.build_flags} + -DIBUS_RX_PIN=16 + -DIBUS_TX_PIN=17 + -DIBUS_LED_PIN=2 + -DIBUS_UART_NUM=1 + -DKLINE_TX_INVERT=0 diff --git a/firmware/src/obd2_scanner.cpp b/firmware/src/obd2_scanner.cpp new file mode 100644 index 0000000..ca3e37a --- /dev/null +++ b/firmware/src/obd2_scanner.cpp @@ -0,0 +1,144 @@ +// OBD-II K-line Scanner — ESP32 + transistor/optocoupler interface +// Polls common PIDs (RPM, speed, coolant temp) and prints to serial. +// +// Hardware: Transistor circuit (Tucker k-line-board) or PC817 optocoupler. +// Set KLINE_TX_INVERT=1 in build flags for optocoupler, 0 for transistor. + +#include +#include "config.h" +#include "KLineTransport.h" +#include "KLineObd2.h" +#include "Obd2Pids.h" + +KLineTransport transport; +KLineObd2* scanner = nullptr; + +static unsigned long lastPollMs = 0; +static const unsigned long POLL_INTERVAL_MS = 500; + +void setup() { + Serial.begin(115200); + delay(500); + + Serial.println(); + Serial.println("=== OBD-II K-line Scanner ==="); + Serial.print("Bus UART: "); + Serial.print(KLINE_BAUD); + Serial.println(" 8N1"); + Serial.print("RX pin: GPIO "); + Serial.print(KLINE_RX_PIN); + Serial.print(" TX pin: GPIO "); + Serial.println(KLINE_TX_PIN); + Serial.println(); + + KLineConfig config; + config.baud = KLINE_BAUD; + config.framing = KLINE_FRAMING; + config.idleTimeoutUs = 0; // master/slave — no idle detection needed + config.idleCheckUs = 0; + config.packetGapMs = 50; // P3: inter-message timing + config.txInvert = KLINE_TX_INVERT; + config.checksumType = CHECKSUM_MOD256; + + transport.begin(Serial1, KLINE_RX_PIN, KLINE_TX_PIN, KLINE_LED_PIN, + KLINE_UART_NUM, config); + + scanner = new KLineObd2(transport); + + // Try slow init first (ISO 9141-2) + Serial.print("Slow init (0x"); + Serial.print(KLINE_INIT_ADDR, HEX); + Serial.print(")... "); + + if (scanner->slowInit(KLINE_INIT_ADDR)) { + Serial.println("OK"); + Serial.print("Keywords: "); + Serial.print(scanner->keyword1(), HEX); + Serial.print(" "); + Serial.println(scanner->keyword2(), HEX); + } else { + Serial.println("failed"); + Serial.print("Fast init... "); + if (scanner->fastInit()) { + Serial.println("OK"); + Serial.print("Keywords: "); + Serial.print(scanner->keyword1(), HEX); + Serial.print(" "); + Serial.println(scanner->keyword2(), HEX); + } else { + Serial.println("failed"); + Serial.println("No ECU response. Check wiring and key position."); + } + } + + Serial.println(); + if (scanner->isInitialized()) { + Serial.println("Polling PIDs..."); + Serial.println("RPM | Speed | Coolant | Throttle | Voltage"); + Serial.println("---------+---------+---------+----------+--------"); + } +} + +void loop() { + if (!scanner || !scanner->isInitialized()) { + delay(1000); + return; + } + + if (millis() - lastPollMs < POLL_INTERVAL_MS) return; + lastPollMs = millis(); + + uint8_t data[4]; + + // RPM (2 data bytes) + uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data)); + if (len >= 2) { + float rpm = obd2::decodeRpm(data[0], data[1]); + Serial.print(rpm, 0); + Serial.print(" rpm"); + } else { + Serial.print("--- "); + } + Serial.print(" | "); + + // Speed (1 data byte) + len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_SPEED, data, sizeof(data)); + if (len >= 1) { + Serial.print(obd2::decodeSpeed(data[0])); + Serial.print(" km/h"); + } else { + Serial.print("--- "); + } + Serial.print(" | "); + + // Coolant temp (1 data byte) + len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_COOLANT_TEMP, data, sizeof(data)); + if (len >= 1) { + Serial.print(obd2::decodeCoolantTemp(data[0])); + Serial.print(" C"); + } else { + Serial.print("--- "); + } + Serial.print(" | "); + + // Throttle (1 data byte) + len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_THROTTLE, data, sizeof(data)); + if (len >= 1) { + Serial.print(obd2::decodeThrottle(data[0]), 1); + Serial.print(" %"); + } else { + Serial.print("--- "); + } + Serial.print(" | "); + + // Control module voltage (2 data bytes) + len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_CONTROL_VOLTAGE, data, sizeof(data)); + if (len >= 2) { + Serial.print(obd2::decodeControlVoltage(data[0], data[1]), 1); + Serial.print(" V"); + } else { + Serial.print("--- "); + } + + Serial.println(); +}