Apply .gitattributes normalization to convert all CRLF line endings inherited from Windows-origin source files to Unix LF. 175 files, zero content changes.
742 lines
24 KiB
Python
742 lines
24 KiB
Python
"""Config screen — LNB power, DiSEqC switching, and modulation/FEC setup.
|
|
|
|
Manages the hardware configuration layer: LNB voltage/tone control, DiSEqC
|
|
port switching (committed commands, tone burst, raw hex), and the tuning
|
|
parameter set (modulation, FEC, symbol rate, frequency). Bottom status bar
|
|
shows live config register state from the device.
|
|
"""
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container, Horizontal, Vertical
|
|
from textual.widgets import Label, Input, Button, Static, Select
|
|
from textual import work
|
|
from textual.worker import Worker
|
|
|
|
from skywalker_lib import MODULATIONS, FEC_RATES, MOD_FEC_GROUP, format_config_bits
|
|
|
|
|
|
def _fec_options_for_mod(mod_key: str) -> list[tuple[str, str]]:
|
|
"""Return (label, value) tuples for the FEC Select dropdown."""
|
|
group = MOD_FEC_GROUP.get(mod_key, "dvbs")
|
|
rates = FEC_RATES.get(group, {})
|
|
return [(rate_name, rate_name) for rate_name in rates]
|
|
|
|
|
|
def _mod_options() -> list[tuple[str, str]]:
|
|
"""Return (label, value) tuples for the Modulation Select dropdown."""
|
|
return [(desc, key) for key, (_idx, desc) in MODULATIONS.items()]
|
|
|
|
|
|
class ConfigScreen(Container):
|
|
"""LNB, DiSEqC, and modulation/FEC configuration panel."""
|
|
|
|
DEFAULT_CSS = """
|
|
ConfigScreen {
|
|
layout: vertical;
|
|
}
|
|
ConfigScreen #cfg-main {
|
|
height: 1fr;
|
|
layout: horizontal;
|
|
padding: 1 2;
|
|
}
|
|
|
|
/* --- Three column panels --- */
|
|
|
|
ConfigScreen .cfg-panel {
|
|
width: 1fr;
|
|
background: #0e1420;
|
|
border: round #1a2a3a;
|
|
padding: 1 2;
|
|
margin: 0 1 0 0;
|
|
layout: vertical;
|
|
}
|
|
ConfigScreen .cfg-panel:last-of-type {
|
|
margin: 0;
|
|
}
|
|
ConfigScreen .cfg-panel-title {
|
|
color: #00d4aa;
|
|
text-style: bold;
|
|
margin: 0 0 1 0;
|
|
height: 1;
|
|
}
|
|
|
|
/* --- Buttons within panels --- */
|
|
|
|
ConfigScreen .cfg-btn-row {
|
|
height: auto;
|
|
layout: horizontal;
|
|
margin: 0 0 1 0;
|
|
}
|
|
ConfigScreen .cfg-btn-row Label {
|
|
width: auto;
|
|
margin: 1 1 0 0;
|
|
color: #506878;
|
|
min-width: 12;
|
|
}
|
|
ConfigScreen .cfg-btn-row Button {
|
|
margin: 0 1 0 0;
|
|
background: #1a2a40;
|
|
color: #c8d0d8;
|
|
border: round #1a3050;
|
|
}
|
|
ConfigScreen .cfg-btn-row Button:hover {
|
|
background: #00d4aa;
|
|
color: #0a0a12;
|
|
}
|
|
ConfigScreen .cfg-btn-row Button.-active-setting {
|
|
background: #0a3a3a;
|
|
color: #00d4aa;
|
|
border: round #00d4aa;
|
|
text-style: bold;
|
|
}
|
|
|
|
/* --- Warning label --- */
|
|
|
|
ConfigScreen .cfg-warning {
|
|
color: #e8a020;
|
|
margin: 1 0 0 0;
|
|
text-style: italic;
|
|
height: auto;
|
|
}
|
|
|
|
/* --- DiSEqC port buttons --- */
|
|
|
|
ConfigScreen #cfg-diseqc-ports {
|
|
height: auto;
|
|
layout: horizontal;
|
|
margin: 0 0 1 0;
|
|
}
|
|
ConfigScreen #cfg-diseqc-ports Button {
|
|
margin: 0 1 0 0;
|
|
min-width: 10;
|
|
background: #1a2a40;
|
|
color: #c8d0d8;
|
|
border: round #1a3050;
|
|
}
|
|
ConfigScreen #cfg-diseqc-ports Button:hover {
|
|
background: #00d4aa;
|
|
color: #0a0a12;
|
|
}
|
|
ConfigScreen #cfg-diseqc-ports Button.-active-setting {
|
|
background: #0a3a3a;
|
|
color: #00d4aa;
|
|
border: round #00d4aa;
|
|
text-style: bold;
|
|
}
|
|
|
|
/* --- Raw DiSEqC input row --- */
|
|
|
|
ConfigScreen #cfg-diseqc-raw {
|
|
height: auto;
|
|
layout: horizontal;
|
|
margin: 1 0 0 0;
|
|
}
|
|
ConfigScreen #cfg-diseqc-raw Label {
|
|
width: auto;
|
|
margin: 1 1 0 0;
|
|
color: #506878;
|
|
}
|
|
ConfigScreen #cfg-diseqc-raw Input {
|
|
width: 1fr;
|
|
margin: 0 1 0 0;
|
|
background: #121c2a;
|
|
border: round #1a3050;
|
|
color: #c8d0d8;
|
|
}
|
|
ConfigScreen #cfg-diseqc-raw Input:focus {
|
|
border: round #00d4aa;
|
|
}
|
|
ConfigScreen #cfg-diseqc-raw Button {
|
|
margin: 0;
|
|
background: #1a2a40;
|
|
color: #c8d0d8;
|
|
border: round #1a3050;
|
|
}
|
|
|
|
/* --- Modulation/FEC panel --- */
|
|
|
|
ConfigScreen .cfg-field-row {
|
|
height: auto;
|
|
layout: horizontal;
|
|
margin: 0 0 1 0;
|
|
}
|
|
ConfigScreen .cfg-field-row Label {
|
|
width: auto;
|
|
min-width: 12;
|
|
margin: 1 1 0 0;
|
|
color: #506878;
|
|
}
|
|
ConfigScreen .cfg-field-row Select {
|
|
width: 1fr;
|
|
}
|
|
ConfigScreen .cfg-field-row Input {
|
|
width: 1fr;
|
|
background: #121c2a;
|
|
border: round #1a3050;
|
|
color: #c8d0d8;
|
|
}
|
|
ConfigScreen .cfg-field-row Input:focus {
|
|
border: round #00d4aa;
|
|
}
|
|
ConfigScreen #cfg-tune-btn {
|
|
margin: 1 0 0 0;
|
|
width: 100%;
|
|
}
|
|
|
|
/* --- Bottom status bar --- */
|
|
|
|
ConfigScreen #cfg-status-bar {
|
|
height: auto;
|
|
min-height: 3;
|
|
padding: 1 2;
|
|
background: #0e1018;
|
|
border-top: solid #1a2a3a;
|
|
}
|
|
ConfigScreen #cfg-result {
|
|
height: auto;
|
|
min-height: 2;
|
|
padding: 0 2;
|
|
background: #0e1018;
|
|
}
|
|
"""
|
|
|
|
def __init__(self, bridge, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._bridge = bridge
|
|
self._refresh_worker: Worker | None = None
|
|
self._active_port: int | None = None
|
|
self._lnb_power = False
|
|
self._lnb_voltage_high = False
|
|
self._tone_22khz = False
|
|
self._extra_volt = False
|
|
|
|
def compose(self) -> ComposeResult:
|
|
with Horizontal(id="cfg-main"):
|
|
# --- LNB Control ---
|
|
with Vertical(classes="cfg-panel"):
|
|
yield Static("[#00d4aa bold]LNB Control[/]", classes="cfg-panel-title")
|
|
|
|
with Horizontal(classes="cfg-btn-row"):
|
|
yield Label("Power:")
|
|
yield Button("On", id="cfg-lnb-on", variant="success")
|
|
yield Button("Off", id="cfg-lnb-off", variant="error")
|
|
|
|
with Horizontal(classes="cfg-btn-row"):
|
|
yield Label("Voltage:")
|
|
yield Button("13V", id="cfg-volt-13")
|
|
yield Button("18V", id="cfg-volt-18")
|
|
|
|
with Horizontal(classes="cfg-btn-row"):
|
|
yield Label("22kHz Tone:")
|
|
yield Button("On", id="cfg-tone-on")
|
|
yield Button("Off", id="cfg-tone-off")
|
|
|
|
with Horizontal(classes="cfg-btn-row"):
|
|
yield Label("Extra +1V:")
|
|
yield Button("On", id="cfg-extra-on")
|
|
yield Button("Off", id="cfg-extra-off")
|
|
|
|
yield Static(
|
|
"[#e8a020]Max 450mA continuous load[/]",
|
|
classes="cfg-warning",
|
|
)
|
|
|
|
# --- DiSEqC Switch ---
|
|
with Vertical(classes="cfg-panel"):
|
|
yield Static("[#00d4aa bold]DiSEqC Switch[/]", classes="cfg-panel-title")
|
|
|
|
yield Label("Committed Port:", classes="cfg-section-label")
|
|
with Horizontal(id="cfg-diseqc-ports"):
|
|
yield Button("Port 1", id="cfg-port-1")
|
|
yield Button("Port 2", id="cfg-port-2")
|
|
yield Button("Port 3", id="cfg-port-3")
|
|
yield Button("Port 4", id="cfg-port-4")
|
|
|
|
with Horizontal(classes="cfg-btn-row"):
|
|
yield Label("Tone Burst:")
|
|
yield Button("A", id="cfg-burst-a")
|
|
yield Button("B", id="cfg-burst-b")
|
|
|
|
with Horizontal(id="cfg-diseqc-raw"):
|
|
yield Label("Raw Hex:")
|
|
yield Input(
|
|
placeholder="E0 10 38 F0",
|
|
id="cfg-diseqc-hex",
|
|
)
|
|
yield Button("Send", id="cfg-diseqc-send")
|
|
|
|
# --- Modulation / FEC ---
|
|
with Vertical(classes="cfg-panel"):
|
|
yield Static(
|
|
"[#00d4aa bold]Modulation / FEC[/]",
|
|
classes="cfg-panel-title",
|
|
)
|
|
|
|
with Horizontal(classes="cfg-field-row"):
|
|
yield Label("Modulation:")
|
|
yield Select(
|
|
_mod_options(),
|
|
value="qpsk",
|
|
id="cfg-mod-select",
|
|
allow_blank=False,
|
|
)
|
|
|
|
with Horizontal(classes="cfg-field-row"):
|
|
yield Label("FEC Rate:")
|
|
yield Select(
|
|
_fec_options_for_mod("qpsk"),
|
|
value="auto",
|
|
id="cfg-fec-select",
|
|
allow_blank=False,
|
|
)
|
|
|
|
with Horizontal(classes="cfg-field-row"):
|
|
yield Label("Symbol Rate:")
|
|
yield Input("20000", id="cfg-sr-input")
|
|
yield Label("ksps")
|
|
|
|
with Horizontal(classes="cfg-field-row"):
|
|
yield Label("Frequency:")
|
|
yield Input("1200", id="cfg-freq-input")
|
|
yield Label("MHz")
|
|
|
|
yield Button(
|
|
"Tune",
|
|
id="cfg-tune-btn",
|
|
variant="success",
|
|
)
|
|
|
|
yield Static("", id="cfg-result")
|
|
yield Static("[#506878]Config: reading...[/]", id="cfg-status-bar")
|
|
|
|
# ── Lifecycle ──
|
|
|
|
def on_show(self) -> None:
|
|
self._refresh_config()
|
|
|
|
def on_hide(self) -> None:
|
|
if self._refresh_worker is not None:
|
|
self._refresh_worker.cancel()
|
|
self._refresh_worker = None
|
|
|
|
# ── Event handlers ──
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
btn = event.button.id
|
|
if btn is None:
|
|
return
|
|
|
|
# LNB control
|
|
if btn == "cfg-lnb-on":
|
|
self._do_lnb_power(True)
|
|
elif btn == "cfg-lnb-off":
|
|
self._do_lnb_power(False)
|
|
elif btn == "cfg-volt-13":
|
|
self._do_lnb_voltage(high=False)
|
|
elif btn == "cfg-volt-18":
|
|
self._do_lnb_voltage(high=True)
|
|
elif btn == "cfg-tone-on":
|
|
self._do_22khz_tone(on=True)
|
|
elif btn == "cfg-tone-off":
|
|
self._do_22khz_tone(on=False)
|
|
elif btn == "cfg-extra-on":
|
|
self._do_extra_voltage(on=True)
|
|
elif btn == "cfg-extra-off":
|
|
self._do_extra_voltage(on=False)
|
|
|
|
# DiSEqC ports
|
|
elif btn == "cfg-port-1":
|
|
self._do_diseqc_port(1)
|
|
elif btn == "cfg-port-2":
|
|
self._do_diseqc_port(2)
|
|
elif btn == "cfg-port-3":
|
|
self._do_diseqc_port(3)
|
|
elif btn == "cfg-port-4":
|
|
self._do_diseqc_port(4)
|
|
|
|
# Tone burst
|
|
elif btn == "cfg-burst-a":
|
|
self._do_tone_burst(0)
|
|
elif btn == "cfg-burst-b":
|
|
self._do_tone_burst(1)
|
|
|
|
# Raw DiSEqC
|
|
elif btn == "cfg-diseqc-send":
|
|
self._do_diseqc_raw()
|
|
|
|
# Tune
|
|
elif btn == "cfg-tune-btn":
|
|
self._do_tune()
|
|
|
|
def on_select_changed(self, event: Select.Changed) -> None:
|
|
if event.select.id == "cfg-mod-select":
|
|
mod_key = str(event.value)
|
|
self._update_fec_options(mod_key)
|
|
|
|
# ── LNB operations ──
|
|
|
|
@work(thread=True)
|
|
def _do_lnb_power(self, on: bool) -> None:
|
|
try:
|
|
self._bridge.start_intersil(on)
|
|
self._lnb_power = on
|
|
label = "[#00d4aa]ON[/]" if on else "[#e04040]OFF[/]"
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]LNB power:[/] {label}",
|
|
)
|
|
self.app.call_from_thread(self._highlight_lnb_power, on)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]LNB power error: {e}[/]",
|
|
)
|
|
self.app.call_from_thread(self._refresh_config)
|
|
|
|
@work(thread=True)
|
|
def _do_lnb_voltage(self, high: bool) -> None:
|
|
try:
|
|
self._bridge.set_lnb_voltage(high)
|
|
self._lnb_voltage_high = high
|
|
volts = "18V" if high else "13V"
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]LNB voltage set to[/] [#00d4aa]{volts}[/]",
|
|
)
|
|
self.app.call_from_thread(self._highlight_voltage, high)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]Voltage error: {e}[/]",
|
|
)
|
|
self.app.call_from_thread(self._refresh_config)
|
|
|
|
@work(thread=True)
|
|
def _do_22khz_tone(self, on: bool) -> None:
|
|
try:
|
|
self._bridge.set_22khz_tone(on)
|
|
self._tone_22khz = on
|
|
label = "[#00d4aa]ON[/]" if on else "[#e04040]OFF[/]"
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]22kHz tone:[/] {label}",
|
|
)
|
|
self.app.call_from_thread(self._highlight_tone, on)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]22kHz tone error: {e}[/]",
|
|
)
|
|
self.app.call_from_thread(self._refresh_config)
|
|
|
|
@work(thread=True)
|
|
def _do_extra_voltage(self, on: bool) -> None:
|
|
try:
|
|
self._bridge.set_extra_voltage(on)
|
|
self._extra_volt = on
|
|
label = "[#00d4aa]ON[/]" if on else "[#e04040]OFF[/]"
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]Extra +1V:[/] {label}",
|
|
)
|
|
self.app.call_from_thread(self._highlight_extra, on)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]Extra voltage error: {e}[/]",
|
|
)
|
|
self.app.call_from_thread(self._refresh_config)
|
|
|
|
# ── DiSEqC operations ──
|
|
|
|
_DISEQC_PORT_CMDS = {
|
|
1: bytes([0xE0, 0x10, 0x38, 0xF0]),
|
|
2: bytes([0xE0, 0x10, 0x38, 0xF4]),
|
|
3: bytes([0xE0, 0x10, 0x38, 0xF8]),
|
|
4: bytes([0xE0, 0x10, 0x38, 0xFC]),
|
|
}
|
|
|
|
@work(thread=True)
|
|
def _do_diseqc_port(self, port: int) -> None:
|
|
cmd = self._DISEQC_PORT_CMDS[port]
|
|
try:
|
|
self._bridge.send_diseqc_message(cmd)
|
|
self._active_port = port
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]DiSEqC port[/] [#00d4aa]{port}[/]"
|
|
f" [#506878]({cmd.hex(' ')})[/]",
|
|
)
|
|
self.app.call_from_thread(self._highlight_port, port)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]DiSEqC port {port} error: {e}[/]",
|
|
)
|
|
|
|
@work(thread=True)
|
|
def _do_tone_burst(self, mini_cmd: int) -> None:
|
|
label = "A" if mini_cmd == 0 else "B"
|
|
try:
|
|
self._bridge.send_diseqc_tone_burst(mini_cmd)
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]Tone burst[/] [#00d4aa]{label}[/]",
|
|
)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]Tone burst error: {e}[/]",
|
|
)
|
|
|
|
@work(thread=True)
|
|
def _do_diseqc_raw(self) -> None:
|
|
try:
|
|
hex_input = self.app.call_from_thread(self._get_diseqc_hex)
|
|
except Exception:
|
|
return
|
|
|
|
if not hex_input:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
"[#e8a020]Enter hex bytes (e.g. E0 10 38 F0)[/]",
|
|
)
|
|
return
|
|
|
|
try:
|
|
raw = bytes.fromhex(hex_input.replace(",", " "))
|
|
except ValueError:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]Invalid hex: {hex_input}[/]",
|
|
)
|
|
return
|
|
|
|
if len(raw) < 3 or len(raw) > 6:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]DiSEqC message must be 3-6 bytes, got {len(raw)}[/]",
|
|
)
|
|
return
|
|
|
|
try:
|
|
self._bridge.send_diseqc_message(raw)
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#c8d0d8]DiSEqC sent:[/] [#00d4aa]{raw.hex(' ')}[/]",
|
|
)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]DiSEqC send error: {e}[/]",
|
|
)
|
|
|
|
def _get_diseqc_hex(self) -> str:
|
|
"""Read the raw hex input value (must be called from UI thread)."""
|
|
return self.query_one("#cfg-diseqc-hex", Input).value.strip()
|
|
|
|
# ── Tune operation ──
|
|
|
|
@work(thread=True)
|
|
def _do_tune(self) -> None:
|
|
# Read inputs from UI thread
|
|
try:
|
|
params = self.app.call_from_thread(self._read_tune_params)
|
|
except Exception:
|
|
return
|
|
|
|
if params is None:
|
|
return
|
|
|
|
mod_key, fec_key, sr_ksps, freq_mhz = params
|
|
|
|
# Validate
|
|
if not (256 <= sr_ksps <= 30000):
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
"[#e04040]Symbol rate out of range (256-30000 ksps)[/]",
|
|
)
|
|
return
|
|
|
|
if not (950 <= freq_mhz <= 2150):
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
"[#e04040]Frequency out of range (950-2150 MHz)[/]",
|
|
)
|
|
return
|
|
|
|
mod_index = MODULATIONS[mod_key][0]
|
|
fec_group = MOD_FEC_GROUP.get(mod_key, "dvbs")
|
|
fec_rates = FEC_RATES.get(fec_group, {})
|
|
fec_index = fec_rates.get(fec_key, 0)
|
|
|
|
sr_sps = sr_ksps * 1000
|
|
freq_khz = int(freq_mhz * 1000)
|
|
|
|
try:
|
|
self._bridge.ensure_booted()
|
|
self._bridge.tune(sr_sps, freq_khz, mod_index, fec_index)
|
|
mod_desc = MODULATIONS[mod_key][1]
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#00d4aa]Tuned:[/] [#c8d0d8]{freq_mhz:.1f} MHz "
|
|
f"{sr_ksps} ksps {mod_desc} FEC {fec_key}[/]",
|
|
)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._show_result,
|
|
f"[#e04040]Tune error: {e}[/]",
|
|
)
|
|
self.app.call_from_thread(self._refresh_config)
|
|
|
|
def _read_tune_params(self) -> tuple | None:
|
|
"""Read tune parameters from UI widgets (must be called from UI thread)."""
|
|
mod_select = self.query_one("#cfg-mod-select", Select)
|
|
fec_select = self.query_one("#cfg-fec-select", Select)
|
|
sr_input = self.query_one("#cfg-sr-input", Input)
|
|
freq_input = self.query_one("#cfg-freq-input", Input)
|
|
|
|
mod_key = str(mod_select.value) if mod_select.value is not None else "qpsk"
|
|
fec_key = str(fec_select.value) if fec_select.value is not None else "auto"
|
|
|
|
try:
|
|
sr_ksps = int(float(sr_input.value or "20000"))
|
|
except ValueError:
|
|
self._show_result("[#e04040]Invalid symbol rate[/]")
|
|
return None
|
|
|
|
try:
|
|
freq_mhz = float(freq_input.value or "1200")
|
|
except ValueError:
|
|
self._show_result("[#e04040]Invalid frequency[/]")
|
|
return None
|
|
|
|
return (mod_key, fec_key, sr_ksps, freq_mhz)
|
|
|
|
# ── FEC dropdown update ──
|
|
|
|
def _update_fec_options(self, mod_key: str) -> None:
|
|
"""Rebuild the FEC dropdown when modulation changes."""
|
|
fec_select = self.query_one("#cfg-fec-select", Select)
|
|
options = _fec_options_for_mod(mod_key)
|
|
fec_select.set_options(options)
|
|
# Default to "auto" if available, otherwise first option
|
|
auto_keys = [v for (_l, v) in options if v == "auto"]
|
|
if auto_keys:
|
|
fec_select.value = "auto"
|
|
elif options:
|
|
fec_select.value = options[0][1]
|
|
|
|
# ── Config status display ──
|
|
|
|
@work(thread=True)
|
|
def _refresh_config(self) -> None:
|
|
"""Read config register and update the status bar."""
|
|
try:
|
|
status = self._bridge.get_config()
|
|
bits = format_config_bits(status)
|
|
self.app.call_from_thread(self._display_config, status, bits)
|
|
except Exception as e:
|
|
self.app.call_from_thread(
|
|
self._display_config_error, str(e),
|
|
)
|
|
|
|
def _display_config(self, status: int, bits: list) -> None:
|
|
"""Render config bits in the status bar (UI thread)."""
|
|
if not self.is_mounted:
|
|
return
|
|
|
|
parts = []
|
|
for name, is_set in bits:
|
|
if is_set:
|
|
parts.append(f"[#00d4aa]{name}[/]")
|
|
else:
|
|
parts.append(f"[#303840]{name}[/]")
|
|
|
|
text = (
|
|
f"[#506878]Config 0x{status:02X}:[/] "
|
|
+ " ".join(parts)
|
|
)
|
|
self.query_one("#cfg-status-bar", Static).update(text)
|
|
|
|
# Sync button highlights from config bits
|
|
self._lnb_power = bool(status & 0x04)
|
|
self._lnb_voltage_high = bool(status & 0x20)
|
|
self._tone_22khz = bool(status & 0x10)
|
|
|
|
self._highlight_lnb_power(self._lnb_power)
|
|
self._highlight_voltage(self._lnb_voltage_high)
|
|
self._highlight_tone(self._tone_22khz)
|
|
|
|
def _display_config_error(self, error: str) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
self.query_one("#cfg-status-bar", Static).update(
|
|
f"[#e04040]Config read error: {error}[/]"
|
|
)
|
|
|
|
# ── UI highlight helpers ──
|
|
|
|
def _show_result(self, markup: str) -> None:
|
|
"""Display operation result text."""
|
|
if not self.is_mounted:
|
|
return
|
|
self.query_one("#cfg-result", Static).update(markup)
|
|
|
|
def _highlight_lnb_power(self, on: bool) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
btn_on = self.query_one("#cfg-lnb-on", Button)
|
|
btn_off = self.query_one("#cfg-lnb-off", Button)
|
|
if on:
|
|
btn_on.add_class("-active-setting")
|
|
btn_off.remove_class("-active-setting")
|
|
else:
|
|
btn_off.add_class("-active-setting")
|
|
btn_on.remove_class("-active-setting")
|
|
|
|
def _highlight_voltage(self, high: bool) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
btn_13 = self.query_one("#cfg-volt-13", Button)
|
|
btn_18 = self.query_one("#cfg-volt-18", Button)
|
|
if high:
|
|
btn_18.add_class("-active-setting")
|
|
btn_13.remove_class("-active-setting")
|
|
else:
|
|
btn_13.add_class("-active-setting")
|
|
btn_18.remove_class("-active-setting")
|
|
|
|
def _highlight_tone(self, on: bool) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
btn_on = self.query_one("#cfg-tone-on", Button)
|
|
btn_off = self.query_one("#cfg-tone-off", Button)
|
|
if on:
|
|
btn_on.add_class("-active-setting")
|
|
btn_off.remove_class("-active-setting")
|
|
else:
|
|
btn_off.add_class("-active-setting")
|
|
btn_on.remove_class("-active-setting")
|
|
|
|
def _highlight_extra(self, on: bool) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
btn_on = self.query_one("#cfg-extra-on", Button)
|
|
btn_off = self.query_one("#cfg-extra-off", Button)
|
|
if on:
|
|
btn_on.add_class("-active-setting")
|
|
btn_off.remove_class("-active-setting")
|
|
else:
|
|
btn_off.add_class("-active-setting")
|
|
btn_on.remove_class("-active-setting")
|
|
|
|
def _highlight_port(self, port: int) -> None:
|
|
if not self.is_mounted:
|
|
return
|
|
for p in range(1, 5):
|
|
btn = self.query_one(f"#cfg-port-{p}", Button)
|
|
if p == port:
|
|
btn.add_class("-active-setting")
|
|
else:
|
|
btn.remove_class("-active-setting")
|