"""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 ``.""" glyph = to_glyph(value) if glyph.printable: return f"${value:02X} '{glyph.text}'" return f"${value:02X} {glyph.text}"