python-aaronia 0.7.7__tar.gz → 0.8.0__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.
Files changed (120) hide show
  1. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.gitignore +6 -8
  2. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/CHANGELOG.md +146 -1
  3. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/Cargo.lock +2 -2
  4. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/Cargo.toml +1 -1
  5. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/PKG-INFO +1 -1
  6. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/README.md +24 -0
  7. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/APPS.md +7 -2
  8. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/HTTPSPEC.md +96 -5
  9. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/USAGE.md +69 -0
  10. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/include/aaronia.h +55 -0
  11. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/Cargo.toml +1 -1
  12. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/AaroniaSoapyDevice.cpp +142 -24
  13. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/AaroniaSoapyDevice.hpp +20 -0
  14. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/CMakeLists.txt +14 -5
  15. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/README.md +80 -11
  16. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/Registration.cpp +35 -7
  17. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/c_api.rs +293 -0
  18. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_endpoints.rs +818 -0
  19. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_source.rs +261 -4
  20. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/link_budget.rs +34 -0
  21. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/native_sdk.rs +1 -1
  22. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdk_source.rs +4 -2
  23. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/unified_sink.rs +24 -4
  24. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/unified_source.rs +33 -0
  25. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/utils.rs +133 -0
  26. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/live_smoke.rs +84 -0
  27. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeCache.txt +0 -439
  28. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -103
  29. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  30. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -15
  31. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -954
  32. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
  33. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -1
  34. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -1904
  35. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -16
  36. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -3
  37. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -7
  38. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -61
  39. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -157
  40. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -8
  41. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -24
  42. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -131
  43. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -13
  44. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -2
  45. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -2
  46. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -2
  47. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -12
  48. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -1
  49. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -4
  50. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -1
  51. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -1
  52. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -22
  53. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -94
  54. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -9
  55. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -2
  56. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -2
  57. python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -2
  58. python_aaronia-0.7.7/soapy-aaronia/build-test/Makefile +0 -271
  59. python_aaronia-0.7.7/soapy-aaronia/build-test/cmake_install.cmake +0 -78
  60. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.cargo/config.toml +0 -0
  61. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.gitattributes +0 -0
  62. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/CONTRIBUTING.md +0 -0
  63. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/DESIGN.md +0 -0
  64. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/LICENSE +0 -0
  65. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/PLUGINS.md +0 -0
  66. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/decompress_block.rs +0 -0
  67. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/deinterleave_dual_iq.rs +0 -0
  68. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/parse_int16_packet.rs +0 -0
  69. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/rtsa_open_and_read.rs +0 -0
  70. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/deny.toml +0 -0
  71. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/FILESPEC.md +0 -0
  72. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/QUICKSTART.md +0 -0
  73. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/SDKSPEC.md +0 -0
  74. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/VERIFICATION.md +0 -0
  75. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/channel_hopping.rs +0 -0
  76. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/device_control.rs +0 -0
  77. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/dump_metadata.rs +0 -0
  78. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/http_iq_quickstart.rs +0 -0
  79. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/native_sdk_basic.rs +0 -0
  80. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/native_sdk_transmit.rs +0 -0
  81. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/noaa_scanner.rs +0 -0
  82. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/python_arrow_example.py +0 -0
  83. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/read_rtsa_file.rs +0 -0
  84. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/soapy_python_example.py +0 -0
  85. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/packaging/homebrew/README.md +0 -0
  86. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  87. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/pyproject.toml +0 -0
  88. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/README.md +0 -0
  89. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/aaronia.pyi +0 -0
  90. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/src/lib.rs +0 -0
  91. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/test_basic.py +0 -0
  92. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/scripts/ci-local.sh +0 -0
  93. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/scripts/validate-iq-live.py +0 -0
  94. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  95. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/packaging/install.sh +0 -0
  96. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/print.cmake +0 -0
  97. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/decompression.rs +0 -0
  98. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/detection.rs +0 -0
  99. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/error.rs +0 -0
  100. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/file_source.rs +0 -0
  101. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_sink.rs +0 -0
  102. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_streaming.rs +0 -0
  103. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/lib.rs +0 -0
  104. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdk_sink.rs +0 -0
  105. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdr_source_impl.rs +0 -0
  106. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/seify_impl.rs +0 -0
  107. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/test.cmake +0 -0
  108. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/c_api_test.rs +0 -0
  109. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_mock_test.rs +0 -0
  110. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_resilience_test.rs +0 -0
  111. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_sink_test.rs +0 -0
  112. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/integration_test.rs +0 -0
  113. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/native_sdk_load.rs +0 -0
  114. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/properties.proptest-regressions +0 -0
  115. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/properties.rs +0 -0
  116. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/rtsa_negative_test.rs +0 -0
  117. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/sdr_source_impl_test.rs +0 -0
  118. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/spec_coverage.rs +0 -0
  119. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/test_cw_mag.rs +0 -0
  120. {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/test_cw_meta.rs +0 -0
@@ -10,15 +10,13 @@ tests/sdk/
10
10
  # Local agent configuration/memory
11
11
  .agents/
12
12
 
13
- # CMake build directory for SoapySDR plugin
14
- soapy-aaronia/build/
15
- python-aaronia/__pycache__/test_basic.cpython-314-pytest-9.1.1.pyc
16
- python-aaronia/err.txt
17
- python-aaronia/check_errors.txt
18
- branch_diff.txt
13
+ # Local CMake build trees for the SoapySDR plugin. Globbed, not a single
14
+ # `build/`: a second tree under any other name (build-test, build-e2e)
15
+ # was committed once already — 33 files of CMakeCache and compiler probe
16
+ # binaries carrying absolute paths from the machine that made them.
17
+ soapy-aaronia/build*/
19
18
 
20
- # Python bindings dev environment and local plugin builds
19
+ # Python bindings dev environment and build output
21
20
  python-aaronia/venv/
22
21
  python-aaronia/**/__pycache__/
23
- soapy-aaronia/build/
24
22
  dist/
@@ -2,7 +2,152 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [v0.8.0] - 2026-09-07
6
+
7
+ **Breaking:** `StreamStats` gained a `device_health` field, so an
8
+ exhaustive struct literal over it no longer compiles. It is
9
+ `#[non_exhaustive]` now, along with the new `DeviceCapabilities` and
10
+ `DeviceHealthSummary`: these are reports a consumer reads rather than
11
+ builds, and the attribute makes every future field addition free. Only
12
+ the `futuresdr` feature exposes `StreamStats`; the default feature set is
13
+ unaffected.
14
+
15
+ ### Added
16
+ - **The SoapySDR probe now reports what the device says about itself.**
17
+ `SoapySDRUtil --probe` published constants compiled in for one model:
18
+ `hardware=Spectran V6` whatever was attached, a 10 Hz–6 GHz frequency
19
+ range, and a −100…+10 dB gain range. A SPECTRAN V6 ECO declares
20
+ 5.5 MHz–8 GHz and −55…+23 dBm — too low at one end for any tune to
21
+ succeed, two whole GHz short at the other, and a gain slider spanning
22
+ values the device clamps. The plugin now reads `/remoteconfig` and
23
+ `/healthstatus` once at construction and publishes the model, serial,
24
+ firmware version, both ranges with their declared steps, and the
25
+ sample-rate ladder.
26
+
27
+ The ladder is the part that needed care: `status/iqsamples` is the
28
+ device's native undecimated rate but it is a *measurement*
29
+ (61 411 246 Hz for a nominal 61 440 000), so it is snapped to the exact
30
+ `receiver_clock / 1.5` rung it names — new `utils::snap_to_ladder_top`
31
+ — before being halved once per rung `decimation0` offers. A rate
32
+ advertised to an application has to be one the device can be set to.
33
+ `getSampleRateRange` is now taken from the ends of that same ladder, so
34
+ it can no longer disagree with `listSampleRates`; its old 10 kHz floor
35
+ sat below the slowest rung the hardware has.
36
+
37
+ New API: `http_endpoints::DeviceCapabilities` and `ValueRange`,
38
+ `HttpEndpointsClient::get_device_capabilities()`,
39
+ `AaroniaSource::device_capabilities()`, and the C entry points
40
+ `aaronia_source_get_capabilities` / `aaronia_source_capabilities_free`.
41
+ Every field is optional and independently so: a device answering about
42
+ frequency but not gain still gets its frequency range published, and
43
+ the file and native-SDK backends — which have no equivalent surface to
44
+ ask — fall back throughout. Nothing here is a guess; a field that
45
+ cannot be read stays absent.
46
+ - **A stream gap now says whether the device caused it.** `HttpSource`
47
+ reads `/healthstatus` on each gap report and checks the device block's
48
+ own loss counters — `status/errors`, `status/usboverflows`,
49
+ `status/dsboverflows`, all per-second rates, so the reading describes
50
+ the moment of the gap rather than the run so far. Nonzero and the loss
51
+ starts at the device, where no amount of network headroom will recover
52
+ it; zero and the samples went missing downstream, in the server's 8 MB
53
+ outbound buffer or on the wire. The read is detached rather than
54
+ awaited: the control-plane timeout is 30 s, and stalling `work()` that
55
+ long would back the chunk channel up into the very loss being
56
+ diagnosed. `HttpEndpointsClient::get_device_health()` exposes the same
57
+ reduction of the health tree, and each reading is published as
58
+ `StreamStats::device_health`.
59
+
60
+ What it cannot settle is whether another client is on the same server
61
+ block. Nothing in the HTTP surface counts connections, so clearing the
62
+ device narrows a gap to a set of causes that includes contention
63
+ without singling it out — which is why the warning names it rather
64
+ than asserting it.
65
+
66
+ ### Fixed
67
+ - **The SoapySDR plugin reported the clock source as `Internal`, a name
68
+ the device does not use, on hardware running off an external 10 MHz
69
+ reference.** `device/sclksource` offers `Consumer`, `Oscillator`,
70
+ `GPS`, `PPS`, `10MHz` and three `… Provider` variants; the measured V6
71
+ ECO is on `10MHz`. An operator who wired a house reference up for
72
+ frequency accuracy was told the device was free-running.
73
+ `listClockSources` and `getClockSource` now answer from the device.
74
+ `setClockSource` still only reads: it accepts the current source as
75
+ the no-op it is, and warns for anything else naming what the device is
76
+ actually on. `listAntennas` likewise takes its name from
77
+ `device/devicemode` — `RX1` on this device, and correct rather than
78
+ coincidental on a V6 running an RX2 mode.
79
+ - **The SoapySDR plugin could link a stale Rust static library.** The
80
+ CMake rule produced `libsdr_aaronia_rs.a` through an
81
+ `add_custom_command(OUTPUT …)` with no `DEPENDS`, so CMake treated the
82
+ archive as up to date the moment it existed and skipped cargo
83
+ entirely. Editing Rust sources and rebuilding relinked the module
84
+ against the old archive — silently, producing a module that looked
85
+ fine and did not contain the change. It is a custom *target* now,
86
+ whose command runs every build and which cargo no-ops when nothing
87
+ moved, which is what the comment there always claimed happened.
88
+ - **`SoapySDRUtil --probe` reported `Timestamps: NO` on a device that
89
+ timestamps every buffer.** `hasHardwareTime("")` answered with the last
90
+ stream timestamp, so it read as "no capability" until a packet had
91
+ arrived — and a probe never streams. It is a capability query now:
92
+ every RTSA packet header carries a start time, `readStream` has always
93
+ returned `SOAPY_SDR_HAS_TIME` with correct epoch nanoseconds, and an
94
+ application deciding at setup whether to record timestamps no longer
95
+ reads the probe as "cannot". `hasHardwareTime("GPS")` is unchanged and
96
+ still reports a value, since a GPS fix may genuinely not exist.
97
+ - **The SoapySDR plugin advertised a TX channel on every device,
98
+ including receivers that cannot transmit at all.** `SoapySDRUtil
99
+ --probe` on a V6 ECO over HTTP reported `1 Tx`, a full TX channel
100
+ section and `Full-duplex: YES`; an application that believed it failed
101
+ on the first write. `getNumChannels` did guard on holding a sink, but
102
+ `aaronia_sink_build` allocates unconditionally — it succeeds on a build
103
+ carrying no TX code at all, and only `aaronia_sink_initialize` fails —
104
+ so the guard was never false. A sink is now built only when the new
105
+ `aaronia_sink_supported()` reports this binary carries the native-SDK
106
+ TX path *and* the source is that backend, which means a `serial=` open
107
+ with no `url=`/`file=` overriding it. The probe now reads `0 Tx` and
108
+ `Full-duplex: NO`.
109
+
110
+ Still not a device capability check: a native-SDK build opened by
111
+ serial against a V6 ECO would advertise TX, since the ECO has no
112
+ transmitter and nothing here asks the SDK.
113
+ - **Restored a clean CI run.** Three clippy lints had been failing
114
+ `cargo clippy --workspace --all-features --all-targets` on Linux since
115
+ v0.7.7 — a collapsible `if let` in `UnifiedSink::initialize`, a needless
116
+ borrow in `native_sdk`, and a `field_reassign_with_default` in a
117
+ `sdk_source` unit test. All three sit in code gated to Windows and
118
+ Linux, so a macOS `cargo clippy` compiles none of it and reports
119
+ success; only CI's `--all-features` run sees them.
120
+
121
+ ### Documentation
122
+ - **What several clients on one HTTP Server block cost, measured.** The
123
+ block accepts any number of concurrent `/stream` clients and serves
124
+ each one a full copy, so *n* clients cost the server *n* times the
125
+ egress. Two clients at 15.36 MS/s ran contiguous at 61.8 MB/s each;
126
+ five saturated the 2.5GbE path at 293.9 MB/s — the same ceiling a
127
+ single fast stream hits — and the loss fell on an arbitrary two of the
128
+ five, moving to a different pair on a repeat run. Two consequences a
129
+ client cannot escape: a clean stream is not evidence of being alone,
130
+ and a gap is not evidence of company. `/info`, `/healthstatus`,
131
+ `/remoteconfig` and the `/stream` response headers were all checked and
132
+ none counts connections.
133
+ - **Corrected the free-licence claim in `docs/HTTPSPEC.md`.** It read
134
+ that running this crate alongside a second client would meet the
135
+ one-connection limit. Five simultaneous clients were served on a system
136
+ holding one HTTP Server block licence, with no error and no refusal:
137
+ the limit is on block instances in the mission graph, not on
138
+ connections to one block.
139
+ - **The link a device actually needs, measured.** `link_budget`'s module
140
+ docs gain a 2.5GbE table beside the gigabit one, and the README a
141
+ requirements section. A 44 MHz real-time-bandwidth device — an ECO 100 —
142
+ has to run the 61.44 MS/s rung, and that rung costs 245.8 MB/s at 4 bytes
143
+ a sample, which gigabit cannot carry; 2.5 Gbps Ethernet is the floor for
144
+ those devices, and the wire format has to stay at 4 bytes a sample
145
+ because 8 asks 491.5 MB/s and loses 41 % of the stream. Two controls came
146
+ out of the same measurement: the path saturates at 292 MB/s, 93 % of
147
+ 2.5GbE line rate, so the wire is the limit and not the server; and two
148
+ configurations asking the same 245.8 MB/s by different routes — 8 bytes a
149
+ sample at 30.72 MS/s, 4 bytes at 61.44 — deliver the same 244 MB/s,
150
+ confirming that only the byte rate matters.
6
151
 
7
152
  ## [v0.7.7] - 2026-09-06
8
153
 
@@ -3027,7 +3027,7 @@ dependencies = [
3027
3027
 
3028
3028
  [[package]]
3029
3029
  name = "python-aaronia"
3030
- version = "0.7.7"
3030
+ version = "0.8.0"
3031
3031
  dependencies = [
3032
3032
  "arrow",
3033
3033
  "num-complex",
@@ -3556,7 +3556,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3556
3556
 
3557
3557
  [[package]]
3558
3558
  name = "sdr-aaronia-rs"
3559
- version = "0.7.7"
3559
+ version = "0.8.0"
3560
3560
  dependencies = [
3561
3561
  "anyhow",
3562
3562
  "bitflags 2.13.1",
@@ -2,7 +2,7 @@
2
2
  name = "sdr-aaronia-rs"
3
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.7.7"
5
+ version = "0.8.0"
6
6
  edition = "2024"
7
7
  repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
8
8
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aaronia
3
- Version: 0.7.7
3
+ Version: 0.8.0
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+)
@@ -40,6 +40,30 @@ selects a backend and presents the same interface either way.
40
40
  wasted: `link_budget` measures the path end to end and names the
41
41
  widest span on the device's decimation ladder that fits it.
42
42
 
43
+ ## Link requirements
44
+
45
+ The server streams IQ at a fixed number of bytes per sample, so the span
46
+ you ask for sets a byte rate the whole path has to sustain. Miss it and the
47
+ *server* drops what it cannot send — unsignalled gaps that look fine in a
48
+ waterfall and defeat any digital demodulator.
49
+
50
+ | Real-time bandwidth | Sample rate | Needs (4 B/sample) | Link |
51
+ |---|---|---|---|
52
+ | up to 12.2 MHz | 15.36 MS/s | 61.4 MB/s | gigabit |
53
+ | up to 24.5 MHz | 30.72 MS/s | 122.9 MB/s | gigabit measured 1024 skips; prefer 2.5GbE |
54
+ | up to 49.1 MHz | 61.44 MS/s | 245.8 MB/s | **2.5GbE** |
55
+
56
+ **A 44 MHz device — an ECO 100 — needs a 2.5 Gbps Ethernet link.** Only the
57
+ top rung covers 44 MHz of real-time bandwidth (30.72 MS/s affords 24.5), and
58
+ that rung costs 245.8 MB/s, which gigabit cannot carry. Measured over
59
+ 2.5GbE: 244.3 MB/s delivered against a 245.8 MB/s requirement, on a path
60
+ that saturates at 292 MB/s. Keep to a 4-byte wire format there — `float32`
61
+ doubles the requirement to 491.5 MB/s and loses 41 % of the stream.
62
+
63
+ `link_budget` measures *your* path end to end and names the widest span that
64
+ fits it, so treat the table as a starting point rather than a substitute for
65
+ measuring.
66
+
43
67
  ## Installation
44
68
 
45
69
  Add the following to your `Cargo.toml`:
@@ -114,7 +114,12 @@ directly and skip the SoapySDR layer.
114
114
  - `readStream` honours `timeoutUs` and returns partial reads within the
115
115
  deadline, per the SoapySDR contract.
116
116
  - Retuning mid-stream is safe and requires no Aaronia licence.
117
+ - The frequency range, gain range and sample-rate list an application
118
+ shows come from the device itself over the HTTP backend, so they match
119
+ the model attached rather than a compiled-in default.
117
120
  - Gaps detected in the stream are counted by `readSensor("cumulative_drops")`.
118
121
  An overrun sets `SOAPY_SDR_END_ABRUPT` on the affected read.
119
- - TX is hardware-unverified and exists only when the plugin is built
120
- against the native SDK on Windows or Linux.
122
+ - TX is hardware-unverified, and a TX channel is reported only when the
123
+ plugin was built against the native SDK on Windows or Linux *and* the
124
+ device was opened by `serial=`. Over `url=` or `file=` the probe shows
125
+ `0 Tx`.
@@ -189,6 +189,62 @@ the network cannot carry all end here, so reducing the wire format
189
189
  (`format=int16`) or the rate (`rate_reduction=n`) is the fix rather
190
190
  than a larger client-side buffer.
191
191
 
192
+ ### Several clients on one server block
193
+
194
+ The HTTP Server block accepts any number of concurrent `/stream`
195
+ clients, silently, and serves **each one a full copy** of the stream. It
196
+ does not split the data and it does not refuse the second connection, so
197
+ *n* clients cost the server *n* times the egress. Measured against a
198
+ V6 ECO at 15.36 MS/s (`format=int16`, 61.4 MB/s a client), counting
199
+ bytes off the socket over 6 s after a 500 ms settle:
200
+
201
+ | clients | per client | aggregate | result |
202
+ |---------|------------|------------|------------------------------------|
203
+ | 1 | 61.8 MB/s | 61.8 MB/s | contiguous |
204
+ | 2 | 61.8 MB/s | 123.5 MB/s | contiguous, both clients |
205
+ | 5 | 52–62 MB/s | 293.9 MB/s | saturated; 2 of 5 clients lost data |
206
+
207
+ The five-client rows are the ones to read. The aggregate pins at
208
+ 293.9 MB/s — the same ~292 MB/s ceiling this 2.5GbE path saturates at
209
+ with a single fast stream — so the constraint is the path, not a
210
+ per-client limit. And the loss is **not shared out fairly**: three of the
211
+ five clients ran contiguous at the full 61.7 MB/s while the other two ran
212
+ short with hundreds of timestamp gaps each. Repeating the run moved the
213
+ loss to a different pair.
214
+
215
+ Two consequences for a client:
216
+
217
+ - **A clean stream is not evidence of being alone.** You may be one of
218
+ the connections the server is keeping up with while it starves another.
219
+ - **A gap is not evidence of company either.** A slow link, a slow
220
+ consumer and a second client all arrive as the same 8 MB outbound
221
+ buffer overflowing.
222
+
223
+ #### Nothing reports the number of clients
224
+
225
+ There is no endpoint, field or header that counts connections. Checked
226
+ against a live RTSA-Suite PRO:
227
+
228
+ | Surface | What it carries |
229
+ | :--- | :--- |
230
+ | `/info` | `name`, `title`, `uuid`, `port`, `mission`, `features` — no counts |
231
+ | `/healthstatus` | the device block only; the HTTP Server block publishes no health at all |
232
+ | `/remoteconfig` | the device block only, same as above |
233
+ | `/stream` response headers | `Access-Control-Allow-Origin`, `Transfer-Encoding`, `Date`, `DateMS` |
234
+
235
+ So a client cannot ask. What it can do is rule the *device* out: at the
236
+ moment of a gap, `/healthstatus` reports the device block's own loss
237
+ counters — `status/errors`, `status/usboverflows`, `status/dsboverflows`,
238
+ all per-second rates — and if they are zero the samples went missing
239
+ downstream of the device, in the server's outbound buffer or on the wire.
240
+ That narrows a gap to a set of causes that includes another client
241
+ without singling one out. `HttpEndpointsClient::get_device_health()`
242
+ performs the read; `HttpSource` runs it automatically on each stream-gap
243
+ report and publishes the result on `StreamStats::device_health`.
244
+
245
+ For a definitive answer, look at the RTSA host itself — the HTTP Server
246
+ block's own panel, or the host's socket table for peers on port 54664.
247
+
192
248
  ### Liveness
193
249
 
194
250
  There is no status endpoint. Aaronia's own remote control notes probe
@@ -595,7 +651,10 @@ when it is wrong.
595
651
  Note: this crate's `get_health_status()` parses the response as a generic
596
652
  configuration tree (`HealthStatus` is an alias for `ConfigItem`) rather
597
653
  than the typed shape below, which describes the upstream block-health
598
- fields.
654
+ fields. `get_device_health()` reduces that tree to the per-block loss
655
+ counters — `status/errors`, `status/usboverflows`, `status/dsboverflows`,
656
+ plus `status/devstate` — which is the read that says whether a stream gap
657
+ started at the device; see "Several clients on one server block" above.
599
658
 
600
659
  **Subgroups** per health-aware block, per Aaronia's specification:
601
660
  `info`, `status`, `health`, `settings`, and `components` — the last a
@@ -630,6 +689,32 @@ from packet metadata for that.
630
689
  | `title` | User-facing block title |
631
690
  | `uuid` | Global unique identifier |
632
691
 
692
+ #### What the config tree declares about limits
693
+
694
+ `/remoteconfig`'s numeric items carry `min`, `max` and `step`, and its
695
+ enums carry their full `values` list, so a client can read a device's
696
+ limits instead of compiling in constants for one model. Measured on a
697
+ SPECTRAN V6 ECO:
698
+
699
+ | Item | Declares |
700
+ | :--- | :--- |
701
+ | `centerfreq0` | `min` 5 500 000, `max` 8e9, `step` 1000, unit `Frequency` |
702
+ | `reflevel0` | `min` -55, `max` 23, `step` 0.5, unit `dBm` |
703
+ | `decimation0` | `values` `Full,1 / 2,…,1 / 512` — ten rungs |
704
+
705
+ Two of those are worth stating plainly because published clients have
706
+ had them wrong: the ECO tunes from **5.5 MHz**, not from ~0, and it
707
+ reaches **8 GHz**, not 6. Combined with `/healthstatus`'s
708
+ `status/iqsamples` — the native undecimated rate — the decimation list
709
+ gives the full set of settable sample rates.
710
+
711
+ `iqsamples` is a running measurement (61 411 246 Hz against a nominal
712
+ 61 440 000), so snap it to the exact `receiver_clock / 1.5` rung it
713
+ names before treating it as a ladder top; a rate advertised to an
714
+ application must be one the device can be set to.
715
+ `DeviceCapabilities::from_trees` in this crate does all of the above,
716
+ and `HttpEndpointsClient::get_device_capabilities()` performs the reads.
717
+
633
718
  #### User Information (`/user`)
634
719
  **URL**: `/user`
635
720
  **Method**: GET
@@ -862,10 +947,15 @@ connection is free. Stream Merger and Stream Splitter, the blocks that
862
947
  would otherwise let several streams share one connection, are not in
863
948
  the free licence either.
864
949
 
865
- This is the licence limit most likely to be met in practice: running
866
- this crate and a second client — a SoapySDR application, say — against
867
- one server at the same time is a second connection. It has nothing to
868
- do with the Remote Config licence discussed below.
950
+ An earlier revision of this document read that limit as covering
951
+ *connections* and warned that running this crate alongside a second
952
+ client — a SoapySDR application, say — would meet it. **That is not what
953
+ a live system does.** On the same RTSA-Suite PRO holding one HTTP Server
954
+ block licence, five simultaneous `/stream` clients were all served, with
955
+ no error, no refusal and no licence complaint. The limit is on block
956
+ instances in the mission graph, not on connections to one block. What
957
+ several clients cost is bandwidth, not licence — see below. It has
958
+ nothing to do with the Remote Config licence discussed further down.
869
959
 
870
960
  **Remote Configuration** (`/remoteconfig`):
871
961
  - Device parameter configuration.
@@ -1029,6 +1119,7 @@ fn parse_iq_int16(data: &[u8], metadata_scale: f32) -> Vec<Complex32> {
1029
1119
  |---------|------|---------|
1030
1120
  | 1.0 | 2025-01-11 | Initial HTTP specification from original documentation |
1031
1121
  | 2.0 | 2025-01-11 | Enhanced with comprehensive streaming protocol specification and implementation guidelines |
1122
+ | 2.5 | 2026-09-06 | Measured what several clients on one HTTP Server block actually do: the block serves each connection a full copy and refuses none, so *n* clients cost *n* times the egress — five concurrent clients saturated a 2.5GbE path at 293.9 MB/s and the loss landed on an arbitrary two of them, moving on a repeat run. Corrected the free-licence claim: five clients were served on a one-block licence, so the limit is on block instances, not connections. Confirmed no surface counts connections (`/info`, `/healthstatus`, `/remoteconfig`, `/stream` headers), and documented the device-block loss counters as the cross-check that at least rules the device out |
1032
1123
  | 2.4 | 2026-08-12 | Added Aaronia support's full `/control` settings list (`deviceconnect`, `camera`, per-type fields, `receiverUUID`/`receiverName` scoping), which supersedes the specification's claim that commands cannot be addressed to a block; documented that an unrecognised `format=` silently serves the RTSA file format and that `raw16` aliases `int16`, both verified live |
1033
1124
  | 2.3 | 2026-08-12 | Folded in Aaronia's endpoint specification (rev 11) and the block forum threads: `/control` broadcasts to every block and is PUT-only, the server drops data past an 8 MB outbound buffer, `/healthstatus` subgroups and the fields a V6 ECO reports, and the one-server/one-client free-licence limit. Measured that `status/iqsamples` is the native rate, not the delivered one. Corrected the marker-stream entry: it declares `payload: "spectra"`, so its nested samples are the spectra form and not a counter-example to flat categories |
1034
1125
  | 2.2 | 2026-08-12 | Verified Aaronia's V6 remote control notes (rev 4) against hardware: enum writes by index, multi-group and non-`main` `simpleconfig` PUTs, the silent no-op on an unknown block name, the ignored receiver name in the config-tree form; documented mission loading and the `type` requirement on `/control`, the absence of a status endpoint, and the unresolved conflict over what "Full" means on a full V6; resolved a contradiction over what the Remote Config licence gates |
@@ -176,6 +176,75 @@ naming a narrower span from the device's own ladder that fits. The
176
176
  verdict is also published as `StreamStats::link_budget` on the shared
177
177
  stats handle, and is re-measured after a configuration restart.
178
178
 
179
+ ### When a gap happens anyway: was it the device?
180
+
181
+ A gap that arrives despite a link with headroom has more than one cause,
182
+ and the crate asks the device about the one it can settle. On each
183
+ stream-gap report `HttpSource` reads `/healthstatus` and checks the
184
+ device block's own loss counters — errors, USB overflows and DSP
185
+ overflows, all per-second rates, so the reading describes the moment of
186
+ the gap. If they are nonzero the loss starts at the device and no amount
187
+ of network headroom will recover it. If they are zero the samples went
188
+ missing downstream, in the RTSA HTTP server's outbound buffer (it drops
189
+ past 8 MB) or on the wire. The reading is logged beside the gap warning
190
+ and published as `StreamStats::device_health`; the same read is available
191
+ directly:
192
+
193
+ ```rust,no_run
194
+ # async fn health() -> sdr_aaronia_rs::Result<()> {
195
+ use sdr_aaronia_rs::http_endpoints::{AuthMethod, HttpEndpointsClient};
196
+
197
+ let client = HttpEndpointsClient::new("http://localhost:54664".into(), AuthMethod::None)?;
198
+ for block in client.get_device_health().await? {
199
+ // `None` means the block reported none of the counters — the honest
200
+ // answer is "cannot say", not a device that has been cleared.
201
+ println!("{} -> losing nothing: {:?}", block.describe(), block.losing_nothing());
202
+ }
203
+ # Ok(())
204
+ # }
205
+ ```
206
+
207
+ ### What the device says it can do
208
+
209
+ The same control-plane surface declares the device's own limits, and
210
+ `get_device_capabilities()` reduces the two trees to them: model, serial
211
+ and firmware version, the bounds on centre frequency and reference
212
+ level, and the decimation ladder's depth.
213
+
214
+ ```rust,no_run
215
+ # async fn caps() -> sdr_aaronia_rs::Result<()> {
216
+ use sdr_aaronia_rs::http_endpoints::{AuthMethod, HttpEndpointsClient};
217
+
218
+ let client = HttpEndpointsClient::new("http://localhost:54664".into(), AuthMethod::None)?;
219
+ let caps = client.get_device_capabilities().await;
220
+ println!("{:?} serial {:?}", caps.model, caps.serial);
221
+ if let Some(range) = caps.center_frequency {
222
+ println!("tunes {:.3}-{:.3} MHz", range.min / 1e6, range.max / 1e6);
223
+ }
224
+ // Highest first, and every rung one the device can actually be set to:
225
+ // its reported native rate is a measurement, so it is snapped to an
226
+ // exact ladder top before being halved down.
227
+ println!("sample rates: {:?}", caps.sample_rates());
228
+ # Ok(())
229
+ # }
230
+ ```
231
+
232
+ Every field is optional and independently so, because the answer feeds
233
+ things that must not be guessed. The SoapySDR plugin publishes exactly
234
+ these at probe time — a V6 ECO declares 5.5 MHz–8 GHz and −55…+23 dBm,
235
+ where the plugin previously advertised 10 Hz–6 GHz and −100…+10 dB for
236
+ every model — and falls back per field for a device that cannot answer.
237
+
238
+ **Clearing the device does not mean you are alone on the server.** One
239
+ of the causes it leaves standing is another client: the HTTP Server block
240
+ serves each connection a full copy of the stream and refuses none, so a
241
+ second client doubles the server's egress. Nothing in the HTTP surface
242
+ counts connections, and the loss under saturation lands on an arbitrary
243
+ subset of the clients — measured, three of five ran contiguous while two
244
+ lost data — so a clean stream is not evidence of being alone either. For
245
+ a definitive count, look at the RTSA host. See `docs/HTTPSPEC.md`,
246
+ "Several clients on one server block".
247
+
179
248
  ## Reusable configuration profiles
180
249
 
181
250
  Build your own configuration for specific bands:
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <stdint.h>
5
5
  #include <stdbool.h>
6
+ #include <stddef.h>
6
7
 
7
8
  #ifdef __cplusplus
8
9
  extern "C" {
@@ -58,6 +59,43 @@ typedef struct FfiSourceInfo {
58
59
  const char* device_serial;
59
60
  } FfiSourceInfo;
60
61
 
62
+ // --- What the device reports about itself --- //
63
+ //
64
+ // Optionality is explicit: 0.0 is a legitimate reference level and a
65
+ // legitimate step, so no numeric sentinel could distinguish "the device
66
+ // declares 0" from "the device did not say". Strings are NULL when
67
+ // absent, the ranges carry a has_ flag, and sample_rate_count == 0
68
+ // means the ladder could not be derived. Fall back per field, not on
69
+ // the whole struct.
70
+ typedef struct FfiDeviceCapabilities {
71
+ const char* model; // e.g. "SPECTRAN V6 ECO"; NULL when unknown
72
+ const char* serial; // NULL when unknown
73
+ const char* version; // firmware/FPGA revisions; NULL when unknown
74
+
75
+ bool has_center_frequency;
76
+ double center_frequency_min_hz;
77
+ double center_frequency_max_hz;
78
+ double center_frequency_step_hz; // 0.0 = device declares no step
79
+
80
+ bool has_reference_level;
81
+ double reference_level_min_dbm;
82
+ double reference_level_max_dbm;
83
+ double reference_level_step_db; // 0.0 = device declares no step
84
+
85
+ // Settable IQ sample rates in Hz, highest first. Owned by this
86
+ // struct; NULL and 0 when the device could not be asked.
87
+ size_t sample_rate_count;
88
+ const double* sample_rates;
89
+
90
+ // Stream-clock sources in the device's own vocabulary (a V6 ECO:
91
+ // Consumer, Oscillator, GPS, PPS, 10MHz, and three "... Provider"
92
+ // variants). Owned by this struct; NULL and 0 when it did not say.
93
+ size_t clock_source_count;
94
+ const char* const* clock_sources;
95
+ const char* clock_source; // currently selected; NULL if unknown
96
+ const char* rx_antenna; // e.g. "RX1"; NULL if the mode names none
97
+ } FfiDeviceCapabilities;
98
+
61
99
  // Opaque pointers
62
100
  typedef struct AaroniaSourceBuilder AaroniaSourceBuilder;
63
101
  typedef struct AaroniaSource AaroniaSource;
@@ -125,6 +163,15 @@ AaroniaFfiError aaronia_source_set_reference_level(AaroniaSource* source, double
125
163
  FfiSourceInfo* aaronia_source_get_source_info(AaroniaSource* source);
126
164
  void aaronia_source_info_free(FfiSourceInfo* info);
127
165
 
166
+ // Read the device's declared capabilities. BLOCKING: two control-plane
167
+ // GETs, so call it once and cache. Returns NULL only for a null source
168
+ // or when called from a current-thread tokio runtime; a device that
169
+ // cannot be asked yields a struct with every field absent. Answers for
170
+ // the HTTP backend — file and native-SDK sources report nothing, having
171
+ // no equivalent surface to ask.
172
+ FfiDeviceCapabilities* aaronia_source_get_capabilities(AaroniaSource* source);
173
+ void aaronia_source_capabilities_free(FfiDeviceCapabilities* caps);
174
+
128
175
  // --- Sink FFI --- //
129
176
  //
130
177
  // WARNING: the whole TX path is hardware-unverified (driven per the
@@ -148,6 +195,14 @@ typedef struct AaroniaSink AaroniaSink; // Opaque UnifiedSink
148
195
  #define AARONIA_TX_SEGMENT_END ((uint64_t)0x00000008)
149
196
  #define AARONIA_TX_PUSH ((uint64_t)0x00008000)
150
197
 
198
+ // True when this build carries the native-SDK transmit path. Ask before
199
+ // advertising a TX capability: aaronia_sink_build succeeds everywhere,
200
+ // so a non-null sink proves nothing. Compile-time only — it cannot say
201
+ // whether the attached device has a transmitter (a V6 ECO does not), and
202
+ // TX also requires the native-SDK source backend, so a device opened
203
+ // over HTTP or from a file has no TX path regardless.
204
+ bool aaronia_sink_supported(void);
205
+
151
206
  AaroniaSinkBuilder* aaronia_sink_builder_new(void);
152
207
  void aaronia_sink_builder_free(AaroniaSinkBuilder* builder);
153
208
  void aaronia_sink_builder_center_frequency(AaroniaSinkBuilder* builder, double hz);
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "python-aaronia"
3
- version = "0.7.7"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  description = "Python bindings for sdr-aaronia-rs (Aaronia SPECTRAN V6 SDR source)"
6
6
  license = "GPL-3.0-or-later"