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]]
name = "alabaster"
@ -1096,7 +1096,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.3.6"
jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4"
rpds-py = ">=0.25.0"

View File

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

View File

@ -13,6 +13,11 @@ import json
import logging
import threading
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import paramiko
import zmq
logger = logging.getLogger(__name__)
@ -158,16 +163,21 @@ class RemoteTransmitterController:
"""
logger.info(
"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(
{
"function_name": "init_tx",
"center_frequency": center_frequency,
"sample_rate": sample_rate,
"gain": gain,
"channel": channel,
"gain_mode": gain_mode,
}
)
self._send({
"function_name": "init_tx",
"center_frequency": center_frequency,
"sample_rate": sample_rate,
"gain": gain,
"channel": channel,
"gain_mode": gain_mode,
})
def transmit_async(self, duration_s: float) -> None:
"""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
import json
import sys
import threading
import time
from types import ModuleType
from unittest.mock import MagicMock, patch
@ -199,15 +197,11 @@ class TestErrorHandling:
def test_missing_paramiko_raises_runtime_error(self):
"""If paramiko is absent, connecting gives a clear RuntimeError."""
import importlib
import ria_toolkit_oss.remote_control.remote_transmitter_controller as mod
with patch.dict("sys.modules", {"paramiko": None}):
with pytest.raises((RuntimeError, ImportError)):
mod.RemoteTransmitterController(
host="h", ssh_user="u", ssh_key_path="/k"
)
mod.RemoteTransmitterController(host="h", ssh_user="u", ssh_key_path="/k")
# ---------------------------------------------------------------------------

View File

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