python-aaronia 0.5.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/CHANGELOG.md +32 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/CONTRIBUTING.md +1 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/Cargo.lock +2 -2
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/Cargo.toml +2 -2
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/DESIGN.md +1 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/PKG-INFO +1 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/README.md +2 -2
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/docs/HTTPSPEC.md +11 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/python-aaronia/src/lib.rs +15 -7
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/scripts/ci-local.sh +14 -8
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/soapy-aaronia/README.md +5 -4
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/http_endpoints.rs +26 -3
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/unified_source.rs +34 -10
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/live_smoke.rs +59 -1
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/sdr_source_impl_test.rs +11 -5
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/.cargo/config.toml +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/.gitattributes +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/.gitignore +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/LICENSE +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/PLUGINS.md +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/deny.toml +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/docs/SDKSPEC.md +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/device_control.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/include/aaronia.h +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/pyproject.toml +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/python-aaronia/README.md +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/soapy-aaronia/Registration.cpp +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/c_api.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/decompression.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/detection.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/error.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/file_source.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/http_sink.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/http_source.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/http_streaming.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/lib.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/native_sdk.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/sdk_source.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/sdr_source_impl.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/seify_impl.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/unified_sink.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/src/utils.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/integration_test.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/properties.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.5.0 → python_aaronia-0.5.1}/tests/test_cw_meta.rs +0 -0
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [v0.5.1] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Mid-stream HTTP retuning was silently broken on real hardware.**
|
|
11
|
+
Live testing against RTSA-Suite PRO (SPECTRAN V6 ECO) showed the
|
|
12
|
+
`/control` capture endpoint applies a frequency change only when
|
|
13
|
+
`frequencyCenter` and `frequencySpan` are both present; a lone
|
|
14
|
+
frequency field returns `{"success":true}` but is ignored, so
|
|
15
|
+
`set_center_frequency` / `set_span_frequency` (and therefore hop-mode
|
|
16
|
+
retuning and the SoapySDR/seify/Python retune surfaces over HTTP)
|
|
17
|
+
reported success while the device kept streaming at the old tuning.
|
|
18
|
+
All HTTP retune paths now send the complete capture tuple (center,
|
|
19
|
+
span, reference level), `configure_capture` warns on lone-frequency
|
|
20
|
+
payloads, and a live smoke test (`live_retune_full_tuple_applies`)
|
|
21
|
+
plus a span-required mock predicate guard the regression.
|
|
22
|
+
`referenceLevel`-only PUTs were unaffected (they apply on their own).
|
|
23
|
+
- Corrected the docs that mis-attributed the silent-ignore behavior to
|
|
24
|
+
the Aaronia "Remote Config" license (READMEs, DESIGN.md, HTTPSPEC.md,
|
|
25
|
+
`probe_remote_config_license` doc comments): retuning goes through the
|
|
26
|
+
license-free `/control` endpoint; the license gates only
|
|
27
|
+
`/remoteconfig` writes.
|
|
28
|
+
|
|
29
|
+
### CI
|
|
30
|
+
- Clippy now runs with `--workspace` (CI, `scripts/ci-local.sh`, and the
|
|
31
|
+
Linux-VM leg): `python-aaronia` is a workspace member that no job
|
|
32
|
+
linted, and it had accumulated 12 unflagged clippy errors — a genuine
|
|
33
|
+
`type_complexity` (fixed with a type alias) and 11 `useless_conversion`
|
|
34
|
+
reports from pyo3 0.22's `#[pymethods]` wrapper codegen (crate-level
|
|
35
|
+
allow, documented for removal at pyo3 ≥ 0.23).
|
|
36
|
+
|
|
5
37
|
## [v0.5.0] - 2026-08-11
|
|
6
38
|
|
|
7
39
|
Breaking 0.x release combining the RTSA file-format verification work
|
|
@@ -13,7 +13,7 @@ cd sdr-aaronia-rs
|
|
|
13
13
|
# Run the standard validation suite
|
|
14
14
|
cargo test # unit + integration + properties (default features)
|
|
15
15
|
cargo test --test spec_coverage -- --nocapture # spec inventory report
|
|
16
|
-
cargo clippy --all-features --all-targets -- -D warnings
|
|
16
|
+
cargo clippy --workspace --all-features --all-targets -- -D warnings
|
|
17
17
|
cargo fmt --all --check
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -3057,7 +3057,7 @@ dependencies = [
|
|
|
3057
3057
|
|
|
3058
3058
|
[[package]]
|
|
3059
3059
|
name = "python-aaronia"
|
|
3060
|
-
version = "0.5.
|
|
3060
|
+
version = "0.5.1"
|
|
3061
3061
|
dependencies = [
|
|
3062
3062
|
"arrow",
|
|
3063
3063
|
"num-complex",
|
|
@@ -3592,7 +3592,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3592
3592
|
|
|
3593
3593
|
[[package]]
|
|
3594
3594
|
name = "sdr-aaronia-rs"
|
|
3595
|
-
version = "0.5.
|
|
3595
|
+
version = "0.5.1"
|
|
3596
3596
|
dependencies = [
|
|
3597
3597
|
"anyhow",
|
|
3598
3598
|
"bitflags 2.13.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "sdr-aaronia-rs"
|
|
3
|
-
description = "Unified Rust interface for Aaronia Spectran
|
|
3
|
+
description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
|
|
4
4
|
license = "GPL-3.0-or-later"
|
|
5
|
-
version = "0.5.
|
|
5
|
+
version = "0.5.1"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
|
|
8
8
|
readme = "README.md"
|
|
@@ -72,7 +72,7 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
|
|
|
72
72
|
Under the `sdr-source` feature, `AaroniaSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
|
|
73
73
|
|
|
74
74
|
- **Native SDK**: re-issues `configure_iq_receiver` with the new center frequency, applying the change to the open device handle via the `main/centerfreq` configuration key. No stream restart is required.
|
|
75
|
-
- **HTTP**: `AaroniaSource::set_center_frequency` wraps `HttpEndpointsClient::configure_capture
|
|
75
|
+
- **HTTP**: `AaroniaSource::set_center_frequency` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). The RTSA-Suite "Remote Config" license gates the separate `/remoteconfig` write path, which the hopping code deliberately avoids — hopping is not gated on the license probe.
|
|
76
76
|
- **File**: not supported.
|
|
77
77
|
|
|
78
78
|
Hop dwell deadlines come from `sdr_source::DwellController`; per-hop pacing additionally inserts a ~75 ms settle after every retune (`RETUNE_SETTLE` in `sdr_source_impl.rs`) to ride out the RTSA's apply-config latency and flush stale samples from the pipeline.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aaronia
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/isaacbentley/sdr-aaronia-rs/actions/workflows/ci.yml)
|
|
6
6
|
[](https://choosealicense.com/licenses/gpl-3.0/)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support.
|
|
9
9
|
|
|
10
10
|
*Disclaimer: This project is not affiliated with Aaronia AG. Aaronia, SPECTRAN, and RTSA-Suite PRO are trademarks of Aaronia AG.*
|
|
11
11
|
|
|
@@ -33,7 +33,7 @@ Interfacing with SDR hardware typically requires choosing between proprietary na
|
|
|
33
33
|
- **Python Data-Science Native:** PyO3 bindings with single-copy reads into NumPy arrays and Apache Arrow buffers (one copy out of the Rust receive buffer per read).
|
|
34
34
|
- **SDR Ecosystem Plugins:** a C++ `SoapySDR` plugin and a Rust-native `seify` backend.
|
|
35
35
|
|
|
36
|
-
*Note: writes to the HTTP `/remoteconfig` endpoint require a separate Aaronia "Remote Config" license; capture control via `/control` (including retuning) does not.*
|
|
36
|
+
*Note: writes to the HTTP `/remoteconfig` endpoint require a separate Aaronia "Remote Config" license; capture control via `/control` (including retuning) does not. One server quirk to know: `/control` applies a frequency change only when `frequencyCenter` **and** `frequencySpan` are both present — a lone frequency field returns `{"success":true}` but is silently ignored. The crate always sends the complete tuple.*
|
|
37
37
|
|
|
38
38
|
## Installation
|
|
39
39
|
|
|
@@ -283,6 +283,16 @@ packet-metadata object, so `payload`, `minPower`, `maxPower`, and
|
|
|
283
283
|
}
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
> **Both frequency fields are required for a retune to apply.** Live
|
|
287
|
+
> testing against RTSA-Suite PRO (HTTP server block fed by a SPECTRAN
|
|
288
|
+
> V6 ECO) shows the server returns `{"success":true}` for a capture
|
|
289
|
+
> `PUT` carrying only `frequencyCenter` or only `frequencySpan` but
|
|
290
|
+
> silently ignores it — the device keeps streaming at its previous
|
|
291
|
+
> tuning. Sending `frequencyCenter` **and** `frequencySpan` together
|
|
292
|
+
> applies reliably. `referenceLevel` on its own does apply. No license
|
|
293
|
+
> is involved: `/control` capture writes work without the Remote
|
|
294
|
+
> Config license.
|
|
295
|
+
|
|
286
296
|
#### Start/Stop Antenna Autorotation
|
|
287
297
|
```json
|
|
288
298
|
{
|
|
@@ -554,7 +564,7 @@ Data captured using antennas with location or directional information.
|
|
|
554
564
|
The client therefore exposes two methods:
|
|
555
565
|
|
|
556
566
|
- `detect_remote_config_license()` — **read-only**. Never touches device state. Classifies 401/403 responses; on read success it returns `Unknown` (write capability unproven).
|
|
557
|
-
- `probe_remote_config_write_license()` — **active probe**. Performs a read-modify-restore cycle on `reflevel` (+1 dB, restored best-effort) to positively verify write capability. Use only when you need proof
|
|
567
|
+
- `probe_remote_config_write_license()` — **active probe**. Performs a read-modify-restore cycle on `reflevel` (+1 dB, restored best-effort) to positively verify write capability. Use only when you genuinely need proof of `/remoteconfig` write access. Frequency hopping does **not** need it — retuning goes through the license-free `/control` endpoint (see the capture-control note above; the silent-ignore behavior once attributed to licensing was traced to partial capture payloads).
|
|
558
568
|
|
|
559
569
|
**Practical Detection (Active Probe)**:
|
|
560
570
|
```rust
|
|
@@ -13,11 +13,16 @@
|
|
|
13
13
|
//! timeout, shutdown) releases the GIL, so other Python threads keep
|
|
14
14
|
//! running and `KeyboardInterrupt` stays deliverable between calls.
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// These allows are all pyo3 0.22 proc-macro fallout, fixed upstream in
|
|
17
|
+
// pyo3 0.23 — drop them when bumping: the expansions trip
|
|
18
|
+
// edition-2024's unsafe_op_in_unsafe_fn, reference the `gil-refs` cfg,
|
|
19
|
+
// and route #[pymethods] `PyResult` returns through an `.into()` in
|
|
20
|
+
// generated wrapper fns that clippy ≥ 1.89 flags as useless_conversion
|
|
21
|
+
// (attributed to the method signature; an impl-level allow doesn't
|
|
22
|
+
// reach the wrappers, hence crate-level).
|
|
19
23
|
#![allow(unsafe_op_in_unsafe_fn)]
|
|
20
24
|
#![allow(unexpected_cfgs)]
|
|
25
|
+
#![allow(clippy::useless_conversion)]
|
|
21
26
|
|
|
22
27
|
use arrow::array::{Array, FixedSizeListArray};
|
|
23
28
|
use arrow::datatypes::{DataType, Field};
|
|
@@ -244,6 +249,12 @@ impl PyAaroniaConfig {
|
|
|
244
249
|
}
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
/// The `(rx1, rx2)` pair returned by `read_samples_dual_numpy`.
|
|
253
|
+
type DualArrays<'py> = (
|
|
254
|
+
Bound<'py, PyArray1<Complex32>>,
|
|
255
|
+
Bound<'py, PyArray1<Complex32>>,
|
|
256
|
+
);
|
|
257
|
+
|
|
247
258
|
/// A streaming IQ source. Construct, `start_streaming(config)`, then
|
|
248
259
|
/// call the `read_samples_*` methods; each blocks (GIL released) until
|
|
249
260
|
/// `count` samples arrive or the internal 30-second timeout raises
|
|
@@ -404,10 +415,7 @@ impl PyAaroniaSource {
|
|
|
404
415
|
&mut self,
|
|
405
416
|
py: Python<'py>,
|
|
406
417
|
count: usize,
|
|
407
|
-
) -> PyResult<
|
|
408
|
-
Bound<'py, PyArray1<Complex32>>,
|
|
409
|
-
Bound<'py, PyArray1<Complex32>>,
|
|
410
|
-
)> {
|
|
418
|
+
) -> PyResult<DualArrays<'py>> {
|
|
411
419
|
if count > MAX_READ_SAMPLES {
|
|
412
420
|
return Err(PyValueError::new_err(format!(
|
|
413
421
|
"count {count} exceeds the per-read limit of {MAX_READ_SAMPLES} samples"
|
|
@@ -73,8 +73,9 @@ if ! skipped fmt; then
|
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
75
|
if ! skipped clippy; then
|
|
76
|
-
|
|
77
|
-
cargo clippy --all-features --all-targets -- -D warnings
|
|
76
|
+
# --workspace pulls in python-aaronia, which no other step lints.
|
|
77
|
+
step "cargo clippy --workspace --all-features --all-targets -- -D warnings"
|
|
78
|
+
cargo clippy --workspace --all-features --all-targets -- -D warnings
|
|
78
79
|
fi
|
|
79
80
|
|
|
80
81
|
if ! skipped test; then
|
|
@@ -139,7 +140,7 @@ fi
|
|
|
139
140
|
# (~/Documents ⇒ EDEADLK), hence the rsync to /private/tmp first.
|
|
140
141
|
if ! skipped vm; then
|
|
141
142
|
if container system status >/dev/null 2>&1; then
|
|
142
|
-
step "CI ubuntu leg in Linux VM: clippy --all-features --all-targets + cargo test --all-features"
|
|
143
|
+
step "CI ubuntu leg in Linux VM: clippy --workspace --all-features --all-targets + cargo test --all-features"
|
|
143
144
|
# Dedicated directory: nothing else may write here (a shared
|
|
144
145
|
# scratch copy once had another tool rsync over it mid-run).
|
|
145
146
|
vm_dir=/private/tmp/aaronia-ci-vm
|
|
@@ -149,17 +150,22 @@ if ! skipped vm; then
|
|
|
149
150
|
# stale call signature shipped red to CI past a --lib-only VM
|
|
150
151
|
# run. This mirrors CI's ubuntu leg command-for-command.
|
|
151
152
|
# rust:slim lacks what the GitHub ubuntu runner image ships
|
|
152
|
-
# preinstalled: clippy, pkg-config,
|
|
153
|
-
# audio feature -> alsa-sys)
|
|
154
|
-
#
|
|
153
|
+
# preinstalled: clippy, pkg-config, ALSA headers (futuresdr's
|
|
154
|
+
# audio feature -> alsa-sys), and python3 (pyo3's build script
|
|
155
|
+
# needs an interpreter for the --workspace clippy leg). Install
|
|
156
|
+
# per run; the persistent target/ under $vm_dir keeps rebuilds
|
|
157
|
+
# incremental.
|
|
155
158
|
# --memory 8g: the all-features lib-test link OOM-kills the
|
|
156
159
|
# container CLI's default allocation (ld dies with SIGKILL).
|
|
160
|
+
# clippy is --workspace (mirrors CI); test stays root-crate —
|
|
161
|
+
# python-aaronia's Python-side behavior is covered by the
|
|
162
|
+
# python step below and CI's maturin+pytest job.
|
|
157
163
|
container run --rm --memory 8g --cpus 4 \
|
|
158
164
|
-v "$vm_dir":/w -w /w -e PROPTEST_CASES="$PROPTEST_CASES" rust:slim \
|
|
159
165
|
sh -c 'rustup component add clippy >/dev/null 2>&1; \
|
|
160
166
|
apt-get update -qq >/dev/null && \
|
|
161
|
-
apt-get install -y -qq pkg-config libasound2-dev >/dev/null; \
|
|
162
|
-
cargo clippy --all-features --all-targets -- -D warnings \
|
|
167
|
+
apt-get install -y -qq pkg-config libasound2-dev python3 >/dev/null; \
|
|
168
|
+
cargo clippy --workspace --all-features --all-targets -- -D warnings \
|
|
163
169
|
&& cargo test --all-features'
|
|
164
170
|
else
|
|
165
171
|
echo "vm step: 'container system start' not running — skipping" \
|
|
@@ -57,7 +57,11 @@ sdr = SoapySDR.Device("driver=aaronia,url=http://atc.local:54664,format=I16")
|
|
|
57
57
|
device arg changes what crosses the network.
|
|
58
58
|
- `readStream` honours `timeoutUs` and returns partial reads within the
|
|
59
59
|
deadline, per the SoapySDR contract. Retuning while streaming is safe
|
|
60
|
-
(fully serialized against the reader)
|
|
60
|
+
(fully serialized against the reader) and needs no Aaronia license:
|
|
61
|
+
the plugin retunes through the RTSA `/control` endpoint, always
|
|
62
|
+
sending center frequency and span together — RTSA servers silently
|
|
63
|
+
ignore capture requests that carry only one of the two (live-verified
|
|
64
|
+
against RTSA-Suite PRO with a SPECTRAN V6 ECO).
|
|
61
65
|
- **TX:** `CF32`, single channel, available only when the module is
|
|
62
66
|
built against the native SDK on Windows/Linux — elsewhere
|
|
63
67
|
`setupStream(TX)` fails with a descriptive error. Bursts are pushed
|
|
@@ -78,8 +82,5 @@ sdr = SoapySDR.Device("driver=aaronia,url=http://atc.local:54664,format=I16")
|
|
|
78
82
|
- One RX channel through the plugin (`rx_channel=Rx2` selects the second
|
|
79
83
|
antenna input; true dual-channel reads are available via the crate's
|
|
80
84
|
Rust/Python/C APIs, not the Soapy streaming interface).
|
|
81
|
-
- Without Aaronia's Remote Config license, the RTSA server accepts
|
|
82
|
-
retune requests over HTTP but may silently ignore them — the device
|
|
83
|
-
keeps streaming at the mission's configured frequency.
|
|
84
85
|
- Enumeration advertises a default localhost candidate without probing
|
|
85
86
|
it (`find()` must not block on the network).
|
|
@@ -917,7 +917,27 @@ impl HttpEndpointsClient {
|
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
/// Configures capture parameters via the `/control` endpoint.
|
|
920
|
+
///
|
|
921
|
+
/// **Frequency changes need `frequencyCenter` and `frequencySpan`
|
|
922
|
+
/// together.** RTSA servers return `{"success":true}` for a capture
|
|
923
|
+
/// `PUT` that carries only one of the two frequency fields but
|
|
924
|
+
/// silently ignore it; the retune applies only when both are present
|
|
925
|
+
/// (verified live against RTSA-Suite PRO driving a SPECTRAN V6 ECO —
|
|
926
|
+
/// center-only and span-only PUTs each no-opped, center+span
|
|
927
|
+
/// applied). `referenceLevel` does apply on its own. A lone frequency
|
|
928
|
+
/// field logs a warning here and is still sent, since other server
|
|
929
|
+
/// versions may accept it.
|
|
920
930
|
pub async fn configure_capture(&self, config: CaptureControl) -> Result<()> {
|
|
931
|
+
let has_range = config.frequency_start.is_some() && config.frequency_end.is_some();
|
|
932
|
+
if !has_range && (config.frequency_center.is_some() != config.frequency_span.is_some()) {
|
|
933
|
+
warn!(
|
|
934
|
+
"Partial capture PUT ({:?}): RTSA servers are known to accept a lone \
|
|
935
|
+
frequencyCenter/frequencySpan with success=true but silently ignore it; \
|
|
936
|
+
include both fields for the retune to apply",
|
|
937
|
+
config
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
921
941
|
let url = format!("{}/control", self.base_url);
|
|
922
942
|
let request = self.control_request(self.client.put(&url)).json(&config);
|
|
923
943
|
Self::ensure_success("Capture configuration", request.send().await?)?;
|
|
@@ -1060,9 +1080,12 @@ impl HttpEndpointsClient {
|
|
|
1060
1080
|
/// `reflevel` parameter by +1 dB and restores it best-effort. If the
|
|
1061
1081
|
/// restore fails (network drop mid-probe), the device is left with the
|
|
1062
1082
|
/// adjusted reference level. Only call this when you genuinely need
|
|
1063
|
-
/// proof of write capability
|
|
1064
|
-
///
|
|
1065
|
-
///
|
|
1083
|
+
/// proof of `/remoteconfig` write capability. Note that retuning does
|
|
1084
|
+
/// **not** — [`Self::configure_capture`] goes through the license-free
|
|
1085
|
+
/// `/control` endpoint. (An earlier revision justified this probe by
|
|
1086
|
+
/// claiming unlicensed `configure_capture` calls silently no-op; live
|
|
1087
|
+
/// testing traced that behavior to partial capture payloads, not
|
|
1088
|
+
/// licensing.)
|
|
1066
1089
|
///
|
|
1067
1090
|
/// # Returns
|
|
1068
1091
|
/// - `RemoteConfigStatus::Active` - License is active, write operations work
|
|
@@ -1012,11 +1012,20 @@ impl AaroniaSource {
|
|
|
1012
1012
|
|
|
1013
1013
|
/// Retune the source to a new centre frequency without rebuilding it.
|
|
1014
1014
|
///
|
|
1015
|
-
/// - **HTTP**: wraps `HttpEndpointsClient::configure_capture
|
|
1016
|
-
///
|
|
1017
|
-
///
|
|
1018
|
-
///
|
|
1019
|
-
///
|
|
1015
|
+
/// - **HTTP**: wraps `HttpEndpointsClient::configure_capture`, a `PUT`
|
|
1016
|
+
/// to the license-free `/control` endpoint, always sending the
|
|
1017
|
+
/// complete capture tuple (centre, span, reference level) with the
|
|
1018
|
+
/// unchanged values taken from the cached config. The full tuple is
|
|
1019
|
+
/// load-bearing: RTSA servers return `{"success":true}` for a
|
|
1020
|
+
/// capture `PUT` that carries only one of the two frequency fields
|
|
1021
|
+
/// but silently ignore it — the retune applies only when
|
|
1022
|
+
/// `frequencyCenter` and `frequencySpan` are both present (verified
|
|
1023
|
+
/// live against RTSA-Suite PRO driving a SPECTRAN V6 ECO; the
|
|
1024
|
+
/// reference level, by contrast, does apply on its own). The
|
|
1025
|
+
/// RTSA-Suite "Remote Config" license gates
|
|
1026
|
+
/// the `/remoteconfig` write path, not this one; callers that need
|
|
1027
|
+
/// a `/remoteconfig` write-license check should call
|
|
1028
|
+
/// [`Self::probe_remote_config_license`] separately.
|
|
1020
1029
|
/// - **Native SDK**: re-issues `configure_iq_receiver` with the new
|
|
1021
1030
|
/// centre frequency, the existing span, and the existing reference
|
|
1022
1031
|
/// level. The SDK config system applies the change to the open device
|
|
@@ -1059,9 +1068,14 @@ impl AaroniaSource {
|
|
|
1059
1068
|
.http_client
|
|
1060
1069
|
.as_ref()
|
|
1061
1070
|
.ok_or_else(|| Error::Config("HTTP client not initialized".to_string()))?;
|
|
1071
|
+
// Full tuple, not just the changed field: the server
|
|
1072
|
+
// ignores capture PUTs that lack frequencySpan (see the
|
|
1073
|
+
// doc comment above).
|
|
1062
1074
|
client
|
|
1063
1075
|
.configure_capture(crate::http_endpoints::CaptureControl {
|
|
1064
1076
|
frequency_center: Some(freq),
|
|
1077
|
+
frequency_span: Some(self.config.span_frequency),
|
|
1078
|
+
reference_level: Some(self.config.reference_level as f32),
|
|
1065
1079
|
control_type: crate::http_endpoints::ControlType::Capture,
|
|
1066
1080
|
..Default::default()
|
|
1067
1081
|
})
|
|
@@ -1118,9 +1132,13 @@ impl AaroniaSource {
|
|
|
1118
1132
|
.http_client
|
|
1119
1133
|
.as_ref()
|
|
1120
1134
|
.ok_or_else(|| Error::Config("HTTP client not initialized".to_string()))?;
|
|
1135
|
+
// Full tuple: partial capture PUTs are silently ignored
|
|
1136
|
+
// by the server (see `set_center_frequency`).
|
|
1121
1137
|
client
|
|
1122
1138
|
.configure_capture(crate::http_endpoints::CaptureControl {
|
|
1139
|
+
frequency_center: Some(self.config.center_frequency),
|
|
1123
1140
|
frequency_span: Some(span),
|
|
1141
|
+
reference_level: Some(self.config.reference_level as f32),
|
|
1124
1142
|
control_type: crate::http_endpoints::ControlType::Capture,
|
|
1125
1143
|
..Default::default()
|
|
1126
1144
|
})
|
|
@@ -1171,8 +1189,12 @@ impl AaroniaSource {
|
|
|
1171
1189
|
.http_client
|
|
1172
1190
|
.as_ref()
|
|
1173
1191
|
.ok_or_else(|| Error::Config("HTTP client not initialized".to_string()))?;
|
|
1192
|
+
// Full tuple: partial capture PUTs are silently ignored
|
|
1193
|
+
// by the server (see `set_center_frequency`).
|
|
1174
1194
|
client
|
|
1175
1195
|
.configure_capture(crate::http_endpoints::CaptureControl {
|
|
1196
|
+
frequency_center: Some(self.config.center_frequency),
|
|
1197
|
+
frequency_span: Some(self.config.span_frequency),
|
|
1176
1198
|
reference_level: Some(ref_level as f32),
|
|
1177
1199
|
control_type: crate::http_endpoints::ControlType::Capture,
|
|
1178
1200
|
..Default::default()
|
|
@@ -1189,11 +1211,13 @@ impl AaroniaSource {
|
|
|
1189
1211
|
|
|
1190
1212
|
/// Probe the RTSA-Suite Remote Config license status, returning
|
|
1191
1213
|
/// [`crate::http_endpoints::RemoteConfigStatus::Active`] for non-HTTP
|
|
1192
|
-
/// sources (they don't need it).
|
|
1193
|
-
///
|
|
1194
|
-
/// the license
|
|
1195
|
-
///
|
|
1196
|
-
///
|
|
1214
|
+
/// sources (they don't need it). This concerns **`/remoteconfig`
|
|
1215
|
+
/// writes only** — mid-stream retuning via [`Self::set_center_frequency`]
|
|
1216
|
+
/// goes through the license-free `/control` endpoint and does not
|
|
1217
|
+
/// need this probe. (An earlier revision claimed unlicensed
|
|
1218
|
+
/// `configure_capture` calls were silently ignored; live testing
|
|
1219
|
+
/// showed the silent ignore was caused by partial capture payloads,
|
|
1220
|
+
/// not licensing — see `set_center_frequency`.)
|
|
1197
1221
|
///
|
|
1198
1222
|
/// **On HTTP sources this is an active probe that temporarily
|
|
1199
1223
|
/// perturbs device state**: it adjusts the reference level by +1 dB
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
use futures::stream::StreamExt;
|
|
15
15
|
use sdr_aaronia_rs::http_endpoints::{
|
|
16
|
-
AuthMethod,
|
|
16
|
+
AuthMethod, CaptureControl, ControlType, HttpEndpointsClient, InputProcessingType,
|
|
17
|
+
StreamParams, TxSampleRequest,
|
|
17
18
|
};
|
|
18
19
|
use sdr_aaronia_rs::http_streaming::{DropDetector, PayloadType, StreamFormat};
|
|
19
20
|
use std::time::{Duration, Instant};
|
|
@@ -594,6 +595,63 @@ fn live_stream_seify() {
|
|
|
594
595
|
streamer.deactivate_at(None).expect("deactivate");
|
|
595
596
|
}
|
|
596
597
|
|
|
598
|
+
/// The `/control` capture endpoint applies a frequency change only when
|
|
599
|
+
/// `frequencyCenter` and `frequencySpan` travel together — a lone
|
|
600
|
+
/// frequency field returns `{"success":true}` but is silently ignored
|
|
601
|
+
/// (no license involved). This proves the crate's full-tuple retune path
|
|
602
|
+
/// actually moves the device, then restores the original tuning.
|
|
603
|
+
#[tokio::test]
|
|
604
|
+
#[ignore = "requires live RTSA-Suite PRO at AARONIA_LIVE_URL / atc.local:54664"]
|
|
605
|
+
async fn live_retune_full_tuple_applies() {
|
|
606
|
+
let c = client();
|
|
607
|
+
|
|
608
|
+
let before = c.get_sample(None).await.expect("/sample baseline");
|
|
609
|
+
let center0 = (before.start_frequency + before.end_frequency) / 2.0;
|
|
610
|
+
let span0 = before.end_frequency - before.start_frequency;
|
|
611
|
+
println!(
|
|
612
|
+
"baseline: center={:.3} MHz span={:.3} MHz",
|
|
613
|
+
center0 / 1e6,
|
|
614
|
+
span0 / 1e6
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// Two span-widths down: unambiguous vs. the baseline, and still
|
|
618
|
+
// comfortably inside the V6 tuning range for any plausible mission.
|
|
619
|
+
let target = center0 - 2.0 * span0;
|
|
620
|
+
let retune = |center: f64| CaptureControl {
|
|
621
|
+
frequency_center: Some(center),
|
|
622
|
+
frequency_span: Some(span0),
|
|
623
|
+
control_type: ControlType::Capture,
|
|
624
|
+
..Default::default()
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
c.configure_capture(retune(target))
|
|
628
|
+
.await
|
|
629
|
+
.expect("retune PUT");
|
|
630
|
+
|
|
631
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
632
|
+
let mut applied = false;
|
|
633
|
+
while Instant::now() < deadline {
|
|
634
|
+
let s = c.get_sample(None).await.expect("/sample poll");
|
|
635
|
+
let center = (s.start_frequency + s.end_frequency) / 2.0;
|
|
636
|
+
if (center - target).abs() < span0 / 2.0 {
|
|
637
|
+
applied = true;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
tokio::time::sleep(Duration::from_millis(250)).await;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Restore before asserting so a failed assertion doesn't leave the
|
|
644
|
+
// device parked off-mission.
|
|
645
|
+
c.configure_capture(retune(center0))
|
|
646
|
+
.await
|
|
647
|
+
.expect("restore PUT");
|
|
648
|
+
|
|
649
|
+
assert!(
|
|
650
|
+
applied,
|
|
651
|
+
"full-tuple /control retune to {target} Hz never reflected in /sample"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
597
655
|
// NOTE: there is intentionally no Rust-side SoapySDR live test here.
|
|
598
656
|
// A `soapysdr` dev-dependency makes `cargo test` unbuildable on any
|
|
599
657
|
// machine without system SoapySDR + libclang. Equivalent live coverage:
|
|
@@ -56,14 +56,20 @@ async fn wait_for_request(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/// `wait_for_request` predicate: a `/control` PUT whose JSON body retunes
|
|
59
|
-
/// `frequencyCenter` to `freq` (within 1 Hz)
|
|
59
|
+
/// `frequencyCenter` to `freq` (within 1 Hz) *and* carries
|
|
60
|
+
/// `frequencySpan`. The span requirement is a regression guard: RTSA
|
|
61
|
+
/// servers silently ignore capture PUTs with a lone frequency field, so
|
|
62
|
+
/// a center-only PUT is not a retune (live-verified; see
|
|
63
|
+
/// `configure_capture`).
|
|
60
64
|
fn control_put_tuning_to(freq: f64) -> impl Fn(&wiremock::Request) -> bool {
|
|
61
65
|
move |r: &wiremock::Request| {
|
|
62
66
|
r.url.path() == "/control"
|
|
63
|
-
&& r.body_json::<serde_json::Value>()
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
&& r.body_json::<serde_json::Value>().ok().is_some_and(|v| {
|
|
68
|
+
v.get("frequencySpan").and_then(|s| s.as_f64()).is_some()
|
|
69
|
+
&& v.get("frequencyCenter")
|
|
70
|
+
.and_then(|f| f.as_f64())
|
|
71
|
+
.is_some_and(|f| (f - freq).abs() < 1.0)
|
|
72
|
+
})
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|