Add LNB polarity toggle (V-pol 13V / H-pol 18V)
Bridge: set_lnb_voltage(mode) wraps firmware lnbdc command, enable_lna() now delegates to it. MCP: new set_lnb_voltage tool + 3 tests. TUI: Signal screen button toggles between V-pol and H-pol instead of one-way LNA enable.
This commit is contained in:
parent
8a6b99bd8c
commit
f8bfd69ceb
@ -55,14 +55,42 @@ def register(mcp):
|
|||||||
async def enable_lna(ctx: Context) -> dict:
|
async def enable_lna(ctx: Context) -> dict:
|
||||||
"""Enable the LNA by setting LNB voltage to 13V (V-pol).
|
"""Enable the LNA by setting LNB voltage to 13V (V-pol).
|
||||||
|
|
||||||
This powers the low-noise amplifier in the dish's outdoor unit.
|
Shortcut for set_lnb_voltage(mode='odu'). Use set_lnb_voltage
|
||||||
Boot default is 18V (H-pol). Required before meaningful RSSI
|
to switch back to 18V (H-pol) when done.
|
||||||
readings when used for radio astronomy.
|
|
||||||
"""
|
"""
|
||||||
state: BirdcageState = ctx.request_context.lifespan_context
|
state: BirdcageState = ctx.request_context.lifespan_context
|
||||||
_require_device(state)
|
_require_device(state)
|
||||||
state.device.enable_lna()
|
state.device.enable_lna()
|
||||||
return {"status": "lna_enabled", "voltage": "13V"}
|
return {"status": "lna_enabled", "voltage": "13V", "polarity": "V"}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def set_lnb_voltage(ctx: Context, mode: str) -> dict:
|
||||||
|
"""Set LNB DC voltage and polarization.
|
||||||
|
|
||||||
|
Controls the LNB bias voltage on the coax feed line. This
|
||||||
|
determines both LNA power state and receive polarization:
|
||||||
|
|
||||||
|
- 'odu' = 13V (V-pol, LNA enabled) — for radio astronomy / ham
|
||||||
|
- 'stb' = 18V (H-pol, boot default) — standard consumer mode
|
||||||
|
|
||||||
|
V-pol and H-pol see different transponders. The PEAK submenu's
|
||||||
|
'rssits' command alternates between them for comparison.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: 'odu' for 13V/V-pol or 'stb' for 18V/H-pol.
|
||||||
|
"""
|
||||||
|
state: BirdcageState = ctx.request_context.lifespan_context
|
||||||
|
_require_device(state)
|
||||||
|
raw = state.device.set_lnb_voltage(mode)
|
||||||
|
voltage = "13V" if mode.lower() == "odu" else "18V"
|
||||||
|
polarity = "V" if mode.lower() == "odu" else "H"
|
||||||
|
return {
|
||||||
|
"status": "set",
|
||||||
|
"mode": mode.lower(),
|
||||||
|
"voltage": voltage,
|
||||||
|
"polarity": polarity,
|
||||||
|
"raw": raw,
|
||||||
|
}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_dvb_config(ctx: Context) -> dict:
|
async def get_dvb_config(ctx: Context) -> dict:
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from conftest import parse_result
|
from conftest import parse_result
|
||||||
from fastmcp import Client
|
from fastmcp import Client
|
||||||
|
from fastmcp.exceptions import ToolError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@ -42,6 +43,37 @@ async def test_enable_lna(mcp_client: Client):
|
|||||||
data = parse_result(result)
|
data = parse_result(result)
|
||||||
assert data["status"] == "lna_enabled"
|
assert data["status"] == "lna_enabled"
|
||||||
assert data["voltage"] == "13V"
|
assert data["voltage"] == "13V"
|
||||||
|
assert data["polarity"] == "V"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_set_lnb_voltage_odu(mcp_client: Client):
|
||||||
|
result = await mcp_client.call_tool(
|
||||||
|
"set_lnb_voltage", {"mode": "odu"}
|
||||||
|
)
|
||||||
|
data = parse_result(result)
|
||||||
|
assert data["voltage"] == "13V"
|
||||||
|
assert data["polarity"] == "V"
|
||||||
|
assert data["mode"] == "odu"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_set_lnb_voltage_stb(mcp_client: Client):
|
||||||
|
result = await mcp_client.call_tool(
|
||||||
|
"set_lnb_voltage", {"mode": "stb"}
|
||||||
|
)
|
||||||
|
data = parse_result(result)
|
||||||
|
assert data["voltage"] == "18V"
|
||||||
|
assert data["polarity"] == "H"
|
||||||
|
assert data["mode"] == "stb"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_set_lnb_voltage_invalid(mcp_client: Client):
|
||||||
|
with pytest.raises(ToolError):
|
||||||
|
await mcp_client.call_tool(
|
||||||
|
"set_lnb_voltage", {"mode": "invalid"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
@ -526,10 +526,26 @@ class SerialBridge:
|
|||||||
raise ValueError(f"Could not parse RSSI: {response!r}")
|
raise ValueError(f"Could not parse RSSI: {response!r}")
|
||||||
|
|
||||||
def enable_lna(self) -> None:
|
def enable_lna(self) -> None:
|
||||||
"""Enable LNA in ODU mode (sets LNB to 13V)."""
|
"""Enable LNA in ODU mode (13V). Alias for set_lnb_voltage('odu')."""
|
||||||
|
self.set_lnb_voltage("odu")
|
||||||
|
|
||||||
|
def set_lnb_voltage(self, mode: str) -> str:
|
||||||
|
"""Set LNB DC voltage mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: 'odu' for 13V (V-pol, LNA enabled) or 'stb' for 18V (H-pol).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Raw firmware response.
|
||||||
|
"""
|
||||||
|
mode = mode.strip().lower()
|
||||||
|
if mode not in ("odu", "stb"):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid LNB mode {mode!r}: use 'odu' (13V) or 'stb' (18V)"
|
||||||
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._ensure_menu(Menu.DVB)
|
self._ensure_menu(Menu.DVB)
|
||||||
self._send("lnbdc odu")
|
return self._send(f"lnbdc {mode}")
|
||||||
|
|
||||||
def get_lock_status(self) -> str:
|
def get_lock_status(self) -> str:
|
||||||
"""Read quick lock status (single-shot)."""
|
"""Read quick lock status (single-shot)."""
|
||||||
|
|||||||
@ -219,6 +219,9 @@ class DemoDevice:
|
|||||||
self._az_accel = 400.0
|
self._az_accel = 400.0
|
||||||
self._el_accel = 400.0
|
self._el_accel = 400.0
|
||||||
|
|
||||||
|
# LNB state (boot default is 18V / H-pol / STB mode).
|
||||||
|
self._lnb_mode = "stb"
|
||||||
|
|
||||||
# Submenu tracking for console simulation.
|
# Submenu tracking for console simulation.
|
||||||
self._menu = _DemoMenu.ROOT
|
self._menu = _DemoMenu.ROOT
|
||||||
|
|
||||||
@ -429,7 +432,18 @@ class DemoDevice:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def enable_lna(self) -> None:
|
def enable_lna(self) -> None:
|
||||||
pass # No-op in demo mode.
|
self._lnb_mode = "odu"
|
||||||
|
|
||||||
|
def set_lnb_voltage(self, mode: str) -> str:
|
||||||
|
mode = mode.strip().lower()
|
||||||
|
if mode not in ("odu", "stb"):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid LNB mode {mode!r}: use 'odu' (13V) or 'stb' (18V)"
|
||||||
|
)
|
||||||
|
self._lnb_mode = mode
|
||||||
|
if mode == "odu":
|
||||||
|
return "Enabled LNB ODU"
|
||||||
|
return "Enabled LNB STB"
|
||||||
|
|
||||||
def get_lock_status(self) -> str:
|
def get_lock_status(self) -> str:
|
||||||
rssi = int(self._compute_rssi())
|
rssi = int(self._compute_rssi())
|
||||||
|
|||||||
@ -115,7 +115,7 @@ class SignalScreen(Container):
|
|||||||
yield Static(" Hz", classes="label")
|
yield Static(" Hz", classes="label")
|
||||||
yield Button("Start", id="btn-start", variant="primary")
|
yield Button("Start", id="btn-start", variant="primary")
|
||||||
yield Button("Stop", id="btn-stop")
|
yield Button("Stop", id="btn-stop")
|
||||||
yield Button("Enable LNA", id="btn-lna")
|
yield Button("V-pol 13V", id="btn-lna")
|
||||||
yield Button("Reset Peak", id="btn-reset-peak")
|
yield Button("Reset Peak", id="btn-reset-peak")
|
||||||
|
|
||||||
# -- Sweep mode -------------------------------------------
|
# -- Sweep mode -------------------------------------------
|
||||||
@ -350,9 +350,13 @@ class SignalScreen(Container):
|
|||||||
label = "YES" if locked else "NO"
|
label = "YES" if locked else "NO"
|
||||||
self.query_one("#lock-status", Static).update(f" Lock: {label}")
|
self.query_one("#lock-status", Static).update(f" Lock: {label}")
|
||||||
|
|
||||||
def _update_lna_label(self) -> None:
|
def _update_lna_ui(self) -> None:
|
||||||
label = "ON" if self._lna_enabled else "OFF"
|
if self._lna_enabled:
|
||||||
self.query_one("#lna-status", Static).update(f" LNA: {label}")
|
self.query_one("#lna-status", Static).update(" LNA: ON (V-pol)")
|
||||||
|
self.query_one("#btn-lna", Button).label = "H-pol 18V"
|
||||||
|
else:
|
||||||
|
self.query_one("#lna-status", Static).update(" LNA: OFF (H-pol)")
|
||||||
|
self.query_one("#btn-lna", Button).label = "V-pol 13V"
|
||||||
|
|
||||||
def _update_status_strip_rssi(self, rssi_avg: int) -> None:
|
def _update_status_strip_rssi(self, rssi_avg: int) -> None:
|
||||||
"""Push RSSI to the app-level StatusStrip."""
|
"""Push RSSI to the app-level StatusStrip."""
|
||||||
@ -386,19 +390,29 @@ class SignalScreen(Container):
|
|||||||
if self._device is None:
|
if self._device is None:
|
||||||
self.app.notify("No device connected", severity="warning")
|
self.app.notify("No device connected", severity="warning")
|
||||||
return
|
return
|
||||||
self._do_enable_lna()
|
self._do_toggle_lnb()
|
||||||
|
|
||||||
@work(thread=True, exclusive=False, group="signal-cmd")
|
@work(thread=True, exclusive=False, group="signal-cmd")
|
||||||
def _do_enable_lna(self) -> None:
|
def _do_toggle_lnb(self) -> None:
|
||||||
try:
|
try:
|
||||||
self._device.enable_lna()
|
if self._lna_enabled:
|
||||||
self._lna_enabled = True
|
self._device.set_lnb_voltage("stb")
|
||||||
self.app.call_from_thread(self._update_lna_label)
|
self._lna_enabled = False
|
||||||
self.app.call_from_thread(self.app.notify, "LNA enabled (13V ODU)")
|
self.app.call_from_thread(self._update_lna_ui)
|
||||||
|
self.app.call_from_thread(
|
||||||
|
self.app.notify, "LNB set to 18V (H-pol)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._device.set_lnb_voltage("odu")
|
||||||
|
self._lna_enabled = True
|
||||||
|
self.app.call_from_thread(self._update_lna_ui)
|
||||||
|
self.app.call_from_thread(
|
||||||
|
self.app.notify, "LNB set to 13V (V-pol, LNA on)"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("LNA enable failed")
|
log.exception("LNB voltage switch failed")
|
||||||
self.app.call_from_thread(
|
self.app.call_from_thread(
|
||||||
self.app.notify, "LNA enable failed", severity="error"
|
self.app.notify, "LNB voltage switch failed", severity="error"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_reset_peak(self) -> None:
|
def _handle_reset_peak(self) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user