python-aaronia 0.8.2__tar.gz → 0.9.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 (89) hide show
  1. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/CHANGELOG.md +129 -72
  2. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/Cargo.lock +2 -2
  3. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/Cargo.toml +1 -1
  4. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/PKG-INFO +1 -1
  5. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/PLUGINS.md +67 -1
  6. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/VERIFICATION.md +3 -1
  7. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/include/aaronia.h +47 -0
  8. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/python-aaronia/Cargo.toml +1 -1
  9. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/AaroniaSoapyDevice.cpp +173 -16
  10. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/AaroniaSoapyDevice.hpp +29 -0
  11. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/Registration.cpp +43 -11
  12. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/c_api.rs +190 -0
  13. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/file_source.rs +23 -1
  14. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/http_endpoints.rs +209 -0
  15. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/lib.rs +10 -7
  16. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/native_sdk.rs +37 -21
  17. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/unified_source.rs +38 -0
  18. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/live_smoke.rs +68 -0
  19. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/native_sdk_live.rs +11 -9
  20. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/.cargo/config.toml +0 -0
  21. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/.gitattributes +0 -0
  22. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/.gitignore +0 -0
  23. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/CONTRIBUTING.md +0 -0
  24. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/DESIGN.md +0 -0
  25. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/LICENSE +0 -0
  26. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/README.md +0 -0
  27. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/benches/decompress_block.rs +0 -0
  28. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/benches/deinterleave_dual_iq.rs +0 -0
  29. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/benches/parse_int16_packet.rs +0 -0
  30. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/benches/rtsa_open_and_read.rs +0 -0
  31. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/deny.toml +0 -0
  32. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/APPS.md +0 -0
  33. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/FILESPEC.md +0 -0
  34. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/HTTPSPEC.md +0 -0
  35. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/QUICKSTART.md +0 -0
  36. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/SDKSPEC.md +0 -0
  37. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/docs/USAGE.md +0 -0
  38. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/channel_hopping.rs +0 -0
  39. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/device_control.rs +0 -0
  40. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/dump_metadata.rs +0 -0
  41. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/http_iq_quickstart.rs +0 -0
  42. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/native_sdk_basic.rs +0 -0
  43. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/native_sdk_transmit.rs +0 -0
  44. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/noaa_scanner.rs +0 -0
  45. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/python_arrow_example.py +0 -0
  46. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/read_rtsa_file.rs +0 -0
  47. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/examples/soapy_python_example.py +0 -0
  48. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/packaging/homebrew/README.md +0 -0
  49. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  50. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/pyproject.toml +0 -0
  51. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/python-aaronia/README.md +0 -0
  52. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/python-aaronia/aaronia.pyi +0 -0
  53. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/python-aaronia/src/lib.rs +0 -0
  54. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/python-aaronia/test_basic.py +0 -0
  55. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/scripts/ci-local.sh +0 -0
  56. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/scripts/fake-rtsa-server.py +0 -0
  57. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/scripts/native-sdk-validate.ps1 +0 -0
  58. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/scripts/sdk-container-test.sh +0 -0
  59. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/scripts/validate-iq-live.py +0 -0
  60. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/CMakeLists.txt +0 -0
  61. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/README.md +0 -0
  62. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  63. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/soapy-aaronia/packaging/install.sh +0 -0
  64. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/decompression.rs +0 -0
  65. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/detection.rs +0 -0
  66. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/error.rs +0 -0
  67. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/http_sink.rs +0 -0
  68. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/http_source.rs +0 -0
  69. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/http_streaming.rs +0 -0
  70. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/link_budget.rs +0 -0
  71. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/sdk_sink.rs +0 -0
  72. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/sdk_source.rs +0 -0
  73. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/sdr_source_impl.rs +0 -0
  74. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/seify_impl.rs +0 -0
  75. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/unified_sink.rs +0 -0
  76. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/src/utils.rs +0 -0
  77. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/c_api_test.rs +0 -0
  78. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/http_mock_test.rs +0 -0
  79. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/http_resilience_test.rs +0 -0
  80. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/http_sink_test.rs +0 -0
  81. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/integration_test.rs +0 -0
  82. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/native_sdk_load.rs +0 -0
  83. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/properties.proptest-regressions +0 -0
  84. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/properties.rs +0 -0
  85. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/rtsa_negative_test.rs +0 -0
  86. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/sdr_source_impl_test.rs +0 -0
  87. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/spec_coverage.rs +0 -0
  88. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/test_cw_mag.rs +0 -0
  89. {python_aaronia-0.8.2 → python_aaronia-0.9.0}/tests/test_cw_meta.rs +0 -0
