"""L-Band screen — direct input spectrum analyzer with allocation annotations. Same sweep mechanics as the spectrum screen, but with LNB disabled (direct input) and band allocation overlays showing what service each frequency range belongs to. """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "tools")) from textual.app import ComposeResult from textual.containers import Container, Horizontal, Vertical from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox from textual import work from textual.worker import Worker from skywalker_lib import LBAND_ALLOCATIONS from skywalker_tui.widgets.spectrum_plot import SpectrumPlot from skywalker_tui.widgets.waterfall import WaterfallDisplay def _alloc_table(start: float, stop: float) -> str: """Build a Rich-markup allocation reference for the visible range.""" lines = ["[#00d4aa bold]L-Band Allocations in range:[/]"] colors = ["#60a0c0", "#80b060", "#c0a050", "#a06080", "#50a0a0", "#a08060", "#6080a0"] for i, (lo, hi, name) in enumerate(LBAND_ALLOCATIONS): if lo < stop and hi > start: overlap_lo = max(lo, start) overlap_hi = min(hi, stop) c = colors[i % len(colors)] lines.append(f" [{c}]{overlap_lo:.0f}-{overlap_hi:.0f} MHz {name}[/]") if len(lines) == 1: lines.append(" [#506878](none in range)[/]") return "\n".join(lines) class LBandScreen(Container): """L-band direct input analyzer with allocation annotations.""" DEFAULT_CSS = """ LBandScreen { layout: vertical; } LBandScreen #lband-main { height: 1fr; layout: horizontal; } LBandScreen #lband-plot-col { width: 2fr; layout: vertical; } LBandScreen #lband-info-col { width: 1fr; padding: 1; background: #0e1420; border-left: solid #1a2a3a; layout: vertical; } LBandScreen #lband-alloc-panel { height: auto; padding: 1; } LBandScreen #lband-progress { height: 3; layout: horizontal; padding: 0 2; background: #0e1018; } LBandScreen #lband-progress Static { width: auto; margin: 1 1 0 0; } LBandScreen #lband-progress ProgressBar { width: 1fr; margin: 1 1 0 0; } LBandScreen #lband-controls { height: auto; padding: 1 2; background: #0e1018; border-top: solid #1a2a3a; layout: horizontal; } LBandScreen #lband-controls Label { width: auto; margin: 1 1 0 0; color: #506878; } LBandScreen #lband-controls Input { width: 10; margin: 0 1; } LBandScreen #lband-controls Button { margin: 0 1; } """ def __init__(self, bridge, **kwargs): super().__init__(**kwargs) self._bridge = bridge self._sweeping = False self._sweep_worker: Worker | None = None def compose(self) -> ComposeResult: with Horizontal(id="lband-main"): with Vertical(id="lband-plot-col"): yield SpectrumPlot(title="L-Band Spectrum (Direct Input)", id="lband-plot") yield WaterfallDisplay(title="Waterfall", id="lband-waterfall") with Vertical(id="lband-info-col"): yield Static(_alloc_table(950, 2150), id="lband-alloc-panel") with Horizontal(id="lband-progress"): yield Static("[#506878]Ready[/]", id="lband-status") yield ProgressBar(total=100, show_eta=False, id="lband-pbar") with Horizontal(id="lband-controls"): yield Label("Start:") yield Input("950", id="lband-start") yield Label("Stop:") yield Input("2150", id="lband-stop") yield Label("Step:") yield Input("2", id="lband-step") yield Label("Dwell:") yield Input("20", id="lband-dwell") yield Button("23cm", id="lband-23cm-btn") yield Button("Sweep", id="lband-sweep-btn", variant="success") yield Button("Stop", id="lband-stop-btn", variant="error") def on_show(self) -> None: if self._bridge.is_demo and not self._sweeping: self._start_sweep() def on_hide(self) -> None: self._stop_sweep() def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "lband-sweep-btn": self._start_sweep() elif event.button.id == "lband-stop-btn": self._stop_sweep() elif event.button.id == "lband-23cm-btn": self.query_one("#lband-start", Input).value = "1240" self.query_one("#lband-stop", Input).value = "1300" self.query_one("#lband-step", Input).value = "0.5" # Update allocation display self.query_one("#lband-alloc-panel", Static).update( _alloc_table(1240, 1300) ) def _start_sweep(self) -> None: if self._sweeping: return self._sweeping = True start = float(self.query_one("#lband-start", Input).value or "950") stop = float(self.query_one("#lband-stop", Input).value or "2150") step = float(self.query_one("#lband-step", Input).value or "2") dwell = int(self.query_one("#lband-dwell", Input).value or "20") # Update allocation panel for current range self.query_one("#lband-alloc-panel", Static).update( _alloc_table(start, stop) ) self._sweep_worker = self._do_sweep(start, stop, step, dwell) def _stop_sweep(self) -> None: self._sweeping = False if self._sweep_worker: self._sweep_worker.cancel() self._sweep_worker = None @work(thread=True) def _do_sweep(self, start: float, stop: float, step: float, dwell: int) -> None: """L-band sweep with LNB disabled.""" try: self._bridge.ensure_booted() # Disable LNB for direct input self._bridge.configure_lnb(disable_lnb=True) except Exception: pass def progress_cb(freq, step_num, total, result): pct = (step_num + 1) / total * 100 self.app.call_from_thread(self._update_progress, pct, freq) self.app.call_from_thread(self._set_status, "Sweeping...") freqs, powers, results = self._bridge.sweep_spectrum( start, stop, step, dwell, sr_ksps=20000, callback=progress_cb, ) self.app.call_from_thread(self._show_results, freqs, powers, results) self._sweeping = False def _update_progress(self, pct: float, freq: float) -> None: if not self.is_mounted: return self.query_one("#lband-pbar", ProgressBar).update(progress=pct) self.query_one("#lband-status", Static).update( f"[#00d4aa]{freq:.1f} MHz[/]" ) def _set_status(self, msg: str) -> None: if not self.is_mounted: return self.query_one("#lband-status", Static).update(f"[#506878]{msg}[/]") def _show_results(self, freqs, powers, results) -> None: if not self.is_mounted: return self.query_one("#lband-plot", SpectrumPlot).update_data( freqs, powers, results, lnb_lo=0.0, ) self.query_one("#lband-waterfall", WaterfallDisplay).add_sweep(powers) self.query_one("#lband-status", Static).update("[#506878]Complete[/]") self.query_one("#lband-pbar", ProgressBar).update(progress=100)