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.
- python_aaronia-0.8.2/CHANGELOG.md +1152 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/CONTRIBUTING.md +14 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/Cargo.lock +80 -2
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/Cargo.toml +13 -2
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/DESIGN.md +1 -1
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/PKG-INFO +28 -11
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/PLUGINS.md +21 -4
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/README.md +8 -11
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/APPS.md +3 -2
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/FILESPEC.md +9 -16
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/HTTPSPEC.md +148 -46
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/QUICKSTART.md +8 -10
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/SDKSPEC.md +117 -9
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/docs/USAGE.md +6 -7
- python_aaronia-0.8.2/docs/VERIFICATION.md +77 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/python-aaronia/Cargo.toml +8 -1
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/python-aaronia/README.md +27 -10
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/python-aaronia/aaronia.pyi +3 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/python-aaronia/src/lib.rs +52 -11
- python_aaronia-0.8.2/scripts/fake-rtsa-server.py +83 -0
- python_aaronia-0.8.2/scripts/native-sdk-validate.ps1 +139 -0
- python_aaronia-0.8.2/scripts/sdk-container-test.sh +41 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/AaroniaSoapyDevice.cpp +56 -19
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/AaroniaSoapyDevice.hpp +7 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/CMakeLists.txt +27 -5
- python_aaronia-0.8.2/soapy-aaronia/README.md +180 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/Registration.cpp +17 -25
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/c_api.rs +135 -5
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/detection.rs +24 -6
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/http_endpoints.rs +221 -31
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/http_source.rs +25 -7
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/http_streaming.rs +27 -25
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/native_sdk.rs +339 -37
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/sdk_source.rs +12 -9
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/seify_impl.rs +16 -8
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/unified_source.rs +200 -16
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/utils.rs +13 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/live_smoke.rs +32 -0
- python_aaronia-0.8.2/tests/native_sdk_live.rs +648 -0
- python_aaronia-0.8.0/CHANGELOG.md +0 -1204
- python_aaronia-0.8.0/docs/VERIFICATION.md +0 -60
- python_aaronia-0.8.0/soapy-aaronia/README.md +0 -230
- python_aaronia-0.8.0/soapy-aaronia/print.cmake +0 -1
- python_aaronia-0.8.0/test.cmake +0 -1
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/.cargo/config.toml +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/.gitattributes +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/.gitignore +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/LICENSE +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/deny.toml +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/device_control.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/include/aaronia.h +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/pyproject.toml +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/decompression.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/error.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/file_source.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/http_sink.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/lib.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/link_budget.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/sdr_source_impl.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/src/unified_sink.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/http_resilience_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/integration_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/properties.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.8.0 → python_aaronia-0.8.2}/tests/test_cw_mag.rs +0 -0
- {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.
|