Compare commits

...

9 Commits

Author SHA1 Message Date
414597e940 Merge pull request 'updated tags for new release' (#25) from clifix into main
All checks were successful
Test with tox / Test with tox (3.11) (push) Successful in 6m10s
Build Project / Build Project (3.10) (push) Successful in 15m3s
Build Sphinx Docs Set / Build Docs (push) Successful in 20m10s
Test with tox / Test with tox (3.10) (push) Successful in 20m10s
Build Project / Build Project (3.11) (push) Successful in 21m38s
Build Project / Build Project (3.12) (push) Successful in 21m40s
Test with tox / Test with tox (3.12) (push) Successful in 15m26s
Reviewed-on: #25
Reviewed-by: madrigal <madrigal@qoherent.ai>
2026-04-20 14:36:59 -04:00
ben
1fa9ab2495 Fix onnxruntime Python 3.10 incompatibility and hackrf test import failure
All checks were successful
Build Project / Build Project (3.10) (pull_request) Successful in 13m7s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 16m5s
Test with tox / Test with tox (3.10) (pull_request) Successful in 16m2s
Build Project / Build Project (3.11) (pull_request) Successful in 16m29s
Build Project / Build Project (3.12) (pull_request) Successful in 16m28s
Test with tox / Test with tox (3.11) (pull_request) Successful in 16m44s
Test with tox / Test with tox (3.12) (pull_request) Successful in 3m59s
- Restrict onnxruntime to Python >=3.11 (1.24.3+ dropped cp310 wheels)
- Fix hackrf tests to mock sys.modules instead of using patch(), which
  triggered a CDLL import of libhackrf.so.0 at module load time in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:18:04 -04:00
ben
ab4cb0ea5a Remove poetry.toml system-site-packages workaround; regenerate lock file
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 7m9s
Test with tox / Test with tox (3.10) (pull_request) Failing after 8m38s
Test with tox / Test with tox (3.11) (pull_request) Failing after 9m24s
Build Project / Build Project (3.12) (pull_request) Successful in 9m40s
Build Project / Build Project (3.11) (pull_request) Successful in 9m53s
Test with tox / Test with tox (3.12) (pull_request) Failing after 3m1s
Build Project / Build Project (3.10) (pull_request) Successful in 10m17s
system-site-packages = true caused poetry install to fail in CI due to
conflicting system packages. Lock file regenerated without that constraint.
2026-04-20 13:56:18 -04:00
ben
ae07eef885 Regenerate poetry.lock to sync with pyproject.toml paramiko addition
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 22s
Build Project / Build Project (3.10) (pull_request) Successful in 2m54s
Test with tox / Test with tox (3.10) (pull_request) Failing after 3m24s
Build Project / Build Project (3.12) (pull_request) Successful in 4m55s
Build Project / Build Project (3.11) (pull_request) Successful in 10m10s
Test with tox / Test with tox (3.11) (pull_request) Has been cancelled
Test with tox / Test with tox (3.12) (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 13:33:00 -04:00
e506d26450 Replaces the getting_started.rst placeholder with a full CLI reference
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 19s
Test with tox / Test with tox (3.11) (pull_request) Failing after 17s
Test with tox / Test with tox (3.12) (pull_request) Failing after 16s
Test with tox / Test with tox (3.10) (pull_request) Failing after 33s
Build Project / Build Project (3.10) (pull_request) Successful in 1m5s
Build Project / Build Project (3.12) (pull_request) Successful in 1m4s
Build Project / Build Project (3.11) (pull_request) Successful in 1m6s
covering installation, commands, YAML config patterns, and a cheat sheet.
Adds custom CSS/JS for heading colours, warning admonition styling, code
block colours, and ria command highlighting. Fixes .gitignore to exclude
docs/_build/.
2026-04-20 13:10:31 -04:00
138fdeb68b Added getting started guide 2026-04-20 13:06:54 -04:00
0642dcc2db Added paramiko to dependencies
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 8m50s
Test with tox / Test with tox (3.11) (pull_request) Failing after 18s
Test with tox / Test with tox (3.10) (pull_request) Failing after 8m16s
Test with tox / Test with tox (3.12) (pull_request) Failing after 16s
Build Project / Build Project (3.10) (pull_request) Successful in 9m23s
Build Project / Build Project (3.11) (pull_request) Successful in 8m52s
Build Project / Build Project (3.12) (pull_request) Successful in 8m53s
2026-04-20 11:51:57 -04:00
84a7893c8f Updated poetry.lock, linting
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 17s
Test with tox / Test with tox (3.12) (pull_request) Failing after 2m24s
Test with tox / Test with tox (3.11) (pull_request) Failing after 2m12s
Build Project / Build Project (3.12) (pull_request) Successful in 8m59s
Build Project / Build Project (3.10) (pull_request) Successful in 9m17s
Build Project / Build Project (3.11) (pull_request) Successful in 9m16s
Test with tox / Test with tox (3.10) (pull_request) Failing after 16m0s
2026-04-20 11:43:03 -04:00
8e542919a8 updated tags for new release
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 18s
Test with tox / Test with tox (3.10) (pull_request) Failing after 13m9s
Build Project / Build Project (3.11) (pull_request) Successful in 13m21s
Build Project / Build Project (3.12) (pull_request) Successful in 13m19s
Build Project / Build Project (3.10) (pull_request) Successful in 13m26s
Test with tox / Test with tox (3.11) (pull_request) Failing after 13m40s
Test with tox / Test with tox (3.12) (pull_request) Failing after 13m33s
2026-04-20 11:21:36 -04:00
14 changed files with 2826 additions and 408 deletions

1
.gitignore vendored
View File

@ -52,6 +52,7 @@ tests/sdr/
# Sphinx documentation # Sphinx documentation
docs/build/ docs/build/
docs/_build/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
/* Change the hex values below to customize heading colours */
.rst-content h1 { color: #2c3e50; }
.rst-content h2,
.rst-content h2 a { color: #ffffff !important; font-size: 22px !important; }
.rst-content h3,
.rst-content h3 a { color: #ffffff !important; font-size: 16px !important; }
.rst-content h3 code { font-size: inherit !important; }
.rst-content .admonition.warning {
background: #1a1a2e !important;
border-left: 4px solid #c0392b !important;
}
.rst-content .admonition.warning .admonition-title {
background: #c0392b !important;
color: #ffffff !important;
}
.rst-content .admonition.warning p {
color: #ffffff !important;
}
.rst-content h4 { color: #404040; }
.highlight * { color: #ffffff !important; }
.ria-cmd { color: #2980b9 !important; }

View File

@ -0,0 +1,8 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.highlight pre').forEach(function (pre) {
pre.innerHTML = pre.innerHTML.replace(
/((?:^|\n|>))(ria)(?=[ \t]|<)/g,
'$1<span class="ria-cmd">$2</span>'
);
});
});

View File

@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
project = 'ria-toolkit-oss' project = 'ria-toolkit-oss'
copyright = '2025, Qoherent Inc' copyright = '2025, Qoherent Inc'
author = 'Qoherent Inc.' author = 'Qoherent Inc.'
release = '0.1.4' release = '0.1.5'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@ -73,3 +73,6 @@ def setup(app):
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme' html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_css_files = ['custom.css']
html_js_files = ['custom.js']

File diff suppressed because it is too large Load Diff

901
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
[virtualenvs.options]
system-site-packages = true

View File

@ -1,6 +1,6 @@
[project] [project]
name = "ria-toolkit-oss" name = "ria-toolkit-oss"
version = "0.1.4" version = "0.1.5"
description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications" description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications"
license = { text = "AGPL-3.0-only" } license = { text = "AGPL-3.0-only" }
readme = "README.md" readme = "README.md"
@ -49,7 +49,8 @@ dependencies = [
"pyzmq (>=27.1.0,<28.0.0)", "pyzmq (>=27.1.0,<28.0.0)",
"pyyaml (>=6.0.3,<7.0.0)", "pyyaml (>=6.0.3,<7.0.0)",
"click (>=8.1.0,<9.0.0)", "click (>=8.1.0,<9.0.0)",
"matplotlib (>=3.8.0,<4.0.0)" "matplotlib (>=3.8.0,<4.0.0)",
"paramiko (>=4.0.0)"
] ]
# [project.optional-dependencies] Commented out to prevent Tox tests from failing # [project.optional-dependencies] Commented out to prevent Tox tests from failing
@ -87,7 +88,7 @@ pytest = "^8.0.0"
tox = "^4.19.0" tox = "^4.19.0"
fastapi = ">=0.111,<1.0" fastapi = ">=0.111,<1.0"
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]} uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
onnxruntime = ">=1.17,<2.0" onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"}
httpx = ">=0.27,<1.0" httpx = ">=0.27,<1.0"
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
@ -121,7 +122,7 @@ ria-agent = "ria_toolkit_oss.agent:main"
[tool.poetry.group.server.dependencies] [tool.poetry.group.server.dependencies]
fastapi = ">=0.111,<1.0" fastapi = ">=0.111,<1.0"
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]} uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
onnxruntime = ">=1.17,<2.0" onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"}
[tool.black] [tool.black]
line-length = 119 line-length = 119

