BMW_E8x_EPS/gateway/rules.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

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()))