BMW_E8x_EPS/eps-comms/README.md
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

101 lines
5.3 KiB
Markdown

# 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](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](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.**