View File

@ -40,15 +40,19 @@ class RemoteTransmitter:
try: try:
if radio_str in ("pluto", "plutosdr"): if radio_str in ("pluto", "plutosdr"):
from ria_toolkit_oss.sdr.pluto import Pluto from ria_toolkit_oss.sdr.pluto import Pluto
self._sdr = Pluto(identifier) self._sdr = Pluto(identifier)
elif radio_str in ("usrp",): elif radio_str in ("usrp",):
from ria_toolkit_oss.sdr.usrp import USRP from ria_toolkit_oss.sdr.usrp import USRP
self._sdr = USRP(identifier) self._sdr = USRP(identifier)
elif radio_str in ("hackrf", "hackrf_one"): elif radio_str in ("hackrf", "hackrf_one"):
from ria_toolkit_oss.sdr.hackrf import HackRF from ria_toolkit_oss.sdr.hackrf import HackRF
self._sdr = HackRF(identifier) self._sdr = HackRF(identifier)
elif radio_str in ("bladerf", "blade"): elif radio_str in ("bladerf", "blade"):
from ria_toolkit_oss.sdr.blade import Blade from ria_toolkit_oss.sdr.blade import Blade
self._sdr = Blade(identifier) self._sdr = Blade(identifier)
else: else:
raise ValueError(f"Unknown SDR type: {radio_str!r}") raise ValueError(f"Unknown SDR type: {radio_str!r}")
@ -77,6 +81,7 @@ class RemoteTransmitter:
if self._sdr is None: if self._sdr is None:
raise RuntimeError("Call set_radio() and init_tx() before transmit()") raise RuntimeError("Call set_radio() and init_tx() before transmit()")
import time import time
# Transmit in a loop until duration has elapsed # Transmit in a loop until duration has elapsed
end = time.monotonic() + duration_s end = time.monotonic() + duration_s
while time.monotonic() < end: while time.monotonic() < end:

