python-aaronia 0.8.0__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. python_aaronia-0.8.1/CHANGELOG.md +985 -0
  2. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/Cargo.lock +2 -2
  3. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/Cargo.toml +1 -1
  4. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/PKG-INFO +10 -11
  5. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/PLUGINS.md +6 -4
  6. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/README.md +8 -11
  7. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/QUICKSTART.md +4 -1
  8. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/SDKSPEC.md +35 -3
  9. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/USAGE.md +5 -6
  10. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/python-aaronia/Cargo.toml +1 -1
  11. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/python-aaronia/README.md +9 -10
  12. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/python-aaronia/src/lib.rs +5 -2
  13. python_aaronia-0.8.1/scripts/sdk-container-test.sh +41 -0
  14. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/AaroniaSoapyDevice.cpp +56 -19
  15. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/AaroniaSoapyDevice.hpp +7 -0
  16. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/CMakeLists.txt +20 -4
  17. python_aaronia-0.8.1/soapy-aaronia/README.md +180 -0
  18. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/Registration.cpp +17 -25
  19. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/c_api.rs +135 -5
  20. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/detection.rs +21 -6
  21. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/http_endpoints.rs +221 -31
  22. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/http_source.rs +17 -2
  23. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/native_sdk.rs +25 -1
  24. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/seify_impl.rs +16 -8
  25. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/unified_source.rs +30 -0
  26. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/utils.rs +13 -0
  27. python_aaronia-0.8.0/CHANGELOG.md +0 -1204
  28. python_aaronia-0.8.0/soapy-aaronia/README.md +0 -230
  29. python_aaronia-0.8.0/soapy-aaronia/print.cmake +0 -1
  30. python_aaronia-0.8.0/test.cmake +0 -1
  31. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/.cargo/config.toml +0 -0
  32. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/.gitattributes +0 -0
  33. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/.gitignore +0 -0
  34. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/CONTRIBUTING.md +0 -0
  35. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/DESIGN.md +0 -0
  36. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/LICENSE +0 -0
  37. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/benches/decompress_block.rs +0 -0
  38. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/benches/deinterleave_dual_iq.rs +0 -0
  39. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/benches/parse_int16_packet.rs +0 -0
  40. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/benches/rtsa_open_and_read.rs +0 -0
  41. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/deny.toml +0 -0
  42. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/APPS.md +0 -0
  43. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/FILESPEC.md +0 -0
  44. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/HTTPSPEC.md +0 -0
  45. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/docs/VERIFICATION.md +0 -0
  46. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/channel_hopping.rs +0 -0
  47. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/device_control.rs +0 -0
  48. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/dump_metadata.rs +0 -0
  49. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/http_iq_quickstart.rs +0 -0
  50. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/native_sdk_basic.rs +0 -0
  51. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/native_sdk_transmit.rs +0 -0
  52. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/noaa_scanner.rs +0 -0
  53. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/python_arrow_example.py +0 -0
  54. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/read_rtsa_file.rs +0 -0
  55. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/examples/soapy_python_example.py +0 -0
  56. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/include/aaronia.h +0 -0
  57. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/packaging/homebrew/README.md +0 -0
  58. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/packaging/homebrew/soapy-aaronia.rb +0 -0
  59. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/pyproject.toml +0 -0
  60. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/python-aaronia/aaronia.pyi +0 -0
  61. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/python-aaronia/test_basic.py +0 -0
  62. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/scripts/ci-local.sh +0 -0
  63. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/scripts/validate-iq-live.py +0 -0
  64. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/packaging/install.ps1 +0 -0
  65. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/soapy-aaronia/packaging/install.sh +0 -0
  66. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/decompression.rs +0 -0
  67. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/error.rs +0 -0
  68. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/file_source.rs +0 -0
  69. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/http_sink.rs +0 -0
  70. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/http_streaming.rs +0 -0
  71. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/lib.rs +0 -0
  72. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/link_budget.rs +0 -0
  73. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/sdk_sink.rs +0 -0
  74. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/sdk_source.rs +0 -0
  75. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/sdr_source_impl.rs +0 -0
  76. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/src/unified_sink.rs +0 -0
  77. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/c_api_test.rs +0 -0
  78. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/http_mock_test.rs +0 -0
  79. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/http_resilience_test.rs +0 -0
  80. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/http_sink_test.rs +0 -0
  81. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/integration_test.rs +0 -0
  82. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/live_smoke.rs +0 -0
  83. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/native_sdk_load.rs +0 -0
  84. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/properties.proptest-regressions +0 -0
  85. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/properties.rs +0 -0
  86. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/rtsa_negative_test.rs +0 -0
  87. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/sdr_source_impl_test.rs +0 -0
  88. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/spec_coverage.rs +0 -0
  89. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/test_cw_mag.rs +0 -0
  90. {python_aaronia-0.8.0 → python_aaronia-0.8.1}/tests/test_cw_meta.rs +0 -0
