python-aaronia 0.8.1__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 (90) hide show
  1. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/CHANGELOG.md +224 -0
  2. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/CONTRIBUTING.md +14 -0
  3. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/Cargo.lock +80 -2
  4. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/Cargo.toml +13 -2
  5. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/DESIGN.md +1 -1
  6. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/PKG-INFO +19 -1
  7. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/PLUGINS.md +82 -1
  8. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/APPS.md +3 -2
  9. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/FILESPEC.md +9 -16
  10. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/HTTPSPEC.md +148 -46
  11. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/QUICKSTART.md +4 -9
  12. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/SDKSPEC.md +87 -11
  13. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/docs/USAGE.md +1 -1
  14. python_aaronia-0.9.0/docs/VERIFICATION.md +79 -0
  15. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/include/aaronia.h +47 -0
  16. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/python-aaronia/Cargo.toml +8 -1
  17. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/python-aaronia/README.md +18 -0
  18. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/python-aaronia/aaronia.pyi +3 -0
  19. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/python-aaronia/src/lib.rs +47 -9
  20. python_aaronia-0.9.0/scripts/fake-rtsa-server.py +83 -0
  21. python_aaronia-0.9.0/scripts/native-sdk-validate.ps1 +139 -0
  22. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/AaroniaSoapyDevice.cpp +173 -16
  23. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/AaroniaSoapyDevice.hpp +29 -0
  24. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/CMakeLists.txt +7 -1
  25. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/Registration.cpp +43 -11
  26. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/c_api.rs +190 -0
  27. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/detection.rs +4 -1
  28. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/file_source.rs +23 -1
  29. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/http_endpoints.rs +209 -0
  30. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/http_source.rs +8 -5
  31. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/http_streaming.rs +27 -25
  32. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/lib.rs +10 -7
  33. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/native_sdk.rs +344 -50
  34. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/sdk_source.rs +12 -9
  35. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/unified_source.rs +208 -16
  36. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/live_smoke.rs +100 -0
  37. python_aaronia-0.9.0/tests/native_sdk_live.rs +650 -0
  38. python_aaronia-0.8.1/docs/VERIFICATION.md +0 -60
  39. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/.cargo/config.toml +0 -0
  40. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/.gitattributes +0 -0
  41. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/.gitignore +0 -0
  42. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/LICENSE +0 -0
  43. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/README.md +0 -0
  44. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/benches/decompress_block.rs +0 -0
  45. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/benches/deinterleave_dual_iq.rs +0 -0
  46. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/benches/parse_int16_packet.rs +0 -0
  47. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/benches/rtsa_open_and_read.rs +0 -0
  48. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/deny.toml +0 -0
  49. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/channel_hopping.rs +0 -0
  50. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/device_control.rs +0 -0
  51. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/dump_metadata.rs +0 -0
  52. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/http_iq_quickstart.rs +0 -0
  53. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/native_sdk_basic.rs +0 -0
  54. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/native_sdk_transmit.rs +0 -0
  55. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/noaa_scanner.rs +0 -0
  56. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/python_arrow_example.py +0 -0
  57. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/read_rtsa_file.rs +0 -0
  58. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/examples/soapy_python_example.py +0 -0
  59. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/packaging/homebrew/README.md +0 -0
  60. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  61. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/pyproject.toml +0 -0
  62. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/python-aaronia/test_basic.py +0 -0
  63. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/scripts/ci-local.sh +0 -0
  64. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/scripts/sdk-container-test.sh +0 -0
  65. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/scripts/validate-iq-live.py +0 -0
  66. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/README.md +0 -0
  67. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  68. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/soapy-aaronia/packaging/install.sh +0 -0
  69. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/decompression.rs +0 -0
  70. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/error.rs +0 -0
  71. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/http_sink.rs +0 -0
  72. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/link_budget.rs +0 -0
  73. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/sdk_sink.rs +0 -0
  74. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/sdr_source_impl.rs +0 -0
  75. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/seify_impl.rs +0 -0
  76. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/unified_sink.rs +0 -0
  77. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/src/utils.rs +0 -0
  78. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/c_api_test.rs +0 -0
  79. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/http_mock_test.rs +0 -0
  80. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/http_resilience_test.rs +0 -0
  81. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/http_sink_test.rs +0 -0
  82. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/integration_test.rs +0 -0
  83. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/native_sdk_load.rs +0 -0
  84. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/properties.proptest-regressions +0 -0
  85. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/properties.rs +0 -0
  86. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/rtsa_negative_test.rs +0 -0
  87. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/sdr_source_impl_test.rs +0 -0
  88. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/spec_coverage.rs +0 -0
  89. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/test_cw_mag.rs +0 -0
  90. {python_aaronia-0.8.1 → python_aaronia-0.9.0}/tests/test_cw_meta.rs +0 -0