@@ -4,80 +4,59 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ### Performance
8
- - **The int16 and float16 IQ decoders are ~23% faster end to end.** Both
9
- built their output with `Vec::with_capacity` then `push` per sample,
10
- which carries a capacity check the compiler cannot elide and which was
11
- blocking vectorisation. Collecting from the slice iterator instead —
12
- `TrustedLen`, so the vector is sized once — measured 495 to 2433 MS/s
13
- on the decode loop alone, and 123.5 to 152 MS/s over the whole HTTP
14
- path including framing and transport. float32 was already a single
15
- `copy_nonoverlapping` and is unchanged.
16
-
17
- Worth stating what this does not buy: at 605 MB/s the crate is about
18
- 2.5x the fastest a V6 can produce and 6x a WiFi 6E link, so it was not
19
- the bottleneck before and is not now. What it buys is CPU left over
20
- for whatever consumes the samples.
21
-
22
7
  ### Added
23
- - **`scripts/fake-rtsa-server.py`** serves `/stream` in the real wire
24
- format at loopback speed, so a decode path can be measured without a
25
- device or a network. It is how the figures above were taken: over any
26
- real link the transport dominates and a CPU change is invisible. Its
27
- docstring carries the caveat that goes with it — over loopback hyper's
28
- adaptive read buffer never leaves its 8 KiB floor, so kernel time there
29
- is a property of the harness, not of the crate.
8
+ - **SoapySDR: device sensors.** The plugin exposed one sensor
9
+ (`cumulative_drops`); it now surfaces the device's live telemetry from
10
+ `/healthstatus` — FPGA and frontend temperature, ADC headroom (dB below
11
+ full scale, so a client can raise the reference level before it clips),
12
+ USB and DSP buffer fill, error and overflow rates, GPS satellites and
13
+ position. `listSensors` / `readSensor` / `getSensorInfo`, read through a
14
+ dedicated connection and briefly cached, so polling them during a
15
+ capture neither stalls the sample stream nor costs a fetch per key.
16
+ HTTP backend only: the native SDK's own `AARTSAAPI_ConfigHealth` tree
17
+ reads all zeros in raw-SDK mode — the live telemetry is computed and
18
+ populated by RTSA-Suite, the managing application, not by the raw SDK,
19
+ confirmed by dumping the tree live from a V6 ECO — so over `sdk=true`
20
+ the plugin reports only `cumulative_drops`, which is real and
21
+ client-side. Verified live over HTTP on a V6 ECO.
22
+ - **SoapySDR: bandwidth API.** `setBandwidth` / `getBandwidth` /
23
+ `listBandwidths` / `getBandwidthRange`. The device's alias-free
24
+ bandwidth is 0.8x its sample rate; these expose that as SoapySDR's
25
+ separate knob, mapping a requested bandwidth to a sample rate and
26
+ driving the already-verified `setSampleRate` — no new device-write path.
27
+ Verified on a V6 ECO: 15.36 MS/s reports 12.288 MHz, `setBandwidth`
28
+ snaps to a rung.
29
+ - **SoapySDR: the native SDK is discoverable.** `find` advertises a
30
+ second `sdk=true` device beside the HTTP one whenever the SDK is
31
+ installed, and `sdk=true` / `serial=` force the native backend instead
32
+ of silently falling back to localhost HTTP. Verified streaming 15.357
33
+ MS/s over the SDK through the plugin from Python.
34
+ - **C ABI.** `aaronia_source_read_sensors` (fills a value struct, `NaN`
35
+ for absent — no ownership, nothing to free);
36
+ `aaronia_source_builder_force_source_type` and `aaronia_sdk_installed`,
37
+ the C equivalent of `force_native_sdk`, which C had no way to request;
38
+ and the stateless bandwidth helpers `aaronia_usable_bandwidth_hz` /
39
+ `aaronia_iq_sample_rate_for_bandwidth`. `DeviceSensors` is the Rust type
40
+ behind the first.
30
41
 
