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:
parent
e6fc5ad4e9
commit
1464fcabe6
60
firmware/include/config.h
Normal file
60
firmware/include/config.h
Normal 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
|
||||
586
firmware/lib/AutoWire/E46Codes.h
Normal file
586
firmware/lib/AutoWire/E46Codes.h
Normal 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
|
||||
52
firmware/lib/AutoWire/IbusEsp32.cpp
Normal file
52
firmware/lib/AutoWire/IbusEsp32.cpp
Normal 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);
|
||||
}
|
||||
55
firmware/lib/AutoWire/IbusEsp32.h
Normal file
55
firmware/lib/AutoWire/IbusEsp32.h
Normal 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;
|
||||
};
|
||||
139
firmware/lib/AutoWire/IbusHandler.cpp
Normal file
139
firmware/lib/AutoWire/IbusHandler.cpp
Normal 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;
|
||||
}
|
||||
62
firmware/lib/AutoWire/IbusHandler.h
Normal file
62
firmware/lib/AutoWire/IbusHandler.h
Normal 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;
|
||||
};
|
||||
250
firmware/lib/AutoWire/KLineTransport.cpp
Normal file
250
firmware/lib/AutoWire/KLineTransport.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
121
firmware/lib/AutoWire/KLineTransport.h
Normal file
121
firmware/lib/AutoWire/KLineTransport.h
Normal 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;
|
||||
};
|
||||
61
firmware/lib/AutoWire/RingBuffer.cpp
Normal file
61
firmware/lib/AutoWire/RingBuffer.cpp
Normal 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];
|
||||
}
|
||||
25
firmware/lib/AutoWire/RingBuffer.h
Normal file
25
firmware/lib/AutoWire/RingBuffer.h
Normal 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;
|
||||
};
|
||||
21
firmware/lib/AutoWire/library.json
Normal file
21
firmware/lib/AutoWire/library.json
Normal 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
51
firmware/platformio.ini
Normal 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
119
firmware/src/main.cpp
Normal 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();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user