From 1464fcabe660c912b76231b162a377ca977a992c Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 13 Feb 2026 05:41:39 -0700 Subject: [PATCH] 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). --- firmware/include/config.h | 60 +++ firmware/lib/AutoWire/E46Codes.h | 586 +++++++++++++++++++++++ firmware/lib/AutoWire/IbusEsp32.cpp | 52 ++ firmware/lib/AutoWire/IbusEsp32.h | 55 +++ firmware/lib/AutoWire/IbusHandler.cpp | 139 ++++++ firmware/lib/AutoWire/IbusHandler.h | 62 +++ firmware/lib/AutoWire/KLineTransport.cpp | 250 ++++++++++ firmware/lib/AutoWire/KLineTransport.h | 121 +++++ firmware/lib/AutoWire/RingBuffer.cpp | 61 +++ firmware/lib/AutoWire/RingBuffer.h | 25 + firmware/lib/AutoWire/library.json | 21 + firmware/platformio.ini | 51 ++ firmware/src/main.cpp | 119 +++++ 13 files changed, 1602 insertions(+) create mode 100644 firmware/include/config.h create mode 100644 firmware/lib/AutoWire/E46Codes.h create mode 100644 firmware/lib/AutoWire/IbusEsp32.cpp create mode 100644 firmware/lib/AutoWire/IbusEsp32.h create mode 100644 firmware/lib/AutoWire/IbusHandler.cpp create mode 100644 firmware/lib/AutoWire/IbusHandler.h create mode 100644 firmware/lib/AutoWire/KLineTransport.cpp create mode 100644 firmware/lib/AutoWire/KLineTransport.h create mode 100644 firmware/lib/AutoWire/RingBuffer.cpp create mode 100644 firmware/lib/AutoWire/RingBuffer.h create mode 100644 firmware/lib/AutoWire/library.json create mode 100644 firmware/platformio.ini create mode 100644 firmware/src/main.cpp diff --git a/firmware/include/config.h b/firmware/include/config.h new file mode 100644 index 0000000..93d8076 --- /dev/null +++ b/firmware/include/config.h @@ -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 diff --git a/firmware/lib/AutoWire/E46Codes.h b/firmware/lib/AutoWire/E46Codes.h new file mode 100644 index 0000000..e0d9628 --- /dev/null +++ b/firmware/lib/AutoWire/E46Codes.h @@ -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 + +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 diff --git a/firmware/lib/AutoWire/IbusEsp32.cpp b/firmware/lib/AutoWire/IbusEsp32.cpp new file mode 100644 index 0000000..67f314f --- /dev/null +++ b/firmware/lib/AutoWire/IbusEsp32.cpp @@ -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); +} diff --git a/firmware/lib/AutoWire/IbusEsp32.h b/firmware/lib/AutoWire/IbusEsp32.h new file mode 100644 index 0000000..223b969 --- /dev/null +++ b/firmware/lib/AutoWire/IbusEsp32.h @@ -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 +#include +#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; + + 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; +}; diff --git a/firmware/lib/AutoWire/IbusHandler.cpp b/firmware/lib/AutoWire/IbusHandler.cpp new file mode 100644 index 0000000..6392ce1 --- /dev/null +++ b/firmware/lib/AutoWire/IbusHandler.cpp @@ -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; +} diff --git a/firmware/lib/AutoWire/IbusHandler.h b/firmware/lib/AutoWire/IbusHandler.h new file mode 100644 index 0000000..b6ace4f --- /dev/null +++ b/firmware/lib/AutoWire/IbusHandler.h @@ -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 +#include +#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; + + 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; +}; diff --git a/firmware/lib/AutoWire/KLineTransport.cpp b/firmware/lib/AutoWire/KLineTransport.cpp new file mode 100644 index 0000000..eb4caac --- /dev/null +++ b/firmware/lib/AutoWire/KLineTransport.cpp @@ -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(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(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); + } +} diff --git a/firmware/lib/AutoWire/KLineTransport.h b/firmware/lib/AutoWire/KLineTransport.h new file mode 100644 index 0000000..7df3fe6 --- /dev/null +++ b/firmware/lib/AutoWire/KLineTransport.h @@ -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 +#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; +}; diff --git a/firmware/lib/AutoWire/RingBuffer.cpp b/firmware/lib/AutoWire/RingBuffer.cpp new file mode 100644 index 0000000..0a9026b --- /dev/null +++ b/firmware/lib/AutoWire/RingBuffer.cpp @@ -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 + +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]; +} diff --git a/firmware/lib/AutoWire/RingBuffer.h b/firmware/lib/AutoWire/RingBuffer.h new file mode 100644 index 0000000..939d9ad --- /dev/null +++ b/firmware/lib/AutoWire/RingBuffer.h @@ -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 + +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; +}; diff --git a/firmware/lib/AutoWire/library.json b/firmware/lib/AutoWire/library.json new file mode 100644 index 0000000..63544e9 --- /dev/null +++ b/firmware/lib/AutoWire/library.json @@ -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" +} diff --git a/firmware/platformio.ini b/firmware/platformio.ini new file mode 100644 index 0000000..4fddf18 --- /dev/null +++ b/firmware/platformio.ini @@ -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 diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp new file mode 100644 index 0000000..65e60f7 --- /dev/null +++ b/firmware/src/main.cpp @@ -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 +#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(); +}