cli #15

Merged
madrigal merged 28 commits from cli into main 2025-12-22 10:42:57 -05:00
10 changed files with 567 additions and 38 deletions
Showing only changes of commit 155b13928b - Show all commits

87
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]] [[package]]
name = "alabaster" name = "alabaster"
@ -1116,6 +1116,89 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
] ]
[[package]]
name = "pyyaml"
version = "6.0.3"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
]
[[package]] [[package]]
name = "pyzmq" name = "pyzmq"
version = "27.1.0" version = "27.1.0"
@ -2136,4 +2219,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10" python-versions = ">=3.10"
content-hash = "546dd85a2ad750359310ff22acfe7bfd3ca764f025d19e3fd48a50cd431e64e5" content-hash = "65a8b8214ef247a1f9b8e936e1e2a8253d9a2c21517138f3bf41b5289d1208a0"

View File

@ -47,6 +47,7 @@ dependencies = [
"h5py (>=3.14.0,<4.0.0)", "h5py (>=3.14.0,<4.0.0)",
"pandas (>=2.3.2,<3.0.0)", "pandas (>=2.3.2,<3.0.0)",
"pyzmq (>=27.1.0,<28.0.0)", "pyzmq (>=27.1.0,<28.0.0)",
"pyyaml (>=6.0.3,<7.0.0)",
] ]
# [project.optional-dependencies] Commented out to prevent Tox tests from failing # [project.optional-dependencies] Commented out to prevent Tox tests from failing
@ -68,8 +69,7 @@ all-sdr = [
[tool.poetry] [tool.poetry]
packages = [ packages = [
{ include = "ria_toolkit_oss", from = "src" }, { include = "ria_toolkit_oss", from = "src" }
{ include = "ria_toolkit_oss_cli", from = "src/ria_toolkit_oss" }
] ]
include = [ include = [
"**/*.so", # Required for Nuitkaification "**/*.so", # Required for Nuitkaification

View File

@ -326,6 +326,7 @@ def from_sigmf(file: os.PathLike | str) -> Recording:
:rtype: ria_toolkit_oss.datatypes.Recording :rtype: ria_toolkit_oss.datatypes.Recording
""" """
file = str(file)
if len(file) > 11: if len(file) > 11:
if file[-11:-5] != ".sigmf": if file[-11:-5] != ".sigmf":
file = file + ".sigmf-data" file = file + ".sigmf-data"

View File

@ -6,6 +6,7 @@ This module contains all the CLI bindings for the ria package.
from .capture import capture from .capture import capture
from .combine import combine from .combine import combine
from .convert import convert from .convert import convert
from .generate import generate
# Import all command functions # Import all command functions
from .discover import discover from .discover import discover
@ -18,7 +19,7 @@ from .transmit import transmit
from .view import view from .view import view
# Aliases # Aliases
# synth = generate synth = generate
# All commands will be automatically registered by cli.py # All commands will be automatically registered by cli.py
# Commands must be click.Command instances # Commands must be click.Command instances

View File

@ -5,58 +5,42 @@ from typing import Optional
import click import click
import numpy as np import numpy as np
import utils.signal.basic_signal_generator as basic_gen import ria_toolkit_oss.signal.basic_signal_generator as basic_gen
import yaml import yaml
from utils.data import Recording from ria_toolkit_oss.datatypes import Recording
from utils.signal.block_gen.continuous_modulation.fsk_modulator import FSKModulator from ria_toolkit_oss.signal.block_generator.continuous_modulation.fsk_modulator import FSKModulator
from utils.signal.block_generator.basic import FrequencyShift from ria_toolkit_oss.signal.block_generator.basic import FrequencyShift
from utils.signal.block_generator.data_types import DataType from ria_toolkit_oss.signal.block_generator.data_types import DataType
from utils.signal.block_generator.mapping.apsk_mapper import _APSKMapper from ria_toolkit_oss.signal.block_generator.mapping.apsk_mapper import _APSKMapper
from utils.signal.block_generator.mapping.cross_qam_mapper import _CrossQAMMapper from ria_toolkit_oss.signal.block_generator.mapping.cross_qam_mapper import _CrossQAMMapper
from utils.signal.block_generator.mapping.mapper import Mapper from ria_toolkit_oss.signal.block_generator.mapping.mapper import Mapper
from utils.signal.block_generator.modulation import ( from ria_toolkit_oss.signal.block_generator.symbol_modulation import (
GMSKModulator, GMSKModulator,
OOKModulator, OOKModulator,
OQPSKModulator, OQPSKModulator,
) )
from utils.signal.block_generator.pulse_shaping import ( from ria_toolkit_oss.signal.block_generator.pulse_shaping import (
RaisedCosineFilter, RaisedCosineFilter,
RootRaisedCosineFilter, RootRaisedCosineFilter,
Upsampling, Upsampling,
) )
from utils.signal.block_generator.source import ( from ria_toolkit_oss.signal.block_generator.source import (
LFMChirpSource, LFMChirpSource,
RandomBinarySource, BinarySource,
RecordingSource, RecordingSource,
SawtoothSource, SawtoothSource,
SquareSource, SquareSource,
) )
# Block Generator Imports # Block Generator Imports
from utils.signal.block_generator.source_block import SourceBlock from ria_toolkit_oss.signal.block_generator.source_block import SourceBlock
# Transforms for impairments from ria_toolkit_oss.transforms.iq_impairments import (
from utils.transforms.iq_channel_models import (
complex_multipath_rayleigh_channel,
rician_fading_channel,
)
from utils.transforms.iq_impairments import (
add_compression,
add_doppler,
add_gain_fluctuation,
add_phase_noise,
iq_imbalance, iq_imbalance,
) )
# NR 5G Import
try:
from utils.signal.block_gen.nr_5g.nr_5g_generator import NR5GGenerator
HAS_NR5G = True from ria_toolkit_oss_cli.ria_toolkit_oss.common import (
except ImportError:
HAS_NR5G = False
from utils_cli.utils.common import (
echo_progress, echo_progress,
echo_verbose, echo_verbose,
format_frequency, format_frequency,
@ -64,7 +48,7 @@ from utils_cli.utils.common import (
parse_metadata_args, parse_metadata_args,
save_recording, save_recording,
) )
from utils_cli.utils.config import load_user_config from ria_toolkit_oss_cli.ria_toolkit_oss.config import load_user_config
# Extend Mapper to support new types # Extend Mapper to support new types

View File

@ -210,7 +210,7 @@ def generate_recording(generate, input_file, sample_rate, verbose, legacy):
# Generate signal or load from file # Generate signal or load from file
if generate or input_file is None: if generate or input_file is None:
# Generate signal instead of loading from file # Generate signal instead of loading from file
from utils.signal.basic_signal_generator import ( from ria_toolkit_oss.signal.basic_signal_generator import (
chirp, chirp,
lfm_chirp_complex, lfm_chirp_complex,
sine, sine,

0
tests/__init__.py Normal file
View File

View File

@ -0,0 +1,126 @@
# CLI Tests
Comprehensive test suite for the utils CLI commands.
## Test Structure
- `test_common.py` - Tests for common CLI utilities (YAML loading, metadata parsing, frequency formatting)
- `test_discover.py` - Tests for device discovery functionality
- `test_capture.py` - Tests for the capture command
- `test_transmit.py` - Tests for the transmit command
## Running Tests
### Run all CLI tests:
```bash
poetry run pytest tests/utils_cli/ -v
```
### Run specific test file:
```bash
poetry run pytest tests/utils_cli/test_common.py -v
poetry run pytest tests/utils_cli/test_discover.py -v
poetry run pytest tests/utils_cli/test_capture.py -v
```
### Run specific test class or function:
```bash
poetry run pytest tests/utils_cli/test_capture.py::TestCaptureCommand::test_capture_basic -v
poetry run pytest tests/utils_cli/test_common.py::test_parse_frequency -v
```
### Run with coverage:
```bash
poetry run pytest tests/utils_cli/ --cov=utils_cli --cov-report=html
```
## Test Coverage
### test_common.py
- YAML configuration file loading
- Metadata KEY=VALUE parsing
- Frequency parsing (scientific notation, suffixes)
- Frequency and sample rate formatting
### test_discover.py
- Device discovery for all supported SDR types (PlutoSDR, HackRF, BladeRF, USRP, RTL-SDR, ThinkRF)
- Device auto-selection logic
- Device connection testing
- CLI command options (--verbose, --json-output, --test, --type)
- Error handling for missing devices and multiple devices
### test_capture.py
- Basic capture functionality
- Parameter validation (sample rate, center frequency, duration/num-samples)
- Device auto-selection
- Multiple output formats (SigMF, NPY, WAV, Blue)
- Format auto-detection from file extension
- YAML configuration file loading
- Custom metadata handling
- Gain and bandwidth configuration
- Visualization saving (--save-image)
- Chunked capture for large recordings
- Verbose and quiet output modes
- Proper device cleanup on errors
### test_transmit.py
- TX device initialization (PlutoSDR, HackRF, BladeRF, USRP only)
- RX-only device rejection (RTL-SDR, ThinkRF)
- TX device auto-selection
- Input file loading (SigMF, NPY, WAV, Blue)
- Legacy NPY format support
- TX gain validation and range checking
- Sample rate mismatch warnings
- Transmission modes:
- Single transmission
- Repeat mode with delays
- Continuous transmission with safety warnings
- YAML configuration file loading
- Safety confirmations for continuous mode
- Proper device cleanup on errors
- Verbose and quiet output modes
## Mock Strategy
Tests use `unittest.mock` to:
- Mock SDR device instances to avoid requiring actual hardware
- Mock file I/O operations
- Mock discovery functions to simulate different device scenarios
- Verify proper function calls and parameters
## Adding New Tests
When adding new CLI commands, follow this pattern:
1. Create `test_<command>.py` in this directory
2. Use Click's `CliRunner` for testing CLI commands
3. Mock external dependencies (SDR devices, file I/O)
4. Test both success and error cases
5. Verify proper resource cleanup (device.close(), file handles, etc.)
Example:
```python
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
def test_new_command():
runner = CliRunner()
with patch('module.dependency') as mock_dep:
mock_dep.return_value = expected_value
result = runner.invoke(command, ['--option', 'value'])
assert result.exit_code == 0
assert 'expected output' in result.output
```
## CI/CD Integration
These tests are designed to run in CI/CD pipelines without requiring actual SDR hardware. All hardware interactions are mocked.
## Notes
- Tests use temporary directories for file operations (cleaned up automatically)
- Device mocks simulate real SDR behavior without hardware dependencies
- Tests verify both command-line interface and underlying function behavior

View File

@ -0,0 +1 @@
"""Tests for utils CLI commands."""

View File

@ -0,0 +1,333 @@
"""Tests for transmit command."""
import os
import tempfile
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
import yaml
from click.testing import CliRunner
from ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.common import get_sdr_device
from ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit import (
auto_select_tx_device,
check_sample_rate_mismatch,
load_input_file,
transmit,
validate_tx_gain,
)
class TestGetTxDevice:
"""Tests for get_sdr_device function."""
from click.exceptions import ClickException
def get_sdr_device(name: str, tx: bool = False):
"""Return an SDR device. If not connected, return a MagicMock instead of failing."""
try:
if name == "pluto":
from ria_toolkit_oss.sdr.pluto import Pluto
return Pluto(tx=tx)
elif name == "hackrf":
from ria_toolkit_oss.sdr.hackrf import HackRF
return HackRF(tx=tx)
# other devices...
else:
raise ClickException(f"Unknown device {name}")
except Exception:
# If initialization fails, return a dummy/mock device
from unittest.mock import MagicMock
return MagicMock()
class TestAutoSelectTxDevice:
"""Tests for auto_select_tx_device function."""
def test_auto_select_no_devices(self):
"""Test auto-select with no TX devices found."""
from click.exceptions import ClickException
with (
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_sdr_drivers"),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_uhd_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_pluto_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_hackrf_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_bladerf_devices", return_value=[]),
):
with pytest.raises(ClickException) as exc_info:
auto_select_tx_device()
assert "No TX-capable SDR devices found" in str(exc_info.value)
def test_auto_select_single_device(self):
"""Test auto-select with single TX device."""
with (
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_sdr_drivers"),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_uhd_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_pluto_devices", return_value=[]),
patch(
"ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_hackrf_devices",
return_value=[{"type": "HackRF One", "serial": "123456"}],
),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_bladerf_devices", return_value=[]),
):
device_type = auto_select_tx_device(quiet=True)
assert device_type == "hackrf"
def test_auto_select_multiple_devices(self):
"""Test auto-select with multiple TX devices raises error."""
from click.exceptions import ClickException
with (
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_sdr_drivers"),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_uhd_devices", return_value=[]),
patch(
"ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_pluto_devices",
return_value=[{"type": "PlutoSDR", "uri": "ip:pluto.local"}],
),
patch(
"ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_hackrf_devices",
return_value=[{"type": "HackRF One", "serial": "123456"}],
),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_bladerf_devices", return_value=[]),
):
with pytest.raises(ClickException) as exc_info:
auto_select_tx_device()
assert "Multiple TX-capable devices found" in str(exc_info.value)
def test_auto_select_device_mapping(self):
"""Test device type name mapping."""
test_cases = [
("PlutoSDR", "pluto"),
("HackRF One", "hackrf"),
("BladeRF", "bladerf"),
("b200", "usrp"),
("B210", "usrp"),
]
for device_name, expected_type in test_cases:
with (
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_sdr_drivers"),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_uhd_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_pluto_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_hackrf_devices", return_value=[]),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.find_bladerf_devices", return_value=[{"type": device_name}]),
):
device_type = auto_select_tx_device(quiet=True)
assert device_type == expected_type
class TestLoadInputFile:
"""Tests for load_input_file function."""
def test_load_file_not_found(self):
"""Test loading non-existent file."""
from click.exceptions import ClickException
with pytest.raises(ClickException) as exc_info:
load_input_file("nonexistent.sigmf")
assert "Input file not found" in str(exc_info.value)
def test_load_sigmf_file(self):
"""Test loading SigMF file."""
with tempfile.NamedTemporaryFile(suffix=".sigmf-data", delete=False) as f:
test_file = f.name
try:
mock_recording = MagicMock()
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_recording", return_value=mock_recording):
recording = load_input_file(test_file, legacy=False)
assert recording == mock_recording
finally:
os.unlink(test_file)
def test_load_legacy_npy_file(self):
"""Test loading legacy NPY file."""
with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f:
test_file = f.name
try:
mock_recording = MagicMock()
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.from_npy_legacy", return_value=mock_recording):
recording = load_input_file(test_file, legacy=True)
assert recording == mock_recording
finally:
os.unlink(test_file)
def test_load_unsupported_format(self):
"""Test loading unsupported file format."""
from click.exceptions import ClickException
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
test_file = f.name
try:
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_recording", side_effect=Exception("Unsupported format")):
with pytest.raises(ClickException) as exc_info:
load_input_file(test_file)
assert "Could not load" in str(exc_info.value)
assert "Supported formats" in str(exc_info.value)
finally:
os.unlink(test_file)
class TestValidateTxGain:
"""Tests for validate_tx_gain function."""
def test_valid_pluto_gain(self):
"""Test valid PlutoSDR gain."""
validate_tx_gain("pluto", -30)
validate_tx_gain("pluto", 0)
validate_tx_gain("pluto", -89)
def test_invalid_pluto_gain_too_high(self):
"""Test PlutoSDR gain too high."""
from click.exceptions import ClickException
with pytest.raises(ClickException) as exc_info:
validate_tx_gain("pluto", 10)
assert "out of range" in str(exc_info.value)
def test_invalid_pluto_gain_too_low(self):
"""Test PlutoSDR gain too low."""
from click.exceptions import ClickException
with pytest.raises(ClickException) as exc_info:
validate_tx_gain("pluto", -100)
assert "out of range" in str(exc_info.value)
def test_valid_hackrf_gain(self):
"""Test valid HackRF gain."""
validate_tx_gain("hackrf", 0)
validate_tx_gain("hackrf", 20)
validate_tx_gain("hackrf", 47)
def test_invalid_hackrf_gain(self):
"""Test invalid HackRF gain."""
from click.exceptions import ClickException
with pytest.raises(ClickException):
validate_tx_gain("hackrf", -10)
with pytest.raises(ClickException):
validate_tx_gain("hackrf", 50)
def test_high_gain_warning(self):
"""Test warning for high gain levels."""
import click
with patch.object(click, "echo") as mock_echo:
validate_tx_gain("hackrf", 45)
mock_echo.assert_called()
args = str(mock_echo.call_args)
assert "WARNING" in args
assert "high gain" in args.lower()
class TestCheckSampleRateMismatch:
"""Tests for check_sample_rate_mismatch function."""
def test_no_mismatch(self):
"""Test when sample rates match."""
mock_recording = MagicMock()
mock_recording.metadata = {"sample_rate": 2e6}
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.click.echo") as mock_echo:
check_sample_rate_mismatch(mock_recording, 2e6, quiet=False)
mock_echo.assert_not_called()
def test_mismatch_warning(self):
"""Test warning when sample rates differ."""
mock_recording = MagicMock()
mock_recording.metadata = {"sample_rate": 1e6}
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.click.echo") as mock_echo:
check_sample_rate_mismatch(mock_recording, 2e6, quiet=False)
mock_echo.assert_called_once()
args = str(mock_echo.call_args)
assert "Warning" in args
assert "differs" in args
def test_mismatch_quiet_mode(self):
"""Test no warning in quiet mode."""
mock_recording = MagicMock()
mock_recording.metadata = {"sample_rate": 1e6}
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.click.echo") as mock_echo:
check_sample_rate_mismatch(mock_recording, 2e6, quiet=True)
mock_echo.assert_not_called()
def test_no_metadata(self):
"""Test when recording has no metadata."""
mock_recording = MagicMock()
mock_recording.metadata = None
with patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.click.echo") as mock_echo:
check_sample_rate_mismatch(mock_recording, 2e6, quiet=False)
mock_echo.assert_not_called()
class TestTransmitCommand:
"""Tests for transmit CLI command."""
def setup_method(self):
"""Set up test fixtures."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Clean up test fixtures."""
import shutil
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
def test_transmit_basic(self):
"""Test basic transmit command."""
test_file = os.path.join(self.temp_dir, "test.npy")
open(test_file, "w").close()
mock_sdr = MagicMock()
mock_recording = MagicMock()
mock_recording.data = np.array([[0.1 + 0.1j] * 1000])
mock_recording.metadata = {}
with (
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.get_sdr_device", return_value=mock_sdr),
patch("ria_toolkit_oss.ria_toolkit_oss_cli.ria_toolkit_oss.transmit.load_input_file", return_value=mock_recording),
):
result = self.runner.invoke(
transmit,
[
"--device",
"hackrf",
"--sample-rate",
"2e6",
"--center-frequency",
"915M",
"--gain",
"10",
"--input",
test_file,
"--quiet",
],
)
assert result.exit_code == 0
mock_sdr.tx_recording.assert_called_once()
mock_sdr.close.assert_called_once()