from __future__ import annotations from dataclasses import dataclass from ..formatting import h16 @dataclass(frozen=True) class PanelInput: name: str source: int shadow: int previous: int bit: int dirty: int dirty_bit: int selector: int | None = None note: str = "" @property def spec(self) -> str: return f"{h16(self.shadow)}.{self.bit}" @property def dirty_spec(self) -> str: return f"{h16(self.dirty)}.{self.dirty_bit}" @dataclass(frozen=True) class PanelAction: panel_input: PanelInput pressed: bool raw: str = "" @property def label(self) -> str: state = "press" if self.pressed else "release" return f"{self.panel_input.name}:{state}" @dataclass(frozen=True) class PanelInjection: action: PanelAction source_before: int source_after: int shadow_before: int shadow_after: int previous_before: int previous_after: int dirty_before: int dirty_after: int def summary(self) -> str: panel_input = self.action.panel_input selector = "" if panel_input.selector is None else f" selector=0x{panel_input.selector:04X}" return ( f"{self.action.label} source={h16(panel_input.source)} " f"shadow={panel_input.spec} previous={h16(panel_input.previous)} " f"dirty={panel_input.dirty_spec}{selector}" ) PANEL_LANES: tuple[tuple[int, int, int, int, int], ...] = ( (0xF102, 0xF6D7, 0xF6E7, 0xF6F2, 7), (0xF103, 0xF6D6, 0xF6E6, 0xF6F2, 6), (0xF104, 0xF6D5, 0xF6E5, 0xF6F2, 5), (0xF105, 0xF6D4, 0xF6E4, 0xF6F2, 4), (0xF106, 0xF6D3, 0xF6E3, 0xF6F2, 3), (0xF107, 0xF6D2, 0xF6E2, 0xF6F2, 2), (0xF108, 0xF6D1, 0xF6E1, 0xF6F2, 1), (0xF109, 0xF6D0, 0xF6E0, 0xF6F2, 0), (0xF005, 0xF6DC, 0xF6EC, 0xF6F3, 4), (0xF006, 0xF6DB, 0xF6EB, 0xF6F3, 3), ) KNOWN_PANEL_INPUTS: dict[str, PanelInput] = { "cam-power": PanelInput( name="cam-power", source=0xF105, shadow=0xF6D4, previous=0xF6E4, bit=3, dirty=0xF6F2, dirty_bit=4, selector=0x0007, note="CAM POWER button; queues selector 0x0007 when gates allow", ), "call": PanelInput( name="call", source=0xF006, shadow=0xF6DB, previous=0xF6EB, bit=5, dirty=0xF6F3, dirty_bit=3, selector=0x0015, note="CALL button; queues selector 0x0015 active/inactive reports", ), } PANEL_ALIASES: dict[str, str] = { "cam": "cam-power", "camera-power": "cam-power", "camera_power": "cam-power", "cam_power": "cam-power", "campower": "cam-power", "power": "cam-power", } def resolve_panel_input(text: str) -> PanelInput: token = _normalize_token(text) token = PANEL_ALIASES.get(token, token) if token in KNOWN_PANEL_INPUTS: return KNOWN_PANEL_INPUTS[token] if "." not in token: raise ValueError(f"unknown panel input {text!r}; use cam-power, call, or an address bit like F6D4.3") address_text, bit_text = token.split(".", 1) address = _parse_address(address_text) try: bit = int(bit_text, 0) except ValueError as exc: raise ValueError(f"invalid panel bit in {text!r}") from exc if not 0 <= bit <= 7: raise ValueError(f"panel bit out of range in {text!r}") for source, shadow, previous, dirty, dirty_bit in PANEL_LANES: if address in {source, shadow}: return PanelInput( name=f"{h16(shadow)}.{bit}", source=source, shadow=shadow, previous=previous, bit=bit, dirty=dirty, dirty_bit=dirty_bit, note="raw panel matrix input inferred from ROM shadow/dirty lane", ) raise ValueError(f"{h16(address)} is not a known A8 panel byte shadow/source") def parse_panel_action(text: str, *, default_pressed: bool = True) -> PanelAction: raw = text.strip() spec = raw pressed = default_pressed for separator in ("=", ":"): if separator not in raw: continue left, right = raw.rsplit(separator, 1) state = _parse_state(right) if state is None: continue spec = left pressed = state break return PanelAction(resolve_panel_input(spec), pressed=pressed, raw=raw) def _parse_state(text: str) -> bool | None: token = _normalize_token(text) if token in {"press", "pressed", "on", "down", "1", "true", "active"}: return True if token in {"release", "released", "off", "up", "0", "false", "inactive"}: return False return None def _normalize_token(text: str) -> str: return text.strip().lower().replace("_", "-") def _parse_address(text: str) -> int: token = text.strip().upper() if token.startswith("H'"): token = token[2:] elif token.startswith("$"): token = token[1:] elif token.startswith("0X"): token = token[2:] try: value = int(token, 16) except ValueError as exc: raise ValueError(f"invalid panel address {text!r}") from exc if not 0 <= value <= 0xFFFF: raise ValueError(f"panel address out of range {text!r}") return value def set_bit(value: int, bit: int, enabled: bool) -> int: mask = 1 << bit return (value | mask) & 0xFF if enabled else (value & ~mask) & 0xFF __all__ = [ "KNOWN_PANEL_INPUTS", "PANEL_ALIASES", "PANEL_LANES", "PanelAction", "PanelInjection", "PanelInput", "parse_panel_action", "resolve_panel_input", "set_bit", ]