Refactor: extract KLineTransport and IbusHandler from IbusEsp32

Split the monolithic IbusEsp32 class into composable layers:
- KLineTransport: UART, GPIO ISR, ring buffers, idle detection
- IbusHandler: BMW I/K-Bus FSM, source filtering, packet callback
- IbusEsp32: thin facade preserving the original API

Library renamed from IbusEsp32 to AutoWire. Existing sniffer
sketch (main.cpp) requires zero changes. All 3 ESP32 environments
build cleanly (esp32dev, esp32-c3, esp32-s3).
This commit is contained in:
Ryan Malloy 2026-02-13 05:41:39 -07:00
parent e6fc5ad4e9
commit 1464fcabe6
13 changed files with 1602 additions and 0 deletions

60
firmware/include/config.h Normal file
View File

@ -0,0 +1,60 @@
#pragma once
// BMW I/K-Bus interface configuration
// All values can be overridden via platformio.ini build_flags (-D flags)
// --- UART pin assignments ---
#ifndef IBUS_RX_PIN
#define IBUS_RX_PIN 16
#endif
#ifndef IBUS_TX_PIN
#define IBUS_TX_PIN 17
#endif
#ifndef IBUS_LED_PIN
#define IBUS_LED_PIN 2
#endif
#ifndef IBUS_UART_NUM
#define IBUS_UART_NUM 1
#endif
// --- Protocol timing ---
// Bus must be quiet for this long before we can transmit (microseconds)
#ifndef IBUS_IDLE_TIMEOUT_US
#define IBUS_IDLE_TIMEOUT_US 1500
#endif
// Minimum gap between our own transmitted packets (milliseconds)
#ifndef IBUS_PACKET_GAP_MS
#define IBUS_PACKET_GAP_MS 10
#endif
// Periodic timer resolution for idle detection (microseconds)
// Smaller = more responsive but more CPU overhead. 250us gives
// worst-case detection at idle_timeout + 250us = 1.75ms, well
// within the ~10ms inter-packet budget on a busy bus.
#ifndef IBUS_IDLE_CHECK_US
#define IBUS_IDLE_CHECK_US 250
#endif
// --- Buffer sizes ---
#ifndef IBUS_RX_BUFFER_SIZE
#define IBUS_RX_BUFFER_SIZE 256
#endif
#ifndef IBUS_TX_BUFFER_SIZE
#define IBUS_TX_BUFFER_SIZE 128
#endif
// --- Protocol constants ---
#define IBUS_BAUD 9600
#define IBUS_FRAMING SERIAL_8E1
// Message length byte must be between these values (inclusive)
#define IBUS_MIN_LENGTH 0x03
#define IBUS_MAX_LENGTH 0x24
// Maximum raw message size (source + length + up to 36 body bytes)
#define IBUS_MAX_MSG 40

View File

