traces
This commit is contained in:
203
h8536/emulator/panel.py
Normal file
203
h8536/emulator/panel.py
Normal file
@@ -0,0 +1,203 @@
|
||||
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",
|
||||
]
|
||||
Reference in New Issue
Block a user