@@ -2,6 +2,230 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ### Added
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.
41
+
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.
60
+
61
+ ## [v0.8.2] - 2026-09-08
62
+
63
+ The native-SDK backend, validated on a Spectran V6 ECO on Windows 11 for
64
+ the first time. Every item below was found by running against the device;
65
+ the suite that found them ships as `tests/native_sdk_live.rs`.
66
+
67
+ ### Fixed
68
+
69
+ - **The SDK library could not load on a stock Windows install.** RTSA-Suite
70
+ PRO puts `AaroniaRTSAAPI.dll` in `sdk\` and its ~23 dependencies (Qt6,
71
+ avcodec, libcrypto, …) in the install root; `LoadLibraryExW` searches
72
+ neither, so detection found the file and the load failed with a bare
73
+ `LoadLibraryExW failed`. The install root is added to the DLL search list
74
+ (`AddDllDirectory`) and the library loaded with the
75
+ `LOAD_LIBRARY_SEARCH_*` flags that consult it — not `SetDllDirectory`,
76
+ which would switch safe search mode off for the whole host process.
77
+ - **A V6 ECO was never found.** `init_native_sdk` enumerated the family
78
+ `spectranv6` only; the ECO answers to `spectranv6eco`, so a machine
79
+ holding one reported "No Spectran V6 devices found" with the device on
80
+ the bus. Each known family is now tried, as `detect_device_family`
81
+ already documented.
82
+ - **ECO IQ came from the spectrum pipeline at 0.4 MS/s.** The ECO's raw-IQ
83
+ mode was mapped to `spectranv6eco/rtsa`, which is its spectrum pipeline
84
+ (`RawSpectrumEco.cpp`); IQ read from it arrived at ~0.4 MS/s whatever
85
+ rate was asked for. `spectranv6eco/iqreceiver` — the mode Aaronia's own
86
+ `IQReceiverEco.cpp` opens — is used instead. `/raw` also opens on an ECO
87
+ but has no `main/spanfreq`, so it cannot honour a requested rate.
88
+ - **The ECO delivered 1.5× the requested rate.** In `iqreceiver` mode
89
+ `main/spanfreq` is a bandwidth and the pipeline streams at 1.5× it —
90
+ every rung exactly, 10 MHz → 15.0 MS/s, 15.36 → 23.04 — up to a
91
+ 59.2 MS/s USB ceiling. The request is now translated so the caller gets
92
+ the rate it named on every backend: a 15.36 MS/s request measures
93
+ 15.360 MS/s over 153 M samples.
94
+ - **Native-SDK loss was invisible.** Packet flags `WARN_DROPPED` and
95
+ `TIME_DISCONTINUITY` were logged at debug level and nothing else, so
96
+ `cumulative_drops()` read 0 through any amount of loss and
97
+ `last_timestamp_ns()` read 0 always. Both, and `take_overrun()`, now
98
+ report the native source's packets. `WARN_INACCURATE` is deliberately
99
+ not an overrun: the ECO's fractional resampler sets it routinely.
100
+ - **`sample_rate_hz()` echoed the request on native sources.** It now
101
+ reports the rate the device's packets carry once one has been read, as
102
+ the HTTP backend already did.
103
+ - **`read_samples` and `read_samples_dual` returned one packet per call**
104
+ however large `max_samples` was. Both now drain already-queued packets
105
+ until the caller is satisfied, waiting only for the first — and not
106
+ even that when carry-over samples were already handed out, where the
107
+ old code could sleep out the whole poll deadline on a backlog.
108
+ - **A `device_serial` in the second family was never found.** Family
109
+ enumeration stopped at the first family holding any device, so a
110
+ machine with both a V6 and an ECO could not select the ECO by serial.
111
+ Every family is enumerated and the serial resolved across them.
112
+ - **Python could not ask for the SDK.** `python-aaronia` depended on the
113
+ crate's default features, which exclude the backend, and `aaronia.open()`
114
+ with neither `url` nor `file` pinned to localhost HTTP rather than
115
+ auto-detecting. A `native-sdk` feature and `open(sdk=True, serial=…)` /
116
+ `AaroniaConfig.native_sdk` select it explicitly; a missing SDK is then
117
+ an error, never a silent fallback.
118
+ - **A unit test assumed no SDK on the machine.**
119
+ `test_detect_best_source_type_localhost_fallback` asserted the
120
+ localhost-HTTP fallback unconditionally, so `cargo test` failed on any
121
+ machine with RTSA-Suite installed — the one place the backend gets
122
+ tested. It now asserts the actual rule: the SDK when installed, HTTP
123
+ otherwise.
124
+ - **The Soapy plugin's Windows build assumed MSVC.** The Rust static-lib
125
+ name is keyed on the compiler now, not the OS; the windows-gnu toolchain
126
+ emits `libsdr_aaronia_rs.a`.
127
+
128
+ ### Added
129
+
130
+ - `tests/native_sdk_live.rs`: thirteen hardware tests — detection,
131
+ identity, ten open/close cycles, a centre-frequency sweep, mid-stream
132
+ retune, a span-ladder characterisation that records the device-reported
133
+ rate per rung, a steady-state rate check, a soak with drop accounting,
134
+ error-message quality, and the Seify and C-ABI paths.
135
+ - `scripts/native-sdk-validate.ps1`: runs the whole matrix on a Windows
136
+ machine with a device.
137
+ - `NativeSdkSource::{observed_sample_rate_hz, cumulative_drops,
138
+ take_overrun, last_timestamp_ns}`.
139
+
140
+ ### Measured on a V6 ECO (Windows 11, RTSA-Suite PRO 3.0.3)
141
+
142
+ Every ladder rung from 3.84 to 49.152 MHz delivers exactly the requested
143
+ rate, 3/3 trials each; 61.44 MHz caps at 59.214 MS/s. Steady-state
144
+ delivery is 100.0% of the reported rate. The `iqreceiver` pipeline
145
+ delivers ~40% of rate for the first ~5 s after start, then settles; the
146
+ rate and soak tests warm up past it. The SoapySDR plugin, built with
147
+ MSVC against radioconda's SoapySDR, streams 15.357 MS/s over the SDK
148
+ into Python; see [docs/VERIFICATION.md](docs/VERIFICATION.md) for the
149
+ two limits found on the way.
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
+
5
229
  ## [v0.8.1] - 2026-09-07
6
230
 
7
231
  ### Fixed
@@ -127,6 +127,20 @@ cargo bench # Run all benchmarks
127
127
  cargo bench --bench parse_int16_packet # Run a specific harness
128
128
  ```
