104 lines
3.4 KiB
Python
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}"
|