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

66 lines
2.4 KiB
Python

"""Read and decode CAN frames from the CANdapter (candapter.com) USB adapter.
Usage:
python read_can.py [--bitrate 500000] [--port /dev/cu.usbserial-DNBJV4F5]
Connect the adapter to the CAN bus first, then run this script. It opens the
channel at the given bitrate and prints every frame it receives: timestamp,
arbitration ID (with an 'x' suffix for 29-bit extended IDs), DLC, raw bytes
(hex), and an ASCII preview.
If you don't know the bus's bitrate, common values are 125000, 250000,
500000 (most likely - the adapter is rated for automotive use at 500 kbps),
and 1000000. Wrong bitrate usually means zero frames or garbled data --
try another value.
"""
import argparse
import sys
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"
DEFAULT_BITRATE = 500000
def ascii_preview(data: bytes) -> str:
return "".join(chr(b) if 32 <= b < 127 else "." for b in data)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", default=DEFAULT_PORT, help="Serial device path")
parser.add_argument("--bitrate", type=int, default=DEFAULT_BITRATE, help="CAN bus bitrate")
args = parser.parse_args()
try:
adapter = Candapter(args.port, args.bitrate, timestamps=True)
except (OSError, ValueError) as exc:
print(f"Failed to open adapter on {args.port}: {exc}", file=sys.stderr)
return 1
print(f"Listening on {args.port} at {args.bitrate} bps (Ctrl+C to stop)...\n")
print(f"{'timestamp':>14} {'id':>10} {'dlc':>3} data (hex) ascii")
try:
with adapter:
while True:
frame = adapter.read_frame(timeout=None)
if frame is None:
continue
id_str = f"{frame.arbitration_id:08X}x" if frame.is_extended else f"{frame.arbitration_id:03X}"
data_hex = " ".join(f"{b:02X}" for b in frame.data)
ts = frame.timestamp_ms if frame.timestamp_ms is not None else frame.recv_time
print(
f"{ts:14.3f} {id_str:>10} {len(frame.data):>3} "
f"{data_hex:<24} {ascii_preview(frame.data)}"
)
except KeyboardInterrupt:
print("\nStopped.")
return 0
if __name__ == "__main__":
raise SystemExit(main())