BMW_E8x_EPS/tools/scan_bitrate.py
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

87 lines
3.3 KiB
Python

"""Scan common automotive CAN bitrates to find one that yields valid frames.
Usage:
python scan_bitrate.py [--port /dev/cu.usbserial-DNBJV4F5] [--seconds 2]
Tries each candidate bitrate in turn, listens briefly, and reports how many
frames came back and whether they look well-formed (DLC 0-8, plausible IDs).
The bitrate with the most valid frames is your best bet.
Note on wiring: CAN H and L being swapped is NOT usually something a bitrate
scan can "see through" - CAN transceivers work on the differential voltage
(H minus L), so swapping the two wires inverts dominant/recessive and
corrupts every bit, at every bitrate. If this scan finds zero valid frames
at all common bitrates, that's a strong signal to double check H/L wiring
(and termination - a CAN bus needs ~120 ohm resistors at each end) rather
than trying more bitrates.
"""
import argparse
import sys
import time
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from adapters.candapter import Candapter # noqa: E402
DEFAULT_PORT = "/dev/cu.usbserial-DNBJV4F5"
# Common automotive bitrates, most-likely-first for a steering/body bus.
CANDIDATE_BITRATES = [500_000, 125_000, 250_000, 1_000_000, 100_000, 50_000, 20_000, 10_000]
def scan_one(port: str, bitrate: int, seconds: float) -> tuple[int, int, Counter]:
total = 0
valid = 0
ids = Counter()
with Candapter(port, bitrate) as adapter:
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
frame = adapter.read_frame(timeout=0.2)
if frame is None:
continue
total += 1
if 0 <= len(frame.data) <= 8:
valid += 1
ids[frame.arbitration_id] += 1
return total, valid, ids
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", default=DEFAULT_PORT, help="Serial device path")
parser.add_argument("--seconds", type=float, default=2.0, help="Listen time per bitrate")
args = parser.parse_args()
print(f"Scanning {args.port} across {len(CANDIDATE_BITRATES)} bitrates ({args.seconds}s each)...\n")
print(f"{'bitrate':>10} {'frames':>7} {'valid':>6} {'unique ids':>10}")
results = []
for bitrate in CANDIDATE_BITRATES:
try:
total, valid, ids = scan_one(args.port, bitrate, args.seconds)
except OSError as exc:
print(f"{bitrate:>10} error opening port: {exc}")
continue
print(f"{bitrate:>10} {total:>7} {valid:>6} {len(ids):>10}")
results.append((bitrate, valid, ids))
results.sort(key=lambda r: r[1], reverse=True)
best = results[0] if results else None
if best and best[1] > 0:
bitrate, valid, ids = best
print(f"\nBest match: {bitrate} bps ({valid} valid frames, {len(ids)} unique IDs).")
print("Top IDs seen:", ", ".join(f"0x{i:X}x{c}" for i, c in ids.most_common(10)))
else:
print(
"\nNo valid frames at any bitrate. Since this affects every bitrate equally, "
"check the physical connection first: CAN H/L polarity and bus termination "
"(~120 ohm at each end) before trying more bitrates."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())