@ -0,0 +1,586 @@
#pragma once
// BMW E46 K-Bus command table
// Ported from muki01/I-K_Bus E46_Codes.h (MIT license)
// PROGMEM removed (ESP32 flash is memory-mapped), wrapped in namespace.
//
// Message format: [source, length, destination, command, data...]
// Checksums are NOT included — IbusEsp32::write() appends them automatically.
//
// Length byte = count of remaining bytes including the checksum that write() adds.
// So for a 7-element array, length byte (index 1) should be 0x05 (5 bytes after length).
#include <Arduino.h>
namespace ibus {
// --- Module addresses ---
constexpr uint8_t M_GM5 = 0x00; // Body control module
constexpr uint8_t M_CDC = 0x18; // CD Changer
constexpr uint8_t M_DIA = 0x3F; // Diagnostic computer
constexpr uint8_t M_EWS = 0x44; // Immobilizer
constexpr uint8_t M_MFL = 0x50; // Steering wheel controls
constexpr uint8_t M_IHKA = 0x5B; // Climate control panel
constexpr uint8_t M_RAD = 0x68; // Radio
constexpr uint8_t M_DSP = 0x6A; // Digital Sound Processor
constexpr uint8_t M_IKE = 0x80; // Instrument cluster
constexpr uint8_t M_SES = 0xB0; // Speed-dependent volume
constexpr uint8_t M_ALL = 0xBF; // Broadcast
constexpr uint8_t M_TEL = 0xC8; // Telephone
constexpr uint8_t M_LCM = 0xD0; // Light Control Module
constexpr uint8_t M_ANZV = 0xE7; // Display (phone status)
constexpr uint8_t M_RLS = 0xE8; // Rain/Light Sensor
// --- GM5 diagnostic I/O ---
constexpr uint8_t GM5_SET_IO = 0x0C;
constexpr uint8_t GM5_BTN_DOME_LIGHT = 0x01;
constexpr uint8_t GM5_BTN_CENTER_LOCK = 0x03;
constexpr uint8_t GM5_BTN_TRUNK_OPEN = 0x05;
constexpr uint8_t GM5_BTN_WINDOW_DRIVER_DOWN = 0x0A;
constexpr uint8_t GM5_BTN_WINDOW_DRIVER_UP = 0x0B;
constexpr uint8_t GM5_BTN_WINDOW_PASS_DOWN = 0x0C;
constexpr uint8_t GM5_BTN_WINDOW_PASS_UP = 0x0D;
constexpr uint8_t GM5_LED_ALARM_WARNING = 0x4E;
constexpr uint8_t GM5_INPUT_STATE_DIGITAL = 0x00;
constexpr uint8_t GM5_INPUT_STATE_ANALOG = 0x01;
// ===================================================================
// Lighting commands (DIA -> LCM or DIA -> ALL broadcast)
// ===================================================================
const uint8_t TurnOffLights[] = {
0x3F, 0x0F, 0xD0, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0xE4, 0xFF, 0x00
};
const uint8_t ParkLights_And_Signals[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x7A, 0x48, 0x0A, 0x06
};
const uint8_t ParkLights_And_Signals_And_FogLights[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x7A, 0x48, 0x0B, 0x06
};
const uint8_t Low_Beams[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x4E, 0x0A, 0x06
};
const uint8_t FollowMeHome[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x06
};
const uint8_t GoodbyeLights[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x62, 0x08, 0xA0, 0x06
};
// --- Individual light control (DIA -> ALL, 12 bytes + checksum) ---
const uint8_t Fog[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06
};
const uint8_t LeftTail[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x06
};
const uint8_t RearLight[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x06
};
const uint8_t Brake_Above[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06
};
const uint8_t Brake_Left[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x06
};
const uint8_t Brake_Right[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x06
};
const uint8_t FrontRightLowBeam[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x06
};
const uint8_t FrontLeftLowBeam[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x06
};
const uint8_t LowBeamDelayed[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06
};
const uint8_t MainBeamLeft[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x06
};
const uint8_t HighBeamRightLow[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x06
};
const uint8_t IgnitionLowBeam[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x06
};
const uint8_t LeftRearContin[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x06
};
const uint8_t RightRearContin[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x06
};
const uint8_t LeftFrontTurnContin[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x06
};
const uint8_t RightFrontTurnContin[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x06
};
const uint8_t LeftParking[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x06
};
const uint8_t RightParking[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x06
};
const uint8_t HazardLights[] = {
0x3F, 0x0B, 0xBF, 0x0C, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06
};
// ===================================================================
// Window control (DIA -> GM5)
// ===================================================================
const uint8_t Window_FrontDriver_Open[] = {
0x3F, 0x05, 0x00, 0x0C, 0x52, 0x01
};
const uint8_t Window_FrontDriver_Close[] = {
0x3F, 0x05, 0x00, 0x0C, 0x53, 0x01
};
const uint8_t Window_FrontPassenger_Open[] = {
0x3F, 0x05, 0x00, 0x0C, 0x54, 0x01
};
const uint8_t Window_FrontPassenger_Close[] = {
0x3F, 0x05, 0x00, 0x0C, 0x55, 0x01
};
const uint8_t Window_RearDriver_Open[] = {
0x3F, 0x05, 0x00, 0x0C, 0x41, 0x01
};
const uint8_t Window_RearDriver_Close[] = {
0x3F, 0x05, 0x00, 0x0C, 0x42, 0x01
};
const uint8_t Window_RearPassenger_Open[] = {
0x3F, 0x05, 0x00, 0x0C, 0x44, 0x01
};
const uint8_t Window_RearPassenger_Close[] = {
0x3F, 0x05, 0x00, 0x0C, 0x43, 0x01
};
// ===================================================================
// Interior lighting (DIA -> GM5)
// ===================================================================
const uint8_t Interior_Off[] = {
0x3F, 0x05, 0x00, 0x0C, 0x01, 0x01
};
const uint8_t Interior_Dim2[] = {
0x3F, 0x05, 0x00, 0x0C, 0x30, 0x01
};
const uint8_t Interior_On3s[] = {
0x3F, 0x05, 0x00, 0x0C, 0x60, 0x01
};
const uint8_t Interior_OffDim[] = {
0x3F, 0x05, 0x00, 0x0C, 0x68, 0x01
};
// ===================================================================
// Door locks (DIA -> GM5)
// ===================================================================
const uint8_t Doors_Unlock_Interior[] = {
0x3F, 0x05, 0x00, 0x0C, 0x03, 0x01
};
const uint8_t Doors_Lock_Key[] = {
0x3F, 0x05, 0x00, 0x0C, 0x34, 0x01
};
const uint8_t Doors_Fuel_Trunk[] = {
0x3F, 0x05, 0x00, 0x0C, 0x46, 0x01
};
const uint8_t DriverDoor_Lock[] = {
0x3F, 0x05, 0x00, 0x0C, 0x47, 0x01
};
const uint8_t AllExceptDriver_Lock[] = {
0x3F, 0x05, 0x00, 0x0C, 0x4F, 0x01
};
const uint8_t Doors_HardLock[] = {
0x3F, 0x05, 0x00, 0x0C, 0x97, 0x01
};
// ===================================================================
// Trunk (DIA -> GM5)
// ===================================================================
const uint8_t Trunk_Open[] = {
0x3F, 0x05, 0x00, 0x0C, 0x02, 0x01
};
const uint8_t Trunk_Open2[] = {
0x3F, 0x05, 0x00, 0x0C, 0x05, 0x01
};
const uint8_t Trunk_Open3[] = {
0x3F, 0x05, 0x00, 0x0C, 0x95, 0x01
};
// ===================================================================
// Wipers and washers (DIA -> GM5)
// ===================================================================
const uint8_t Wipers_Front[] = {
0x3F, 0x05, 0x00, 0x0C, 0x49, 0x01
};
const uint8_t Washer_Front[] = {
0x3F, 0x05, 0x00, 0x0C, 0x62, 0x01
};
// ===================================================================
// Hazard / special (DIA -> GM5)
// ===================================================================
const uint8_t Hazard_IKE_LCM[] = {
0x3F, 0x05, 0x00, 0x0C, 0x70, 0x01
};
const uint8_t Hazard_LCM_3s[] = {
0x3F, 0x05, 0x00, 0x0C, 0x75, 0x01
};
const uint8_t CLOWN_FLASH[] = {
0x3F, 0x05, 0x00, 0x0C, 0x4E, 0x01
};
// ===================================================================
// Instrument cluster / speed (DIA -> IKE)
// ===================================================================
const uint8_t AvgSpeedDelete[] = {
0x3B, 0x05, 0x80, 0x41, 0x10, 0x0A
};
const uint8_t RequestMileage[] = {
0xBF, 0x03, 0x80, 0x16
};
// Speed limit: last byte is speed value in km/h
const uint8_t SpeedLimitBeep[] = {
0x3B, 0x06, 0x80, 0x40, 0x09, 0x00, 0x00
};
const uint8_t SpeedLimitCurrent[] = {
0x3B, 0x05, 0x80, 0x41, 0x09, 0x20
};
const uint8_t SpeedLimitDisable[] = {
0x3B, 0x05, 0x80, 0x41, 0x09, 0x08
};
// ===================================================================
// Remote control events (GM5 -> ALL broadcast)
// ===================================================================
const uint8_t Remote_CloseButton[] = {
0x00, 0x04, 0xBF, 0x72, 0x16
};
const uint8_t Remote_OpenButton[] = {
0x00, 0x04, 0xBF, 0x72, 0x26
};
const uint8_t Remote_ReleaseButton[] = {
0x00, 0x04, 0xBF, 0x72, 0x06
};
// ===================================================================
// Ignition and key state events
// ===================================================================
const uint8_t KEY_IN[] = {
0x44, 0x05, 0xBF, 0x74, 0x04, 0x00
};
const uint8_t KEY_OUT[] = {
0x44, 0x05, 0xBF, 0x74, 0x00, 0xFF
};
const uint8_t IGNITION_OFF[] = {
0x80, 0x04, 0xBF, 0x11, 0x00
};
const uint8_t IGNITION_POS1[] = {
0x80, 0x04, 0xBF, 0x11, 0x01
};
const uint8_t IGNITION_POS2[] = {
0x80, 0x04, 0xBF, 0x11, 0x03
};
const uint8_t REMOTE_UNLOCK[] = {
0x00, 0x04, 0xBF, 0x72, 0x22
};
const uint8_t REMOTE_LOCK[] = {
0x00, 0x04, 0xBF, 0x72, 0x12
};
// ===================================================================
// Steering wheel controls (MFL -> RAD/TEL)
// ===================================================================
const uint8_t MFL_VOL_UP[] = {
0x50, 0x04, 0x68, 0x32, 0x11
};
const uint8_t MFL_VOL_DOWN[] = {
0x50, 0x04, 0x68, 0x32, 0x10
};
const uint8_t MFL_TEL_VOL_UP[] = {
0x50, 0x04, 0xC8, 0x32, 0x11
};
const uint8_t MFL_TEL_VOL_DOWN[] = {
0x50, 0x04, 0xC8, 0x32, 0x10
};
const uint8_t MFL_SES_PRESS[] = {
0x50, 0x04, 0xB0, 0x3B, 0x80
};
const uint8_t MFL_SEND_END_PRESS[] = {
0x50, 0x04, 0xC8, 0x3B, 0x80
};
const uint8_t MFL_RT_PRESS[] = {
0x50, 0x04, 0x68, 0x3B, 0x02
};
// ===================================================================
// CD changer control (RAD -> CDC)
// ===================================================================
const uint8_t CD_STOP[] = {
0x68, 0x05, 0x18, 0x38, 0x01, 0x00
};
const uint8_t CD_PLAY[] = {
0x68, 0x05, 0x18, 0x38, 0x03, 0x00
};
const uint8_t CD_PAUSE[] = {
0x68, 0x05, 0x18, 0x38, 0x02, 0x00
};
const uint8_t CD_STOP_STATUS[] = {
0x18, 0x0A, 0x68, 0x39, 0x00, 0x02, 0x00, 0x3F, 0x00, 0x07, 0x01
};
const uint8_t CD_PLAY_STATUS[] = {
0x18, 0x0A, 0x68, 0x39, 0x02, 0x09, 0x00, 0x3F, 0x00, 0x07, 0x01
};
const uint8_t BACK_ONE[] = {
0x68, 0x05, 0x18, 0x38, 0x08, 0x00
};
const uint8_t BACK_TWO[] = {
0x68, 0x05, 0x18, 0x38, 0x08, 0x01
};
const uint8_t LEFT[] = {
0x68, 0x05, 0x18, 0x38, 0x0A, 0x01
};
const uint8_t RIGHT[] = {
0x68, 0x05, 0x18, 0x38, 0x0A, 0x00
};
const uint8_t SELECT[] = {
0x68, 0x05, 0x18, 0x38, 0x07, 0x01
};
const uint8_t BUTTON_ONE[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x01
};
const uint8_t BUTTON_TWO[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x02
};
const uint8_t BUTTON_THREE[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x03
};
const uint8_t BUTTON_FOUR[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x04
};
const uint8_t BUTTON_FIVE[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x05
};
const uint8_t BUTTON_SIX[] = {
0x68, 0x05, 0x18, 0x38, 0x06, 0x06
};
// ===================================================================
// CDC status (CDC -> RAD or CDC -> ALL)
// ===================================================================
const uint8_t CDC_STATUS_REPLY_RST[] = {
0x18, 0x04, 0xFF, 0x02, 0x01
};
const uint8_t CDC_STATUS_REQUEST[] = {
0x68, 0x03, 0x18, 0x01
};
const uint8_t CDC_STATUS_REPLY[] = {
0x18, 0x04, 0xFF, 0x02, 0x00
};
const uint8_t CD_STATUS[] = {
0x18, 0x0E, 0x68, 0x39, 0x00, 0x82, 0x00, 0x3F,
0x00, 0x07, 0x00, 0x00, 0x01, 0x01, 0x01
};
const uint8_t BUTTON_PRESSED[] = {
0x68, 0x04, 0xFF, 0x3B, 0x00
};
// ===================================================================
// DSP control (RAD -> DSP)
// ===================================================================
const uint8_t DSP_STATUS_REQUEST[] = {
0x68, 0x03, 0x6A, 0x01
};
const uint8_t DSP_STATUS_REPLY[] = {
0x6A, 0x04, 0xFF, 0x02, 0x00
};
const uint8_t DSP_STATUS_REPLY_RST[] = {
0x6A, 0x04, 0xFF, 0x02, 0x01
};
const uint8_t DSP_VOL_UP_1[] = {
0x68, 0x04, 0x6A, 0x32, 0x11
};
const uint8_t DSP_VOL_UP_2[] = {
0x68, 0x04, 0x6A, 0x32, 0x21
};
const uint8_t DSP_VOL_UP_3[] = {
0x68, 0x04, 0x6A, 0x32, 0x31
};
const uint8_t DSP_VOL_DOWN_1[] = {
0x68, 0x04, 0x6A, 0x32, 0x10
};
const uint8_t DSP_VOL_DOWN_2[] = {
0x68, 0x04, 0x6A, 0x32, 0x20
};
const uint8_t DSP_VOL_DOWN_3[] = {
0x68, 0x04, 0x6A, 0x32, 0x30
};
const uint8_t DSP_FUNC_0[] = {
0x68, 0x04, 0x6A, 0x36, 0x30
};
const uint8_t DSP_FUNC_1[] = {
0x68, 0x04, 0x6A, 0x36, 0xE1
};
const uint8_t DSP_SRCE_OFF[] = {
0x68, 0x04, 0x6A, 0x36, 0xAF
};
const uint8_t DSP_SRCE_CD[] = {
0x68, 0x04, 0x6A, 0x36, 0xA0
};
const uint8_t DSP_SRCE_TUNER[] = {
0x68, 0x04, 0x6A, 0x36, 0xA1
};
// ===================================================================
// Telephone (TEL -> ANZV display)
// ===================================================================
const uint8_t INCOMING_CALL[] = {
0xC8, 0x04, 0xE7, 0x2C, 0x05
};
const uint8_t PHONE_ON[] = {
0xC8, 0x04, 0xE7, 0x2C, 0x10
};
const uint8_t HANDSFREE_PHONE_ON[] = {
0xC8, 0x04, 0xE7, 0x2C, 0x11
};
const uint8_t ACTIVE_CALL[] = {
0xC8, 0x04, 0xE7, 0x2C, 0x33
};
// ===================================================================
// Radio navigation (RAD -> misc)
// ===================================================================
const uint8_t GO_TO_RADIO[] = {
0x68, 0x04, 0xFF, 0x3B, 0x00
};
const uint8_t REQUEST_TIME[] = {
0x68, 0x05, 0x80, 0x41, 0x01, 0x01
};
// ===================================================================
// Volume increment table (DSP volume steps, 0-63)
// ===================================================================
const uint8_t VOL_INCREMENT[] = {
0, 68, 70, 72, 74, 76, 78, 80,
82, 84, 86, 88, 90, 92, 94, 96,
98, 100, 102, 104, 106, 108, 110, 112,
114, 116, 118, 120, 122, 124, 126, 128,
130, 132, 134, 136, 138, 140, 142, 144,
146, 148, 150, 152, 154, 156, 158, 160,
162, 164, 166, 168, 170, 172, 174, 176,
178, 180, 182, 184, 186, 188, 190, 192
};
} // namespace ibus

View File

@ -0,0 +1,52 @@
// BMW I/K-Bus interface — backward-compatible facade
// Delegates everything to KLineTransport + IbusHandler.
#include "IbusEsp32.h"
IbusEsp32::IbusEsp32()
: _transport(),
_handler(_transport) {}
void IbusEsp32::begin(HardwareSerial& serial, int8_t rxPin, int8_t txPin,
int8_t ledPin, uint8_t uartNum) {
KLineConfig config;
config.baud = IBUS_BAUD;
config.framing = IBUS_FRAMING;
config.idleTimeoutUs = IBUS_IDLE_TIMEOUT_US;
config.idleCheckUs = IBUS_IDLE_CHECK_US;
config.packetGapMs = IBUS_PACKET_GAP_MS;
config.txInvert = true;
config.checksumType = CHECKSUM_XOR;
_transport.begin(serial, rxPin, txPin, ledPin, uartNum, config);
}
void IbusEsp32::run() {
_transport.drainUart();
_handler.process();
_transport.run();
}
void IbusEsp32::write(const uint8_t* message, uint8_t size) {
_handler.write(message, size);
}
void IbusEsp32::onPacket(PacketCallback callback) {
_handler.onPacket(callback);
}
void IbusEsp32::setFilterEnabled(bool enabled) {
_handler.setFilterEnabled(enabled);
}
void IbusEsp32::setFilterAddress(uint8_t addr) {
_handler.setFilterAddress(addr);
}
void IbusEsp32::setFilterAddresses(const uint8_t* addrs, uint8_t count) {
_handler.setFilterAddresses(addrs, count);
}
uint8_t IbusEsp32::calculateChecksum(const uint8_t* data, uint8_t length) {
return KLineTransport::checksumXor(data, length);
}

