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
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
"""Decode BMW E8x PT-CAN frames and keep rolling state for the UI.
|
|
|
|
Reuses the signal definitions from files/decode_ptcan.py (which mirrors
|
|
files/PTCAN_protocol.md and files/bmw_e8x_ptcan.dbc) so the CLI decoder and
|
|
the GUI stay in sync.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import threading
|
|
from collections import defaultdict, deque
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "files"))
|
|
from decode_ptcan import CHECKSUMS, COUNTERS, NAMES, SIGNALS, TERMINAL, i16le, ocsum, u16le # noqa: E402,F401
|
|
|
|
HISTORY_LEN = 6000 # samples kept per ID for charting
|
|
RATE_WINDOW = 20 # frames used for the rolling Hz estimate
|
|
|
|
|
|
class FrameStore:
|
|
"""Thread-safe: a background capture thread (or replay loop) feeds it,
|
|
the Streamlit render pass reads snapshots from it."""
|
|
|
|
def __init__(self) -> None:
|
|
self._lock = threading.Lock()
|
|
self._init_state()
|
|
|
|
def _init_state(self) -> None:
|
|
self.latest: dict[int, dict] = {}
|
|
self.prev_bytes: dict[int, bytes] = {}
|
|
self.counts: dict[int, int] = defaultdict(int)
|
|
self.recent_times: dict[int, deque] = defaultdict(lambda: deque(maxlen=RATE_WINDOW))
|
|
self.history: dict[int, deque] = defaultdict(lambda: deque(maxlen=HISTORY_LEN))
|
|
self.t0: Optional[float] = None
|
|
|
|
def reset(self) -> None:
|
|
with self._lock:
|
|
self._init_state()
|
|
|
|
def feed(self, arbitration_id: int, data: bytes, t: float) -> None:
|
|
with self._lock:
|
|
if self.t0 is None:
|
|
self.t0 = t
|
|
decoded = {}
|
|
for name, (fn, unit) in SIGNALS.get(arbitration_id, {}).items():
|
|
try:
|
|
decoded[name] = (fn(data), unit)
|
|
except Exception:
|
|
pass
|
|
|
|
prev = self.prev_bytes.get(arbitration_id)
|
|
changed = bytes(a ^ b for a, b in zip(prev, data)) if prev is not None and len(prev) == len(data) else None
|
|
self.prev_bytes[arbitration_id] = data
|
|
|
|
self.latest[arbitration_id] = {"t": t, "data": data, "decoded": decoded, "changed": changed}
|
|
self.counts[arbitration_id] += 1
|
|
self.recent_times[arbitration_id].append(t)
|
|
self.history[arbitration_id].append((t, decoded))
|
|
|
|
def hz(self, arbitration_id: int) -> float:
|
|
times = self.recent_times.get(arbitration_id)
|
|
if not times or len(times) < 2:
|
|
return 0.0
|
|
span = times[-1] - times[0]
|
|
return (len(times) - 1) / span if span > 0 else 0.0
|
|
|
|
def snapshot(self) -> dict:
|
|
"""Shallow copy safe to read/render from outside the writer."""
|
|
with self._lock:
|
|
return {"latest": dict(self.latest), "counts": dict(self.counts), "t0": self.t0}
|
|
|
|
def series(self, arbitration_id: int, signal: str, window_s: Optional[float] = None):
|
|
with self._lock:
|
|
hist = list(self.history.get(arbitration_id, ()))
|
|
if window_s is not None and hist:
|
|
cutoff = hist[-1][0] - window_s
|
|
hist = [h for h in hist if h[0] >= cutoff]
|
|
xs, ys = [], []
|
|
for t, decoded in hist:
|
|
if signal in decoded:
|
|
xs.append(t)
|
|
ys.append(decoded[signal][0])
|
|
return xs, ys
|