31
- ### Documentation
32
- - **`rate_reduction` is time compression, not a sample-rate divider.**
33
- Five places described it as reducing the rate or optimising bandwidth.
34
- It thins frames over time — the operation the `waterfall` payload is
35
- described by — so a continuous IQ stream, having no frames, is
36
- unaffected: measured at factors of 2, 10 and 64, `sampleFrequency`
37
- holds at 15,359,988 Hz and the byte rate does not move. That is the
38
- parameter behaving as specified on a payload it was not meant for.
39
- `live_stream_rate_reduction_and_scale` had asserted only that packets
40
- arrived, which is true either way, so nothing caught the wrong
41
- description; it now pins the IQ behaviour.
42
- - **The stream can be compressed, up to 6.55x, via `format=rtsa`.**
43
- Captured from RTSA-Suite's own HTTP Client block:
44
- `GET /stream?format=rtsa&rate_reduction=8&input=main&compression=5&rate_adaption=0`.
45
- `format=rtsa` streams the file container and is the only format that
46
- accepts `compression=N`, which applies the file format's own lossy
47
- codec. The container carries `float32` (`mSampleType` 11, `DSST_F32N`),
48
- so level 0 is plain float32 with under 1% of chunk overhead — 123.0
49
- MB/s against 122.9 theoretical at 15.36 MS/s. Against that baseline the
50
- codec buys 2.74x at level 1, 4.43x at level 5 and 13.10x at level 9;
51
- even level 1 undercuts plain `int16` while carrying float precision.
52
- Ratios hold at 3.84 MS/s too.
53
-
54
- HTTPSPEC had documented `format=rtsa` only as the thing a typo falls
55
- back to — "a completely different wire format rather than an error" —
56
- and this release had gone on to claim compression was neither offered
57
- nor useful. Both are corrected. Generic HTTP compression is still not
58
- available on `/stream` and still would not help (zlib manages 1.07x on
59
- `float32`); Aaronia's codec wins by being lossy and signal-aware.
60
-
61
- This crate cannot use it for IQ, tested rather than assumed: a real
62
- compressed payload handed to `Decompressor::decompress` comes back
63
- rejected as proprietary `DSPT_IQ`, the same wall that stops compressed
64
- IQ *files*. `format=rtsa` also defaults to `mCompression=1`, so only
65
- `compression=0` is decodable and that is 20% larger than plain `int16`.
66
- Spectra should decode, `DSPT_SPECTRA` being documented, but this
67
- mission has no spectra input to try.
68
-
69
- ### Performance
70
- - **The control plane is now requested compressed.** `reqwest` gains the
71
- `deflate` and `gzip` features, so the client sends
72
- `Accept-Encoding: gzip,deflate` where it previously sent none. Measured
73
- against the device: `/remoteconfig` 17,432 bytes to 3,299 deflated,
74
- `/healthstatus` 6,100 to 1,436. Both are read at every device open, and
75
- `/healthstatus` again on each stream-gap report. No effect on `/stream`,
76
- which the server does not compress at any level.
77
- - The reader channel's size comment claimed ~157 KiB chunks and ~10 MB of
78
- queue. Measured against a live server it is 64 KiB for 71% of chunks,
79
- so the queue is ~4 MB, about 45 ms at the 88 MB/s a WiFi 6E path
80
- delivers.
42
+ ### Fixed
43
+ - **File playback reported the decompression time as the capture time.**
44
+ A DSPT_IQ-compressed `.rtsa` is decompressed through RTSAFileTool, which
45
+ writes a fresh header stamped with the moment of conversion, so the
46
+ re-opened file reported *now* — a 2020 capture read as 2026. The
47
+ original header's `creation_time`, parsed before compression was
48
+ detected, is now carried across the decompression, and the derived
49
+ start/end fall-backs follow it. Surfaced the first time the fixture test
50
+ ran on a machine with RTSA-Suite installed.
51
+ - **The SoapySDR plugin could not load the SDK inside a host that carries
52
+ its own Qt6.** The loader was given `LOAD_LIBRARY_SEARCH_DEFAULT_DIRS`,
53
+ which searches the host application's directory first; a GNU Radio
54
+ (radioconda) install holds a different Qt6 build there under the same
55
+ names, and the load failed. It now searches only the DLL's own
56
+ directory, the SDK install root, and System32.
57
+ - **The Seify native-SDK test panicked on teardown.** `AaroniaSeifyDevice`
58
+ owns a tokio runtime, and dropping one inside an async context is a
59
+ tokio panic; the test is synchronous now.
81
60
 
82
61
  ## [v0.8.2] - 2026-09-08
83
62
 
@@ -169,6 +148,84 @@ MSVC against radioconda's SoapySDR, streams 15.357 MS/s over the SDK
169
148
  into Python; see [docs/VERIFICATION.md](docs/VERIFICATION.md) for the
170
149
  two limits found on the way.
171
150
 
151
+
152
+ _Also in 0.8.2 (recorded here on the 0.9.0 pass; these landed across the 0.8.x docs/perf commits and were never restamped):_
153
+
154
+ ### Performance
155
+ - **The int16 and float16 IQ decoders are ~23% faster end to end.** Both
156
+ built their output with `Vec::with_capacity` then `push` per sample,
157
+ which carries a capacity check the compiler cannot elide and which was
158
+ blocking vectorisation. Collecting from the slice iterator instead —
159
+ `TrustedLen`, so the vector is sized once — measured 495 to 2433 MS/s
160
+ on the decode loop alone, and 123.5 to 152 MS/s over the whole HTTP
161
+ path including framing and transport. float32 was already a single
162
+ `copy_nonoverlapping` and is unchanged.
163
+
164
+ Worth stating what this does not buy: at 605 MB/s the crate is about
165
+ 2.5x the fastest a V6 can produce and 6x a WiFi 6E link, so it was not
166
+ the bottleneck before and is not now. What it buys is CPU left over
167
+ for whatever consumes the samples.
168
+
169
+ ### Added
170
+ - **`scripts/fake-rtsa-server.py`** serves `/stream` in the real wire
171
+ format at loopback speed, so a decode path can be measured without a
172
+ device or a network. It is how the figures above were taken: over any
173
+ real link the transport dominates and a CPU change is invisible. Its
174
+ docstring carries the caveat that goes with it — over loopback hyper's
175
+ adaptive read buffer never leaves its 8 KiB floor, so kernel time there
176
+ is a property of the harness, not of the crate.
177
+
178
+ ### Documentation
179
+ - **`rate_reduction` is time compression, not a sample-rate divider.**
180
+ Five places described it as reducing the rate or optimising bandwidth.
181
+ It thins frames over time — the operation the `waterfall` payload is
182
+ described by — so a continuous IQ stream, having no frames, is
183
+ unaffected: measured at factors of 2, 10 and 64, `sampleFrequency`
184
+ holds at 15,359,988 Hz and the byte rate does not move. That is the
185
+ parameter behaving as specified on a payload it was not meant for.
186
+ `live_stream_rate_reduction_and_scale` had asserted only that packets
187
+ arrived, which is true either way, so nothing caught the wrong
188
+ description; it now pins the IQ behaviour.
189
+ - **The stream can be compressed, up to 6.55x, via `format=rtsa`.**
190
+ Captured from RTSA-Suite's own HTTP Client block:
191
+ `GET /stream?format=rtsa&rate_reduction=8&input=main&compression=5&rate_adaption=0`.
192
+ `format=rtsa` streams the file container and is the only format that
193
+ accepts `compression=N`, which applies the file format's own lossy
194
+ codec. The container carries `float32` (`mSampleType` 11, `DSST_F32N`),
195
+ so level 0 is plain float32 with under 1% of chunk overhead — 123.0
196
+ MB/s against 122.9 theoretical at 15.36 MS/s. Against that baseline the
197
+ codec buys 2.74x at level 1, 4.43x at level 5 and 13.10x at level 9;
198
+ even level 1 undercuts plain `int16` while carrying float precision.
199
+ Ratios hold at 3.84 MS/s too.
200
+
201
+ HTTPSPEC had documented `format=rtsa` only as the thing a typo falls
202
+ back to — "a completely different wire format rather than an error" —
203
+ and this release had gone on to claim compression was neither offered
204
+ nor useful. Both are corrected. Generic HTTP compression is still not
205
+ available on `/stream` and still would not help (zlib manages 1.07x on
206
+ `float32`); Aaronia's codec wins by being lossy and signal-aware.
207
+
208
+ This crate cannot use it for IQ, tested rather than assumed: a real
209
+ compressed payload handed to `Decompressor::decompress` comes back
210
+ rejected as proprietary `DSPT_IQ`, the same wall that stops compressed
211
+ IQ *files*. `format=rtsa` also defaults to `mCompression=1`, so only
212
+ `compression=0` is decodable and that is 20% larger than plain `int16`.
213
+ Spectra should decode, `DSPT_SPECTRA` being documented, but this
214
+ mission has no spectra input to try.
215
+
216
+ ### Performance
217
+ - **The control plane is now requested compressed.** `reqwest` gains the
218
+ `deflate` and `gzip` features, so the client sends
219
+ `Accept-Encoding: gzip,deflate` where it previously sent none. Measured
220
+ against the device: `/remoteconfig` 17,432 bytes to 3,299 deflated,
221
+ `/healthstatus` 6,100 to 1,436. Both are read at every device open, and
222
+ `/healthstatus` again on each stream-gap report. No effect on `/stream`,
223
+ which the server does not compress at any level.
224
+ - The reader channel's size comment claimed ~157 KiB chunks and ~10 MB of
225
+ queue. Measured against a live server it is 64 KiB for 71% of chunks,
226
+ so the queue is ~4 MB, about 45 ms at the 88 MB/s a WiFi 6E path
227
+ delivers.
228
+
172
229
  ## [v0.8.1] - 2026-09-07
173
230
 
174
231
  ### Fixed
@@ -3092,7 +3092,7 @@ dependencies = [
3092
3092
 
3093
3093
  [[package]]
3094
3094
  name = "python-aaronia"
3095
- version = "0.8.2"
3095
+ version = "0.9.0"
3096
3096
  dependencies = [
3097
3097
  "arrow",
3098
3098
  "num-complex",
@@ -3621,7 +3621,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3621
3621
 
3622
3622
  [[package]]
3623
3623
  name = "sdr-aaronia-rs"
3624
- version = "0.8.2"
3624
+ version = "0.9.0"
3625
3625
  dependencies = [
3626
3626
  "anyhow",
3627
3627
  "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.8.2"
5
+ version = "0.9.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.8.2
3
+ Version: 0.9.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+)
@@ -23,6 +23,11 @@ crate built with both features — `features = ["seify", "native-sdk"]` —
23
23
  on Windows or Linux with RTSA-Suite PRO installed. Without the feature
24
24
  the request is a clean error, never a fallback to HTTP.
25
25
 
26
+ `AaroniaSeifyDevice` owns a tokio runtime. Drop it from synchronous
27
+ code: dropping it inside an `async` context (a `#[tokio::test]`, a task)
28
+ is a tokio panic, "Cannot drop a runtime in a context where blocking is
29
+ not allowed".
30
+
26
31
  ```rust
27
32
  use sdr_aaronia_rs::seify_impl::AaroniaSeifyDevice;
28
33
  use seify::{Args, RxDevice, RxStreamer, DeviceInfo};
@@ -95,13 +100,29 @@ SoapySDRUtil --info
95
100
 
96
101
  ### Usage
97
102
 
98
- The driver expects the `url` argument to connect to the Spectran V6 RTSA HTTP server.
103
+ `url=` connects to an RTSA-Suite HTTP server block, `file=` plays back a
104
+ recording, and `sdk=true` (or `serial=<device serial>`) opens the device
105
+ through the Aaronia native SDK — a build with `-DAARONIA_NATIVE_SDK=ON`
106
+ on a machine with RTSA-Suite PRO installed. A bare `driver=aaronia` keeps
107
+ its old meaning, the HTTP server on localhost; when the SDK is installed,
108
+ `SoapySDRUtil --find` lists a second, `sdk=true` entry beside it.
99
109
 
100
110
  ```bash
101
111
  # Example testing with SoapySDRUtil
102
112
  SoapySDRUtil --probe="driver=aaronia,url=http://localhost:54664"
113
+ SoapySDRUtil --probe="driver=aaronia,sdk=true"
103
114
  ```
104
115
 
116
+ Two things learned running the plugin on Windows inside a GNU Radio
117
+ (radioconda) install: that build's Python binding rejects the `dict`
118
+ form for every driver — `SoapySDR.Device("driver=aaronia,sdk=true")`
119
+ works where `SoapySDR.Device(dict(driver="aaronia", sdk="true"))` raises
120
+ "no match" — and a host process that has already loaded its own Qt6
121
+ (GNU Radio Companion itself) cannot also load the Aaronia SDK, which
122
+ brings a different Qt6; use `url=` from inside GRC. Loading the SDK from
123
+ a host whose *directory* merely contains Qt6 copies, such as
124
+ `SoapySDRUtil` in radioconda's `Library\bin`, works since 0.8.3.
125
+
105
126
  In Python (using `SoapySDR` python bindings):
106
127
  ```python
107
128
  import SoapySDR
@@ -161,3 +182,48 @@ In SoapySDR, you can access these metrics via the `readSensor()` API:
161
182
  drops = sdr.readSensor("cumulative_drops")
162
183
  print(f"Gaps seen: {drops}")
163
184
  ```
185
+
186
+ ### Sensors
187
+
188
+ Over the **HTTP backend** the plugin also surfaces the device's live
189
+ telemetry from `/healthstatus` as SoapySDR sensors. `listSensors()`
190
+ returns only those the device is currently reporting:
191
+
192
+ ```python
193
+ for name in sdr.listSensors():
194
+ info = sdr.getSensorInfo(name)
195
+ print(f"{name} = {sdr.readSensor(name)} {info.units}")
196
+ # fpga_temp = 54.3 C
197
+ # frontend_temp = 65.1 C
198
+ # adc_range = 29.4 dB # headroom below full scale; near 0 is close to clipping
199
+ # usb_buffer = 0.0625 # transfer-buffer fill, fraction 0-1
200
+ # dsp_buffer = 0
201
+ # gps_satellites = 0
202
+ # ...plus cumulative_drops
203
+ ```
204
+
205
+ `adc_range` is the one to watch when setting the reference level, and a
206
+ climbing `usb_buffer` is the first sign the host is not draining the
207
+ stream fast enough. The reads are briefly cached, so a probe's burst of
208
+ `readSensor` calls costs one HTTP fetch, and they go through a separate
209
+ connection from the sample stream — polling sensors during a capture does
210
+ not stall sample delivery. Over the **native SDK** backend only
211
+ `cumulative_drops` is reported: the raw SDK's own `AARTSAAPI_ConfigHealth`
212
+ tree reads all zeros (the live telemetry is populated by RTSA-Suite, the
213
+ managing application, not by the raw SDK), so exposing it there would
214
+ report a device permanently at 0 °C.
215
+
216
+ ### Bandwidth
217
+
218
+ SoapySDR keeps sample rate and analog/usable bandwidth as separate knobs.
219
+ The RTSA's alias-free bandwidth is ~0.8x its sample rate, exposed here:
220
+
221
+ ```python
222
+ sdr.setBandwidth(SoapySDR.SOAPY_SDR_RX, 0, 10e6) # asks for ~10 MHz usable
223
+ print(sdr.getBandwidth(SoapySDR.SOAPY_SDR_RX, 0)) # the bandwidth actually delivered
224
+ print(sdr.listBandwidths(SoapySDR.SOAPY_SDR_RX, 0))
225
+ ```
226
+
227
+ `setBandwidth` maps the request to the nearest sample-rate rung and
228
+ drives `setSampleRate`; the two stay consistent, so setting either
229
+ updates the other.
@@ -38,7 +38,9 @@ full V6 are marked unverified.
38
38
  | Native SDK from Python (`aaronia.open(sdk=True)`) | Native SDK | **Verified manually** on the same machine against radioconda's Python 3.12: 15.363 MS/s of a 15.36 MS/s request into NumPy, retune, timestamps and drop counters populated |
39
39
  | Native SDK from the C ABI | Native SDK | **Live-verified**: a builder with neither URL nor file auto-detects the SDK; there is still no explicit selector over C |
40
40
  | Native SDK from Seify (`sdk=true`) | Native SDK | Compiled with `--features seify,native-sdk`; the live test exists but was not run on this pass |
41
- | SoapySDR plugin over the native SDK | Native SDK | **Verified manually** on Windows 11: built with MSVC (VS 2022 Build Tools) against radioconda's SoapySDR 0.8.1, loads into it, and streams 15.357 MS/s of a 15.36 MS/s request into Python via `driver=aaronia,serial=…` (`SoapySDR.Device("driver=aaronia,serial=…")` — that build's binding rejects the `dict` form for every driver). The plugin reaches the SDK only through `serial=`; a bare `driver=aaronia` still pins to localhost HTTP. In the C++ host `SoapySDRUtil`, which sits beside radioconda's own Qt6 copies, the SDK library failed to load: the loader searched the host's directory ahead of the SDK's, fixed after 0.8.2 |
41
+ | SoapySDR plugin over the native SDK | Native SDK | **Verified manually** on Windows 11: built with MSVC (VS 2022 Build Tools) against radioconda's SoapySDR 0.8.1, loads into it, and streams 15.357 MS/s of a 15.36 MS/s request into Python. `find` advertises a `sdk=true` device when the SDK is installed, and `sdk=true`/`serial=` open it; `SoapySDR.Device("driver=aaronia,sdk=true")` works (that build's binding rejects the `dict` form for every driver). The C++-host DLL-load failure — the loader reaching radioconda's own Qt6 first — is fixed in 0.9.0 (search restricted to the DLL dir, SDK root, System32) |
42
+ | SoapySDR bandwidth API (`setBandwidth`, `getBandwidth`, `listBandwidths`, `getBandwidthRange`) | Both | **Verified manually** on a V6 ECO over the native SDK: `getBandwidth` at 15.36 MS/s reports 12.288 MHz (0.8x), the ladder lists one usable bandwidth per rung, and `setBandwidth` snaps to a rung through the live-verified `setSampleRate`. Pure reparametrization, so the HTTP path follows |
43
+ | SoapySDR sensors (temperatures, ADC range, buffer fill, GPS) | HTTP | **Live-verified** on a V6 ECO: the crate's `live_device_sensors` reads real values over HTTP (FPGA temp, ADC range), and the plugin's `readSensor`/`listSensors` return the full set through `SoapySDRUtil` / Python. Parser also unit-tested against a `/healthstatus`-shaped tree. Over the native SDK the plugin reports only `cumulative_drops`: the raw SDK's `AARTSAAPI_ConfigHealth` tree reads all zeros (dumped live), the live telemetry being RTSA-Suite's, not the raw SDK's — so rich sensors are HTTP-only by design |
42
44
  | HTTP TX push (`/sample`) | HTTP | Endpoint exercised live; RF output not measured |
43
45
 
44
46
  "Live-verified" means an `#[ignore]`d test in
@@ -96,6 +96,24 @@ typedef struct FfiDeviceCapabilities {
96
96
  const char* rx_antenna; // e.g. "RX1"; NULL if the mode names none
97
97
  } FfiDeviceCapabilities;
98
98
 
99
+ /* Live sensor readings. A plain value struct the caller allocates; every
100
+ * field is a reading or NaN for "not reported". No ownership, nothing to
101
+ * free. Fill it with aaronia_source_read_sensors. */
102
+ typedef struct FfiDeviceSensors {
103
+ double fpga_temp_c;
104
+ double frontend_temp_c;
105
+ double board_power_w;
106
+ double adc_range_db; // ADC headroom below full scale, dB
107
+ double usb_buffer_fill; // fraction 0.0-1.0
108
+ double dsp_buffer_fill; // fraction 0.0-1.0
109
+ double errors_per_second;
110
+ double usb_overflows_per_second;
111
+ double dsp_overflows_per_second;
112
+ double gps_satellites;
113
+ double gps_latitude; // degrees; 0 with no fix
114
+ double gps_longitude; // degrees; 0 with no fix
115
+ } FfiDeviceSensors;
116
+
99
117
  // Opaque pointers
100
118
  typedef struct AaroniaSourceBuilder AaroniaSourceBuilder;
101
119
  typedef struct AaroniaSource AaroniaSource;
@@ -116,6 +134,24 @@ void aaronia_source_builder_reference_level(AaroniaSourceBuilder* builder, doubl
116
134
  void aaronia_source_builder_http_source(AaroniaSourceBuilder* builder, const char* base_url);
117
135
  void aaronia_source_builder_file_source(AaroniaSourceBuilder* builder, const char* file_path);
118
136
  void aaronia_source_builder_device_serial(AaroniaSourceBuilder* builder, const char* serial);
137
+
138
+ /* Pin the source to one backend instead of auto-detecting. Forcing
139
+ * NativeSdk makes a missing SDK a build error rather than a silent
140
+ * fallback to localhost HTTP. */
141
+ void aaronia_source_builder_force_source_type(AaroniaSourceBuilder *builder,
142
+ CAaroniaSourceType source_type);
143
+
144
+ /* Whether the native SDK library is present, by the search the builder
145
+ * uses; does not load it. */
146
+ bool aaronia_sdk_installed(void);
147
+
148
+ /* Alias-free bandwidth (Hz) delivered at an IQ sample rate; smaller than
149
+ * the rate. Stateless. */
150
+ double aaronia_usable_bandwidth_hz(double sample_rate_hz);
151
+
152
+ /* The IQ sample rate (Hz) whose alias-free bandwidth covers bandwidth_hz;
153
+ * the inverse of aaronia_usable_bandwidth_hz. Stateless. */
154
+ double aaronia_iq_sample_rate_for_bandwidth(double bandwidth_hz);
119
155
  // RX channel selection (native-SDK backend): 0 = Rx1 (default),
120
156
  // 1 = Rx2, 2 = Rx1+Rx2 dual capture (read with
121
157
  // aaronia_source_read_samples_dual). Other values ignored.
@@ -172,6 +208,12 @@ void aaronia_source_info_free(FfiSourceInfo* info);
172
208
  FfiDeviceCapabilities* aaronia_source_get_capabilities(AaroniaSource* source);
173
209
  void aaronia_source_capabilities_free(FfiDeviceCapabilities* caps);
174
210
 
211
+ /* Fill *out with the device's live sensors. Returns true when the read
212
+ * completed (fields may be NaN where unreported; file/native-SDK backends
213
+ * complete all-NaN), false (out untouched) for a null pointer or a
214
+ * current-thread runtime. Blocking: one /healthstatus GET on HTTP. */
215
+ bool aaronia_source_read_sensors(AaroniaSource* source, FfiDeviceSensors* out);
216
+
175
217
  // --- Sink FFI --- //
176
218
  //
177
219
  // WARNING: the whole TX path is hardware-unverified (driven per the
@@ -233,6 +275,11 @@ AaroniaFfiError aaronia_sink_write_samples(
233
275
  HttpEndpointsClient* aaronia_endpoints_client_new(const char* base_url);
234
276
  void aaronia_endpoints_client_free(HttpEndpointsClient* client);
235
277
  FfiServerInfo* aaronia_endpoints_client_get_info(HttpEndpointsClient* client);
278
+
279
+ /* Fill *out with the device's live sensors through a standalone client,
280
+ * off the streaming source's read lock. Same reading and return contract
281
+ * as aaronia_source_read_sensors. */
282
+ bool aaronia_endpoints_client_read_sensors(HttpEndpointsClient* client, FfiDeviceSensors* out);
236
283
  void aaronia_server_info_free(FfiServerInfo* info);
237
284
  AaroniaFfiError aaronia_endpoints_client_control_streaming(HttpEndpointsClient* client, bool start);
238
285
  AaroniaFfiError aaronia_endpoints_client_control_recording(HttpEndpointsClient* client, bool start, const char* name);
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "python-aaronia"
3
- version = "0.8.2"
3
+ version = "0.9.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"