View File

@ -0,0 +1,55 @@
#pragma once
// BMW I/K-Bus interface — backward-compatible facade
// Wraps KLineTransport + IbusHandler so existing sketches need zero changes.
// For new code, use KLineTransport + IbusHandler (or KLineObd2) directly.
#include <Arduino.h>
#include <functional>
#include "KLineTransport.h"
#include "IbusHandler.h"
// Re-export library defaults for existing config.h compatibility
#ifndef IBUS_BAUD
#define IBUS_BAUD 9600
#endif
#ifndef IBUS_FRAMING
#define IBUS_FRAMING SERIAL_8E1
#endif
#ifndef IBUS_IDLE_TIMEOUT_US
#define IBUS_IDLE_TIMEOUT_US 1500
#endif
#ifndef IBUS_IDLE_CHECK_US
#define IBUS_IDLE_CHECK_US 250
#endif
#ifndef IBUS_PACKET_GAP_MS
#define IBUS_PACKET_GAP_MS 10
#endif
#ifndef IBUS_UART_NUM
#define IBUS_UART_NUM 1
#endif
class IbusEsp32 {
public:
using PacketCallback = std::function<void(const uint8_t* packet, uint8_t length)>;
IbusEsp32();
void begin(HardwareSerial& serial, int8_t rxPin, int8_t txPin,
int8_t ledPin = -1, uint8_t uartNum = IBUS_UART_NUM);
void run();
void write(const uint8_t* message, uint8_t size);
void onPacket(PacketCallback callback);
void setFilterEnabled(bool enabled);
void setFilterAddress(uint8_t addr);
void setFilterAddresses(const uint8_t* addrs, uint8_t count);
static uint8_t calculateChecksum(const uint8_t* data, uint8_t length);
private:
KLineTransport _transport;
IbusHandler _handler;
};

