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