129
129
 
130
+ Those measure decode paths in isolation. To measure the whole HTTP path
131
+ end to end without hardware, `scripts/fake-rtsa-server.py` serves
132
+ `/stream` in the real wire format as fast as a client will take it:
133
+
134
+ ```bash
135
+ scripts/fake-rtsa-server.py 54999 int16 &
136
+ # then point HttpEndpointsClient::start_stream at http://127.0.0.1:54999
137
+ ```
138
+
139
+ It saturates loopback at several GB/s, so the client is always the
140
+ bottleneck. Worth reaching for when a change looks like it should be
141
+ faster but a real link hides the difference — over WiFi or even 2.5GbE
142
+ the network dominates and CPU work does not show up at all.
143
+
130
144
  ### 8. Mutation Testing
131
145
 
132
146
  `cargo mutants` runs in CI on a weekly cron (Mondays) and on manual dispatch, advisory-only. To run it locally:
@@ -2,6 +2,12 @@
2
2
  # It is not intended for manual editing.
3
3
  version = 4
4
4
 
5
+ [[package]]
6
+ name = "adler2"
7
+ version = "2.0.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
+
5
11
  [[package]]
6
12
  name = "ahash"
7
13
  version = "0.8.12"
@@ -356,6 +362,18 @@ dependencies = [
356
362
  "pin-project-lite",
357
363
  ]
