Getting Started
K-Line is a multi-protocol automotive bus library for ESP32. It supports BMW I/K-Bus (9600 baud, 8E1, optocoupler isolated) and OBD-II K-line (10400 baud, 8N1, ISO 9141/14230). The firmware ships with two sketches: a BMW bus sniffer and an OBD-II scanner.
Prerequisites
Section titled “Prerequisites”- PlatformIO (CLI or VS Code extension)
- An ESP32 development board (ESP32, ESP32-C3, or ESP32-S3)
- USB cable for flashing and serial monitor
PlatformIO handles all toolchain and library dependencies automatically. No manual SDK installation needed.
Clone and Build
Section titled “Clone and Build”git clone <repository-url>cd i-k-bus-board/firmwareBMW I/K-Bus Sniffer
Section titled “BMW I/K-Bus Sniffer”The default build target. Prints all bus traffic with module name lookup.
# Build for ESP32 (classic, dual-core)pio run -e esp32dev
# Build for ESP32-C3 (single-core RISC-V)pio run -e esp32-c3
# Build for ESP32-S3 (dual-core, USB native)pio run -e esp32-s3OBD-II K-line Scanner
Section titled “OBD-II K-line Scanner”Polls RPM, speed, coolant temperature, throttle position, and battery voltage from an ECU via ISO 9141/14230 K-line.
pio run -e obd2-scannerFlash and Monitor
Section titled “Flash and Monitor”# Flash the default environment to a connected boardpio run -t upload
# Open serial monitor at 115200 baudpio device monitorPin Assignments
Section titled “Pin Assignments”Each ESP32 variant has different GPIO numbering. The build environments in platformio.ini set the correct pins for each board via -D build flags.
ESP32 (Classic)
Section titled “ESP32 (Classic)”| Function | GPIO | UART | Notes |
|---|---|---|---|
| Bus TX | GPIO 17 | UART1 TX | Through R5 -> R3 -> Q1 -> U2 |
| Bus RX | GPIO 16 | UART1 RX | From U1 emitter (pin 3) |
| Debug TX | GPIO 1 | UART0 TX | USB serial monitor |
| Debug RX | GPIO 3 | UART0 RX | USB serial monitor |
| Status LED | GPIO 2 | — | Activity indicator |
ESP32-C3
Section titled “ESP32-C3”| Function | GPIO | Notes |
|---|---|---|
| Bus TX | GPIO 5 | UART1 TX |
| Bus RX | GPIO 4 | UART1 RX |
| Status LED | GPIO 8 | Onboard LED on most C3 devkits |
ESP32-S3
Section titled “ESP32-S3”| Function | GPIO | Notes |
|---|---|---|
| Bus TX | GPIO 16 | UART1 TX |
| Bus RX | GPIO 15 | UART1 RX |
| Status LED | GPIO 48 | RGB LED |
Overriding Pins
Section titled “Overriding Pins”All pin assignments can be overridden in platformio.ini build flags without modifying source code:
build_flags = -DIBUS_RX_PIN=4 -DIBUS_TX_PIN=5 -DIBUS_LED_PIN=8 -DIBUS_UART_NUM=1For the OBD-II scanner, the same pins are used by default (they alias to KLINE_RX_PIN, etc.). Override with KLINE_* defines if needed:
build_flags = -DKLINE_RX_PIN=16 -DKLINE_TX_PIN=17 -DKLINE_TX_INVERT=0Set KLINE_TX_INVERT=1 for PC817 optocoupler circuits, 0 for direct transistor circuits.
Firmware Structure
Section titled “Firmware Structure”firmware/├── platformio.ini # 4 environments (BMW sniffer x3, OBD-II scanner)├── include/config.h # Pin defaults + protocol constants├── lib/KLine/│ ├── library.json # PlatformIO lib metadata│ ├── KLineTransport.h/.cpp # Shared hardware layer (UART, ISR, ring buffers)│ ├── IbusHandler.h/.cpp # BMW I/K-Bus protocol FSM│ ├── IbusEsp32.h/.cpp # Backward-compatible facade│ ├── KLineObd2.h/.cpp # OBD-II K-line handler│ ├── Obd2Pids.h # PID constants + SAE J1979 decode helpers│ ├── RingBuffer.h/.cpp # Circular buffer│ └── E46Codes.h # ~100 BMW E46 command arrays└── src/ ├── main.cpp # BMW I/K-Bus sniffer with module name lookup └── obd2_scanner.cpp # OBD-II scanner (RPM, speed, coolant, throttle, voltage)The Two Sketches
Section titled “The Two Sketches”BMW Bus Sniffer (main.cpp)
Section titled “BMW Bus Sniffer (main.cpp)”Listens to all I/K-Bus traffic and prints each message with human-readable module names:
50 MFL -> 68 RAD [32 11] chk=1FThat output means: MFL (steering wheel, address 0x50) sent a message to RAD (radio, address 0x68) with data bytes 32 11 and XOR checksum 1F. This is a volume-up command.
Uses the IbusEsp32 facade, so the code is minimal:
IbusEsp32 ibus;ibus.begin(Serial1, RX_PIN, TX_PIN, LED_PIN);ibus.onPacket(callback);// in loop():ibus.run();OBD-II Scanner (obd2_scanner.cpp)
Section titled “OBD-II Scanner (obd2_scanner.cpp)”Initializes a diagnostic session with an ECU (tries 5-baud slow init, falls back to fast init) and polls five PIDs in a loop:
- Engine RPM
- Vehicle speed (km/h)
- Coolant temperature (C)
- Throttle position (%)
- Control module voltage (V)
Sends TesterPresent every 4 seconds to keep the session alive. After 5 consecutive failures, re-initializes the session automatically.
Uses KLineTransport and KLineObd2 directly (no facade needed):
KLineTransport transport;KLineObd2* scanner = new KLineObd2(transport);
// Configure for OBD-IIKLineConfig config;config.baud = 10400;config.framing = SERIAL_8N1;config.idleTimeoutUs = 0; // master/slave, no idle detectconfig.txInvert = KLINE_TX_INVERT;config.checksumType = CHECKSUM_MOD256;
transport.begin(Serial1, RX_PIN, TX_PIN, LED_PIN, UART_NUM, config);scanner->slowInit(0x33);
// Poll a PID:uint8_t data[4];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]);}Next Steps
Section titled “Next Steps”After building the firmware, the recommended testing sequence is:
- Loopback test — wire TX to RX on the same board, verify messages round-trip through the library
- Bench test — 12V supply through a 1k ohm resistor to simulate the bus, inject bytes from a USB-UART adapter at 9600 baud 8E1
- Vehicle test — connect to the BMW K-Bus via the CD changer connector (E46) or junction block, key position 1 (accessories), sniff traffic before attempting any TX
See the circuit design reference for the hardware schematic, and the bill of materials for components.