Expand from 5 to 8 mode screens. F6 Device provides firmware management, EEPROM flash with full safety state machine (C2 validation, auto-backup, 3s countdown, page write, byte verify), and diagnostics (boot test, I2C scan, register dump). F7 Stream does live TS capture with PID distribution and PAT/PMT tree. F8 Config manages LNB power, DiSEqC switching, and modulation/FEC. Foundation: 12 new SkyWalker1 methods (device info, FX2 RAM, EEPROM I2C, diagnostics), matching DemoDevice synthetics with realistic C2 image and TS packets, 20 bridge wrappers (RLock).
112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
"""Tree-style display of MPEG-2 PSI structure (PAT/PMT).
|
|
|
|
Renders a hierarchical view of the Program Association Table and its
|
|
child Program Map Tables using Rich markup inside a Static widget.
|
|
Shows transport stream ID, program numbers, PMT PIDs, PCR PIDs,
|
|
and elementary stream types with their PIDs.
|
|
"""
|
|
|
|
from textual.widget import Widget
|
|
from textual.widgets import Static
|
|
from textual.app import ComposeResult
|
|
|
|
|
|
class PsiTree(Widget):
|
|
"""Hierarchical PAT/PMT display for transport stream program structure."""
|
|
|
|
DEFAULT_CSS = """
|
|
PsiTree {
|
|
height: 1fr;
|
|
min-height: 8;
|
|
background: #0e1420;
|
|
border: round #1a2a3a;
|
|
padding: 1;
|
|
overflow-y: auto;
|
|
}
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._pat: dict | None = None
|
|
self._pmts: dict[int, dict] = {}
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Static(
|
|
"[#506878]Waiting for PSI data...[/]", id="psi-content"
|
|
)
|
|
|
|
def update_pat(self, pat: dict) -> None:
|
|
"""Update the Program Association Table and redraw."""
|
|
self._pat = pat
|
|
self._refresh()
|
|
|
|
def update_pmt(self, pmt_pid: int, pmt: dict) -> None:
|
|
"""Update a Program Map Table and redraw."""
|
|
self._pmts[pmt_pid] = pmt
|
|
self._refresh()
|
|
|
|
def clear_tree(self) -> None:
|
|
"""Reset all PSI state and show placeholder."""
|
|
self._pat = None
|
|
self._pmts.clear()
|
|
if self.is_mounted:
|
|
self.query_one("#psi-content", Static).update(
|
|
"[#506878]Waiting for PSI data...[/]"
|
|
)
|
|
|
|
def _refresh(self) -> None:
|
|
"""Rebuild the tree markup from current PAT/PMT data."""
|
|
if not self.is_mounted:
|
|
return
|
|
|
|
lines: list[str] = []
|
|
|
|
if self._pat:
|
|
tsid = self._pat.get("transport_stream_id", 0)
|
|
ver = self._pat.get("version", 0)
|
|
lines.append(
|
|
f"[bold #00d4aa]PAT[/] [#506878]TSID=0x{tsid:04X} v{ver}[/]"
|
|
)
|
|
|
|
programs = self._pat.get("programs", {})
|
|
# Separate NIT (program 0) from real programs
|
|
real_progs = {k: v for k, v in programs.items() if k != 0}
|
|
|
|
for prog_num, pmt_pid in sorted(programs.items()):
|
|
if prog_num == 0:
|
|
lines.append(
|
|
f" [#7090a8]\u251c\u2500[/] [#506878]NIT[/] "
|
|
f"PID=0x{pmt_pid:04X}"
|
|
)
|
|
else:
|
|
is_last = prog_num == max(real_progs.keys())
|
|
prefix = "\u2514\u2500" if is_last else "\u251c\u2500"
|
|
lines.append(
|
|
f" [#7090a8]{prefix}[/] "
|
|
f"[bold #c8d0d8]Program {prog_num}[/] "
|
|
f"PMT=0x{pmt_pid:04X}"
|
|
)
|
|
# Expand PMT details if available
|
|
if pmt_pid in self._pmts:
|
|
pmt = self._pmts[pmt_pid]
|
|
pcr_pid = pmt.get("pcr_pid", 0)
|
|
indent = " " if is_last else "\u2502 "
|
|
lines.append(
|
|
f" {indent} [#506878]PCR PID=0x{pcr_pid:04X}[/]"
|
|
)
|
|
streams = pmt.get("streams", [])
|
|
for j, s in enumerate(streams):
|
|
s_last = j == len(streams) - 1
|
|
s_prefix = "\u2514\u2500" if s_last else "\u251c\u2500"
|
|
type_name = s.get("type_name", "Unknown")
|
|
epid = s.get("elementary_pid", 0)
|
|
lines.append(
|
|
f" {indent} [#7090a8]{s_prefix}[/] "
|
|
f"[#c8d0d8]{type_name}[/] "
|
|
f"PID=0x{epid:04X}"
|
|
)
|
|
else:
|
|
lines.append("[#506878]No PAT received yet[/]")
|
|
|
|
self.query_one("#psi-content", Static).update("\n".join(lines))
|