358
364
 
365
+ [[package]]
366
+ name = "async-compression"
367
+ version = "0.4.43"
368
+ source = "registry+https://github.com/rust-lang/crates.io-index"
369
+ checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8"
370
+ dependencies = [
371
+ "compression-codecs",
372
+ "compression-core",
373
+ "pin-project-lite",
374
+ "tokio",
375
+ ]
376
+
359
377
  [[package]]
360
378
  name = "async-executor"
361
379
  version = "1.14.0"
@@ -777,6 +795,23 @@ dependencies = [
777
795
  "memchr",
778
796
  ]
779
797
 
798
+ [[package]]
799
+ name = "compression-codecs"
800
+ version = "0.4.38"
801
+ source = "registry+https://github.com/rust-lang/crates.io-index"
802
+ checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
803
+ dependencies = [
804
+ "compression-core",
805
+ "flate2",
806
+ "memchr",
807
+ ]
808
+
809
+ [[package]]
810
+ name = "compression-core"
811
+ version = "0.4.32"
812
+ source = "registry+https://github.com/rust-lang/crates.io-index"
813
+ checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
814
+
780
815
  [[package]]
781
816
  name = "concurrent-queue"
782
817
  version = "2.5.0"
@@ -930,6 +965,15 @@ dependencies = [
930
965
  "libc",
931
966
  ]
932
967
 
968
+ [[package]]
969
+ name = "crc32fast"
970
+ version = "1.5.1"
971
+ source = "registry+https://github.com/rust-lang/crates.io-index"
972
+ checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
973
+ dependencies = [
974
+ "cfg-if",
975
+ ]
976
+
933
977
  [[package]]
934
978
  name = "criterion"
935
979
  version = "0.5.1"
@@ -1326,6 +1370,17 @@ dependencies = [
1326
1370
  "rustc_version",
1327
1371
  ]
1328
1372
 
1373
+ [[package]]
1374
+ name = "flate2"
1375
+ version = "1.1.10"
1376
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1377
+ checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
1378
+ dependencies = [
1379
+ "crc32fast",
1380
+ "miniz_oxide",
1381
+ "zlib-rs",
1382
+ ]
1383
+
1329
1384
  [[package]]
1330
1385
  name = "fnv"
1331
1386
  version = "1.0.7"
@@ -2401,6 +2456,16 @@ dependencies = [
2401
2456
  "unicase",
2402
2457
  ]
2403
2458
 
2459
+ [[package]]
2460
+ name = "miniz_oxide"
2461
+ version = "0.9.1"
2462
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2463
+ checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
2464
+ dependencies = [
2465
+ "adler2",
2466
+ "simd-adler32",
2467
+ ]
2468
+
2404
2469
  [[package]]