View File

@ -0,0 +1,139 @@
// BMW I/K-Bus protocol handler
// Extracted from IbusEsp32 — same FSM logic, reads from KLineTransport.
#include "IbusHandler.h"
IbusHandler::IbusHandler(KLineTransport& transport)
: _transport(transport),
_state(FIND_SOURCE),
_source(0),
_length(0),
_findMsgEnteredMs(0),
_callback(nullptr),
_filterEnabled(false),
_filterCount(0) {
memset(_msgBuf, 0, sizeof(_msgBuf));
memset(_filterAddrs, 0, sizeof(_filterAddrs));
}
void IbusHandler::process() {
switch (_state) {
case FIND_SOURCE:
if (_transport.rxAvailable() >= 1) {
_source = _transport.rxPeek(0);
_state = FIND_LENGTH;
}
break;
case FIND_LENGTH:
if (_transport.rxAvailable() >= 2) {
_length = _transport.rxPeek(1);
if (_length >= IBUS_MIN_LENGTH && _length <= IBUS_MAX_LENGTH) {
_state = FIND_MESSAGE;
_findMsgEnteredMs = millis();
} else {
_transport.rxRemove(1);
_state = FIND_SOURCE;
}
}
break;
case FIND_MESSAGE:
if (_transport.rxAvailable() >= _length + 2) {
uint8_t checksum = 0;
for (int i = 0; i <= _length; i++) {
checksum ^= _transport.rxPeek(i);
}
if (_transport.rxPeek(_length + 1) == checksum) {
_state = GOOD_CHECKSUM;
} else {
_state = BAD_CHECKSUM;
}
} else if (millis() - _findMsgEnteredMs > 100) {
// Timeout: partial message never completed (bus glitch).
// At 9600 baud, max-length message takes ~44ms. 100ms is generous.
#ifdef IBUS_DEBUG
Serial.print("RX TIMEOUT: len=0x");
Serial.print(_length, HEX);
Serial.print(" have=");
Serial.println(_transport.rxAvailable());
#endif
_transport.rxRemove(1);
_state = FIND_SOURCE;
}
break;
case GOOD_CHECKSUM:
if (_filterEnabled && _filterCount > 0) {
bool match = false;
for (uint8_t i = 0; i < _filterCount; i++) {
if (_source == _filterAddrs[i]) { match = true; break; }
}
if (!match) {
_transport.rxRemove(_length + 2);
_state = FIND_SOURCE;
return;
}
}
for (int i = 0; i < _length + 2; i++) {
_msgBuf[i] = _transport.rxRead();
}
_transport.ledOn();
if (_callback) {
_callback(_msgBuf, _length + 2);
}
_transport.ledOff();
_state = FIND_SOURCE;
break;
case BAD_CHECKSUM:
#ifdef IBUS_DEBUG
{
uint8_t n = min((int)(_length + 2), _transport.rxAvailable());
for (uint8_t i = 0; i < n; i++) {
_msgBuf[i] = _transport.rxPeek(i);
}
Serial.print("BAD CHK: ");
for (uint8_t i = 0; i < n; i++) {
if (_msgBuf[i] < 0x10) Serial.print('0');
Serial.print(_msgBuf[i], HEX);
Serial.print(' ');
}
Serial.println();
}
#endif
_transport.rxRemove(1);
_state = FIND_SOURCE;
break;
}
}
void IbusHandler::write(const uint8_t* message, uint8_t size) {
_transport.write(message, size);
}
void IbusHandler::onPacket(PacketCallback callback) {
_callback = callback;
}
void IbusHandler::setFilterEnabled(bool enabled) {
_filterEnabled = enabled;
}
void IbusHandler::setFilterAddress(uint8_t addr) {
_filterAddrs[0] = addr;
_filterCount = 1;
_filterEnabled = true;
}
void IbusHandler::setFilterAddresses(const uint8_t* addrs, uint8_t count) {
uint8_t n = min(count, (uint8_t)16);
memcpy(_filterAddrs, addrs, n);
_filterCount = n;
_filterEnabled = true;
}

