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