diff --git a/README.md b/README.md new file mode 100644 index 0000000..b347dd2 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# Running a BMW electric power steering rack outside its car + +A day of reverse engineering: taking the electric power steering (EPS) unit +from a 2010 BMW 1-series and working out exactly what it needs to be told in +order to work on a bench — and eventually in a custom EV. + +![Bench setup](media/IMG_9160.JPG) +![Bench setup](media/IMG_9164.JPG) + +## The short version + +A modern power steering rack isn't a dumb hydraulic pump you can just bolt in +and give 12V. It's a computer. It sits on the car's CAN bus listening to +dozens of other computers, and it refuses to do anything until it's satisfied +that it's installed in a real, running car. + +The question was: **what does it actually need to hear?** + +The answer turned out to be far less than expected: + +> **Two CAN messages and one 12V wire.** +> +> - `0x130` — the ignition/terminal status, sent 10 times a second +> - `0x1A0` — the road speed, sent 50 times a second +> +> That's 60 messages a second. The car itself puts about **1,330** a second on +> that wire — so more than 95% of the traffic turned out to be irrelevant to +> steering. + +Road speed is the interesting one: it's what tells the rack *how much* assist +to give. Slow speed means lots of assist for parking; high speed means less, +so the steering firms up on the motorway. Feed it a speed value and you +control the assist directly. + +--- + +## The setup + +The donor car and the steering rack were both kept intact — the only change +was **cutting the CAN bus wire between the car and the EPS unit**, and +inserting a laptop in the gap. + +``` + ┌─────────┐ ┌──────────────┐ ┌─────────┐ + │ CAR │◄──────►│ LAPTOP │◄──────►│ EPS │ + │ PT-CAN │ │ (gateway) │ │ rack │ + └─────────┘ └──────────────┘ └─────────┘ + adapter 1 adapter 2 + │ + └── 12V enable signal (a plain wire, not CAN) +``` + +Two CAN adapters, one on each side of the cut: + +- **Adapter 1 — the car side.** A commercial USB-CAN adapter (a CANdapter) + listening to the car's powertrain bus. +- **Adapter 2 — the EPS side.** A small custom board (Seeed XIAO ESP32-S3) + that does double duty: it's a USB-CAN adapter *and* it has relay outputs + and opto-isolated inputs, which turned out to be essential. + +That second board mattered because of something easy to miss: **the EPS +doesn't wake up from CAN alone.** The car also sends it a separate 12V enable +signal on an ordinary wire. Without that, the unit is completely dead — +no CAN, no assist, nothing. So the board reads that 12V signal coming from +the car on an input, and can reproduce it to the EPS on a relay output. That +let us take over the enable line and switch the rack on and off ourselves. + +--- + +## How it went — the steps + +### 1. Talk to the adapters at all + +Started from nothing: which USB device is this, what protocol does it speak? +It identified as an FTDI serial chip, and turned out to be a CANdapter +speaking a *variant* of the standard SLCAN protocol — close enough to look +standard, different enough that the off-the-shelf library silently mangled +every frame. Wrote a small driver for it instead. + +### 2. Find the bus speed + +Scanned the common automotive bitrates. The first attempt found nothing at +any speed — which was actually informative, because a wrong bitrate and a +wrong *wiring* look different in the data. CAN H and L had been swapped. +Once corrected: **500 kbit/s**, clean traffic. + +### 3. Watch the car and understand it + +Recorded the car starting and stopping, then decoded the bus: engine speed, +steering angle, road speed, ignition state, wheel speeds. Built a live +dashboard to see it all moving in real time. Roughly 70 different message +types on that one wire. + +### 4. Sit in the middle — the transparent relay + +With the bus cut and the laptop bridging it, everything was forwarded in both +directions. If done right, the car shouldn't notice. It worked — the steering +assisted normally with a laptop in the middle of the loop. + +### 5. Start taking things away + +This is where the real answer comes from: block messages one at a time and +see if the rack still works. Turn off the VIN broadcast, the radio buttons, +the diagnostics — does steering still assist? Keep cutting until something +breaks. + +### 6. Cut the car out entirely + +Replay a recording of the car to the rack, with no car connected at all. Then +narrow the recording down. Then stop replaying and *generate* the messages +from scratch. Each step is a stronger claim: from "we can copy the car" to +"we understand the car well enough to be the car". + +### 7. Take control + +The end result is a control panel: a button to bring the rack up, a button to +shut it down, and a slider for road speed that changes how much assist it +gives — with no car involved anywhere. + +--- + +## Things that went wrong (and what they taught us) + +The interesting part of a day like this is rarely the plan working. Four +problems, each of which looked like a hardware fault and wasn't: + +**"The rack won't wake up."** We were replaying recorded messages perfectly — +byte for byte. Too perfectly. Each message carries a small counter that ticks +up every time it's sent, and replaying a frozen snapshot meant that counter +never moved. Every other computer on the bus reads a stuck counter as *"this +sender has crashed"* and ignores it. Fixed by cycling through the real +recorded sequence so the counters advance naturally. + +**"It works, then randomly drops out."** The laptop was only managing about +264 messages a second against the ~1,330 needed — five times too slow, so +everything arrived late. To the rack, chronically late messages are +indistinguishable from a broken sender. The cause was one line of code: a +serial read that could block for 100ms while waiting for data that wasn't +coming. Fixed, and got to 1,282/s. But the better fix was realising we only +needed 60 messages a second in the first place. + +**"The relay output won't switch."** Two separate bugs stacked. The board was +transmitting the switch command onto the CAN bus correctly — but never acting +on it locally, because a CAN chip doesn't hear its own transmissions. And the +firmware had an automatic rule that kept overriding manual control within +40ms. Removed the rule, handled the command locally. + +**"The input isn't being read."** It was — perfectly. But the *report* about +it travelled over CAN, and CAN needs at least one other live device to +acknowledge a message. With the rack not yet powered, there was nobody to +acknowledge anything, so the reports never arrived. A neat chicken-and-egg: +the status needed to switch the rack on couldn't be seen until the rack was +on. Fixed by giving the board a direct USB reporting channel that doesn't +depend on CAN at all. + +--- + +## What's still unknown + +Being straight about the limits, because this isn't finished: + +- **The checksums aren't cracked.** Each message carries a checksum, and we + couldn't derive the formula (best fit: 87.5% for one, 64% for the other). + The current system sidesteps this by reusing genuine recorded frames. That + works, but a production build should solve them properly. +- **Assist has only been tested stationary.** The car never moved during any + recording, so while the speed slider changes the value the rack receives, + the actual speed→assist curve is unverified. +- **Untested failure modes.** What the rack does if CAN stops while driving, + or how it reacts to a bad checksum, isn't known yet. + +Full list in the [verification checklist](eps-comms/EPS_PROTOCOL.md#9-verification-checklist). + +--- + +## Documentation index + +| Document | What's in it | +|---|---| +| **[eps-comms/EPS_PROTOCOL.md](eps-comms/EPS_PROTOCOL.md)** | **The main result.** How to run the EPS standalone: wiring, both required messages byte-by-byte, startup/shutdown sequences, timing, and what's proven vs. inferred | +| [eps-comms/findings.md](eps-comms/findings.md) | Chronological log — what was tested, what broke, why | +| [files/PTCAN_protocol.md](files/PTCAN_protocol.md) | Wider reverse engineering of the whole car bus (engine, DSC, cluster…) | +| [files/bmw_e8x_ptcan.dbc](files/bmw_e8x_ptcan.dbc) | Standard-format CAN database for the decoded messages | +| [can-io/README.md](can-io/README.md) | The custom CAN + relay board: hardware, pinout, firmware | +| [can-io/PROTOCOL.md](can-io/PROTOCOL.md) | That board's own message format | + +## The software + +| Folder | Purpose | +|---|---| +| `gateway/` | The main application — a 4-tab interface: live relay, replay, message bench, EPS control | +| `adapters/` | Drivers for both CAN adapters | +| `decoder/` | Turning raw CAN frames into meaningful values | +| `tools/` | Analysis: capture, bitrate scan, startup ordering, checksum solving | +| `can-io/` | Firmware for the custom board | +| `captures/` | Reference recordings | + +Running it: + +```bash +python -m venv .venv && .venv/bin/pip install -r requirements.txt +.venv/bin/python -m streamlit run gateway/gateway_app.py +``` + +--- + +## A note on the approach + +Almost every dead end on this project came from trusting an assumption +instead of checking it. The rack wasn't broken; the recordings weren't +corrupt; the wiring was mostly fine. Each time, the fault was in something we +believed rather than something we'd measured. + +So the tooling here leans heavily on *looking at the data*: which bytes +actually change, what arrived and when, what we sent versus what we intended +to send. The analysis scripts in `tools/` exist because eyeballing 60,000 +messages doesn't work, and guessing is slower than measuring. + +--- + +*Vehicle: BMW 1-series (E87), 2010. Donor car for an EV conversion.* diff --git a/media/IMG_9160.JPG b/media/IMG_9160.JPG new file mode 100644 index 0000000..a3951cd Binary files /dev/null and b/media/IMG_9160.JPG differ diff --git a/media/IMG_9164.JPG b/media/IMG_9164.JPG new file mode 100644 index 0000000..7338418 Binary files /dev/null and b/media/IMG_9164.JPG differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..84fb6a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# Direct dependencies only; versions are those the project was developed against. +pyserial==3.5 +streamlit==1.62.0 +pandas==3.0.5 +plotly==7.0.0