View File

@ -0,0 +1,62 @@
#pragma once
// BMW I/K-Bus protocol handler
// Extracted from IbusEsp32 — FSM, source filtering, packet callback.
// Reads from a KLineTransport's RX ring buffer, delegates TX to transport.
#include <Arduino.h>
#include <functional>
#include "KLineTransport.h"
#ifndef IBUS_MIN_LENGTH
#define IBUS_MIN_LENGTH 0x03
#endif
#ifndef IBUS_MAX_LENGTH
#define IBUS_MAX_LENGTH 0x24
#endif
#ifndef IBUS_MAX_MSG
#define IBUS_MAX_MSG 40
#endif
class IbusHandler {
public:
using PacketCallback = std::function<void(const uint8_t* packet, uint8_t length)>;
explicit IbusHandler(KLineTransport& transport);
// Run the receive FSM — call from loop() after transport.drainUart()
void process();
// Queue a BMW I/K-Bus message for TX (checksum appended by transport)
void write(const uint8_t* message, uint8_t size);
// Register callback for received packets
void onPacket(PacketCallback callback);
// Source address filtering
void setFilterEnabled(bool enabled);
void setFilterAddress(uint8_t addr);
void setFilterAddresses(const uint8_t* addrs, uint8_t count);
private:
enum FsmState {
FIND_SOURCE,
FIND_LENGTH,
FIND_MESSAGE,
GOOD_CHECKSUM,
BAD_CHECKSUM,
};
KLineTransport& _transport;
FsmState _state;
uint8_t _source;
uint8_t _length;
uint8_t _msgBuf[IBUS_MAX_MSG];
unsigned long _findMsgEnteredMs;
PacketCallback _callback;
bool _filterEnabled;
uint8_t _filterAddrs[16];
uint8_t _filterCount;
};

View File

@ -0,0 +1,250 @@
// 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();
}
// --- 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);
}
}

View File

@ -0,0 +1,121 @@
#pragma once
// Hardware transport for single-wire automotive buses (K-line family)
// Extracted from IbusEsp32 — owns UART, GPIO ISR, ring buffers, idle detection.
// Protocol-agnostic: BMW I/K-Bus and OBD-II K-line use different configs.
#include <Arduino.h>
#include "esp_timer.h"
#include "RingBuffer.h"
// Map IBUS_DEBUG to KLINE_DEBUG for backward compatibility
#if defined(IBUS_DEBUG) && !defined(KLINE_DEBUG)
#define KLINE_DEBUG
#endif
// Accept either KLINE_ or IBUS_ prefixed buffer size defines
#ifndef KLINE_RX_BUFFER_SIZE
#ifdef IBUS_RX_BUFFER_SIZE
#define KLINE_RX_BUFFER_SIZE IBUS_RX_BUFFER_SIZE
#else
#define KLINE_RX_BUFFER_SIZE 256
#endif
#endif
#ifndef KLINE_TX_BUFFER_SIZE
#ifdef IBUS_TX_BUFFER_SIZE
#define KLINE_TX_BUFFER_SIZE IBUS_TX_BUFFER_SIZE
#else
#define KLINE_TX_BUFFER_SIZE 128
#endif
#endif
#ifndef KLINE_MAX_MSG
#define KLINE_MAX_MSG 40
#endif
enum ChecksumType : uint8_t {
CHECKSUM_XOR, // BMW I/K-Bus: XOR all bytes
CHECKSUM_MOD256, // OBD-II K-line: sum mod 256
};
struct KLineConfig {
uint32_t baud = 9600;
uint32_t framing = SERIAL_8E1;
uint16_t idleTimeoutUs = 1500; // 0 = no idle detection (master/slave mode)
uint16_t idleCheckUs = 250;
uint16_t packetGapMs = 10;
bool txInvert = true; // true for optocoupler, false for transistor
ChecksumType checksumType = CHECKSUM_XOR;
};
class KLineTransport {
public:
KLineTransport();
~KLineTransport();
void begin(HardwareSerial& serial, int8_t rxPin, int8_t txPin,
int8_t ledPin = -1, uint8_t uartNum = 1,
const KLineConfig& config = KLineConfig());
// Call from loop() — handles TX scheduling
void run();
// Queue a message for TX. Checksum is appended per config.checksumType.
void write(const uint8_t* message, uint8_t size);
// Send raw bytes without framing/checksum (for init sequences)
void sendRaw(const uint8_t* data, uint8_t len);
// Drain UART FIFO into RX ring buffer
void drainUart();
// RX ring buffer accessors (protocol handlers read from these)
int rxAvailable();
int rxPeek(int n = 0);
void rxRemove(int n);
int rxRead();
// Bus idle state
bool clearToSend() const { return _clearToSend; }
// TX state — protocol handlers need this to suppress loopback echo
bool isTransmitting() const { return _isTransmitting; }
// LED control for protocol handlers
void ledOn();
void ledOff();
// Access to serial port (for direct byte ops during init sequences)
HardwareSerial* serial() { return _serial; }
// Pin access (for bit-bang init sequences that detach UART)
int8_t txPin() const { return _txPin; }
uint8_t uartNum() const { return _uartNum; }
// Checksum utilities
static uint8_t checksumXor(const uint8_t* data, uint8_t length);
static uint8_t checksumMod256(const uint8_t* data, uint8_t length);
uint8_t calculateChecksum(const uint8_t* data, uint8_t length) const;
private:
void trySend();
void sendNextPacket();
void setupBusIdleDetection(int8_t rxPin);
static void IRAM_ATTR onRxPinChange(void* arg);
static void onIdleTimerCallback(void* arg);
HardwareSerial* _serial;
int8_t _ledPin;
int8_t _txPin;
uint8_t _uartNum;
KLineConfig _config;
RingBuffer _rxRing;
RingBuffer _txRing;
// Bus idle detection
esp_timer_handle_t _idleTimer;
volatile uint32_t _lastRxTransitionUs;
volatile bool _clearToSend;
volatile bool _isTransmitting;
unsigned long _lastTxMs;
};

