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
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""Capture CAN frames to a log file, with start/stop control.
|
|
|
|
Usage:
|
|
python log_can.py [--bitrate 500000] [--port /dev/cu.usbserial-DNBJV4F5]
|
|
[--output capture.csv] [--duration 30]
|
|
|
|
Prints each frame as it arrives and appends it to a CSV log file. Without
|
|
--duration it runs until you press Ctrl+C; with --duration it stops itself
|
|
after that many seconds. The CSV columns are:
|
|
|
|
timestamp_ms, arbitration_id (hex), is_extended, dlc, data_hex
|
|
|
|
timestamp_ms is the adapter's own millisecond counter (wraps every 60000ms),
|
|
so it's only useful for relative timing within one capture, not wall-clock
|
|
time.
|
|
"""
|
|
import argparse
|
|
import csv
|
|
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
|
|
|
|
CAPTURES_DIR = Path(__file__).resolve().parents[1] / "captures"
|
|
DEFAULT_PORT = "/dev/cu.usbserial-DNBJV4F5"
|
|
DEFAULT_BITRATE = 500000
|
|
|
|
|
|
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")
|
|
parser.add_argument("--output", default=None, help="CSV log file (default: capture_<timestamp>.csv)")
|
|
parser.add_argument("--duration", type=float, default=None, help="Stop automatically after N seconds")
|
|
args = parser.parse_args()
|
|
|
|
output = args.output or str(CAPTURES_DIR / time.strftime("capture_%Y%m%d_%H%M%S.csv"))
|
|
|
|
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
|
|
|
|
ids = Counter()
|
|
frame_count = 0
|
|
start_monotonic = time.monotonic()
|
|
start_wall = time.time() # frame.recv_time uses time.time(), not monotonic()
|
|
deadline = None if args.duration is None else start_monotonic + args.duration
|
|
|
|
stop_msg = f"for {args.duration:.0f}s" if args.duration else "until Ctrl+C"
|
|
print(f"Capturing {stop_msg} -> {output}\n")
|
|
print(f"{'timestamp':>14} {'id':>10} {'dlc':>3} data (hex)")
|
|
|
|
try:
|
|
with adapter, open(output, "w", newline="") as f:
|
|
writer = csv.writer(f)
|
|
writer.writerow(["timestamp_ms", "arbitration_id", "is_extended", "dlc", "data_hex"])
|
|
while deadline is None or time.monotonic() < deadline:
|
|
|
|
frame = adapter.read_frame(timeout=0.5)
|
|
if frame is None:
|
|
continue
|
|
|
|
frame_count += 1
|
|
ids[frame.arbitration_id] += 1
|
|
|
|
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)
|
|
print(f"{frame.recv_time - start_wall:14.3f} {id_str:>10} {len(frame.data):>3} {data_hex}")
|
|
|
|
writer.writerow(
|
|
[
|
|
frame.timestamp_ms if frame.timestamp_ms is not None else "",
|
|
f"{frame.arbitration_id:X}",
|
|
int(frame.is_extended),
|
|
len(frame.data),
|
|
frame.data.hex().upper(),
|
|
]
|
|
)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
elapsed = time.monotonic() - start_monotonic
|
|
print(f"\nStopped after {elapsed:.1f}s: {frame_count} frames, {len(ids)} unique IDs.")
|
|
print("Frames per ID:", ", ".join(f"0x{i:X}={c}" for i, c in ids.most_common()))
|
|
print(f"Log written to {output}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|