View File

@ -14,6 +14,9 @@ import logging
import threading import threading
import time import time
import paramiko
import zmq
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_STARTUP_WAIT_S = 2.0 # seconds to wait for remote ZMQ server to bind _STARTUP_WAIT_S = 2.0 # seconds to wait for remote ZMQ server to bind
@ -158,16 +161,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

@ -12,7 +12,6 @@ import pytest
from ria_toolkit_oss.remote_control.remote_transmitter import RemoteTransmitter from ria_toolkit_oss.remote_control.remote_transmitter import RemoteTransmitter
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -63,14 +62,18 @@ class TestSetRadio:
def test_hackrf_alias(self): def test_hackrf_alias(self):
tx = RemoteTransmitter() tx = RemoteTransmitter()
mock_sdr = _make_mock_sdr() mock_sdr = _make_mock_sdr()
with patch("ria_toolkit_oss.sdr.hackrf.HackRF", return_value=mock_sdr): mock_module = MagicMock()
mock_module.HackRF = MagicMock(return_value=mock_sdr)
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.hackrf": mock_module}):
tx.set_radio("hackrf", "") tx.set_radio("hackrf", "")
assert tx._sdr is mock_sdr assert tx._sdr is mock_sdr
def test_hackrf_one_alias(self): def test_hackrf_one_alias(self):
tx = RemoteTransmitter() tx = RemoteTransmitter()
mock_sdr = _make_mock_sdr() mock_sdr = _make_mock_sdr()
with patch("ria_toolkit_oss.sdr.hackrf.HackRF", return_value=mock_sdr): mock_module = MagicMock()
mock_module.HackRF = MagicMock(return_value=mock_sdr)
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.hackrf": mock_module}):
tx.set_radio("hackrf_one", "") tx.set_radio("hackrf_one", "")
assert tx._sdr is mock_sdr assert tx._sdr is mock_sdr
@ -241,34 +244,40 @@ class TestRunFunction:
def test_init_tx_without_radio_returns_failure(self): def test_init_tx_without_radio_returns_failure(self):
tx = RemoteTransmitter() tx = RemoteTransmitter()
resp = tx.run_function({ resp = tx.run_function(
{
"function_name": "init_tx", "function_name": "init_tx",
"center_frequency": 2.4e9, "center_frequency": 2.4e9,
"sample_rate": 20e6, "sample_rate": 20e6,
"gain": 0, "gain": 0,
}) }
)
assert resp["status"] is False assert resp["status"] is False
assert resp["error_message"] assert resp["error_message"]
def test_init_tx_with_radio_success(self): def test_init_tx_with_radio_success(self):
tx = self._tx_with_mock_sdr() tx = self._tx_with_mock_sdr()
resp = tx.run_function({ resp = tx.run_function(
{
"function_name": "init_tx", "function_name": "init_tx",
"center_frequency": 2.4e9, "center_frequency": 2.4e9,
"sample_rate": 20e6, "sample_rate": 20e6,
"gain": 30, "gain": 30,
}) }
)
assert resp["status"] is True assert resp["status"] is True
def test_transmit_runs_for_short_duration(self): def test_transmit_runs_for_short_duration(self):
tx = self._tx_with_mock_sdr() tx = self._tx_with_mock_sdr()
tx._sdr.init_tx = MagicMock() tx._sdr.init_tx = MagicMock()
resp = tx.run_function({ resp = tx.run_function(
{
"function_name": "init_tx", "function_name": "init_tx",
"center_frequency": 2.4e9, "center_frequency": 2.4e9,
"sample_rate": 20e6, "sample_rate": 20e6,
"gain": 0, "gain": 0,
}) }
)
resp = tx.run_function({"function_name": "transmit", "duration_s": 0.02}) resp = tx.run_function({"function_name": "transmit", "duration_s": 0.02})
assert resp["status"] is True assert resp["status"] is True

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()