View File

@ -0,0 +1,61 @@
// Ring buffer for async I/K-Bus message handling
// Based on muki01/I-K_Bus RingBuffer (MIT license)
#include "RingBuffer.h"
#include <cstring>
RingBuffer::RingBuffer(int size) : _size(size), _head(0), _tail(0) {
_buf = (byte*)malloc(size);
if (_buf) {
memset(_buf, 0, size);
} else {
// Allocation failed — disable all operations
log_e("RingBuffer: malloc(%d) failed", size);
_size = 0;
}
}
RingBuffer::~RingBuffer() {
if (_buf) free(_buf);
}
int RingBuffer::available() {
return (_size + _head - _tail) % _size;
}
int RingBuffer::capacity() {
return (_size > 0) ? _size - 1 : 0;
}
int RingBuffer::read() {
if (_head == _tail) return -1;
byte c = _buf[_tail];
_tail = (_tail + 1) % _size;
return c;
}
byte RingBuffer::write(int c) {
if (_size == 0) return -1;
if ((_head + 1) % _size == _tail) return -1; // full
_buf[_head] = c;
_head = (_head + 1) % _size;
return 0;
}
void RingBuffer::remove(int n) {
if (_head == _tail) return;
int avail = available();
if (n > avail) n = avail; // clamp to prevent state corruption
if (n <= 0) return;
_tail = (_tail + n) % _size;
}
int RingBuffer::peek() {
if (_head == _tail) return -1;
return _buf[_tail];
}
int RingBuffer::peek(int n) {
if (n < 0 || n >= available()) return -1;
return _buf[(_tail + n) % _size];
}

View File

@ -0,0 +1,25 @@
#pragma once
// Ring buffer for async I/K-Bus message handling
// Based on muki01/I-K_Bus RingBuffer (MIT license)
#include <Arduino.h>
class RingBuffer {
public:
RingBuffer(int size);
~RingBuffer();
int available();
int capacity(); // usable slots (size - 1, ring buffer wastes one)
int peek();
int peek(int n);
void remove(int n);
int read();
byte write(int c);
private:
int _size;
unsigned int _head;
unsigned int _tail;
byte* _buf;
};

View File

@ -0,0 +1,21 @@
{
"name": "AutoWire",
"version": "2026.02.13",
"description": "Multi-protocol automotive bus library for ESP32 — BMW I/K-Bus + OBD-II K-line",
"keywords": ["bmw", "ibus", "kbus", "obd2", "kline", "iso9141", "iso14230", "optocoupler", "pc817", "esp32", "e46", "ford"],
"license": "MIT",
"authors": [
{
"name": "muki01",
"url": "https://github.com/muki01/I-K_Bus",
"maintainer": false
},
{
"name": "Ryan Malloy",
"email": "ryan@supported.systems",
"maintainer": true
}
],
"frameworks": "arduino",
"platforms": "espressif32"
}

51
firmware/platformio.ini Normal file
View File

