python-aaronia 0.8.2__tar.gz → 0.10.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 (93) hide show
  1. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/CHANGELOG.md +314 -71
  2. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/Cargo.lock +2 -2
  3. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/Cargo.toml +1 -1
  4. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/DESIGN.md +11 -11
  5. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/PKG-INFO +29 -26
  6. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/PLUGINS.md +105 -6
  7. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/README.md +33 -13
  8. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/FILESPEC.md +7 -12
  9. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/HTTPSPEC.md +29 -34
  10. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/QUICKSTART.md +25 -23
  11. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/SDKSPEC.md +155 -65
  12. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/USAGE.md +62 -54
  13. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/VERIFICATION.md +5 -2
  14. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/channel_hopping.rs +5 -5
  15. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/dump_metadata.rs +9 -9
  16. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/http_iq_quickstart.rs +6 -6
  17. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/native_sdk_basic.rs +10 -5
  18. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/native_sdk_transmit.rs +7 -7
  19. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/noaa_scanner.rs +39 -34
  20. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/python_arrow_example.py +6 -6
  21. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/read_rtsa_file.rs +3 -3
  22. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/soapy_python_example.py +3 -3
  23. python_aaronia-0.10.0/include/spectran.h +309 -0
  24. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/python-aaronia/Cargo.toml +1 -1
  25. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/python-aaronia/README.md +28 -25
  26. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/python-aaronia/aaronia.pyi +51 -42
  27. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/python-aaronia/src/lib.rs +110 -101
  28. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/python-aaronia/test_basic.py +26 -24
  29. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/scripts/ci-local.sh +20 -0
  30. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/scripts/fake-rtsa-server.py +1 -1
  31. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/scripts/validate-iq-live.py +10 -2
  32. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/soapy-aaronia/CMakeLists.txt +3 -3
  33. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/soapy-aaronia/README.md +4 -3
  34. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/soapy-aaronia/Registration.cpp +85 -53
  35. python_aaronia-0.8.2/soapy-aaronia/AaroniaSoapyDevice.cpp → python_aaronia-0.10.0/soapy-aaronia/SpectranSoapyDevice.cpp +273 -113
  36. python_aaronia-0.8.2/soapy-aaronia/AaroniaSoapyDevice.hpp → python_aaronia-0.10.0/soapy-aaronia/SpectranSoapyDevice.hpp +36 -7
  37. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/c_api.rs +648 -378
  38. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/file_source.rs +91 -69
  39. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/http_endpoints.rs +345 -55
  40. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/http_sink.rs +19 -19
  41. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/http_source.rs +90 -85
  42. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/http_streaming.rs +20 -14
  43. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/lib.rs +22 -14
  44. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/link_budget.rs +65 -63
  45. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/native_sdk.rs +337 -130
  46. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/sdk_sink.rs +41 -12
  47. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/sdk_source.rs +84 -173
  48. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/sdr_source_impl.rs +34 -34
  49. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/seify_impl.rs +41 -41
  50. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/unified_sink.rs +23 -23
  51. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/unified_source.rs +390 -303
  52. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/utils.rs +185 -14
  53. python_aaronia-0.10.0/tests/abi_drift.rs +311 -0
  54. python_aaronia-0.10.0/tests/c_api_test.rs +72 -0
  55. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/http_mock_test.rs +159 -5
  56. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/http_resilience_test.rs +15 -15
  57. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/http_sink_test.rs +2 -2
  58. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/integration_test.rs +6 -6
  59. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/live_smoke.rs +104 -20
  60. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/native_sdk_live.rs +66 -60
  61. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/properties.rs +1 -1
  62. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/sdr_source_impl_test.rs +14 -14
  63. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/spec_coverage.rs +1 -1
  64. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/test_cw_meta.rs +3 -3
  65. python_aaronia-0.10.0/tests/wire_contract.rs +232 -0
  66. python_aaronia-0.8.2/include/aaronia.h +0 -256
  67. python_aaronia-0.8.2/tests/c_api_test.rs +0 -72
  68. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/.cargo/config.toml +0 -0
  69. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/.gitattributes +0 -0
  70. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/.gitignore +0 -0
  71. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/CONTRIBUTING.md +0 -0
  72. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/LICENSE +0 -0
  73. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/benches/decompress_block.rs +0 -0
  74. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/benches/deinterleave_dual_iq.rs +0 -0
  75. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/benches/parse_int16_packet.rs +0 -0
  76. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/benches/rtsa_open_and_read.rs +0 -0
  77. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/deny.toml +0 -0
  78. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/docs/APPS.md +0 -0
  79. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/examples/device_control.rs +0 -0
  80. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/packaging/homebrew/README.md +0 -0
  81. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  82. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/pyproject.toml +0 -0
  83. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/scripts/native-sdk-validate.ps1 +0 -0
  84. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/scripts/sdk-container-test.sh +0 -0
  85. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  86. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/soapy-aaronia/packaging/install.sh +0 -0
  87. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/decompression.rs +0 -0
  88. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/detection.rs +0 -0
  89. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/src/error.rs +0 -0
  90. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/native_sdk_load.rs +0 -0
  91. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/properties.proptest-regressions +0 -0
  92. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/rtsa_negative_test.rs +0 -0
  93. {python_aaronia-0.8.2 → python_aaronia-0.10.0}/tests/test_cw_mag.rs +0 -0
