204 lines
5.6 KiB
Python
204 lines
5.6 KiB
Python
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",
|
|
]
|