2405
2470
  name = "mio"
2406
2471
  version = "1.2.2"
@@ -3027,7 +3092,7 @@ dependencies = [
3027
3092
 
3028
3093
  [[package]]
3029
3094
  name = "python-aaronia"
3030
- version = "0.8.1"
3095
+ version = "0.9.0"
3031
3096
  dependencies = [
3032
3097
  "arrow",
3033
3098
  "num-complex",
@@ -3556,7 +3621,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3556
3621
 
3557
3622
  [[package]]
3558
3623
  name = "sdr-aaronia-rs"
3559
- version = "0.8.1"
3624
+ version = "0.9.0"
3560
3625
  dependencies = [
3561
3626
  "anyhow",
3562
3627
  "bitflags 2.13.1",
@@ -3790,6 +3855,12 @@ version = "2.0.1"
3790
3855
  source = "registry+https://github.com/rust-lang/crates.io-index"
3791
3856
  checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
3792
3857
 
3858
+ [[package]]
3859
+ name = "simd-adler32"
3860
+ version = "0.3.10"
3861
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3862
+ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
3863
+
3793
3864
  [[package]]
3794
3865
  name = "simd_cesu8"
3795
3866
  version = "1.2.0"
@@ -4376,6 +4447,7 @@ version = "0.6.11"
4376
4447
  source = "registry+https://github.com/rust-lang/crates.io-index"
4377
4448
  checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
4378
4449
  dependencies = [
4450
+ "async-compression",
4379
4451
  "bitflags 2.13.1",
4380
4452
  "bytes",
4381
4453
  "futures-core",
@@ -5206,6 +5278,12 @@ dependencies = [
5206
5278
  "syn 2.0.119",
5207
5279
  ]
5208
5280
 
5281
+ [[package]]
5282
+ name = "zlib-rs"
5283
+ version = "0.6.7"
5284
+ source = "registry+https://github.com/rust-lang/crates.io-index"
5285
+ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
5286
+
5209
5287
  [[package]]
5210
5288
  name = "zmij"
5211
5289
  version = "1.0.23"
@@ -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.1"
5
+ version = "0.9.0"
6
6
  edition = "2024"
7
7
  repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
8
8
  readme = "README.md"
@@ -18,7 +18,13 @@ rustdoc-args = ["--cfg", "docsrs"]
18
18
  crate-type = ["cdylib", "staticlib", "rlib"]
19
19
 
20
20
  [dependencies]
21
- reqwest = { version = "0.13", features = ["blocking", "json", "stream"], optional = true }
21
+ # `deflate`/`gzip`: the RTSA control plane compresses when asked and we
22
+ # were not asking — /remoteconfig is 17,432 bytes plain against 3,299
23
+ # deflated, /healthstatus 6,100 against 1,436, and both are fetched on
24
+ # every device open. RTSA-Suite's own HTTP Client sends
25
+ # `Accept-Encoding: deflate`. Costs nothing on /stream, which the server
26
+ # does not compress at any level.
27
+ reqwest = { version = "0.13", features = ["blocking", "json", "stream", "deflate", "gzip"], optional = true }
22
28
  serde = { version = "1.0", features = ["derive"], optional = true }
23
29
  serde_json = { version = "1.0", optional = true }
24
30
  thiserror = "2.0"
@@ -57,6 +63,11 @@ proptest = "1.5"
57
63
  criterion = { version = "0.5", features = ["html_reports"] }
58
64
  wiremock = "0.6" # For HTTP endpoint resiliency testing
59
65
 
66
+ # The native-SDK live suite uses `#[tokio::test]`, and must build in a
67
+ # `--no-default-features --features native-sdk` configuration where the
68
+ # `http` feature is not pulling `dep:tokio` in.
69
+ tokio = { version = "1.43", features = ["rt-multi-thread", "macros", "time", "sync"] }
70
+
60
71
  # Each `[[bench]]` declares one criterion harness binary. Run with
61
72
  # `cargo bench --bench <name>`. Coverage:
62
73
  # parse_int16_packet — HTTP int16 packet parse throughput
@@ -81,7 +81,7 @@ Hop dwell deadlines come from `sdr_source::DwellController`; per-hop pacing addi
81
81
 
82
82
  The HTTP reader task (spawned by `init_http_source` in `unified_source.rs`) runs a `DropDetector` over each packet's `start_time`/`end_time` metadata. A timestamp gap larger than the tolerance latches `AaroniaSource::pending_overrun`, which `take_overrun()` reads and clears. `single_channel_pump` and `hop_pump` (`sdr_source_impl.rs`) call `take_overrun()` once per emitted `IqPacket`, so a drop detected anywhere since the last read surfaces as `IqPacket::overrun = true` on the next packet. This is a per-call signal, not a precise per-sample one, since drop timing is lost once chunks merge into the flat `sample_buffer`.
83
83
 
84
- The native-SDK and file backends do not populate this yet (`overrun` is always `false` for them). Native-SDK overrun detection would need to read the overflow/dropped warning bits from the packet `flags` field, which the RX path currently ignores.
84
+ The native-SDK and file backends do not populate this yet (`overrun` is always `false` for them). Native-SDK overrun detection would need the overflow/dropped warning bits from the packet `flags` field. The RX path reads and logs those at debug level, but does not yet latch them into `pending_overrun`.
85
85
 
86
86
  The Aaronia capture thread (`AaroniaSdrSource::start`) is wrapped in `catch_unwind`: a panic inside the pump loop is logged rather than silently unwinding the thread.
87
87
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aaronia
3
- Version: 0.8.1
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+)
@@ -40,6 +40,14 @@ cd python-aaronia
40
40
  maturin develop --release
41
41
  ```
42
42
 
43
+ To reach the Aaronia native SDK as well — a Spectran on this machine's
44
+ USB, with RTSA-Suite PRO installed, on Windows or Linux — build with the
45
+ feature; the default wheel speaks only HTTP and files:
46
+
47
+ ```bash
48
+ maturin develop --release --features native-sdk
49
+ ```
50
+
43
51
  Check your setup before writing any code:
44
52
 
45
53
  ```bash
@@ -70,6 +78,15 @@ own schedule, or for Apache Arrow:
70
78
 
71
79
  ```python
72
80
  src = aaronia.open(freq=2.44e9, rate=15.36e6, format="I16")
81
+ ```
82
+
83
+ `sdk=True` opens the device through the native SDK instead of a server
84
+ (`serial=` picks one of several). It is an error, not a fallback, when
85
+ the SDK is missing — a capture never quietly comes from another backend:
86
+
87
+ ```python
88
+ src = aaronia.open(sdk=True, freq=2.44e9, rate=15.36e6)
89
+ src = aaronia.open(sdk=True, serial="C2-P-03000105", freq=2.44e9)
73
90
  samples = src.read_samples_numpy(65536) # numpy complex64 array
74
91
  batch = src.read_samples_arrow(65536) # pyarrow FixedSizeListArray of [re, im]
75
92
  src.set_center_frequency(2.41e9) # live retune, no teardown
@@ -157,6 +174,7 @@ Every field is readable and writable.
157
174
  | `http_base_url` | RTSA-Suite HTTP server URL; pins the HTTP backend |
158
175
  | `file_path` | Path to a recorded `.rtsa` file; pins the file backend |
159
176
  | `device_serial` | Device selection for the native-SDK backend |
177
+ | `native_sdk` | `True` pins the source to the native SDK; missing SDK is an error |
160
178
  | `center_freq` | Center frequency, Hz |
161
179
  | `sample_rate` | IQ sample rate, Hz (the Aaronia "span") |
162
180
  | `reference_level` | Reference level, dBm |
@@ -17,6 +17,17 @@ sdr-aaronia-rs = { version = "0.8", features = ["seify"] }
17
17
 
18
18
  Instantiate the device with `AaroniaSeifyDevice::from_args` and use it directly (or via `seify::dev::DynDeviceBackend`). The backend is **not** part of seify's built-in enumeration registry — `seify::enumerate()` will not discover it.
19
19
 
20
+ `url=` selects the HTTP backend and `file=` playback. `sdk=true` (or
21
+ `serial=<device serial>`) selects the Aaronia native SDK; that needs the
22
+ crate built with both features — `features = ["seify", "native-sdk"]` —
23
+ on Windows or Linux with RTSA-Suite PRO installed. Without the feature
24
+ the request is a clean error, never a fallback to HTTP.
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
+
20
31
  ```rust
21
32
  use sdr_aaronia_rs::seify_impl::AaroniaSeifyDevice;
22
33
  use seify::{Args, RxDevice, RxStreamer, DeviceInfo};
@@ -64,6 +75,15 @@ The plugin requires `cmake`, `SoapySDR`, and the compiled `sdr-aaronia-rs` stati
64
75
  make
65
76
  ```
66
77
 
78
+ On Windows, point CMake at the SoapySDR you will load the module into
79
+ — `-DSoapySDR_DIR="C:\Program Files\PothosSDR\cmake"` for PothosSDR,
80
+ or `<radioconda>\Library\cmake` for a GNU Radio (radioconda) install —
81
+ and build with **MSVC**. Both of those runtimes are MSVC-built, and a
82
+ SoapySDR module is C++ (virtual classes, `std::string` across the
83
+ boundary), so a MinGW-built module cannot load into them; the Rust
84
+ side must then be the `x86_64-pc-windows-msvc` toolchain too. Add
85
+ `-DAARONIA_NATIVE_SDK=ON` for the native-SDK backend.
86
+
67
87
  3. Ensure SoapySDR can find the plugin. You can install it to your system's Soapy modules directory (e.g. `/usr/local/lib/SoapySDR/modules0.8/`) or set the `SOAPY_SDR_PLUGIN_PATH` environment variable:
68
88
  ```bash
69
89
  export SOAPY_SDR_PLUGIN_PATH=$(pwd)/soapy-aaronia/build
@@ -80,13 +100,29 @@ SoapySDRUtil --info
80
100
 
81
101
  ### Usage
82
102
 
83
- 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.
84
109
 
85
110
  ```bash
86
111
  # Example testing with SoapySDRUtil
87
112
  SoapySDRUtil --probe="driver=aaronia,url=http://localhost:54664"
113
+ SoapySDRUtil --probe="driver=aaronia,sdk=true"
88
114
  ```
89
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
+
90
126
  In Python (using `SoapySDR` python bindings):
91
127
  ```python
92
128
  import SoapySDR
@@ -146,3 +182,48 @@ In SoapySDR, you can access these metrics via the `readSensor()` API:
146
182
  drops = sdr.readSensor("cumulative_drops")
147
183
  print(f"Gaps seen: {drops}")
148
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.
@@ -54,8 +54,9 @@ device's real ladder rather than round numbers it cannot produce: on a
54
54
  V6 ECO that is 61.44 MHz halved down to 120 kHz, which the RTSA GUI
55
55
  labels Full through 1/512. Nothing in between exists, so a request
56
56
  between rungs snaps to the nearest and logs a warning. A full V6 has a
57
- selectable receiver clock and reaches higher; the plugin advertises the
58
- ECO ladder, so check the rate the device reports if you are on one.
57
+ selectable receiver clock and reaches higher. The plugin advertises
58
+ whatever ladder the device declares, falling back to the ECO's only
59
+ when the device cannot be asked.
59
60
 
60
61
  The single gain element is `REF`, the Aaronia reference level in dBm. It
61
62
  is not an amplifier gain: raising it reduces sensitivity. Start near