@@ -0,0 +1,985 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [v0.8.1] - 2026-09-07
6
+
7
+ ### Fixed
8
+ - **SoapySDR TX gate read the arguments, not the backend.** It also
9
+ required a compile-time feature no CMake build passed, so no shipped
10
+ plugin could ever report TX. The sink is now built when the constructed
11
+ source's `source_type` is `NativeSdk` — which covers a bare
12
+ `driver=aaronia` that auto-detects the SDK, and correctly refuses when
13
+ a `serial=` open fell back to HTTP because the SDK is not installed.
14
+ New `-DAARONIA_NATIVE_SDK=ON` builds the Rust library with the feature.
15
+ - **`hasHardwareTime("")` returned true on the file and native-SDK
16
+ backends**, which never set a timestamp — the 1970 bug the previous
17
+ fix removed from HTTP, relocated. Gated on the backend now.
18
+ - **`getAntenna` still hardcoded `RX1`/`TX1`** while `listAntennas` had
19
+ become device-derived, so a V6 in an RX2 mode reported an antenna not
20
+ in its own list. Both now agree; `getClockSource` likewise always
21
+ returns a member of `listClockSources`, and `setClockSource` re-reads
22
+ the device before deciding a request is a no-op instead of comparing
23
+ against a construction-time cache.
24
+ - **A trailing slash on the base URL produced `//info`, which the RTSA
25
+ server answers with 404** — every control-plane request failed.
26
+ Stripped in `HttpEndpointsClient::new`.
27
+ - **`DeviceHealthSummary::losing_nothing` used exact float equality** on
28
+ counters that decay: a stale 2e-7 errors/s blamed the device on every
29
+ gap report. A 0.5/s threshold now.
30
+ - **Device identity was read from the first `info` group in the tree**,
31
+ which on a mission listing the HTTP Server block first reported
32
+ `hardware=HTTP Server`. Lookups anchor to the block owning
33
+ `centerfreq0` / `devstate`.
34
+ - **A declared `0..0` range was published as a capability**, leaving a
35
+ GUI that clamps to it unable to tune. Ranges must be finite with
36
+ `min < max`.
37
+ - **`enum_options` kept empty entries** (a trailing comma became a blank
38
+ clock source); it filters them, and `enum_values_of` is gone.
39
+ - **The sample-rate ladder was capped at 10 rungs** whatever the device
40
+ declared; it is as deep as `decimation0` says
41
+ (`utils::iq_ladder_from_top_n`). `getSampleRateRange` now returns one
42
+ zero-width range per rung instead of a continuous span.
43
+ - **`make clean` in the plugin build deleted the shared Rust archive**
44
+ via `BYPRODUCTS`; removed.
45
+ - **Overlapping stream-gap health probes could overwrite a newer reading
46
+ with an older one.** Readings carry the drop count they were taken at
47
+ (`StreamStats::device_health_drops`) and never regress.
48
+ - **SDK detection only looked in `sdk/`**, so RTSA-Suite 3.0.3 for Linux
49
+ — which ships `libAaroniaRTSAAPI.so` in the install root — was reported
50
+ absent. Both layouts are checked, and an `AARONIA_SDK_PATH` pointing at
51
+ `sdk/` resolves via its parent. Health walker accepts `gpssats`; the
52
+ receive path logs packet warning flags at debug.
53
+
54
+ ### Added
55
+ - **`scripts/sdk-container-test.sh`** loads the real SDK library in an
56
+ x86-64 Linux container and runs the crate's native-SDK load test
57
+ against it — no hardware and no x86-64 host needed. Verified against
58
+ 3.0.3.16655: `AARTSAAPI_Version()` reports 1.4 and the default Linux
59
+ install path is detected with no environment variable. The nine host
60
+ packages the bundled Qt needs are recorded in SDKSPEC.
61
+
62
+ ### Performance
63
+ - **No per-read allocation on the hot paths.** The C ABI and seify reads
64
+ allocated a fresh Vec per call — 512 KiB malloc/free hundreds of times
65
+ a second at full rate; they borrow a scratch buffer held by the source.
66
+ The Python dual-channel read pre-sizes its two vectors.
67
+ - `get_device_capabilities` issues its two GETs concurrently, halving
68
+ `Device::make`'s cost and its worst case.
69
+
70
+ ### Documentation
71
+ - README and PLUGINS pinned `"0.7"` after the breaking 0.8.0; now `"0.8"`.
72
+ PLUGINS.md's TX section matches the new condition. SDKSPEC's Verified
73
+ Architecture and Qt-dependencies sections carry the 3.0.3.16655
74
+ findings. Two dead CMake debug probes (`test.cmake`,
75
+ `soapy-aaronia/print.cmake`) are gone.
76
+
77
+ ## [v0.8.0] - 2026-09-07
78
+
79
+ **Breaking:** `StreamStats` gained a `device_health` field, so exhaustive
80
+ struct literals over it stop compiling. It is `#[non_exhaustive]` now, with
81
+ the new `DeviceCapabilities` and `DeviceHealthSummary`. Only the `futuresdr`
82
+ feature exposes `StreamStats`.
83
+
84
+ ### Added
85
+ - **The SoapySDR probe reports the attached device, not compiled-in
86
+ constants.** It reads `/remoteconfig` and `/healthstatus` once at open and
87
+ publishes model, serial, firmware version, frequency and gain ranges with
88
+ their steps, clock sources, antenna and the sample-rate ladder. A V6 ECO
89
+ declares 5.5 MHz–8 GHz and −55…+23 dBm; the constants said 10 Hz–6 GHz and
90
+ −100…+10 dB.
91
+
92
+ Sample rates come from `status/iqsamples` snapped to an exact
93
+ `receiver_clock / 1.5` rung — new `utils::snap_to_ladder_top`, because the
94
+ reported 61 411 246 Hz is a measurement of a nominal 61 440 000 — then
95
+ halved over `decimation0`'s rungs. `getSampleRateRange` takes its ends
96
+ from that ladder; its old 10 kHz floor sat below the slowest rung.
97
+
98
+ New API: `DeviceCapabilities`, `ValueRange`,
99
+ `HttpEndpointsClient::get_device_capabilities`,
100
+ `AaroniaSource::device_capabilities`, and the C entry points
101
+ `aaronia_source_get_capabilities` / `aaronia_source_capabilities_free`.
102
+ Fields are independently optional; a backend that cannot answer falls back
103
+ per field rather than wholesale.
104
+ - **A stream gap says whether the device caused it.** `HttpSource` reads the
105
+ device's own per-second loss counters (`status/errors`, `usboverflows`,
106
+ `dsboverflows`) on each gap report. Nonzero: the loss starts at the
107
+ device. Zero: it went missing downstream, in the server's 8 MB outbound
108
+ buffer or on the wire, where a second client is one cause. The read is
109
+ detached — the control-plane timeout is 30 s, and stalling `work()` would
110
+ cause the loss it diagnoses. Also `get_device_health()` and
111
+ `StreamStats::device_health`.
112
+
113
+ ### Fixed
114
+ - **Clock source reported as `Internal`, a name the device does not use, on
115
+ hardware locked to an external 10 MHz reference.** `device/sclksource`
116
+ offers `Consumer`, `Oscillator`, `GPS`, `PPS`, `10MHz` and three
117
+ `… Provider` variants. `listClockSources` and `getClockSource` now answer
118
+ from the device; `setClockSource` accepts the current source and warns
119
+ otherwise. `listAntennas` takes its name from `device/devicemode`.
120
+ - **A TX channel was advertised on every device, including receivers that
121
+ cannot transmit.** `aaronia_sink_build` allocates unconditionally, so the
122
+ existing `_sink ? 1 : 0` guard was never false and an application failed
123
+ on the first write. TX now needs the new `aaronia_sink_supported()` and a
124
+ `serial=` open with no `url=`/`file=`. The probe reads `0 Tx`. Still not a
125
+ hardware check: a native-SDK build opened by serial against an ECO would
126
+ advertise TX.
127
+ - **`--probe` reported `Timestamps: NO` on a device that timestamps every
128
+ buffer.** `hasHardwareTime("")` returned the last timestamp, which is 0
129
+ until a packet arrives — and a probe never streams. It is a capability
130
+ query now. `hasHardwareTime("GPS")` still reports a value, since a fix may
131
+ not exist.
132
+ - **The plugin could link a stale Rust archive.** The CMake rule used
133
+ `add_custom_command(OUTPUT …)` with no `DEPENDS`, so cargo was skipped
134
+ whenever the `.a` existed and source edits silently never reached the
135
+ module. It is a custom target now, run every build.
136
+ - **Three clippy lints left CI red since v0.7.7** — a collapsible `if let`,
137
+ a needless borrow and a `field_reassign_with_default`, all in code gated
138
+ to Windows and Linux, which a macOS `cargo clippy` never compiles.
139
+
140
+ ### Documentation
141
+ - **Several clients on one HTTP Server block, measured.** The block serves
142
+ each connection a full copy and refuses none, so *n* clients cost *n*
143
+ times the egress. Two at 15.36 MS/s ran contiguous; five saturated 2.5GbE
144
+ at 293.9 MB/s and the loss fell on an arbitrary two, a different pair on a
145
+ repeat run. No endpoint counts connections — `/info`, `/healthstatus`,
146
+ `/remoteconfig` and the `/stream` headers were all checked. So a clean
147
+ stream is not evidence of being alone, and a gap is not evidence of
148
+ company.
149
+ - **Corrected HTTPSPEC's free-licence claim.** Five clients were served on a
150
+ system holding one HTTP Server block licence: the limit is on block
151
+ instances, not connections to one block.
152
+ - **Link requirements, measured over 2.5GbE.** A 2.5GbE table in
153
+ `link_budget` and a requirements section in the README. An ECO 100 needs
154
+ the 61.44 MS/s rung at 245.8 MB/s, past gigabit. The path saturates at
155
+ 292 MB/s, 93 % of line rate, so the wire is the limit and not the server;
156
+ and two configurations asking 245.8 MB/s by different routes both
157
+ delivered 244 MB/s, confirming only the byte rate matters.
158
+
159
+ ## [v0.7.7] - 2026-09-06
160
+
161
+ ### Added
162
+ - **`link_budget`, so a span the path cannot carry is caught before the
163
+ capture instead of after it.** `required_byte_rate` gives what a span
164
+ costs, `max_sustainable_span` the widest rung a measured rate affords, and
165
+ `measure_link_throughput` measures the path end to end off `/stream` — end
166
+ to end because the bottleneck may be the server, a switch, the air or this
167
+ host's ingest, and the NIC's link speed sees none of them. Measured over
168
+ gigabit: `--span 10M` (15.36 MS/s, 61.4 MB/s) ran contiguous; `--span 20M`
169
+ (30.72 MS/s, 122.9 MB/s) lost 1.84 s of 35 s across 1024 skips.
170
+
171
+ The probe discards a 500 ms settle window (`LINK_PROBE_SETTLE`) first.
172
+ Without it a probe *lies*: the server hands over ~0.27–0.35 s of
173
+ pre-connect backlog faster than real time, so counting it reads above the
174
+ true rate and waves through a span that cannot fit. An unreachable server,
175
+ an idle mission or a stream that stops mid-window is an error and never a
176
+ rate, since "0 MB/s" would condemn every span on the ladder.
177
+ - **`HttpSource` runs the same check passively and warns once**, on the
178
+ stream it is already reading — no second connection. It names the span,
179
+ its requirement, what was measured and the widest span that would have
180
+ fitted. Complements `DropDetector`; when both fire, the gap warning cites
181
+ the verdict's figures instead of reading as an unrelated second fault.
182
+ - **`utils::IQ_RATE_CLOCK_RATIO`, `utils::iq_ladder_from_top`,
183
+ `StreamFormat::iq_bytes_per_sample` and `PacketMetadata::sample_rate()`** —
184
+ names for constants and rules that had been duplicated literals.
185
+
186
+ ### Fixed
187
+
188
+ The link-budget check, all within this release:
189
+ - Judges against the device-reported rate, not the requested one.
190
+ `current_sample_rate`'s 10 % hysteresis meant the builder's 1 MS/s default
191
+ (served by the 0.96 MS/s rung, a 4 % gap against a 2 % tolerance) warned on
192
+ every healthy start. A mid-measurement external retune restarts the meter.
193
+ - Counts decoded IQ payload once per sweep rather than raw chunk bytes.
194
+ Per-chunk counting stamped a whole drained batch at one instant, inflating
195
+ the first post-settle sweep; raw bytes also counted JSON headers and any
196
+ spectra sharing the stream.
197
+ - A consumer stall no longer dilutes the window — an 8 s stall used to
198
+ average dead time into the sustained rate. The observation crossing the
199
+ boundary is excluded in both directions, and `finish` refuses to answer
200
+ before the window closes or when it observed under half its span.
201
+ - A configuration restart re-arms the check; reconnects keep the verdict. A
202
+ parse error in the closing sweep no longer discards a finished measurement.
203
+ - The remedy halves down from the device's own rate, so it names a rung the
204
+ device has, and takes its figures from the stream format instead of
205
+ hardcoding int16's. float32 streams are told int16 halves the requirement.
206
+ - Rate tracking and the meter key on payload type. A spectra header carries
207
+ no `sampleFrequency`, so it yielded a frame rate that ping-ponged the
208
+ tracker, and its scalars were counted at IQ byte width.
209
+ - A failed measurement re-arms instead of retiring permanently; the
210
+ no-verdict state is reserved for configurations that cannot be measured.
211
+ - `measure_link_throughput` refuses windows under `MIN_PROBE_WINDOW`
212
+ (100 ms) and takes the capture's own `StreamParams` and settle window.
213
+
214
+ Streaming and parsing:
215
+ - `rate_reduction(0)` is refused with `Error::Config` at all three entry
216
+ points (new `StreamParams::validate`); it used to reach the server.
217
+ - One framing implementation, `scan_packet_header`, shared by the parser and
218
+ the probe's sniff. They had disagreed on resync, and the old scan was
219
+ quadratic in brace-dense garbage — 256 KiB of `{` took 20 s of a tokio
220
+ worker.
221
+ - An HTTP source starts again after `stop_streaming`; a SoapySDR
222
+ deactivate/activate cycle used to fail.
223
+ - An undecodable packet is skipped instead of wedging the stream and growing
224
+ the buffer without bound. `HttpSource` resets parser and drop detector on
225
+ every reconnect.
226
+ - `read_samples` returns a partial block on timeout instead of losing it — an
227
+ unflagged gap through the C and Python APIs.
228
+ - `IqPacket.sample_rate_hz` reports the streamed rate, not the requested one.
229
+ - The shared client builder no longer pins HTTP/1.1, so ALPN negotiates over
230
+ `https://`, with HTTP/2 adaptive flow control — hyper's default 2 MiB
231
+ window would otherwise have been measured and blamed on the path.
232
+
233
+ RTSA files, native SDK and API surface:
234
+ - `mSampleSize` counts values per sample, not bytes. Used as a byte stride,
235
+ a second partial chunk read landed mid-sample and spectra returned one
236
+ scalar per spectrum.
237
+ - A negative DSFT stream offset no longer overflows the header search;
238
+ `seek_to_sample` works on reverse-order files; spectrum decompression caps
239
+ its output size from packet metadata.
240
+ - The native SDK is shut down by the last client, not the first —
241
+ `AARTSAAPI_Shutdown` is process-wide, and each source and sink called it
242
+ from its own `Drop`.
243
+ - A bare `spectranv6eco` opens `spectranv6eco/rtsa`; the ECO has no `/raw`.
244
+ - Native SDK: `aaronia_source_read_samples_timeout` honours its deadline (it
245
+ polled up to 500 ms regardless); spectra reads honour the packet stride; an
246
+ SDK call returning no object is an error, not a null pointer; a second
247
+ device open on a live source is refused; the device closes on drop.
248
+ - The remote-config licence probe writes the discovered block, so it finds
249
+ `reflevel0` in a V6's receiver block and can report `Active` — it always
250
+ answered `NotLicensed`.
251
+ - `HttpSource` built outside a Tokio runtime errors at build rather than
252
+ panicking inside `init()`.
253
+ - `aaronia_get_error_message` takes an `int` and answers unknown codes, where
254
+ an out-of-range enum was undefined behaviour. C callers are unaffected; a
255
+ Rust caller passes `code as i32`.
256
+ - `HttpSink` clamps `buffer_size(0)` to one; a quiet dwell in hop mode is an
257
+ empty read, not an error counted toward the source-dead bailout; the first
258
+ packet after a failed initial connection is not flagged as an overrun;
259
+ seify reports the observed rate over the ladder's 120 kS/s floor.
260
+ - Clippy passes on Rust 1.98 (`chunks_exact_to_as_chunks`, eight sites).
261
+
262
+ ### Changed
263
+ - **Byte-rate helpers answer `Option<f64>`, never a `0.0` sentinel** —
264
+ `required <= measured` on a garbage rate read "fits", a convention every
265
+ comparison site had to remember. `StreamFormat::iq_bytes_per_sample` is
266
+ `Option<usize>` (`None` for JSON) for the same reason. New
267
+ `max_sustainable_sample_rate_below` anchors a remedy to the device's rate.
268
+ - **The verdict is data, not just a log line.** `LinkBudgetVerdict` is
269
+ published as `StreamStats::link_budget`, so a GUI or orchestrator can
270
+ auto-narrow a span without scraping logs, and `LinkBudgetVerdict::judge` is
271
+ its one producer, so the struct's invariants hold by construction.
272
+ `StreamFormat::CAPTURE_DEFAULT` replaces three separately written `Int16`
273
+ literals.
274
+ - **One copy each of the RTSA client plumbing.** Construction, auth,
275
+ base-URL validation, `/stream` query serialization and status-to-error
276
+ mapping had hand-rolled duplicates in the probe — five drift risks, and the
277
+ three reqwest clients had already drifted to three setting subsets.
278
+ - **`HttpSource`'s stream-open failure is `Error::Http { status, context }`**,
279
+ the variant the rest of the crate returns. Code matching the old
280
+ `Error::Protocol("Stream endpoint returned error: …")` text needs updating.
281
+ - `work()`'s output copy is two bulk `copy_from_slice` calls over the deque's
282
+ contiguous halves instead of a `pop_front` per sample.
283
+
284
+ ### Performance
285
+ - The parser drops an over-cap payload as it arrives instead of buffering it;
286
+ uncompressed spectra decode without a copy; spectra file reads decode in
287
+ place; the C API reserves the caller's length before a read;
288
+ `sample_rate_hz` is an atomic load, so stamping every packet takes no lock.
289
+
290
+ ### Documentation
291
+ - **The HTTP transport measured on WiFi 7.** `curl` on `/stream` sustains
292
+ ~75 MB/s (0.6 Gbit/s) station to station at a 2.4 Gbps PHY — both ends on
293
+ air halves the medium — and the figure is identical for all three wire
294
+ formats, so the encoding is not the limit. Two parallel connections
295
+ aggregate *less* (63.9 against 74 MB/s), so one connection is optimal.
296
+ Against that, the crate's framing-plus-decode path measures ~3 GB/s and the
297
+ individual decoders 0.8–10 GS/s: fortyfold headroom, never the bottleneck.
298
+ At 4 bytes a sample the link buys ~19 MS/s — the 15.36 rung fits, 30.72
299
+ does not, and full span needs a wired path. Two ignored throughput-meter
300
+ tests keep the numbers re-measurable in one command.
301
+ - README and PLUGINS pinned `sdr-aaronia-rs = "0.6"`, a major behind the
302
+ crate, so nothing they described resolved. Now `"0.7"`.
303
+ - `cumulative_drops` counts client-detected timestamp gaps; four docs called
304
+ them server-reported drops. Bandwidth figures use the 61.44 MS/s top rate
305
+ rather than the 92 MHz clock.
306
+
307
+ ## [v0.7.6] - 2026-08-15
308
+
309
+ All of this is the FutureSDR `HttpSource` block (the `futuresdr` feature),
310
+ whose streaming path turned out to be losing most of the stream. Found and
311
+ measured against a live SPECTRAN V6 ECO running a 49 MHz survey.
312
+
313
+ ### Added
314
+ - `iq_sample_rate_for_decimation_index`, `decimation_index_for_rate` and
315
+ `decimation_index_for_bandwidth` in `utils`: the RTSA "Span" enum (`Full`
316
+ … `1 / 512`) mapped onto the sample-rate ladder and back, so a requested
317
+ span becomes the index the device takes.
318
+ - `HttpEndpointsClient::apply_capture_config` — retune via `/remoteconfig`
319
+ with read-back confirmation — plus `find_block_name_with_field`, which
320
+ discovers the receiver block by the field the write targets rather than
321
+ assuming its category.
322
+
323
+ ### Fixed
324
+ - **The sample buffer guillotined every packet bigger than itself.** With
325
+ capacity fixed at `buffer_size * 2`, a device sending 49k-sample packets
326
+ into a 16384-sample capacity lost ~75 % of every packet before the
327
+ consumer saw any of it. Measured live: 61.4 MS/s at the device, 0.33 MS/s
328
+ reaching the pipeline, digital decode unable to hold frame sync. Capacity
329
+ now floors at four times the largest packet observed.
330
+ - **The initial tune could claim success while the device never moved.**
331
+ `/control` answers `success=true` whether or not a block applies the
332
+ command. The start-up tune now writes `centerfreq0`, `decimation0` and
333
+ `reflevel0` via `/remoteconfig` to the block found by walking the config
334
+ tree, reads them back and warns about anything that did not take. Where no
335
+ such block exists it falls back to `/control` and says the result is
336
+ unverified. The tune is one-shot, so a restart after an external retune no
337
+ longer re-pushes a stale target.
338
+ - **Launching an app no longer overwrites the operator's gain.** The
339
+ builder's reference level was pushed on every start; it is optional now
340
+ and left untouched unless set.
341
+ - **`work()` spun.** The runtime ran it whenever the output port had any
342
+ room — 106,000–390,000 calls a second averaging 14 free samples each,
343
+ burning ~60 % of a core and starving the downstream block. The port now
344
+ requires a worthwhile block of room; call rate dropped to ~14/s.
345
+ - The refill loop kept iterating through its 50 ms idle sleeps when the
346
+ channel was empty, holding buffered samples for up to 800 ms. It flushes
347
+ immediately and sleeps only with nothing to flush.
348
+ - A parse error mid-stream reconnected without reaping the reader task,
349
+ which on a stalled socket holds a server connection open.
350
+
351
+ ### Changed
352
+ - **The HTTP socket is drained by a dedicated task**, not inside `work()`.
353
+ Reading it only when the scheduler ran the block capped throughput at a
354
+ tenth of what `curl` pulls from the same endpoint. A background task now
355
+ pushes chunks into a bounded channel (~10 MB) whose fill is the
356
+ backpressure point: consumer falls behind, channel fills, reader blocks,
357
+ TCP flow control stops the server. Measured at 49 MHz span: 1.8 → ~7–9
358
+ MS/s. The ceiling is the link (~57 MB/s ≈ 14 MS/s as float32).
359
+ - Buffer-overflow drops are counted and logged geometrically rather than per
360
+ occurrence — 5040 log lines in 25 seconds at wide span, for one fact.
361
+ - File playback decodes little-endian cf32 straight into the sample buffer;
362
+ the per-sample decode remains for big-endian hosts, with a round-trip test
363
+ pinning the two to identical output.
364
+
365
+ ## [v0.7.5] - 2026-08-13
366
+
367
+ ### Added
368
+ - **`scale` on the Python config and `aaronia.open()`**, the integer encode
369
+ multiplier for the `I16` wire format. Rust, the C API and the SoapySDR
370
+ plugin all had it; Python did not, leaving no way out of the trap below.
371
+ - **`scripts/validate-iq-live.py`**, an end-to-end check that the samples an
372
+ application receives are the ones the device sent. Every wire format must
373
+ decode to the same spectrum, the Python and SoapySDR paths must agree, and
374
+ a known transmitter must land where it should. Against a live server it
375
+ places a NOAA carrier within 312 Hz of 162.400 MHz and on the correct side
376
+ of zero — the one check that catches transposed I and Q.
377
+
378
+ ### Fixed
379
+ - **`format="I16"` silently discards weak signals at the default scale.**
380
+ The server sends `round(value * scale)`, so the step is `1 / scale`, and
381
+ the default 16384 gives 6.1e-5 — coarser than a quiet band's noise floor.
382
+ Measured: **68 % of int16 samples came back exactly zero** where float32
383
+ had none. At `scale=1e6` the zero fraction was 0.0 % and the amplitude
384
+ matched float32.
385
+
386
+ ### Changed
387
+ - **The Homebrew formula is no longer a release asset**, but the
388
+ `homebrew-formula` workflow artifact. Homebrew 4 cannot install from a
389
+ formula URL, so on the release page it was a file no user could act on.
390
+ Checksums are still rendered against the published archives.
391
+
392
+ ### Documentation
393
+ - **Wire format is a throughput decision, and the default is the expensive
394
+ one.** At 15.36 MS/s over a LAN float32 needs 123 MB/s: measured, it
395
+ delivered 6.5 MS/s with 290 drops, while float16 and int16 both delivered
396
+ 15.1 MS/s. The Python README carries the numbers.
397
+ - **What the 80 % usable-bandwidth figure is.** RTSA declares exactly
398
+ 0.8 × Fs as the packet's frequency range at every rate — checked at 61.44,
399
+ 15.36, 7.68 and 3.84 MHz — and every sample still arrives, so an FFT spans
400
+ the whole rate while only that 80 % is flat and calibrated. Sweeping a V6
401
+ ECO's own noise floor confirms it: flat within 0.5 dB across 0.80 of the
402
+ rate at 15.36 MHz and 0.89 at 7.68 MHz, and at full span the analog filter
403
+ is ~1 dB down by the declared edge — which is where Aaronia's 44 MHz
404
+ data-sheet figure comes from, against the 49.152 MHz the device declares.
405
+ To see N Hz of spectrum, sample at N / 0.8.
406
+ - **The READMEs stated the V6 ECO's ladder as if it were every device's.**
407
+ 61.44 MHz halved to 120 kHz is measured and true for an ECO; a full V6
408
+ selects its receiver clock and starts higher. The Python, SoapySDR,
409
+ quickstart and applications docs now say whose ladder it is, as does
410
+ `iq_sample_rates`. The SoapySDR README gained a Sample rates section.
411
+ - The same sweep caught universal-sounding claims that are one device's
412
+ measurements, in `unified_source`, HTTPSPEC's `/control` span note and the
413
+ 0.8 ratio. `seify_impl`'s range is capped at 61.44 MHz for the same reason
414
+ and says so: seify has no device handle there to ask for better.
415
+ - SDKSPEC gave the eco family's clock as 61.44 MHz in a second place. It is
416
+ 92.16 MHz; 61.44 MHz is the top IQ rate.
417
+
418
+ ## [v0.7.4] - 2026-08-12
419
+
420
+ ### Fixed
421
+ - **The SoapySDR plugin ignored an unrecognised `format=` silently.** A
422
+ device string carrying `format=int16` — the wire name rather than the
423
+ plugin's `I16` — streamed the default format while claiming otherwise. It
424
+ warns and continues now. The server behaves worse: an unrecognised
425
+ `format=` on `/stream` serves the RTSA file format with HTTP 200 rather
426
+ than an error, so a typo changes the wire format entirely. `raw16`, which
427
+ Aaronia's Qt reference client sends, is a working alias for `int16`.
428
+
429
+ ### Documentation
430
+ - Checked Aaronia's V6 remote control notes (rev 4, May 2026) against the
431
+ hardware. `/remoteconfig` enum fields take an index as well as a label;
432
+ one `simpleconfig` PUT can carry several groups, and groups other than
433
+ `main` work; a PUT naming a block not in the mission returns 200 and
434
+ changes nothing, which `simple_remote_config` now warns about since it
435
+ reported `Ok(())` for a write that did not happen. In the config-tree form
436
+ the receiver name is ignored — the write is routed by `config.name`.
437
+ - Documented loading a mission over `/control`, and that every `/control`
438
+ payload needs its `type` or the server answers `400`. Loading a mission is
439
+ deliberately not exposed: swapping the mission under a running capture
440
+ should be a caller's decision, not a side effect.
441
+ - RTSA-Suite has no status endpoint; Aaronia's own liveness check reads the
442
+ `404` from `/api/status` as proof the server is up.
443
+ - **What "Full" means on a full V6 is unresolved.** A V6 ECO follows the
444
+ SDK's `spanfreq <= receiverclock / 1.5`, measured. Aaronia's Remote Config
445
+ screenshots show a full V6 at a 92 MHz clock delivering 92.16 MHz of IQ
446
+ samples per second at span "Full" — the clock itself.
447
+ `iq_sample_rates_for_clock` may therefore understate the top of the ladder
448
+ by 1.5× for a full V6 at a non-default clock; it says so now.
449
+ - HTTPSPEC contradicted itself on the Remote Config licence. A live
450
+ unlicensed system accepts writes, re-confirmed for centre frequency,
451
+ decimation, reference level and the preamplifier.
452
+ - SDKSPEC gave the V6 ECO's receiver clock as 61.44 MHz, which 0.6.2
453
+ corrected in code to 92.16 MHz. 61.44 MHz is the ECO's top IQ rate.
454
+ - **A marker stream is not a categories packet**, which the previous draft
455
+ of this entry got wrong. Aaronia's example declares `payload: "spectra"`,
456
+ and spectra samples are a 2D array, so its nesting is correct. Its three
457
+ frequency fields are all zero, so the category names and ranges are the
458
+ only description of what the numbers mean.
459
+ - Folded in Aaronia's endpoint specification (rev 11). `/control` takes PUT
460
+ only, and a command reaches every block that understands it unless
461
+ `receiverUUID` or `receiverName` scopes it. Per-type settings are listed
462
+ in full, including `deviceconnect` and `camera`, which this crate does not
463
+ model; zones cannot be configured remotely. The server drops data once its
464
+ outbound TCP buffer passes 8 MB, the mechanism behind most unexplained
465
+ gaps. `/healthstatus` is organised as `info`, `status`, `health`,
466
+ `settings` and `components`.
467
+ - **`status/iqsamples` is the native rate, not the delivered one.** It held
468
+ at 61.44 MHz while the same device delivered 15.36, then 7.68, then
469
+ 61.44 MS/s. Read `sampleFrequency` from packet metadata instead.
470
+ - One HTTP Server and one HTTP Client instance are free; additional
471
+ instances are licensed separately, as are Stream Merger and Stream
472
+ Splitter. **This entry originally read that a second concurrent client
473
+ meets the limit — corrected in v0.8.0**, where five simultaneous clients
474
+ were served on a one-block licence. The limit is on block instances in the
475
+ mission graph, not connections to one block.
476
+
477
+ ## [v0.7.3] - 2026-08-12
478
+
479
+ ### Fixed
480
+ - The Windows leg of the new module load check could not run: vcpkg's
481
+ SoapySDR port ships no `SoapySDRUtil`, so the check failed rather
482
+ than verifying anything, and 0.7.2 published no release archives.
483
+ Where the tool is absent the packaged DLL is now loaded directly,
484
+ which still catches a module whose dependencies do not resolve away
485
+ from the build machine.
486
+
487
+ ## [v0.7.2] - 2026-08-12
488
+
489
+ ### Fixed
490
+ - **The published macOS SoapySDR module would not load, on any Mac.**
491
+ `dlopen` failed with "symbol not found in flat namespace" for a Rust
492
+ vtable entry that the linker had itself defined and localised in the
493
+ same image. SoapySDR's installed CMake export lists `-flat_namespace`
494
+ in the imported target's interface, so it reached the end of every
495
+ module's link line and forced flat-namespace binding; the module
496
+ links directly against libSoapySDR and does not need it. Removing it
497
+ takes the module from 5670 flat-namespace binds to none, so the
498
+ failure cannot recur rather than depending on a linker version — some
499
+ hit the defect and some did not, which is why local builds worked.
500
+ Every 0.5.x, 0.6.x, 0.7.0 and 0.7.1 macOS archive is affected; the
501
+ Linux and Windows modules are not.
502
+ - **The packaged module's load check ran on Linux only**, which is how
503
+ the above shipped for as long as it did. It now runs on all three
504
+ platforms at release time, and CI builds and checks the plugin on
505
+ macOS as well as Linux. Both check the reported text: `--check` exits
506
+ 0 even when the driver failed to load.
507
+
508
+ ### Documentation
509
+ - The Linux module needs glibc 2.38 or later, so it does not load on
510
+ Ubuntu 22.04 or Debian 12. Said so in the plugin README.
511
+
512
+ ## [v0.7.1] - 2026-08-12
513
+
514
+ An incomplete fix for the macOS module, superseded by 0.7.2. Like
515
+ 0.7.2, it reached crates.io and PyPI but published no release archives,
516
+ because the new load check refused to ship a macOS module that would
517
+ not open. Its crate and wheels are sound and identical in content to
518
+ 0.7.3.
519
+
520
+ ## [v0.7.0] - 2026-08-12
521
+
522
+ ### Added
523
+ - **`aaronia.open()`, block iteration and context-manager support in
524
+ Python.** The shortest working program is now three lines. `open()`
525
+ takes the URL, frequency and either an exact `rate` or the
526
+ `bandwidth` you want covered, connects, and starts streaming;
527
+ `for block in src.blocks(65536)` ends when the source runs out
528
+ instead of raising; and `with` stops the stream even when the body
529
+ fails, raising a failed teardown only if the body itself succeeded.
530
+ The old config-object path is unchanged and still the way to reach
531
+ every option.
532
+ - **`aaronia-doctor`, a command that checks an RTSA server.** It reports
533
+ whether the server answers, whether the mission has an input carrying
534
+ IQ, and what rate the device is running, and prints the fix for each
535
+ failure. The same checks are available as `aaronia.diagnose(url)`,
536
+ which returns `(ok, message, fix)` tuples, bounded at 20 seconds so a
537
+ stalled server cannot leave it waiting. Every failure it names is one
538
+ that otherwise shows up as a timeout with no explanation.
539
+ - **`aaronia.sample_rates()` and `aaronia.sample_rate_for_bandwidth()`**,
540
+ exposing the crate's rate ladder to Python so a program can ask for a
541
+ rate the hardware will actually run.
542
+ - **`Error::StreamClosed`**, separating "the stream ended" from "a read
543
+ failed". Both used to arrive as `Error::Protocol`, so a consumer
544
+ could not tell a capture that finished from one that was cut short.
545
+ Rust code matching on `Error::Protocol` for the closed-stream case
546
+ needs the new variant instead; the enum is `#[non_exhaustive]`, so
547
+ existing wildcard arms keep compiling. In Python the matching
548
+ exception is `AaroniaStreamClosed`, a subclass of
549
+ `AaroniaConnectionError`, so existing handlers are unaffected. It is
550
+ what lets `blocks()` end a loop on a finished stream while still
551
+ raising on a timeout or a transport failure, which would otherwise
552
+ make a truncated capture look like one that simply ran out.
553
+ - **An installer in every SoapySDR release archive.** `install.sh`
554
+ (`install.ps1` on Windows) finds SoapySDR's module directory, clears
555
+ the macOS quarantine flag, copies the module in, and confirms it
556
+ loads. It prints instructions rather than guessing when SoapySDR is
557
+ missing.
558
+ - **A Homebrew formula for the SoapySDR plugin**, in
559
+ `packaging/homebrew`. The release workflow renders it against the
560
+ published archives, checksums included, and attaches it to the
561
+ release, so updating a tap is a copy.
562
+
563
+ ### Fixed
564
+ - The SoapySDR application guide still listed the old invented sample
565
+ rates for GQRX. It now describes the real ladder.
566
+
567
+ ## [v0.6.2] - 2026-08-12
568
+
569
+ ## [v0.6.1] - 2026-08-12
570
+
571
+ ### Fixed
572
+ - **The SoapySDR plugin advertised sample rates the hardware cannot
573
+ produce.** Seven of the ten rates it listed, 1, 2, 5, 10 and 20 MHz
574
+ among them, do not exist on the device, which runs at 61.44 MHz
575
+ divided by a power of two. Applications build their rate dropdowns
576
+ from that list, so choosing 10 MHz ran the device at a different rate
577
+ while the application went on displaying 10. The list is now the real
578
+ ladder, 61.44 MHz down to 120 kHz, and `setSampleRate` snaps to the
579
+ nearest one and logs when it has to.
580
+ - **The crate reported the requested sample rate rather than the one in
581
+ use.** The device adjusts a rate it cannot produce, so
582
+ `get_source_info()` described a capture that was not happening. HTTP
583
+ sources now report the rate, centre frequency and usable bandwidth
584
+ from the stream's own metadata once packets arrive.
585
+
586
+ ### Added
587
+ - `iq_sample_rates`, `usable_bandwidth_hz`, `nearest_iq_sample_rate` and
588
+ `iq_sample_rate_for_bandwidth` in `utils`, with the constants
589
+ `IQ_CLOCK_HZ` and `USABLE_BANDWIDTH_RATIO`. The device samples at
590
+ 61.44 MHz divided by a power of two and delivers 0.8 of that as
591
+ alias-free bandwidth. Callers were deriving their own rates from
592
+ guesses, so the relationships now live in one tested place. Wanting
593
+ 8 MHz of spectrum needs 10 MHz of sampling, and
594
+ `iq_sample_rate_for_bandwidth` returns the 15.36 MHz that provides it.
595
+ `iq_sample_rates_for_clock` covers devices whose receiver clock is not
596
+ the default: a V6 ECO has a fixed clock and gives the measured ladder,
597
+ while a full V6 can select a faster one and reach further. Aaronia's
598
+ samples set `device/receiverclock` to "92MHz" or "245MHz"; only the
599
+ default has been checked against hardware.
600
+
601
+ ### Added (native SDK)
602
+ - **Device-family auto-detection.** `detect_device_family` and
603
+ `open_detected_device` try each known family in turn, so an ECO owner
604
+ no longer has to know that the default `spectranv6` will not find
605
+ their device and that `spectranv6eco` is the string they needed.
606
+ - **`read_spectra`**, with the stream index taken from the open mode
607
+ rather than assumed. `spectranv6/raw` carries spectra on stream 2 and
608
+ IQ on stream 0; every other mode uses stream 0. Hardware-unverified.
609
+ - **`receiver_clock_hz`** on the native source, and
610
+ `spectranv6eco/rtsa` added to the known open modes. The clock sets the
611
+ rate ladder's ceiling, so callers that need to know which rates exist
612
+ can now ask instead of assuming.
613
+
614
+ ### Fixed (native SDK)
615
+ - **The V6 ECO's fixed receiver clock was recorded as 61.44 MHz.** It is
616
+ 92.16 MHz: an ECO streams at 61.44 MHz sampling, measured against real
617
+ hardware, and the constraint checked at configuration time is
618
+ `span * 1.5 <= clock`. The old value rejected every span above
619
+ 40.96 MHz, including the device's own maximum.
620
+ - **Dual-channel capture selected the wrong mode and would have
621
+ returned corrupted samples.** `RxChannel::Rx1And2` wrote
622
+ `device/receiverchannel = "Rx1+Rx2"`, which delivers the two inputs as
623
+ two independent streams at indices 0 and 1. This crate reads a single
624
+ stream and deinterleaves it, which is the contract of the other mode,
625
+ `"Rx12"`. On a two-input V6 the result would have been Rx1's samples
626
+ split into two bogus channels, with no error anywhere. It now writes
627
+ `"Rx12"`. Aaronia's `RawIQ2RX` and `RawIQ2RXInterleave` samples
628
+ demonstrate one mode each. Still hardware-unverified.
629
+ - **Sweep mode set the wrong resolution-bandwidth key.** It sent
630
+ `main/rbw`, which no Aaronia sample uses; the key is `main/rbwfreq`.
631
+ Checked against Aaronia's published `SweepSpectrumEco` sample, which
632
+ also confirms `main/startfreq`, `main/stopfreq`, `main/reflevel` and
633
+ the `spectranv6eco/sweepsa` open string that this crate already used.
634
+ The sweep path remains hardware-unverified.
635
+
636
+ ### Documentation
637
+ - **GPS time needs GPS switched on, and the crate does not do it.**
638
+ Devices ship with `device/gpsmode` disabled, so `get_gps_time` would
639
+ return `None` indefinitely and appear broken. Aaronia's `GPSTime`
640
+ sample sets `device/gpsmode` to "Location and Time" and
641
+ `device/sclksource` to "GPS Provider" before starting the device;
642
+ `get_gps_time` now says so.
643
+ - Documented what `/control`'s `frequencySpan` actually means. It is a
644
+ request for usable bandwidth, not a sample rate: the device picks the
645
+ rate whose alias-free span is nearest, so 2.5 MHz yields 3.84 MHz and
646
+ 10 MHz yields 15.36 MHz. Values on the rate ladder round-trip exactly,
647
+ which is why the field looks like a sample rate in ordinary use.
648
+ Verified across nine requests on a V6 ECO.
649
+
650
+ ## [v0.6.0] - 2026-08-11
651
+
652
+ Reliability and documentation release. The HTTP backend now handles a
653
+ server that is still starting up and a stream that drops mid-session.
654
+ The documentation now says which features have been tested on real
655
+ hardware and which have not.
656
+
657
+ ### Breaking
658
+ - `AaroniaConfig` has two new public fields, `read_timeout` and
659
+ `auto_reconnect`. Struct-literal construction needs updating. The
660
+ builder methods are unchanged.
661
+ - Dropped HTTP streams now reconnect by default. Previously a dropped
662
+ stream ended the session and every later read failed. Reads can now
663
+ block for up to about 8 seconds while reconnecting. Call
664
+ `auto_reconnect(false)` for the old behaviour.
665
+ - SoapySDR plugin downloads are now per-platform archives named
666
+ `SoapyAaronia-<version>-<os>-<arch>.tar.gz`, or `.zip` on Windows.
667
+ The bare `.so`, `.dll` and `.dmg` files are gone. Scripts that
668
+ download them by name need to unpack the archive instead.
669
+
670
+ ### Added
671
+ - **Connect retry.** Reaching the server now retries transient failures
672
+ up to 4 times over at most 10 seconds. A `*.local` hostname often
673
+ refuses the first connection while mDNS resolves, which used to look
674
+ like the server was down. Client errors still fail immediately.
675
+ - **Automatic stream reconnection** (`auto_reconnect`, on by default).
676
+ The reader reopens the stream up to 5 times, re-applies the current
677
+ tuning, and marks the first packet after the gap as an overrun.
678
+ Re-applying tuning matters because a restarted server returns to its
679
+ mission's frequency, which would otherwise stream the wrong band
680
+ unnoticed. The retry budget resets only after a connection survives
681
+ 30 seconds, so a server that accepts and immediately hangs up cannot
682
+ reconnect forever. Also available through
683
+ `aaronia_source_builder_auto_reconnect` and the SoapySDR argument
684
+ `reconnect=0|1`.
685
+ - **Configurable read timeout** (`read_timeout`, default 30 seconds),
686
+ replacing a hard-coded value. Available on both builders, as a Python
687
+ property, through `aaronia_source_builder_read_timeout_us`, and as a
688
+ `read_timeout=<seconds>` SoapySDR argument. `read_samples_deadline`,
689
+ which the SoapySDR and seify paths use, still takes its deadline from
690
+ the caller.
691
+ - `DropDetector::resync()` clears the packet-timing history but keeps
692
+ the counters. `reset()` clears both, which would make the running
693
+ total jump backwards after a reconnect.
694
+ - **Python type stubs.** Editors and type checkers now understand the
695
+ module.
696
+
697
+ ### Fixed
698
+ - **Channel hopping could stall for up to 30 seconds.** The hop loop
699
+ waited for a full block of samples, far longer than a 20-40 ms dwell,
700
+ so a slow server starved the remaining hops. It now stops waiting at
701
+ the dwell deadline.
702
+
703
+ ### Documentation
704
+ - **The README says what has been tested on hardware.** Each feature is
705
+ marked live-verified, verified manually, mock-tested, or
706
+ hardware-unverified. Transmit, dual-channel and the native-SDK paths
707
+ have never run against a device.
708
+ - **New quickstart** (`docs/QUICKSTART.md`) covering RTSA-Suite mission
709
+ setup, which everything depends on and nothing documented: adding the
710
+ HTTP Server block, connecting the device output to it, checking it
711
+ with `curl`, and the mistakes that cost the most time.
712
+ - **New application guide** (`docs/APPS.md`) for SDR++, GQRX, GNU Radio
713
+ and SoapySDR from Python. It explains that the single `REF` gain
714
+ element is a reference level in dBm, so raising it reduces
715
+ sensitivity.
716
+ - **New usage guide** (`docs/USAGE.md`) holding the worked examples that
717
+ were 60% of the README. They are compiled as doctests now, so they
718
+ cannot fall out of date. The README is down from 516 lines to 291.
719
+ - **Install instructions for the prebuilt SoapySDR plugin.** Releases
720
+ always attached built modules, but the README only explained building
721
+ from source.
722
+
723
+ ### Release
724
+ - Release archives now carry the module, install instructions and the
725
+ licence, and their names state the version, OS and architecture.
726
+ - The Linux module ships stripped of debug symbols, 19.1 MB down to
727
+ 15.4 MB. The rest is statically linked Rust, not symbols. The release
728
+ job checks the stripped module still loads before publishing it.
729
+ - GitHub releases now use this file's entry for the tag as their
730
+ description, with the generated commit list below it.
731
+
732
+ ## [v0.5.1] - 2026-08-11
733
+
734
+ ### Fixed
735
+ - **Retuning silently did nothing on real hardware.** The `/control`
736
+ endpoint only applies a frequency change when `frequencyCenter` and
737
+ `frequencySpan` are both present. Sending one of them returns
738
+ `{"success":true}` and is ignored, so `set_center_frequency` reported
739
+ success while the device kept streaming at its old frequency. This
740
+ affected hop mode and the SoapySDR, seify and Python retune paths. All
741
+ of them now send the full set of values, and `configure_capture` warns
742
+ when given a partial one. Reference-level changes were unaffected;
743
+ they apply on their own.
744
+ - Corrected the documents that blamed this on the Aaronia "Remote
745
+ Config" licence. Retuning uses the licence-free `/control` endpoint;
746
+ the licence only gates `/remoteconfig` writes.
747
+
748
+ ### CI
749
+ - Clippy now runs across the whole workspace. The `python-aaronia`
750
+ member was never linted and had accumulated 12 errors.
751
+
752
+ ## [v0.5.0] - 2026-08-11
753
+
754
+ Adds Python bindings, a SoapySDR plugin, transmit support and GPS time,
755
+ alongside verification of the RTSA file format against the vendor
756
+ specification. Receive paths were tested against real hardware.
757
+ Transmit and dual-channel paths were not, and are marked as such in the
758
+ documentation.
759
+
760
+ ### Breaking
761
+ - `RtsaMetadata` lost `device_name`, `stream_sample_rate` and
762
+ `stream_center_frequency`, and `RtsaSource::stream_info()` returns a
763
+ 3-tuple. Those fields came from a file layout no real capture uses and
764
+ were always `None`.
765
+ - `SdkConfig` and `AaroniaConfig` gained a public `receiver_channel`
766
+ field, which breaks struct-literal construction.
767
+ `NativeSdkSource::configure_iq_receiver` takes the channel as a fourth
768
+ argument so retunes keep it.
769
+
770
+ ### Added
771
+ - **Python bindings** (`python-aaronia`), published to PyPI: abi3
772
+ wheels for CPython 3.9 and later, typed exceptions, live retuning, and
773
+ single-copy reads into NumPy or PyArrow. Blocking calls release the
774
+ GIL, so other Python threads keep running.
775
+ - **SoapySDR plugin** (`soapy-aaronia`): receive streaming in CF32 and
776
+ CS16, honoured timeouts with partial reads, safe retuning while
777
+ streaming, and device arguments for URL, file, serial, frequency,
778
+ rate, reference level, wire format and RX channel.
779
+ - **Transmit support** through the native SDK, via `UnifiedSink` and the
780
+ `aaronia_sink_*` C API. Hardware-unverified, and unavailable outside
781
+ Windows and Linux.
782
+ - **GPS time** through `get_gps_time`, native SDK only.
783
+ - **Receiver channel selection** (`Rx1`, `Rx2`, `Rx1And2`) and true
784
+ dual-channel capture through `read_samples_dual`. A read-mode latch
785
+ stops single- and dual-channel reads being mixed by accident.
786
+ - `StrmChunk::capture_start_offset`, an undocumented field in RTSA files
787
+ identified as the capture start. Reported time spans now match the
788
+ recorded data.
789
+ - `scripts/ci-local.sh`, which runs CI's checks locally, including a
790
+ Linux VM step covering the OS-gated native-SDK code.
791
+ - Property tests: opening arbitrary bytes as an RTSA file must never
792
+ panic.
793
+
794
+ ### Fixed
795
+ - RTSA chunk parsing corrected against the vendor specification and real
796
+ captures: chunk padding and tail offsets, fixed field sizes, the STRM
797
+ layout, enum numbering, and end-time treated as a duration.
798
+ - Compressed spectra chunks now report an error instead of returning
799
+ compressed bytes as if they were samples.
800
+ - Native SDK: a corrupt packet no longer blocks every later read, and
801
+ buffers are flushed when streaming stops.
802
+
803
+ ## [v0.3.5] - 2026-08-06
804
+
805
+ Documentation release. Every Markdown file was checked against the code
806
+ and corrected. No API changes.
807
+
808
+ ### Changed
809
+ - DESIGN.md, the README, CONTRIBUTING and the three specifications in
810
+ `docs/` corrected against the implementation, with explicit notes
811
+ where only hardware can settle a question.
812
+ - Clarified that HTTP retuning uses the licence-free `/control`
813
+ endpoint.
814
+ - Examples cleaned up. `http_iq_quickstart` takes the server URL as an
815
+ argument instead of hardcoding a private hostname.
816
+ - Five tests that asserted nothing now assert something.
817
+
818
+ ### Fixed
819
+ - Stale comments about symbol counts, renamed tests, and inverted
820
+ `?scale=` semantics.
821
+
822
+ ## [v0.3.4] - 2026-07-31
823
+
824
+ ### Fixed
825
+ - **Native SDK: the end of every oversized packet was discarded.** The
826
+ reader copied out only what the caller asked for, then released the
827
+ whole packet back to the SDK. The SDK chooses its own packet size, so
828
+ everything past the request was lost. This left holes in the IQ
829
+ stream, which breaks the phase continuity that downstream correlation
830
+ and frequency tracking depend on. Extra samples are now kept and
831
+ returned by later calls.
832
+ - **Native SDK: a zero-sized read destroyed a packet.** It now returns
833
+ immediately.
834
+ - **Transmit bursts were scheduled at the Unix epoch.** The FutureSDR
835
+ sink set every burst's start time to zero, against the device's own
836
+ master clock. Timestamps now come from the master clock, falling back
837
+ to immediate dispatch when it cannot be read. Hardware-unverified.
838
+ - **`http_source` reported a buffer capacity it did not enforce.** With
839
+ a buffer size of zero, a consumer computing a fill ratio divided by
840
+ zero.
841
+ - **Diagnostic previews could panic on non-ASCII text.** They cut
842
+ strings at byte offsets, so one accented character in a mission name
843
+ was enough.
844
+
845
+ ### Changed
846
+ - `cargo clippy --all-targets` compiles on a default checkout again.
847
+ - `SdkConfig::timeout` and `SdkSinkConfig::timeout` documented as
848
+ inert. Nothing reads them. They are kept because they are public.
849
+
850
+ ## [v0.3.3] - 2026-07-31
851
+
852
+ ### Fixed
853
+ - **A chunk scan could loop forever** on a file whose signature was
854
+ missing, reachable by opening an untrusted file.
855
+ - **A failed configuration probe left the device 1 dB off.** The restore
856
+ now runs on every exit path.
857
+
858
+ ## [v0.3.2] - 2026-07-13
859
+
860
+ ### Added
861
+ - `DwellAdvice::channel_override` is honoured, and hop mode no longer
862
+ requires the Remote Config licence.
863
+
864
+ ## [v0.3.1] - 2026-07-11
865
+
866
+ ### Fixed
867
+ - **Decompression rejects an out-of-range compression factor** instead
868
+ of overflowing on it. The value can come from a network packet, so a
869
+ corrupt header could previously cause a panic.
870
+ - **Decompression truncates over-long coefficient streams**, having
871
+ previously only padded short ones.
872
+ - **The capture thread is panic-guarded**, so a panic is logged instead
873
+ of killing the thread silently.
874
+ - **HTTP overrun detection reaches `IqPacket::overrun`.** Hop and
875
+ single-channel modes report real overrun status instead of always
876
+ `false`.
877
+ - **The C API sample copy is bounds-checked** against the source length
878
+ as well as the caller's capacity.
879
+ - Raised the `orecchiette-sdr-source-rs` floor to 0.1.2.
880
+
881
+ ## [v0.3.0] - 2026-07-11
882
+
883
+ ### Removed
884
+ - **Breaking: the `file_performance` module is gone**, along with the
885
+ `memmap2` dependency. This memory-mapped reader was never used by the
886
+ crate's actual file path and had no callers. `RtsaSource` covers the
887
+ same needs through buffered I/O.
888
+
889
+ ### Fixed
890
+ - A misplaced doc comment marked `with_shared_stats` a no-op when it is
891
+ not.
892
+ - `HttpSink` counts a batch as dropped when its sender task has died,
893
+ not only on a failed push. It also stops that task on drop.
894
+
895
+ ### Changed
896
+ - The last four native-SDK methods that hand-rolled error checks now use
897
+ the shared path, so failures carry structured errors.
898
+ - Shared device-type parsing extracted out of `SdkConfig` and
899
+ `SdkSinkConfig`.
900
+
901
+ ## [v0.2.6] - 2026-07-10
902
+
903
+ ### Added
904
+ - **Transmit through the native SDK** (`SdkSink`, `SdkSinkConfig`) with
905
+ FutureSDR integration, matching the receive path.
906
+ - `examples/native_sdk_transmit.rs`, sending a LoRa-style chirp.
907
+
908
+ ### Changed
909
+ - Replaced the opaque `Error::Sdk(String)` with a structured
910
+ `Error::SdkApi`, so callers can react to specific failures.
911
+ - SDK warnings are logged as warnings instead of being treated as fatal,
912
+ matching Aaronia's own drivers.
913
+
914
+ ## [v0.2.4] - 2026-07-10
915
+
916
+ ### Performance
917
+ - **HTTP parser:** binary formats read packet headers directly instead
918
+ of building and discarding a JSON document for every packet. Small
919
+ packets parse about 41% faster.
920
+ - **Float32 samples** are bulk-copied on little-endian hosts instead of
921
+ decoded one value at a time.
922
+ - **File replay** reads a block at a time instead of one sample per
923
+ call.
924
+ - Native SDK per-read logging moved to `trace`, off the hot path.
925
+
926
+ ### Fixed
927
+ - **Native SDK unsound read path.** A zero-sample request underflowed a
928
+ length calculation and produced an enormous slice. Packet counts and
929
+ strides are now bounds-checked.
930
+ - **The HTTP reader task leaked.** It is now stopped when streaming
931
+ stops and on drop, instead of holding the connection open and leaving
932
+ the device streaming.
933
+ - **Decompression rejects zero dimensions** instead of looping forever.
934
+ - **`MmapRtsaReader::read_chunk` bounds check** no longer wraps on a
935
+ pathological offset.
936
+ - **Default reference level corrected** from +20 dBm to -20 dBm. The old
937
+ default desensitised the receiver.
938
+ - `HttpSource` reschedules itself after reconnecting.
939
+
940
+ ### Changed
941
+ - `HttpSink` honours `timeout_ms`, which was previously ignored.
942
+
943
+ ## [v0.2.3] - 2026-07-05
944
+
945
+ ### Fixed
946
+ - **The HTTP source did not tune the device.** It opened the stream
947
+ without sending a control request, so it streamed whatever the
948
+ RTSA-Suite was already set to and ignored the requested frequency and
949
+ span.
950
+
951
+ ### Changed
952
+ - `http_iq_quickstart` takes frequency and sample rate arguments and
953
+ logs signal power.
954
+
955
+ ## [v0.2.2] - 2026-07-05
956
+
957
+ ### Changed
958
+ - Dropped the explicit minimum Rust version and track stable instead, so
959
+ a dependency raising its own floor cannot break CI.
960
+
961
+ ## [v0.2.1] - 2026-07-05
962
+
963
+ ### Added
964
+ - Native SDK receiver channel selection (`Rx1`, `Rx2`, `Rx1And2`).
965
+ - HTTP wire format and stream scale settings on `AaroniaConfig` and
966
+ `AaroniaSourceBuilder`.
967
+
968
+ ### Fixed
969
+ - Raised the minimum Rust version to 1.86 to fix CI builds. (v0.2.2
970
+ dropped the fixed minimum entirely.)
971
+ - Default HTTP format changed from JSON to Float32, fixing crashes at
972
+ high bandwidth.
973
+ - Fixed FutureSDR deadlocks in `HttpSink` by moving blocking HTTP calls
974
+ onto a background task.
975
+
976
+ ## [v0.1.1] - 2026-07-03
977
+
978
+ ### Changed
979
+ - Disable `futuresdr` on docs.rs.
980
+ - Fix the licence badge.
981
+
982
+ ## [v0.1.0] - 2026-07-03
983
+
984
+ ### Added
985
+ - Initial release.