@ -0,0 +1,51 @@
; BMW I/K-Bus Interface — ESP32 + PC817 Optocoupler
; Based on muki01/I-K_Bus (MIT), ported for ESP32 with R2=220 fix
[platformio]
default_envs = esp32dev
[env]
platform = espressif32
framework = arduino
monitor_speed = 115200
upload_speed = 921600
lib_deps =
build_flags =
-DIBUS_DEBUG
-DIBUS_RX_BUFFER_SIZE=256
-DIBUS_TX_BUFFER_SIZE=128
-DIBUS_IDLE_TIMEOUT_US=1500
-DIBUS_PACKET_GAP_MS=10
; --- ESP32 (classic, dual-core) ---
; GPIO 16/17 are UART2 defaults, free on most devkits
[env:esp32dev]
board = esp32dev
build_flags =
${env.build_flags}
-DIBUS_RX_PIN=16
-DIBUS_TX_PIN=17
-DIBUS_LED_PIN=2
-DIBUS_UART_NUM=1
; --- ESP32-C3 (single-core RISC-V) ---
; GPIO 4/5 are general-purpose; GPIO 8 is onboard LED on most C3 devkits
[env:esp32-c3]
board = esp32-c3-devkitm-1
build_flags =
${env.build_flags}
-DIBUS_RX_PIN=4
-DIBUS_TX_PIN=5
-DIBUS_LED_PIN=8
-DIBUS_UART_NUM=1
; --- ESP32-S3 (dual-core, USB native) ---
; GPIO 15/16 free on S3-DevKitM; GPIO 48 is RGB LED
[env:esp32-s3]
board = esp32-s3-devkitm-1
build_flags =
${env.build_flags}
-DIBUS_RX_PIN=15
-DIBUS_TX_PIN=16
-DIBUS_LED_PIN=48
-DIBUS_UART_NUM=1

119
firmware/src/main.cpp Normal file
View File

@ -0,0 +1,119 @@
// BMW I/K-Bus Sniffer — ESP32 + PC817 optocoupler interface
// Prints all bus traffic with module name lookup.
//
// Hardware: PC817 x2 + BC547, R2=220 (3.3V fix), D1 1N4007
// See CLAUDE.md for full schematic and SPICE validation results.
#include <Arduino.h>
#include "config.h"
#include "IbusEsp32.h"
IbusEsp32 ibus;
// --- Module address lookup ---
struct ModuleEntry {
uint8_t addr;
const char* id; // 4-char padded
const char* name;
};
static const ModuleEntry modules[] = {
{ 0x00, "GM5 ", "Body control" },
{ 0x18, "CDC ", "CD Changer" },
{ 0x3B, "GT ", "Graphics driver" },
{ 0x3F, "DIA ", "Diagnostic" },
{ 0x44, "EWS ", "Immobilizer" },
{ 0x50, "MFL ", "Steering wheel" },
{ 0x5B, "IHKA", "Climate" },
{ 0x68, "RAD ", "Radio" },
{ 0x6A, "DSP ", "Sound processor" },
{ 0x80, "IKE ", "Instrument cluster" },
{ 0xB0, "SES ", "Speed vol" },
{ 0xBF, "ALL ", "Broadcast" },
{ 0xC8, "TEL ", "Telephone" },
{ 0xD0, "LCM ", "Light control" },
{ 0xE7, "ANZV", "Display" },
{ 0xE8, "RLS ", "Rain/light sensor" },
{ 0xFF, "LOC ", "Local" },
};
static const char* lookupId(uint8_t addr) {
for (const auto& m : modules) {
if (m.addr == addr) return m.id;
}
return "??? ";
}
// --- Packet handler ---
// Format: 50 MFL -> 68 RAD [32 11] chk=1F
static void onPacket(const uint8_t* pkt, uint8_t totalLen) {
if (totalLen < 4) return; // minimum: source + length + dest + checksum
uint8_t src = pkt[0];
uint8_t len = pkt[1];
uint8_t dst = pkt[2];
uint8_t chk = pkt[totalLen - 1];
// Source address + module ID
if (src < 0x10) Serial.print('0');
Serial.print(src, HEX);
Serial.print(' ');
Serial.print(lookupId(src));
Serial.print(" -> ");
// Destination address + module ID
if (dst < 0x10) Serial.print('0');
Serial.print(dst, HEX);
Serial.print(' ');
Serial.print(lookupId(dst));
// Data bytes (skip source, length, destination; exclude checksum)
Serial.print(" [");
uint8_t dataStart = 3;
uint8_t dataEnd = totalLen - 1; // exclude checksum
for (uint8_t i = dataStart; i < dataEnd; i++) {
if (i > dataStart) Serial.print(' ');
if (pkt[i] < 0x10) Serial.print('0');
Serial.print(pkt[i], HEX);
}
Serial.print("]");
// Checksum
Serial.print(" chk=");
if (chk < 0x10) Serial.print('0');
Serial.println(chk, HEX);
}
// --- Setup and loop ---
void setup() {
Serial.begin(115200);
delay(500); // let USB serial stabilize
Serial.println();
Serial.println("=== BMW I/K-Bus Sniffer ===");
Serial.println("Hardware: PC817 optocoupler, R2=220, BC547");
Serial.print("Bus UART: ");
Serial.print(IBUS_BAUD);
Serial.println(" 8E1");
Serial.print("RX pin: GPIO ");
Serial.print(IBUS_RX_PIN);
Serial.print(" TX pin: GPIO ");
Serial.println(IBUS_TX_PIN);
Serial.print("Idle timeout: ");
Serial.print(IBUS_IDLE_TIMEOUT_US);
Serial.println("us");
Serial.println("Filter: OFF (sniffer mode)");
Serial.println("Waiting for bus traffic...");
Serial.println();
ibus.begin(Serial1, IBUS_RX_PIN, IBUS_TX_PIN, IBUS_LED_PIN);
ibus.onPacket(onPacket);
// Filter disabled by default — all messages pass through
}
void loop() {
ibus.run();
}