qac-cli-commands #26

Merged
madrigal merged 15 commits from qac-cli-commands into main 2026-04-21 09:03:29 -04:00
5 changed files with 41 additions and 46 deletions
Showing only changes of commit 8e23558d90 - Show all commits

4
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]] [[package]]
name = "alabaster" name = "alabaster"
@ -1096,7 +1096,7 @@ files = [
[package.dependencies] [package.dependencies]
attrs = ">=22.2.0" attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.3.6" jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4" referencing = ">=0.28.4"
rpds-py = ">=0.25.0" rpds-py = ">=0.25.0"

View File

@ -270,9 +270,7 @@ class Streamer:
) )
self._rx = session self._rx = session
await self._send_status("streaming", app_id) await self._send_status("streaming", app_id)
session.task = asyncio.create_task( session.task = asyncio.create_task(self._capture_loop(session), name="ria-streamer-capture")
self._capture_loop(session), name="ria-streamer-capture"
)
async def _handle_rx_stop(self, msg: dict) -> None: async def _handle_rx_stop(self, msg: dict) -> None:
session = self._rx session = self._rx
@ -310,9 +308,7 @@ class Streamer:
logger.warning("Applying configure failed: %s", exc) logger.warning("Applying configure failed: %s", exc)
try: try:
samples = await loop.run_in_executor( samples = await loop.run_in_executor(None, session.sdr.rx, session.buffer_size)
None, session.sdr.rx, session.buffer_size
)
except Exception as exc: except Exception as exc:
from ria_toolkit_oss.sdr import SdrDisconnectedError from ria_toolkit_oss.sdr import SdrDisconnectedError
@ -342,7 +338,7 @@ class Streamer:
# ================================================================== # ==================================================================
# TX # TX
async def _handle_tx_start(self, msg: dict) -> None: async def _handle_tx_start(self, msg: dict) -> None: # noqa: C901
app_id = msg.get("app_id") or "" app_id = msg.get("app_id") or ""
radio_config = dict(msg.get("radio_config") or {}) radio_config = dict(msg.get("radio_config") or {})
@ -383,9 +379,7 @@ class Streamer:
buffer_size = int(radio_config.pop("buffer_size", _DEFAULT_BUFFER_SIZE)) buffer_size = int(radio_config.pop("buffer_size", _DEFAULT_BUFFER_SIZE))
underrun_policy = str(radio_config.pop("underrun_policy", "pause")) underrun_policy = str(radio_config.pop("underrun_policy", "pause"))
if underrun_policy not in ("pause", "zero", "repeat"): if underrun_policy not in ("pause", "zero", "repeat"):
await self._send_tx_status( await self._send_tx_status(app_id, "error", f"invalid underrun_policy {underrun_policy!r}")
app_id, "error", f"invalid underrun_policy {underrun_policy!r}"
)
return return
if not device: if not device:
await self._send_tx_status(app_id, "error", "tx_start missing radio_config.device") await self._send_tx_status(app_id, "error", "tx_start missing radio_config.device")
@ -404,15 +398,10 @@ class Streamer:
# manifest bug and we want it surfaced immediately, not papered # manifest bug and we want it surfaced immediately, not papered
# over with stale radio state. # over with stale radio state.
if hasattr(sdr, "init_tx"): if hasattr(sdr, "init_tx"):
init_args = { init_args = {k: radio_config.get(f"tx_{k}") for k in ("sample_rate", "center_frequency", "gain")}
k: radio_config.get(f"tx_{k}")
for k in ("sample_rate", "center_frequency", "gain")
}
missing = [f"tx_{k}" for k, v in init_args.items() if v is None] missing = [f"tx_{k}" for k, v in init_args.items() if v is None]
if missing: if missing:
raise ValueError( raise ValueError(f"tx_start missing required radio_config keys: {missing}")
f"tx_start missing required radio_config keys: {missing}"
)
sdr.init_tx( sdr.init_tx(
sample_rate=init_args["sample_rate"], sample_rate=init_args["sample_rate"],
center_frequency=init_args["center_frequency"], center_frequency=init_args["center_frequency"],
@ -498,9 +487,8 @@ class Streamer:
return _silence(n) return _silence(n)
# Max-duration watchdog. # Max-duration watchdog.
if ( if session.max_duration_s is not None and (time.monotonic() - session.started_at) >= float(
session.max_duration_s is not None session.max_duration_s
and (time.monotonic() - session.started_at) >= float(session.max_duration_s)
): ):
session.stop_event.set() session.stop_event.set()
try: try:
@ -528,7 +516,7 @@ class Streamer:
if arr.size < 2 or arr.size % 2 != 0: if arr.size < 2 or arr.size % 2 != 0:
logger.warning("Malformed TX frame: %d floats (must be non-zero even count)", arr.size) logger.warning("Malformed TX frame: %d floats (must be non-zero even count)", arr.size)
return self._underrun_fill(session, n) return self._underrun_fill(session, n)
samples = (arr[0::2].astype(np.complex64) + 1j * arr[1::2].astype(np.complex64)) samples = arr[0::2].astype(np.complex64) + 1j * arr[1::2].astype(np.complex64)
if samples.size < n: if samples.size < n:
out = np.zeros(n, dtype=np.complex64) out = np.zeros(n, dtype=np.complex64)
out[: samples.size] = samples out[: samples.size] = samples
@ -747,6 +735,7 @@ def _default_sdr_factory(device: str, identifier: str | None):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Top-level entry # Top-level entry
async def run_streamer(ws_url: str, token: str, *, cfg: AgentConfig | None = None) -> None: async def run_streamer(ws_url: str, token: str, *, cfg: AgentConfig | None = None) -> None:
"""Connect to *ws_url* and run the streamer loop until cancelled.""" """Connect to *ws_url* and run the streamer loop until cancelled."""
ws = WsClient(ws_url, token) ws = WsClient(ws_url, token)

View File

@ -13,6 +13,11 @@ import json
import logging import logging
import threading import threading
import time import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import paramiko
import zmq
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -158,16 +163,21 @@ class RemoteTransmitterController:
""" """
logger.info( logger.info(
"init_tx: fc=%.3f MHz, fs=%.3f MHz, gain=%.1f dB, ch=%d", "init_tx: fc=%.3f MHz, fs=%.3f MHz, gain=%.1f dB, ch=%d",
center_frequency / 1e6, sample_rate / 1e6, gain, channel, center_frequency / 1e6,
sample_rate / 1e6,
gain,
channel,
) )
self._send({ self._send(
{
"function_name": "init_tx", "function_name": "init_tx",
"center_frequency": center_frequency, "center_frequency": center_frequency,
"sample_rate": sample_rate, "sample_rate": sample_rate,
"gain": gain, "gain": gain,
"channel": channel, "channel": channel,
"gain_mode": gain_mode, "gain_mode": gain_mode,
}) }
)
def transmit_async(self, duration_s: float) -> None: def transmit_async(self, duration_s: float) -> None:
"""Start a timed CW transmission in a background thread. """Start a timed CW transmission in a background thread.

View File

@ -7,8 +7,6 @@ sys.modules so they run regardless of whether the packages are installed.
from __future__ import annotations from __future__ import annotations
import json import json
import sys
import threading
import time import time
from types import ModuleType from types import ModuleType
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -199,15 +197,11 @@ class TestErrorHandling:
def test_missing_paramiko_raises_runtime_error(self): def test_missing_paramiko_raises_runtime_error(self):
"""If paramiko is absent, connecting gives a clear RuntimeError.""" """If paramiko is absent, connecting gives a clear RuntimeError."""
import importlib
import ria_toolkit_oss.remote_control.remote_transmitter_controller as mod import ria_toolkit_oss.remote_control.remote_transmitter_controller as mod
with patch.dict("sys.modules", {"paramiko": None}): with patch.dict("sys.modules", {"paramiko": None}):
with pytest.raises((RuntimeError, ImportError)): with pytest.raises((RuntimeError, ImportError)):
mod.RemoteTransmitterController( mod.RemoteTransmitterController(host="h", ssh_user="u", ssh_key_path="/k")
host="h", ssh_user="u", ssh_key_path="/k"
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, patch
import pytest import pytest
@ -12,7 +12,6 @@ from ria_toolkit_oss.orchestration.campaign import (
TransmitterConfig, TransmitterConfig,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -179,9 +178,7 @@ class TestInitRemoteTxControllers:
} }
] ]
executor = _make_executor(d) executor = _make_executor(d)
with patch( with patch("ria_toolkit_oss.remote_control.RemoteTransmitterController") as mock_cls:
"ria_toolkit_oss.remote_control.RemoteTransmitterController"
) as mock_cls:
executor._init_remote_tx_controllers() executor._init_remote_tx_controllers()
mock_cls.assert_not_called() mock_cls.assert_not_called()
assert executor._remote_tx_controllers == {} assert executor._remote_tx_controllers == {}
@ -264,7 +261,7 @@ class TestStartTransmitterSdrRemote:
tx = executor.config.transmitters[0] tx = executor.config.transmitters[0]
step = CaptureStep(duration=5.0, label="nochan") step = CaptureStep(duration=5.0, label="nochan")
executor._start_transmitter(tx, step) executor._start_transmitter(tx, step)
_, kwargs = mock_ctrl_kwarg = ctrl.init_tx.call_args _, kwargs = ctrl.init_tx.call_args
assert kwargs["channel"] == 0 assert kwargs["channel"] == 0
def test_missing_controller_raises(self): def test_missing_controller_raises(self):
@ -381,7 +378,11 @@ class TestRunWithSdrRemote:
), ),
patch.object(executor, "_close_sdr"), patch.object(executor, "_close_sdr"),
patch.object(executor, "_close_remote_tx_controllers"), patch.object(executor, "_close_remote_tx_controllers"),
patch.object(executor, "_execute_step", return_value=MagicMock(error=None, qa=MagicMock(flagged=False, snr_db=20.0, duration_s=10.0))), patch.object(
executor,
"_execute_step",
return_value=MagicMock(error=None, qa=MagicMock(flagged=False, snr_db=20.0, duration_s=10.0)),
),
): ):
executor.run() executor.run()
@ -401,6 +402,7 @@ class TestTransmitBufferAndTimeout:
def _executor_with_ctrl(self): def _executor_with_ctrl(self):
from ria_toolkit_oss.orchestration.executor import CampaignExecutor from ria_toolkit_oss.orchestration.executor import CampaignExecutor
cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT) cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT)
executor = CampaignExecutor(cfg) executor = CampaignExecutor(cfg)
ctrl = MagicMock() ctrl = MagicMock()