@@ -2,82 +2,247 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased]
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.
5
+ ## [v0.10.0] - 2026-09-08
6
+
7
+ ### Breaking changes
8
+
9
+ **One name per concept, with its unit.** A field, parameter or getter holding a
10
+ bare number now carries its unit (`_hz`, `_dbm`, `_db`, `_s`); a type that
11
+ already carries one, such as `Duration`, does not. `span_frequency` is gone: it
12
+ meant the IQ *sample rate*, while the same word meant alias-free *bandwidth* in
13
+ the link budget. Old names were removed rather than deprecated, since an alias
14
+ that kept working would preserve the ambiguity this release exists to remove.
15
+ The `spanfreq` device key and the `frequencySpan` JSON field are the vendor's
16
+ vocabulary and are unchanged.
17
+
18
+ | Old | New |
19
+ | --- | --- |
20
+ | `AaroniaConfig::center_frequency` (field + builder) | `center_frequency_hz` |
21
+ | `AaroniaConfig::span_frequency` (field + builder) | `sample_rate_hz` |
22
+ | `AaroniaConfig::reference_level` (field + builder) | `reference_level_dbm` |
23
+ | `AaroniaSourceBuilder::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
24
+ | `AaroniaSource::set_center_frequency` / `set_span_frequency` / `set_reference_level` | `set_center_frequency_hz` / `set_sample_rate_hz` / `set_reference_level_dbm` |
25
+ | `SourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
26
+ | `SourceInfo::sample_rate_hz()` (method) | removed — read the field of the same name |
27
+ | `UnifiedSinkConfig::span_frequency`, `trans_gain` | `sample_rate_hz`, `trans_gain_db` |
28
+ | `ThroughputMeasurement::max_sustainable_span_hz` | `max_sustainable_bandwidth` |
29
+ | `ThroughputMeasurement::stream_sample_rate` | `stream_sample_rate_hz` |
30
+ | `LinkBudgetVerdict::sample_rate`, `fit_span_hz` | `sample_rate_hz`, `fit_bandwidth_hz` |
31
+ | `RtsaMetadata` / `StreamingSdrConfig` bare quantities | unit-suffixed (`_hz`, `_s`) |
32
+ | `HttpSourceBuilder::frequency` / `frequency_str` / `sample_rate` / `reference_level` | `center_frequency_hz` / `center_frequency_str` / `sample_rate_hz` / `reference_level_dbm` |
33
+ | `HttpSinkBuilder::frequency`, `sample_rate` | `center_frequency_hz`, `sample_rate_hz` |
34
+ | `HttpSource::new` / `with_advanced_options`, `HttpSink::new` parameters | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
35
+ | `DeviceCapabilities::center_frequency`, `reference_level` | `center_frequency_hz`, `reference_level_dbm` |
36
+ | `PacketMetadata::sample_rate()` | `sample_rate_hz()` |
37
+
38
+ `frequency` became `center_frequency_hz`, not `frequency_hz`: it was always the
39
+ centre frequency. The `_str` variants take a string carrying its own units
40
+ (`"146.52M"`), so they keep no suffix. `DeviceCapabilities` is
41
+ `#[non_exhaustive]`, so callers reach those fields through the API rather than
42
+ by literal.
43
+
44
+ **Rust types name the device: `Aaronia*` becomes `Spectran*`** —
45
+ `SpectranConfig`, `SpectranSource`, `SpectranSourceBuilder`,
46
+ `SpectranSinkBuilder`, `SpectranSeifyDevice`, `SpectranSeifyRxStreamer`,
47
+ `SpectranBackend`, `SpectranSdrSource`.
48
+
49
+ The vendor name stays wherever it is a frozen contract rather than a
50
+ description: the `aaronia_*` C symbols and the `AaroniaSource` /
51
+ `AaroniaFfiError` typedefs, the crate, the PyPI package, the Python module,
52
+ `AaroniaSoapyDevice`, and `driver=aaronia`. That last is the load-bearing one —
53
+ it is typed into GQRX and SDR++ configs already in the world, where breaking it
54
+ reads as "device not found" with nothing pointing at the cause.
55
+
56
+ **Python takes the same vocabulary**, its four exceptions included.
57
+
58
+ | Old Python | New |
59
+ | --- | --- |
60
+ | `aaronia.AaroniaConfig` | `aaronia.SpectranConfig` |
61
+ | `aaronia.AaroniaSource` | `aaronia.SpectranSource` |
62
+ | `aaronia.Aaronia{Connection,Hardware,Timeout}Error`, `AaroniaStreamClosed` | `Spectran…` |
63
+ | `cfg.center_freq` | `cfg.center_frequency_hz` |
64
+ | `cfg.sample_rate` | `cfg.sample_rate_hz` |
65
+ | `cfg.reference_level` | `cfg.reference_level_dbm` |
66
+ | `cfg.read_timeout` | `cfg.read_timeout_s` |
67
+ | `cfg.native_sdk` | `cfg.force_native_sdk` |
68
+ | `src.set_center_frequency()` / `set_sample_rate()` / `set_reference_level()` | `set_center_frequency_hz()` / `set_sample_rate_hz()` / `set_reference_level_dbm()` |
69
+ | `open(freq=, rate=, bandwidth=, ref_level=, read_timeout=)` | `open(center_frequency_hz=, sample_rate_hz=, bandwidth_hz=, reference_level_dbm=, read_timeout_s=)` |
70
+
71
+ `open()`'s keywords changed with them. It is the one-line front door and the
72
+ longer names cost the headline example a wrap, but `open(url, bandwidth=10e6)`
73
+ gives no way to tell Hz from MHz. `sdk=`, `url=`, `file=`, `serial=`, `format=`
74
+ and `scale=` carry no unit and are unchanged.
75
+
76
+ **The whole C ABI moves to `spectran_*`.** Every exported symbol, the header
77
+ (`include/aaronia.h` is now `include/spectran.h`), the opaque typedefs
78
+ (`AaroniaSource` / `AaroniaSourceBuilder` / `AaroniaSink` / `AaroniaSinkBuilder`
79
+ / `AaroniaFfiError` / `CAaroniaSourceType` become `Spectran*`), and the plugin
80
+ class `AaroniaSoapyDevice` become `SpectranSoapyDevice`. The table below lists
81
+ the symbols that also changed shape; the rest changed prefix only, so
82
+ `aaronia_x` is `spectran_x`.
83
+
84
+ What keeps the vendor name: `driver=aaronia`, because it is typed into GQRX and
85
+ SDR++ configuration files already in the world and breaking it reads as "device
86
+ not found"; the crate, the PyPI package and the Python module, which are
87
+ published names; and `AARTSAAPI_*` / `AaroniaRTSAAPI.dll`, which are the
88
+ vendor's own.
89
+
90
+ **The C ABI is renamed with no forwarders**, so a consumer gets an undefined
91
+ symbol at link time and this table is the migration guide. `FfiSourceInfo`'s
92
+ fields moved with the header in the same commit: C reads them positionally, so
93
+ a stale header would go on reading the right bytes under the wrong name.
94
+
95
+ | Old C symbol | New |
96
+ | --- | --- |
97
+ | `aaronia_source_builder_center_frequency` | `spectran_source_builder_center_frequency_hz` |
98
+ | `aaronia_source_builder_span_frequency` | `spectran_source_builder_sample_rate_hz` |
99
+ | `aaronia_source_builder_reference_level` | `spectran_source_builder_reference_level_dbm` |
100
+ | `aaronia_source_set_center_frequency` | `spectran_source_set_center_frequency_hz` |
101
+ | `aaronia_source_set_span_frequency` | `spectran_source_set_sample_rate_hz` |
102
+ | `aaronia_source_set_reference_level` | `spectran_source_set_reference_level_dbm` |
103
+ | `aaronia_sink_builder_center_frequency` | `spectran_sink_builder_center_frequency_hz` |
104
+ | `aaronia_sink_builder_sample_rate` | `spectran_sink_builder_sample_rate_hz` |
105
+ | `aaronia_sink_builder_trans_gain` | `spectran_sink_builder_trans_gain_db` |
106
+ | `aaronia_source_read_sensors` | `spectran_source_get_sensors` |
107
+ | `aaronia_endpoints_client_read_sensors` | `spectran_endpoints_client_get_sensors` |
108
+ | `FfiSourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
109
+ | `aaronia_source_get_gps_time(.., double*)` | `spectran_source_get_gps_time_ns(.., int64_t*)` |
110
+
111
+ The two `read_sensors` are a verb change, not a unit one. `read_` advances a
112
+ stream here (`spectran_source_read_samples`); sensors are a snapshot, so they
113
+ join `spectran_source_get_capabilities`.
114
+
115
+ **GPS time changes type as well as name.** `gps_time_ns() -> Option<i64>`, and
116
+ `int64_t*` in C, matching `last_timestamp_ns`. The conversion moved into the
117
+ library because epoch nanoseconds land near `1.7e18`, where an `f64`'s step is
118
+ 256 ns — a single `seconds * 1e9` discards tens of nanoseconds the reading
119
+ still had. The SoapySDR plugin's own whole/fractional split is deleted.
120
+ `GpsState::time` is now `time_s`, and the C function accepts a `NULL`
121
+ out-parameter meaning "is there a fix?".
122
+
123
+ It does **not** improve precision. The device reports `gpstime` as an `f64`,
124
+ whose step at present-day epoch values is about 240 ns, and nothing downstream
125
+ recovers what was never there. `utils::gps_seconds_to_nanos` documents that
126
+ bound and a test pins it against the naive multiply. Across receivers ~240 ns
127
+ is ~70 m of ranging uncertainty, so correlate on the per-packet stream
128
+ timestamps and keep GPS for disciplining and wall-clock labelling.
129
+
130
+ `CaptureControl`'s fields gained units without moving its JSON keys: each is
131
+ now pinned by an explicit `#[serde(rename)]` rather than derived from the Rust
132
+ field name, so a later rename cannot change the wire format by accident.
133
+ `tests/wire_contract.rs` asserts those keys, and asserts the vendor
134
+ `AARTSAAPI_Packet` mirror still matches its C header field for field.
21
135
 
22
136
  ### 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.
137
+ - **The stream clock source can be set, not just read.** `device/sclksource`
138
+ selects what disciplines the receiver's clock; a V6 ECO offers `Consumer`,
139
+ `Oscillator`, `GPS`, `PPS`, `10MHz` and three `... Provider` variants. It was
140
+ readable but read-only, and SoapySDR's `setClockSource` merely logged a
141
+ warning telling the operator to change it in RTSA-Suite.
142
+ `SpectranSource::set_clock_source`, `spectran_source_set_clock_source` and the
143
+ plugin's `setClockSource` now write it on both backends — native SDK via
144
+ `ConfigSetString`, HTTP via a `simpleconfig` PUT that is read back to confirm,
145
+ because a `/remoteconfig` PUT naming a block outside the running mission
146
+ answers 200 and changes nothing. A confirmed mismatch is an **error** naming
147
+ the sources the device does offer: being told you are on a reference you are
148
+ not is worse than a failed call. A read-back yielding nothing is reported as
149
+ unconfirmed rather than failed.
150
+
151
+ This is what makes cross-receiver correlation possible: lock a fleet to one
152
+ 10 MHz / PPS / GPS reference and their per-packet timestamps share a timebase.
153
+ What is *not* possible is a commanded synchronous start — the SDK's C API has
154
+ no set-time and no arm-at-time entry point, so multi-device work is a shared
155
+ reference plus post-alignment on timestamps.
30
156
 
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.
157
+ ### Fixed
158
+ - **A trailing slash in `device_type` no longer produces a malformed open
159
+ string.** The family/mode split asked whether the string contained a slash, so
160
+ `"spectranv6/"` counted as already mode-qualified and went to
161
+ `AARTSAAPI_OpenDevice` verbatim. The mode is now the text after the *first*
162
+ slash, and an empty one means none was given. The ECO was the sharp edge:
163
+ `"spectranv6eco/"` also slipped past the `spectranv6eco/raw` → `iqreceiver`
164
+ remap, so the device was asked for a pipeline it does not have and answered
165
+ with a result code that said nothing about the typo.
166
+
167
+ A `device_type` naming no family (`""`, `"/"`, `"/raw"`) is no longer
168
+ completed into a family-less `"/raw"`; it comes back untouched and is refused
169
+ at both points where a device type reaches the SDK. Both guards are needed
170
+ because enumeration runs first, and an empty family simply enumerates to
171
+ nothing — the old path reported "no devices found" and never mentioned the
172
+ typo. `device_family` and `device_open_mode` stay infallible: a typo in one
173
+ field is not a reason to make four accessors return `Result`.
174
+ - **An over-wide sample rate no longer reaches the hardware before it is
175
+ refused.** `configure_iq_receiver` checked the IQ-mode constraint as its last
176
+ act, so a rate the receiver clock cannot carry was written to
177
+ `main/centerfreq` and `main/spanfreq` first and rejected afterwards — leaving
178
+ the device holding the misconfiguration the check exists to prevent, and per
179
+ the SDK quietly emitting corrupted samples if anything started the stream.
180
+ The check now runs before the first write.
181
+
182
+ It checks against the clock the call *leaves in place*, which is not always
183
+ the one the device holds on entry. Raw mode writes the clock itself, so that
184
+ write is what counts: a V6 left on its 245.76 MHz clock would otherwise pass a
185
+ 150 MS/s request that stops being valid the moment this same call drops the
186
+ clock to 92.16 MHz. Every other mode skips that write, so there the live
187
+ setting is the honest number. The read-back after the writes is kept, and is
188
+ what `receiver_clock_hz()` reports. Native SDK only; the HTTP backend already
189
+ validated at the API boundary.
190
+
191
+ ## [v0.9.0] - 2026-09-08
60
192
 
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.
193
+ ### Added
194
+ - **SoapySDR: device sensors.** The plugin exposed one sensor
195
+ (`cumulative_drops`); it now surfaces the device's live telemetry from
196
+ `/healthstatus` — FPGA and frontend temperature, ADC headroom (dB below
197
+ full scale, so a client can raise the reference level before it clips),
198
+ USB and DSP buffer fill, error and overflow rates, GPS satellites and
199
+ position. `listSensors` / `readSensor` / `getSensorInfo`, read through a
200
+ dedicated connection and briefly cached, so polling them during a
201
+ capture neither stalls the sample stream nor costs a fetch per key.
202
+ HTTP backend only: the native SDK's own `AARTSAAPI_ConfigHealth` tree
203
+ reads all zeros in raw-SDK mode — the live telemetry is computed and
204
+ populated by RTSA-Suite, the managing application, not by the raw SDK,
205
+ confirmed by dumping the tree live from a V6 ECO — so over `sdk=true`
206
+ the plugin reports only `cumulative_drops`, which is real and
207
+ client-side. Verified live over HTTP on a V6 ECO.
208
+ - **SoapySDR: bandwidth API.** `setBandwidth` / `getBandwidth` /
209
+ `listBandwidths` / `getBandwidthRange`. The device's alias-free
210
+ bandwidth is 0.8x its sample rate; these expose that as SoapySDR's
211
+ separate knob, mapping a requested bandwidth to a sample rate and
212
+ driving the already-verified `setSampleRate` — no new device-write path.
213
+ Verified on a V6 ECO: 15.36 MS/s reports 12.288 MHz, `setBandwidth`
214
+ snaps to a rung.
215
+ - **SoapySDR: the native SDK is discoverable.** `find` advertises a
216
+ second `sdk=true` device beside the HTTP one whenever the SDK is
217
+ installed, and `sdk=true` / `serial=` force the native backend instead
218
+ of silently falling back to localhost HTTP. Verified streaming 15.357
219
+ MS/s over the SDK through the plugin from Python.
220
+ - **C ABI.** `aaronia_source_read_sensors` (fills a value struct, `NaN`
221
+ for absent — no ownership, nothing to free);
222
+ `aaronia_source_builder_force_source_type` and `aaronia_sdk_installed`,
223
+ the C equivalent of `force_native_sdk`, which C had no way to request;
224
+ and the stateless bandwidth helpers `aaronia_usable_bandwidth_hz` /
225
+ `aaronia_iq_sample_rate_for_bandwidth`. `DeviceSensors` is the Rust type
226
+ behind the first.
68
227
 
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.
228
+ ### Fixed
229
+ - **File playback reported the decompression time as the capture time.**
230
+ A DSPT_IQ-compressed `.rtsa` is decompressed through RTSAFileTool, which
231
+ writes a fresh header stamped with the moment of conversion, so the
232
+ re-opened file reported *now* — a 2020 capture read as 2026. The
233
+ original header's `creation_time`, parsed before compression was
234
+ detected, is now carried across the decompression, and the derived
235
+ start/end fall-backs follow it. Surfaced the first time the fixture test
236
+ ran on a machine with RTSA-Suite installed.
237
+ - **The SoapySDR plugin could not load the SDK inside a host that carries
238
+ its own Qt6.** The loader was given `LOAD_LIBRARY_SEARCH_DEFAULT_DIRS`,
239
+ which searches the host application's directory first; a GNU Radio
240
+ (radioconda) install holds a different Qt6 build there under the same
241
+ names, and the load failed. It now searches only the DLL's own
242
+ directory, the SDK install root, and System32.
243
+ - **The Seify native-SDK test panicked on teardown.** `AaroniaSeifyDevice`
244
+ owns a tokio runtime, and dropping one inside an async context is a
245
+ tokio panic; the test is synchronous now.
81
246
 
82
247
  ## [v0.8.2] - 2026-09-08
83
248
 
@@ -169,6 +334,84 @@ MSVC against radioconda's SoapySDR, streams 15.357 MS/s over the SDK
169
334
  into Python; see [docs/VERIFICATION.md](docs/VERIFICATION.md) for the
170
335
  two limits found on the way.
171
336
 
337
+
338
+ _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):_
339
+
340
+ ### Performance
341
+ - **The int16 and float16 IQ decoders are ~23% faster end to end.** Both
342
+ built their output with `Vec::with_capacity` then `push` per sample,
343
+ which carries a capacity check the compiler cannot elide and which was
344
+ blocking vectorisation. Collecting from the slice iterator instead —
345
+ `TrustedLen`, so the vector is sized once — measured 495 to 2433 MS/s
346
+ on the decode loop alone, and 123.5 to 152 MS/s over the whole HTTP
347
+ path including framing and transport. float32 was already a single
348
+ `copy_nonoverlapping` and is unchanged.
349
+
350
+ Worth stating what this does not buy: at 605 MB/s the crate is about
351
+ 2.5x the fastest a V6 can produce and 6x a WiFi 6E link, so it was not
352
+ the bottleneck before and is not now. What it buys is CPU left over
353
+ for whatever consumes the samples.
354
+
355
+ ### Added
356
+ - **`scripts/fake-rtsa-server.py`** serves `/stream` in the real wire
357
+ format at loopback speed, so a decode path can be measured without a
358
+ device or a network. It is how the figures above were taken: over any
359
+ real link the transport dominates and a CPU change is invisible. Its
360
+ docstring carries the caveat that goes with it — over loopback hyper's
361
+ adaptive read buffer never leaves its 8 KiB floor, so kernel time there
362
+ is a property of the harness, not of the crate.
363
+
364
+ ### Documentation
365
+ - **`rate_reduction` is time compression, not a sample-rate divider.**
366
+ Five places described it as reducing the rate or optimising bandwidth.
367
+ It thins frames over time — the operation the `waterfall` payload is
368
+ described by — so a continuous IQ stream, having no frames, is
369
+ unaffected: measured at factors of 2, 10 and 64, `sampleFrequency`
370
+ holds at 15,359,988 Hz and the byte rate does not move. That is the
371
+ parameter behaving as specified on a payload it was not meant for.
372
+ `live_stream_rate_reduction_and_scale` had asserted only that packets
373
+ arrived, which is true either way, so nothing caught the wrong
374
+ description; it now pins the IQ behaviour.
375
+ - **The stream can be compressed, up to 6.55x, via `format=rtsa`.**
376
+ Captured from RTSA-Suite's own HTTP Client block:
377
+ `GET /stream?format=rtsa&rate_reduction=8&input=main&compression=5&rate_adaption=0`.
378
+ `format=rtsa` streams the file container and is the only format that
379
+ accepts `compression=N`, which applies the file format's own lossy
380
+ codec. The container carries `float32` (`mSampleType` 11, `DSST_F32N`),
381
+ so level 0 is plain float32 with under 1% of chunk overhead — 123.0
382
+ MB/s against 122.9 theoretical at 15.36 MS/s. Against that baseline the
383
+ codec buys 2.74x at level 1, 4.43x at level 5 and 13.10x at level 9;
384
+ even level 1 undercuts plain `int16` while carrying float precision.
385
+ Ratios hold at 3.84 MS/s too.
386
+
387
+ HTTPSPEC had documented `format=rtsa` only as the thing a typo falls
388
+ back to — "a completely different wire format rather than an error" —
389
+ and this release had gone on to claim compression was neither offered
390
+ nor useful. Both are corrected. Generic HTTP compression is still not
391
+ available on `/stream` and still would not help (zlib manages 1.07x on
392
+ `float32`); Aaronia's codec wins by being lossy and signal-aware.
393
+
394
+ This crate cannot use it for IQ, tested rather than assumed: a real
395
+ compressed payload handed to `Decompressor::decompress` comes back
396
+ rejected as proprietary `DSPT_IQ`, the same wall that stops compressed
397
+ IQ *files*. `format=rtsa` also defaults to `mCompression=1`, so only
398
+ `compression=0` is decodable and that is 20% larger than plain `int16`.
399
+ Spectra should decode, `DSPT_SPECTRA` being documented, but this
400
+ mission has no spectra input to try.
401
+
402
+ ### Performance
403
+ - **The control plane is now requested compressed.** `reqwest` gains the
404
+ `deflate` and `gzip` features, so the client sends
405
+ `Accept-Encoding: gzip,deflate` where it previously sent none. Measured
406
+ against the device: `/remoteconfig` 17,432 bytes to 3,299 deflated,
407
+ `/healthstatus` 6,100 to 1,436. Both are read at every device open, and
408
+ `/healthstatus` again on each stream-gap report. No effect on `/stream`,
409
+ which the server does not compress at any level.
410
+ - The reader channel's size comment claimed ~157 KiB chunks and ~10 MB of
411
+ queue. Measured against a live server it is 64 KiB for 71% of chunks,
412
+ so the queue is ~4 MB, about 45 ms at the 88 MB/s a WiFi 6E path
413
+ delivers.
414
+
172
415
  ## [v0.8.1] - 2026-09-07
173
416
 
174
417
  ### Fixed
@@ -3092,7 +3092,7 @@ dependencies = [
3092
3092
 
3093
3093
  [[package]]
3094
3094
  name = "python-aaronia"
3095
- version = "0.8.2"
3095
+ version = "0.10.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.10.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.10.0"
6
6
  edition = "2024"
7
7
  repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
8
8
  readme = "README.md"
@@ -4,7 +4,7 @@ This document describes the architecture of the `sdr-aaronia-rs` crate, which pr
4
4
 
5
5
  ## 1. Overview
6
6
 
7
- The Aaronia ecosystem offers three ways to access Spectran hardware: the native RTSA-Suite PRO SDK, the HTTP streaming API, and recorded RTSA files. This crate wraps all three behind a single deterministic facade, `AaroniaSource`, so callers can work with Aaronia hardware without depending on the underlying transport.
7
+ The Aaronia ecosystem offers three ways to access Spectran hardware: the native RTSA-Suite PRO SDK, the HTTP streaming API, and recorded RTSA files. This crate wraps all three behind a single deterministic facade, `SpectranSource`, so callers can work with Aaronia hardware without depending on the underlying transport.
8
8
 
9
9
  ## 2. Architecture
10
10
 
@@ -13,7 +13,7 @@ The crate is built around an automatic source-detection router that instantiates
13
13
  ```mermaid
14
14
  graph TB
15
15
  subgraph "sdr-aaronia-rs Unified API"
16
- API[AaroniaSourceBuilder]
16
+ API[SpectranSourceBuilder]
17
17
  Detection[Deterministic Auto-Detection]
18
18
  end
19
19
 
@@ -32,13 +32,13 @@ graph TB
32
32
 
33
33
  ## 3. Source Selection
34
34
 
35
- `AaroniaSource::new(config)` selects a backend using strict, deterministic rules:
35
+ `SpectranSource::new(config)` selects a backend using strict, deterministic rules:
36
36
 
37
37
  | Configuration | Backend | Fallback | Platforms |
38
38
  |---|---|---|---|
39
- | `AaroniaConfig::from_file("recording.rtsa")` | File | None — fails if the file is missing | All |
40
- | `AaroniaConfig::from_http("http://192.168.1.100")` | HTTP | None — fails if unreachable | All |
41
- | `AaroniaConfig::default()` (nothing specified) | Native SDK, when the `native-sdk` feature is enabled and the SDK library is found | Connects to the default HTTP endpoint (`http://localhost:54664`) otherwise. Since `native-sdk` is not a default feature, a stock build always resolves to HTTP. | All (SDK itself is Windows/Linux only) |
39
+ | `SpectranConfig::from_file("recording.rtsa")` | File | None — fails if the file is missing | All |
40
+ | `SpectranConfig::from_http("http://192.168.1.100")` | HTTP | None — fails if unreachable | All |
41
+ | `SpectranConfig::default()` (nothing specified) | Native SDK, when the `native-sdk` feature is enabled and the SDK library is found | Connects to the default HTTP endpoint (`http://localhost:54664`) otherwise. Since `native-sdk` is not a default feature, a stock build always resolves to HTTP. | All (SDK itself is Windows/Linux only) |
42
42
  | Force option, e.g. `config.force_native_sdk()` | Forced type | None — fails if unavailable | Depends on forced type |
43
43
 
44
44
  ## 4. Backend Implementations
@@ -58,7 +58,7 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
58
58
 
59
59
  - Implements the V9 RTSA HTTP specification.
60
60
  - Supports Basic Auth and token-based authentication.
61
- - **Formats**: supports the `Json`, `Int16`, `Float16`, and `Float32` wire formats, maintaining a persistent chunked-transfer buffer across packet boundaries. The default is `Float32` (binary, lossless); `Int16` is opt-in via `AaroniaConfig::stream_format()` / `stream_scale()`.
61
+ - **Formats**: supports the `Json`, `Int16`, `Float16`, and `Float32` wire formats, maintaining a persistent chunked-transfer buffer across packet boundaries. The default is `Float32` (binary, lossless); `Int16` is opt-in via `SpectranConfig::stream_format()` / `stream_scale()`.
62
62
  - Beyond streaming, `HttpEndpointsClient` exposes device control and health telemetry (as a generic configuration tree) within a headless Rust process.
63
63
 
64
64
  ### 4.3 RTSA Files
@@ -69,21 +69,21 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
69
69
 
70
70
  ## 5. Channel Hopping and Dwell Control
71
71
 
72
- Under the `sdr-source` feature, `AaroniaSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
72
+ Under the `sdr-source` feature, `SpectranSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
73
73
 
74
74
  - **Native SDK**: re-issues `configure_iq_receiver` with the new center frequency, applying the change to the open device handle via the `main/centerfreq` configuration key. No stream restart is required.
75
- - **HTTP**: `AaroniaSource::set_center_frequency` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). Hopping deliberately avoids `/remoteconfig` and is not gated on the license probe. (Whether that path needs the "Remote Config" license at all is unconfirmed: a system without the license accepted `/remoteconfig` writes in testing. See docs/HTTPSPEC.md.)
75
+ - **HTTP**: `SpectranSource::set_center_frequency_hz` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). Hopping deliberately avoids `/remoteconfig` and is not gated on the license probe. (Whether that path needs the "Remote Config" license at all is unconfirmed: a system without the license accepted `/remoteconfig` writes in testing. See docs/HTTPSPEC.md.)
76
76
  - **File**: not supported.
77
77
 
78
78
  Hop dwell deadlines come from `sdr_source::DwellController`; per-hop pacing additionally inserts a ~75 ms settle after every retune (`RETUNE_SETTLE` in `sdr_source_impl.rs`) to ride out the RTSA's apply-config latency and flush stale samples from the pipeline.
79
79
 
80
80
  ## 6. Overrun Detection and Fault Handling
81
81
 
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`.
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 `SpectranSource::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
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
- 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.
86
+ The Aaronia capture thread (`SpectranSdrSource::start`) is wrapped in `catch_unwind`: a panic inside the pump loop is logged rather than silently unwinding the thread.
87
87
 
88
88
  ## 7. FutureSDR Integration
89
89