2026-06-18 22:25:42 +02:00

104 lines
3.4 KiB
Python

"""Symbolic decode of IEC command bytes and state events for the trace.
The kernel module decodes *just enough* addressing to know whether it is the
addressed listener (PLAN.md §7). The rich, human-readable decode -- mnemonics,
primary/secondary address split, "is this for us?" -- lives here in userspace
where it costs nothing.
Command byte structure (research §2.4)::
$20+PA LISTEN pa $3F UNLISTEN
$40+PA TALK pa $5F UNTALK
$60+SA SECOND/DATA sa (reopen channel, after LISTEN/TALK)
$E0+SA CLOSE sa
$F0+SA OPEN sa (filename bytes follow in the data phase)
"""
from __future__ import annotations
from dataclasses import dataclass
from . import device
from .petscii import to_glyph
# command groups
LISTEN = "LISTEN"
UNLISTEN = "UNLISTEN"
TALK = "TALK"
UNTALK = "UNTALK"
SECOND = "DATA" # $60+SA, "reopen"/data channel
CLOSE = "CLOSE"
OPEN = "OPEN"
UNKNOWN = "?"
@dataclass(frozen=True)
class Command:
"""A decoded command byte."""
value: int
mnemonic: str
primary: int | None = None # primary address for LISTEN/TALK
secondary: int | None = None # secondary address for SECOND/CLOSE/OPEN
def targets(self, my_address: int) -> bool:
"""True if this command addresses the device at ``my_address``."""
return self.primary is not None and self.primary == my_address
def describe(self) -> str:
"""Short mnemonic + operand, e.g. ``LISTEN 4`` or ``OPEN SA=0``."""
if self.primary is not None:
return f"{self.mnemonic} {self.primary}"
if self.secondary is not None:
# pad mnemonic so SA-style commands line up in the trace
return f"{self.mnemonic:<5} SA={self.secondary}"
return self.mnemonic
def decode_command(value: int) -> Command:
"""Decode a single ATN-phase command byte."""
if not 0 <= value <= 0xFF:
raise ValueError(f"byte out of range: {value!r}")
if value == 0x3F:
return Command(value, UNLISTEN)
if value == 0x5F:
return Command(value, UNTALK)
if 0x20 <= value <= 0x3E:
return Command(value, LISTEN, primary=value - 0x20)
if 0x40 <= value <= 0x5E:
return Command(value, TALK, primary=value - 0x40)
if 0x60 <= value <= 0x6F:
return Command(value, SECOND, secondary=value - 0x60)
if 0xE0 <= value <= 0xEF:
return Command(value, CLOSE, secondary=value - 0xE0)
if 0xF0 <= value <= 0xFF:
return Command(value, OPEN, secondary=value - 0xF0)
return Command(value, UNKNOWN)
# Human-readable text for each state event (device.EV_*).
_EVENT_TEXT = {
device.EV_IDLE: "waiting for ATN",
device.EV_ATN_ASSERTED: "asserted -> DATA low (ack)",
device.EV_ATN_COMMAND: "asserted -> command phase",
device.EV_RESET: "RESET -> release bus, IDLE",
}
def describe_event(rec: device.IecRecord) -> str:
"""Human-readable text for an event record."""
if rec.value == device.EV_ATN_RELEASED:
if rec.addressed:
return "released -> LISTENER"
return "released -> not addressed -> IDLE"
return _EVENT_TEXT.get(rec.value, f"event ${rec.value:02X}")
def describe_data(value: int) -> str:
"""Render a data byte as the trace shows it: ``$48 'H'`` / ``$0D <CR>``."""
glyph = to_glyph(value)
if glyph.printable:
return f"${value:02X} '{glyph.text}'"
return f"${value:02X} {glyph.text}"