Add project README, photos and requirements
Overview of the day's work aimed at a general reader: what the EPS unit needs, how the two-adapter bench was wired around the cut CAN bus, the steps from transparent relay to standalone control, and the four misleading failures along the way (frozen alive counters, transmit rate starvation, local command dispatch, and CAN needing a second node to ACK). Also acts as an index into the existing documentation, and pins the direct Python dependencies the app needs.
This commit is contained in:
parent
c32c4645b5
commit
dfd8f1d9e3
4 changed files with 226 additions and 0 deletions
221
README.md
Normal file
221
README.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 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.*
|
||||||
BIN
media/IMG_9160.JPG
Normal file
BIN
media/IMG_9160.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 MiB |
BIN
media/IMG_9164.JPG
Normal file
BIN
media/IMG_9164.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 MiB |
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue