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
58 lines
2 KiB
Python
58 lines
2 KiB
Python
"""Per-direction, per-ID pass/block filter rules for the gateway.
|
|
|
|
Thread-safe (read from the relay threads, written from the monitoring UI or
|
|
CLI) and JSON-persistable so a ruleset can be saved and reloaded between
|
|
sessions. Default is transparent: every ID passes until you explicitly
|
|
block it - that matches the "start fully transparent, then narrow down"
|
|
workflow.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
|
|
|
|
class FilterRules:
|
|
def __init__(self, default_allow: bool = True):
|
|
self._lock = threading.Lock()
|
|
self._default_allow = default_allow
|
|
self._overrides: Dict[int, bool] = {} # arbitration_id -> allow?
|
|
|
|
def allows(self, arbitration_id: int) -> bool:
|
|
with self._lock:
|
|
return self._overrides.get(arbitration_id, self._default_allow)
|
|
|
|
def set_allow(self, arbitration_id: int, allow: bool) -> None:
|
|
with self._lock:
|
|
self._overrides[arbitration_id] = allow
|
|
|
|
def clear_override(self, arbitration_id: int) -> None:
|
|
with self._lock:
|
|
self._overrides.pop(arbitration_id, None)
|
|
|
|
def allow_all(self) -> None:
|
|
with self._lock:
|
|
self._default_allow = True
|
|
self._overrides.clear()
|
|
|
|
def block_all(self) -> None:
|
|
with self._lock:
|
|
self._default_allow = False
|
|
self._overrides.clear()
|
|
|
|
def snapshot(self) -> dict:
|
|
with self._lock:
|
|
return {"default_allow": self._default_allow, "overrides": dict(self._overrides)}
|
|
|
|
def load_snapshot(self, data: dict) -> None:
|
|
with self._lock:
|
|
self._default_allow = bool(data.get("default_allow", True))
|
|
self._overrides = {int(k): bool(v) for k, v in data.get("overrides", {}).items()}
|
|
|
|
def save(self, path: Path) -> None:
|
|
Path(path).write_text(json.dumps(self.snapshot(), indent=2, sort_keys=True))
|
|
|
|
def load(self, path: Path) -> None:
|
|
self.load_snapshot(json.loads(Path(path).read_text()))
|