BMW_E8x_EPS/eps-comms
Luca c32c4645b5 Reverse-engineer BMW E8x EPS for standalone operation
Tooling and findings for running an E8x/E9x electric power steering unit
outside its donor car, e.g. in an EV conversion.

Headline result: the EPS needs only two CAN messages plus a 12V enable
wire, not the 69-message set the car puts on the bus:
  0x130 CAS terminal status (100ms) brings the unit up
  0x1A0 DSC road speed (20ms) sets the assist level
Total required rate is 60 frames/s. Protocol write-up, including what is
proven vs. inferred and the open questions, is in eps-comms/.

Contents:
  adapters/   CANdapter (its SLCAN dialect differs) and generic SLCAN
  gateway/    car<->EPS relay, replay, message bench, EPS controller,
              4-tab Streamlit UI
  decoder/    PT-CAN frame decoding and live/replay sources
  can-io/     XIAO ESP32-S3 firmware: CAN IO board + USB-CAN bridge with
              a CAN-independent digital IO channel
  tools/      capture, bitrate scan, startup-order and session analysis,
              checksum solver
  captures/   reference working session + the replay set eps_control reads
2026-08-29 19:34:43 +02:00
..
EPS_PROTOCOL.md Reverse-engineer BMW E8x EPS for standalone operation 2026-08-29 19:34:43 +02:00
findings.md Reverse-engineer BMW E8x EPS for standalone operation 2026-08-29 19:34:43 +02:00
README.md Reverse-engineer BMW E8x EPS for standalone operation 2026-08-29 19:34:43 +02:00

EPS communication notes

Findings about what the Electric Power Steering (EPS) unit needs on the bus, built up from gateway sessions between the car's PT-CAN bus and the EPS's (isolated) bus via the CAN-IO board.

  • EPS_PROTOCOL.md — the consolidated result: how to power up, control and monitor the EPS standalone (e.g. in a custom EV). Start here.
  • findings.md — chronological working log: what was tested, what broke, and why.

Headline result: the EPS needs only two CAN messages (0x130 terminal status, 0x1A0 road speed) plus a 12V enable wire — not the 69 messages the car puts on the bus. Total required rate: 60 frames/s.

Setup

car PT-CAN --[CANdapter]-- gateway (this repo) --[CAN-IO board USB bridge]-- EPS bus -- EPS unit
  • Car side: CANdapter, port varies (/dev/cu.usbserial-DNBJV4F5 as of writing).
  • EPS side: CAN-IO board (XIAO ESP32-S3, can-io/), port varies (/dev/cu.usbmodemXXXX - changes on replug, always re-check with ls /dev/cu.*).
  • Both sides run at 500 kbit/s.
  • Tool: gateway/gateway_app.py (live relay + per-ID filter + IO test) or gateway/replay_to_bus.py (replay a capture onto one side, no car needed).
  • Every gateway session logs every frame seen (relayed or not) to captures/gateway_<timestamp>.csv - columns: t, direction, arbitration_id, is_extended, dlc, data_hex, relayed.

Workflow

  1. Transparent relay (current stage): everything allowed both ways, confirm EPS + car both still behave normally through the gateway.
  2. Filter down: block IDs one at a time (or in groups) via the "Allow" checkboxes in gateway_app.py, re-test after each cut, to find the minimal set of car-bus messages the EPS actually needs.
  3. Replay-only: once step 2 gives a candidate minimal set, use gateway/replay_to_bus.py to feed just those messages to the EPS with the car fully disconnected, to confirm it's really enough to bring the EPS up on its own.
  4. Synthesize: once replay works, write a generator that produces those messages from scratch (no capture needed) - not started yet.

Tools

  • gateway/gateway_app.py - live relay + per-ID filter + IO test (Streamlit).
  • gateway/replay_to_bus.py - replay a capture CSV onto one adapter, with the same FilterRules filtering, --dry-run needs no hardware.
  • tools/gateway_log_to_capture.py - turns a gateway session log (which has both directions + relay/block status) into a plain capture CSV for replay_to_bus.py, e.g. to replay exactly what a working live session sent, filtered to one direction and optionally only the frames that were actually relayed.
  • tools/extract_dio_timeline.py - pulls the CAN-IO board's own digital IN1-4/OUT1-2 timeline out of a gateway log (decoded from its 0x100 status frames) - answers "when was the 12V signal high/low", and feeds replay_to_bus.py --dio-log so a replay reproduces the digital signal (e.g. the 12V repeat to the EPS) in sync with the CAN traffic, not just the CAN frames alone. Resolution is limited to whenever the board sent a status frame (on change, plus a 1s heartbeat) - a change undone within a couple of scheduling ticks might not show up as its own sample.
  • gateway/rules.suggested.json / rules.suggested_car_to_eps.json - current best-guess filter (see findings.md for what's blocked and why): the combined form loads in gateway_app.py's sidebar, the plain form loads via replay_to_bus.py --rules.

Synthesis plan (not started)

Once a minimal, replay-confirmed message set is known:

  1. For each required ID, write an encoder (the mirror image of files/decode_ptcan.py's SIGNALS decoders) that produces the raw bytes from a target value (angle, speed, rpm, terminal state, ...), including the alive-counter and checksum schemes already documented in files/PTCAN_protocol.md.
  2. Start with static/slowly-changing values (terminal state, a fixed "engine running" RPM, zero speed) to prove the EPS accepts synthetic frames at all, before worrying about realistic dynamic values.
  3. Time the generator against gateway/replay_to_bus.py's measured periods per ID (10ms for steering angle, 20ms for road speed, etc.) - this is also where the jitter hypothesis from findings.md gets tested: a dedicated generator (ideally on the CAN-IO board itself, not the PC) can hit those periods far more precisely than the PC-mediated relay.
  4. Longer term: move the generator onto the CAN-IO board's firmware itself, so the final setup needs no PC in the loop at all.

Known pitfalls (see findings.md for detail)

  • TX-echo feedback loop (fixed 2026-08-29): the CAN-IO board's firmware used to mirror every frame it successfully transmitted back over USB, including frames the gateway had just asked it to relay from the car bus - not just its own status/heartbeat. This made every relayed ID falsely reappear as "new EPS-bus traffic" a moment later, which is what session gateway_20260829_162235.csv shows (64 of 69 "eps->car" IDs were things just relayed car->eps seconds earlier). Fixed in can_bus.cpp/usb_bridge.cpp by only mirroring board-originated transmits, not bridge-injected ones. Any gateway log captured before this fix should be treated as unreliable for "what does the EPS bus actually carry" purposes.