Christian Werner d359aa4acc feat(iecpoc): add plain-text output mode with --debug flag
Default output now shows only the received characters; CR/LF become real
newlines and LISTEN/UNLISTEN print a clearly visible divider. Pass --debug
to restore the previous full annotated trace.

Generated by Clanker
2026-06-20 18:19:41 +02:00

109 lines
3.7 KiB
Python

"""Render the annotated debug trace (PLAN.md §8).
Consumes :class:`~iecpoc.device.IecRecord` objects and emits one human-readable
line per record. This is the primary PoC deliverable on the userspace side, and
it is fully decoupled from any timing: it just formats whatever the kernel
pushed up. ``--raw`` switches to a bare hex dump of the byte stream.
"""
from __future__ import annotations
from typing import Iterable, Iterator
from . import decode, device, petscii
# bracketed state tag, padded so the content columns line up
_TAG_WIDTH = 8
def _line(tag: str, content: str) -> str:
return f"{f'[{tag}]':<{_TAG_WIDTH}} {content}"
class TraceFormatter:
"""Format an IEC record stream into the annotated trace.
Parameters
----------
my_address:
Our primary device address; used to annotate which commands target us.
raw:
When True, emit only a bare hex dump of command + data bytes and drop
the symbolic annotations and state events.
"""
def __init__(self, my_address: int = 4, raw: bool = False, debug: bool = False):
self.my_address = my_address
self.raw = raw
self.debug = debug
@property
def plain(self) -> bool:
return not self.debug and not self.raw
def format(self, rec: device.IecRecord) -> str | None:
"""Format one record. Returns ``None`` for records to be skipped."""
if self.plain:
return self._format_plain(rec)
if self.raw:
# raw mode: just the byte value of command/data records
if rec.is_command or rec.is_data:
return f"{rec.value:02X}"
return None
if rec.is_event:
return self._format_event(rec)
if rec.is_command:
return self._format_command(rec)
if rec.is_data:
return self._format_data(rec)
return _line("?", f"unknown kind {rec.kind}")
def format_stream(self, records: Iterable[device.IecRecord]) -> Iterator[str]:
"""Format a stream, dropping skipped records."""
for rec in records:
line = self.format(rec)
if line is not None:
yield line
# -- per-kind formatters -------------------------------------------------
def _format_plain(self, rec: device.IecRecord) -> str | None:
if rec.is_event:
return None
if rec.is_command:
cmd = decode.decode_command(rec.value)
if cmd.mnemonic == decode.LISTEN:
return f"\n--- LISTEN {cmd.primary} ---\n"
if cmd.mnemonic == decode.UNLISTEN:
return "\n--- UNLISTEN ---\n"
return None
if rec.is_data:
if rec.value in (0x0D, 0x0A, 0x8D): # CR, LF, SHIFT-CR
return "\n"
g = petscii.to_glyph(rec.value)
return g.text if g.printable else None
return None
def _format_event(self, rec: device.IecRecord) -> str:
text = decode.describe_event(rec)
if rec.value == device.EV_IDLE:
tag = "IDLE"
elif rec.value == device.EV_RESET:
tag = "RESET"
else:
tag = "ATN"
return _line(tag, text)
def _format_command(self, rec: device.IecRecord) -> str:
cmd = decode.decode_command(rec.value)
content = f"${rec.value:02X} {cmd.describe()}"
if cmd.targets(self.my_address):
content = f"{content:<22} (addressed: ME)"
return _line("CMD", content)
def _format_data(self, rec: device.IecRecord) -> str:
content = decode.describe_data(rec.value)
if rec.eoi:
content = f"{content} <EOI>"
return _line("DATA", content)