python-aaronia 0.7.6__tar.gz → 0.7.7__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.7.6 → python_aaronia-0.7.7}/CHANGELOG.md +310 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/CONTRIBUTING.md +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/Cargo.lock +11 -11
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/Cargo.toml +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/PKG-INFO +2 -2
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/PLUGINS.md +4 -4
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/README.md +5 -2
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/APPS.md +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/FILESPEC.md +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/QUICKSTART.md +6 -4
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/SDKSPEC.md +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/USAGE.md +68 -3
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/include/aaronia.h +3 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/packaging/homebrew/soapy-aaronia.rb +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/README.md +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/aaronia.pyi +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/src/lib.rs +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/README.md +2 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/c_api.rs +39 -14
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/decompression.rs +11 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/file_source.rs +96 -20
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_endpoints.rs +179 -55
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_sink.rs +4 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_source.rs +1342 -132
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_streaming.rs +660 -184
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/lib.rs +12 -0
- python_aaronia-0.7.7/src/link_budget.rs +1490 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/native_sdk.rs +162 -12
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdk_source.rs +33 -3
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdr_source_impl.rs +9 -7
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/seify_impl.rs +5 -2
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/unified_sink.rs +7 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/unified_source.rs +95 -19
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/utils.rs +34 -14
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/c_api_test.rs +1 -1
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.cargo/config.toml +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.gitattributes +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.gitignore +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/DESIGN.md +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/LICENSE +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/deny.toml +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/HTTPSPEC.md +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/VERIFICATION.md +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/device_control.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/pyproject.toml +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/Registration.cpp +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeCache.txt +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/Makefile +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/cmake_install.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/print.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/detection.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/error.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/test.cmake +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_resilience_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/integration_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/live_smoke.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/properties.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/test_cw_meta.rs +0 -0
|
@@ -4,6 +4,316 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.7.7] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`utils::IQ_RATE_CLOCK_RATIO` and `utils::iq_ladder_from_top`.** The
|
|
11
|
+
1.5 receiver-clock-cycles-per-sample rule was a bare literal in three
|
|
12
|
+
places; it has a name now, and the ladder's shape is available
|
|
13
|
+
without the clock rule for a caller that already holds the top rung
|
|
14
|
+
— the link budget's device-anchored remedy, which used to reconstruct
|
|
15
|
+
a clock only to divide it back out (not exact in floating point).
|
|
16
|
+
`PacketMetadata::sample_rate()` likewise names the rate a header
|
|
17
|
+
reports (`sampleFrequency`, else `samples / duration`), shared by
|
|
18
|
+
`StreamingSdrConfig::from_metadata` and the probe's sniff.
|
|
19
|
+
- **A link budget, so a span that cannot fit is caught before the
|
|
20
|
+
capture instead of after it.** The span picks a rate off the
|
|
21
|
+
decimation ladder and at 4 bytes a sample that rate is a byte rate the
|
|
22
|
+
path has to sustain: `--span 10M` is 15.36 MS/s and 61.4 MB/s, which
|
|
23
|
+
measured contiguous over gigabit, while `--span 20M` is 30.72 MS/s and
|
|
24
|
+
122.9 MB/s, which measured 1024 skips and 1.84 s lost of 35 s. The new
|
|
25
|
+
`link_budget` module does the arithmetic in both directions —
|
|
26
|
+
`required_byte_rate` for what a span costs, `max_sustainable_span` for
|
|
27
|
+
the widest rung a measured rate affords, inverted through the existing
|
|
28
|
+
ladder rather than a second copy of it — and `measure_link_throughput`
|
|
29
|
+
measures the path end to end by counting bytes off `/stream`.
|
|
30
|
+
Deliberately end to end: the bottleneck may be the server, a switch,
|
|
31
|
+
the air or this host's own ingest, and the NIC's advertised link speed
|
|
32
|
+
sees none of them.
|
|
33
|
+
|
|
34
|
+
The probe discards a 500 ms settle window before counting
|
|
35
|
+
(`LINK_PROBE_SETTLE`). Without it a probe *lies*: the server hands over
|
|
36
|
+
its pre-connect backlog faster than real time — measured at ~0.27–0.35 s
|
|
37
|
+
of signal, all inside a 345 ms window at connect — so counting it reads
|
|
38
|
+
above the true link rate and waves through a span that cannot fit. Two
|
|
39
|
+
tests pin this, one driving the settle logic off synthetic instants
|
|
40
|
+
with a 200 MB/s burst ahead of a 10 MB/s stream, and one showing the
|
|
41
|
+
same trace measuring several times too fast with the settle window
|
|
42
|
+
removed.
|
|
43
|
+
An unreachable server, an idle mission or a stream that stops mid-window
|
|
44
|
+
is an error and never a rate, because "0 MB/s" would condemn every span
|
|
45
|
+
on the ladder.
|
|
46
|
+
- **`HttpSource` runs that check passively and warns once**, on the
|
|
47
|
+
stream it is already reading — no second connection, no cost beyond
|
|
48
|
+
adding up chunk lengths. It compares the bytes arriving against what
|
|
49
|
+
the device's own reported rate needs and, if the path is short, names
|
|
50
|
+
the requested span, the rate it needs, what was measured, and the
|
|
51
|
+
widest span that would have fitted. Complementary to `DropDetector`,
|
|
52
|
+
which says the server *did* drop data after the fact; when both fire
|
|
53
|
+
the gap warning now says so, rather than reading as a second unrelated
|
|
54
|
+
fault. Silence is the answer whenever the measurement did not happen:
|
|
55
|
+
a warning never fires on a failed one.
|
|
56
|
+
- **`StreamFormat::iq_bytes_per_sample`**, the one definition of the
|
|
57
|
+
constant the whole budget turns on. `calculate_binary_size` now reads
|
|
58
|
+
it instead of carrying its own copy of the same match.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- **The passive link-budget check now judges against the rate the device
|
|
62
|
+
reports, not the rate the caller asked for.** `current_sample_rate`
|
|
63
|
+
adopts a new rate only past a 10% hysteresis band (deliberately, to
|
|
64
|
+
keep the tuning target stable), so the check compared against a
|
|
65
|
+
*requested* rate the device merely came close to — under the builder's
|
|
66
|
+
own 1 MS/s default (served by the 0.96 MS/s rung, a 4% gap against a
|
|
67
|
+
2% tolerance) it warned "this path cannot carry the configured span"
|
|
68
|
+
on every healthy start. The device-reported rate is now tracked
|
|
69
|
+
separately, without hysteresis; no rate reported means no verdict. An
|
|
70
|
+
external mid-measurement retune (RTSA GUI/API, no stream restart)
|
|
71
|
+
restarts the meter, so mixed-rate bytes are never judged against one
|
|
72
|
+
rate's requirement.
|
|
73
|
+
- **The passive meter counts decoded IQ payload, once per sweep,
|
|
74
|
+
instead of raw chunk bytes per chunk.** Per-chunk counting stamped a
|
|
75
|
+
whole drained batch at one instant, so the first post-settle sweep
|
|
76
|
+
counted the queued connect backlog at zero elapsed width — inflating
|
|
77
|
+
the measurement in exactly the case the settle window exists for, and
|
|
78
|
+
enough to wave through a genuinely short path. Raw bytes also counted
|
|
79
|
+
JSON headers and any spectra/histogram packets sharing the stream,
|
|
80
|
+
which under-warned on mixed missions. One payload observation per
|
|
81
|
+
sweep discards the backlog sweep whole (it becomes the meter's mark)
|
|
82
|
+
and compares like against like.
|
|
83
|
+
- **A stall no longer dilutes the measurement, and the window's close is
|
|
84
|
+
exclusive.** The counting window used to run to whenever the next
|
|
85
|
+
observation happened to arrive, so an 8 s consumer stall averaged dead
|
|
86
|
+
time into the "sustained" rate and produced a spurious shortfall
|
|
87
|
+
warning; the observation that crosses the boundary is now excluded in
|
|
88
|
+
both directions. `ThroughputMeter::finish` also refuses to answer
|
|
89
|
+
before the window closes, instead of leaving that check as a
|
|
90
|
+
convention each caller must remember, and the reported settle interval
|
|
91
|
+
is the one actually discarded rather than the one configured.
|
|
92
|
+
- **A configuration restart re-arms the check; a verdict survives
|
|
93
|
+
reconnects.** The `restart_pending` retune path never reset the
|
|
94
|
+
check's once-per-source flags, so a widened span was never measured
|
|
95
|
+
(the case the check exists for) and gap warnings kept citing a stale
|
|
96
|
+
verdict about a span no longer configured. The check's state is now a
|
|
97
|
+
single `LinkCheck` enum — unmeasured / measuring / done — reset to
|
|
98
|
+
unmeasured on configuration restarts only. A parse error in the sweep
|
|
99
|
+
that closes the window no longer discards the finished measurement
|
|
100
|
+
either: the verdict is reported before the error propagates into the
|
|
101
|
+
reconnect path.
|
|
102
|
+
- **The shortfall remedy is computed from the device's own ladder and
|
|
103
|
+
format.** The "widest span that fits" now halves down from the
|
|
104
|
+
device-reported rate, so it is a rung the device actually has —
|
|
105
|
+
inverting through the default-clock ladder named wrong rungs for a
|
|
106
|
+
full V6 on a faster receiver clock — and the no-rung-fits fallback
|
|
107
|
+
computes its figures from the stream format instead of hardcoding
|
|
108
|
+
int16's "120 kS/s, 0.5 MB/s", which was self-contradictory for
|
|
109
|
+
float32. Float32 streams are also told that int16 would halve the
|
|
110
|
+
requirement.
|
|
111
|
+
- **The probe validates its window and probes the capture's stream.**
|
|
112
|
+
`measure_link_throughput` refuses windows under a new
|
|
113
|
+
`MIN_PROBE_WINDOW` (100 ms): two adjacent socket-buffer reads
|
|
114
|
+
microseconds apart would otherwise "measure" gigabytes a second over
|
|
115
|
+
a link that cannot sustain one. `measure_link_throughput_with` now
|
|
116
|
+
takes the capture's `StreamParams` (format *and* input,
|
|
117
|
+
rate-reduction, scale — it used to probe the server's default input
|
|
118
|
+
at full rate whatever the capture would open) plus an explicit settle
|
|
119
|
+
window for servers whose connect backlog outlasts the default, and
|
|
120
|
+
its header sniff parses packet headers only — skipping degenerate
|
|
121
|
+
zero-rate status headers, logging when it gives up — instead of
|
|
122
|
+
running the full sample decoder on payloads it then threw away.
|
|
123
|
+
- **Rate tracking and the meter listen only to IQ packets.** A
|
|
124
|
+
spectra/histogram header carries no `sampleFrequency`, so the parser
|
|
125
|
+
derives a *frame* rate orders of magnitude below the IQ rate; on a
|
|
126
|
+
mixed mission that ping-ponged the device-rate tracker (restarting the
|
|
127
|
+
check on every interleaved packet, so it never finished) and handed it
|
|
128
|
+
a nonsense yardstick. The meter also counted those packets' scalars at
|
|
129
|
+
IQ byte width — half their wire cost — inflating the measured rate.
|
|
130
|
+
Both trackers and the byte count now key on the packet's payload type,
|
|
131
|
+
and a sweep that decodes no IQ does not observe at all. The probe's
|
|
132
|
+
header sniff follows the same rule: `stream_sample_rate` is read from
|
|
133
|
+
an IQ header, never from a spectra frame rate that happened to come
|
|
134
|
+
first.
|
|
135
|
+
- **A failed measurement re-arms the check instead of retiring it.** A
|
|
136
|
+
window that closed without a usable count, or before the device ever
|
|
137
|
+
reported a rate, used to become "checked, no verdict" — permanently,
|
|
138
|
+
on no evidence, so the configuration streamed unjudged forever. It now
|
|
139
|
+
starts a fresh window, reserving the no-verdict terminal state for
|
|
140
|
+
configurations that genuinely cannot be measured. A device retune
|
|
141
|
+
arriving *after* the verdict re-arms too (the verdict described the
|
|
142
|
+
old rate's budget), and `ThroughputMeter::finish` refuses a window
|
|
143
|
+
that actually observed less than half its configured span — a couple
|
|
144
|
+
of packets plus a stray late closing observation say nothing about
|
|
145
|
+
what the path sustains.
|
|
146
|
+
- **`rate_reduction(0)` is rejected at the boundary.** Both builders
|
|
147
|
+
accepted it and sent `?rate_reduction=0` to the server, while the
|
|
148
|
+
link check's measurable gate read `n <= 1` as "no decimation" and
|
|
149
|
+
computed a full-rate requirement against a wire the server was
|
|
150
|
+
decimating by whatever it made of zero. `HttpSourceBuilder::build`,
|
|
151
|
+
`HttpEndpointsClient::start_stream` and the probe now refuse it with
|
|
152
|
+
`Error::Config` (new `StreamParams::validate`), and the gate accepts
|
|
153
|
+
exactly 1.
|
|
154
|
+
- **The shared client builder no longer forces HTTP/1.1.** The
|
|
155
|
+
consolidation carried the endpoints client's `.http1_only()` (which
|
|
156
|
+
the probe had copied) into `rtsa_client_builder`, where it also
|
|
157
|
+
pinned `HttpSource`'s streaming client — which had never had it, and
|
|
158
|
+
streamed through TLS-terminating proxies whose ALPN offers only h2.
|
|
159
|
+
Over plain `http://` reqwest speaks HTTP/1.1 regardless, so the pin
|
|
160
|
+
protected nothing there; over `https://` ALPN now negotiates for
|
|
161
|
+
every client, control plane included, with HTTP/2's adaptive
|
|
162
|
+
flow-control window enabled so a negotiated h2 stream is not capped
|
|
163
|
+
at hyper's default 2 MiB window per round trip — a ceiling the link
|
|
164
|
+
check would otherwise measure and blame on the path.
|
|
165
|
+
- **One framing implementation, shared by the parser and the probe's
|
|
166
|
+
header sniff.** The probe's rate sniff re-scanned its whole
|
|
167
|
+
accumulation on every chunk and resynced a failed header parse past
|
|
168
|
+
the separator where the stream parser resyncs one byte past the `{`
|
|
169
|
+
— binary payloads contain separator bytes, so the two could disagree
|
|
170
|
+
about which header speaks first. `StreamParser` and the sniff now
|
|
171
|
+
frame packets through the same `scan_packet_header`, which finds each
|
|
172
|
+
region's terminator once rather than once per `{` candidate (the old
|
|
173
|
+
loop was quadratic in brace-dense garbage: 256 KiB of `{` took 20 s
|
|
174
|
+
of a tokio worker), honours the hardware-verified LF+RS two-byte
|
|
175
|
+
separator, and reports rejected candidates so the parser's
|
|
176
|
+
`parse_errors` counter is unchanged. The sniff drops bytes as they
|
|
177
|
+
are ruled out and retains at most one candidate awaiting its
|
|
178
|
+
terminator.
|
|
179
|
+
- **An HTTP source starts again after `stop_streaming`.** The reader
|
|
180
|
+
task is spawned at construction and stopping it left nothing to
|
|
181
|
+
restart, so a SoapySDR deactivate/activate cycle (and seify's
|
|
182
|
+
`deactivate_at`/`activate_at`) failed with "HTTP streaming not
|
|
183
|
+
properly initialized". `start_streaming` now reconnects.
|
|
184
|
+
- **A packet whose body cannot be decoded no longer wedges the stream.**
|
|
185
|
+
The parser returned the error but left the packet in its buffer, so
|
|
186
|
+
every later chunk failed on the same bytes while the buffer grew
|
|
187
|
+
without bound. The packet is now skipped. `HttpSource` also resets the
|
|
188
|
+
parser and the drop detector on every reconnect, so a retune no longer
|
|
189
|
+
decodes the new connection's first bytes as the old packet's payload.
|
|
190
|
+
- **`read_samples` returns a partial block on timeout instead of losing
|
|
191
|
+
it.** Samples already moved into the caller's buffer went out with the
|
|
192
|
+
error, an unflagged gap through the C and Python APIs.
|
|
193
|
+
- **`IqPacket.sample_rate_hz` reports the rate the device streams.** The
|
|
194
|
+
`sdr-source` adapter captured the requested rate before the first
|
|
195
|
+
packet and never looked again, so a rate the device snapped to another
|
|
196
|
+
ladder rung was mis-stamped for the whole session.
|
|
197
|
+
- **RTSA files: `mSampleSize` counts values per sample, not bytes.** The
|
|
198
|
+
captures under `tests/` carry 2 for float32 IQ and 1024 for spectra.
|
|
199
|
+
Seeking within a chunk used it as a byte stride, so a second partial
|
|
200
|
+
read of a chunk landed mid-sample, and a spectra read returned one
|
|
201
|
+
scalar per spectrum. Both now use the sample type's width.
|
|
202
|
+
- **The native SDK is shut down by the last client, not the first.**
|
|
203
|
+
`AARTSAAPI_Shutdown` is process-wide, but each source and sink called
|
|
204
|
+
it from its own `Drop`, so re-initialising a source, or pairing a
|
|
205
|
+
source with a sink, tore the SDK down under the survivor.
|
|
206
|
+
- **A bare `spectranv6eco` opens `spectranv6eco/rtsa`.** The ECO has no
|
|
207
|
+
`/raw` mode; `SdkConfig` appended one anyway. SDKSPEC's mode list said
|
|
208
|
+
the same and now agrees with its own V6-vs-ECO table.
|
|
209
|
+
- **Clippy passes on Rust 1.98.** The new `chunks_exact_to_as_chunks`
|
|
210
|
+
lint failed the workspace check at eight decode sites.
|
|
211
|
+
- **The remote-config licence probe can report `Active`.** It looked for
|
|
212
|
+
a bare `reflevel` under a group named `main`; a V6 exposes `reflevel0`
|
|
213
|
+
in its receiver block, so the probe always answered `NotLicensed`. It
|
|
214
|
+
now writes the discovered block, as the retune path does.
|
|
215
|
+
- **`HttpSource` built outside a Tokio runtime errors at build.** It used
|
|
216
|
+
to panic inside `init()` when it spawned its reader.
|
|
217
|
+
- **`HttpSink` with `buffer_size(0)` no longer spins forever**; the size
|
|
218
|
+
is clamped to one.
|
|
219
|
+
- **A quiet dwell in hop mode is an empty read**, not a read error
|
|
220
|
+
counted toward the source-dead bailout.
|
|
221
|
+
- **The first packet after a failed initial connection is not flagged as
|
|
222
|
+
an overrun**; nothing was lost.
|
|
223
|
+
- **seify: `sample_rate()` reports the observed rate**, and the range
|
|
224
|
+
starts at the ladder's 120 kS/s floor instead of 10 kHz.
|
|
225
|
+
- **C API: `aaronia_get_error_message` takes an `int`** and answers
|
|
226
|
+
unknown codes, where an out-of-range enum was undefined behaviour. C
|
|
227
|
+
callers are unaffected (an enum converts to `int`); a Rust caller of
|
|
228
|
+
the `ffi` module's function passes `code as i32`.
|
|
229
|
+
- **Native SDK:** `aaronia_source_read_samples_timeout` honours its
|
|
230
|
+
deadline (it polled for up to 500 ms regardless); spectra reads honour
|
|
231
|
+
the packet stride; an SDK call that returns no object is an error
|
|
232
|
+
rather than a null pointer; opening a second device on a live source
|
|
233
|
+
is refused; the device is closed on drop.
|
|
234
|
+
- **RTSA files:** a negative DSFT stream offset no longer overflows the
|
|
235
|
+
header search, and `seek_to_sample` works on reverse-order files.
|
|
236
|
+
- **Spectrum decompression caps the output size** taken from packet
|
|
237
|
+
metadata.
|
|
238
|
+
|
|
239
|
+
### Performance
|
|
240
|
+
- The stream parser drops an over-cap payload as it arrives instead of
|
|
241
|
+
buffering it; uncompressed spectra packets decode without a copy;
|
|
242
|
+
spectra file reads decode in place; the C API reserves the caller's
|
|
243
|
+
length before a read; `sample_rate_hz` is an atomic load, so stamping
|
|
244
|
+
it on every packet takes no lock.
|
|
245
|
+
|
|
246
|
+
### Changed
|
|
247
|
+
- **Byte-rate helpers answer `Option<f64>`, never a `0.0` sentinel.**
|
|
248
|
+
`required_byte_rate`, `max_sustainable_span` and friends returned
|
|
249
|
+
`0.0` for "unknown / nothing fits", a convention every comparison site
|
|
250
|
+
had to remember — `required <= measured` on a garbage rate reads
|
|
251
|
+
"fits". `None` now makes the compiler enforce it, and
|
|
252
|
+
`StreamFormat::iq_bytes_per_sample` returns `Option<usize>` (`None`
|
|
253
|
+
for JSON) for the same reason. New `max_sustainable_sample_rate_below`
|
|
254
|
+
answers remedy queries anchored to the device's reported rate.
|
|
255
|
+
- **The link-budget verdict is data, not just a log line.** New
|
|
256
|
+
`LinkBudgetVerdict` (measured rate, requirement, fit span, shortness)
|
|
257
|
+
is published as `StreamStats::link_budget`, so a GUI or orchestrator
|
|
258
|
+
can auto-narrow the span without scraping logs; the stream-gap warning
|
|
259
|
+
restates the verdict's actual figures from it. `StreamFormat` gained
|
|
260
|
+
`CAPTURE_DEFAULT`, the one definition behind `HttpSourceBuilder`'s
|
|
261
|
+
default, `StreamStats::default` and `DEFAULT_LINK_FORMAT`, which were
|
|
262
|
+
three separately written `Int16` literals.
|
|
263
|
+
- **One copy each of the RTSA client plumbing.** The probe re-used
|
|
264
|
+
hand-rolled copies of the endpoints client's construction, auth
|
|
265
|
+
application, base-URL validation, `/stream` query serialization and
|
|
266
|
+
status-to-error mapping — five drift risks (the three reqwest clients
|
|
267
|
+
had already drifted to three different setting subsets). All are
|
|
268
|
+
shared now: `AuthMethod::apply_to` owns the `RToken` header,
|
|
269
|
+
`rtsa_client_builder` owns the client settings (the streaming client
|
|
270
|
+
gains the keepalive/nodelay/HTTP-1.1 compatibility settings the
|
|
271
|
+
endpoints client always had), and `StreamParams::stream_url`
|
|
272
|
+
serializes every `/stream` URL, including `HttpSource`'s.
|
|
273
|
+
- **`LinkBudgetVerdict::judge` is the verdict's one producer.**
|
|
274
|
+
Requirement, shortness-against-tolerance and the remedy rung were
|
|
275
|
+
assembled by hand at each caller; the constructor now owns that
|
|
276
|
+
arithmetic (so the struct's invariants — fit fields populated only on
|
|
277
|
+
a shortfall, span matching the fit rate — hold by construction), the
|
|
278
|
+
verdict carries `bytes_per_sample` for report text, and
|
|
279
|
+
`ThroughputMeasurement::max_sustainable_span_hz` takes the stream
|
|
280
|
+
format instead of silently assuming int16. The no-rung-fits warning
|
|
281
|
+
cites the device ladder's actual last rung rather than a hardcoded
|
|
282
|
+
1/512.
|
|
283
|
+
- **`HttpSource`'s stream-open failure is the typed error the rest of
|
|
284
|
+
the crate returns.** A non-success status on `/stream` is now
|
|
285
|
+
`Error::Http { status, context }` — the same variant the endpoints
|
|
286
|
+
client and the probe produce, with `context` naming the stream URL —
|
|
287
|
+
instead of `Error::Protocol("Stream endpoint returned error: …")`.
|
|
288
|
+
Anything matching on that text will need the new form.
|
|
289
|
+
- **`work()`'s output copy is two bulk `copy_from_slice` calls** over the
|
|
290
|
+
deque's contiguous halves instead of a `pop_front` per sample, whose
|
|
291
|
+
per-element wrap-around check the compiler cannot lift. Factored into
|
|
292
|
+
`copy_out` and unit-tested, including the wrapped-deque case.
|
|
293
|
+
|
|
294
|
+
### Documentation
|
|
295
|
+
- **Measured the HTTP transport on WiFi 7 and recorded where the
|
|
296
|
+
ceiling actually is.** `curl` on `/stream` sustains ~75 MB/s
|
|
297
|
+
(0.6 Gbit/s) station-to-station at a 2.4 Gbps PHY — both ends on air
|
|
298
|
+
halves the medium — and the figure is the same for all three wire
|
|
299
|
+
formats, so the encoding is not the limit. Two parallel connections
|
|
300
|
+
measured less in aggregate (63.9 vs 74 MB/s), so a single connection
|
|
301
|
+
is optimal and nothing smarter is available to the client. Against
|
|
302
|
+
that, the crate's full framing-plus-decode path measures ~3 GB/s and
|
|
303
|
+
the individual decoders 0.8–10 GS/s: the parser has fortyfold
|
|
304
|
+
headroom and is never the bottleneck. What the link buys at 4 bytes a
|
|
305
|
+
sample is ~19 MS/s — the 15.36 MS/s rung fits, 30.72 does not, and
|
|
306
|
+
full span (246 MB/s) needs a wired path to the RTSA host. Two ignored
|
|
307
|
+
throughput-meter tests (`decode_throughput_meter`,
|
|
308
|
+
`framing_throughput_meter`) keep these numbers re-measurable in one
|
|
309
|
+
command.
|
|
310
|
+
- README and PLUGINS pinned `sdr-aaronia-rs = "0.6"`, a major behind
|
|
311
|
+
the crate, so nothing they describe resolved. Now `"0.7"`.
|
|
312
|
+
- `cumulative_drops` counts client-detected timestamp gaps; four docs
|
|
313
|
+
called them server-reported drops. Bandwidth figures now use the
|
|
314
|
+
61.44 MS/s top rate rather than the 92 MHz clock. The examples table
|
|
315
|
+
and the QUICKSTART verification link were corrected.
|
|
316
|
+
|
|
7
317
|
## [v0.7.6] - 2026-08-15
|
|
8
318
|
|
|
9
319
|
All of this is about the FutureSDR `HttpSource` block (the `futuresdr`
|
|
@@ -120,7 +120,7 @@ open target/llvm-cov/html/index.html # xdg-open on Linux, start on Windows
|
|
|
120
120
|
|
|
121
121
|
### 7. Criterion Benchmarks (`benches/`)
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Four benchmark harnesses track the hot paths across releases. Benchmarks are run on demand to investigate performance implications of a PR.
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
126
|
cargo bench # Run all benchmarks
|
|
@@ -1275,7 +1275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
1275
1275
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
1276
1276
|
dependencies = [
|
|
1277
1277
|
"libc",
|
|
1278
|
-
"windows-sys 0.
|
|
1278
|
+
"windows-sys 0.52.0",
|
|
1279
1279
|
]
|
|
1280
1280
|
|
|
1281
1281
|
[[package]]
|
|
@@ -1669,9 +1669,9 @@ dependencies = [
|
|
|
1669
1669
|
|
|
1670
1670
|
[[package]]
|
|
1671
1671
|
name = "h2"
|
|
1672
|
-
version = "0.4.
|
|
1672
|
+
version = "0.4.19"
|
|
1673
1673
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1674
|
-
checksum = "
|
|
1674
|
+
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
|
1675
1675
|
dependencies = [
|
|
1676
1676
|
"atomic-waker",
|
|
1677
1677
|
"bytes",
|
|
@@ -2041,7 +2041,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
|
|
2041
2041
|
dependencies = [
|
|
2042
2042
|
"hermit-abi",
|
|
2043
2043
|
"libc",
|
|
2044
|
-
"windows-sys 0.
|
|
2044
|
+
"windows-sys 0.52.0",
|
|
2045
2045
|
]
|
|
2046
2046
|
|
|
2047
2047
|
[[package]]
|
|
@@ -3027,7 +3027,7 @@ dependencies = [
|
|
|
3027
3027
|
|
|
3028
3028
|
[[package]]
|
|
3029
3029
|
name = "python-aaronia"
|
|
3030
|
-
version = "0.7.
|
|
3030
|
+
version = "0.7.7"
|
|
3031
3031
|
dependencies = [
|
|
3032
3032
|
"arrow",
|
|
3033
3033
|
"num-complex",
|
|
@@ -3098,7 +3098,7 @@ dependencies = [
|
|
|
3098
3098
|
"once_cell",
|
|
3099
3099
|
"socket2",
|
|
3100
3100
|
"tracing",
|
|
3101
|
-
"windows-sys 0.
|
|
3101
|
+
"windows-sys 0.52.0",
|
|
3102
3102
|
]
|
|
3103
3103
|
|
|
3104
3104
|
[[package]]
|
|
@@ -3404,7 +3404,7 @@ dependencies = [
|
|
|
3404
3404
|
"errno",
|
|
3405
3405
|
"libc",
|
|
3406
3406
|
"linux-raw-sys",
|
|
3407
|
-
"windows-sys 0.
|
|
3407
|
+
"windows-sys 0.52.0",
|
|
3408
3408
|
]
|
|
3409
3409
|
|
|
3410
3410
|
[[package]]
|
|
@@ -3461,7 +3461,7 @@ dependencies = [
|
|
|
3461
3461
|
"security-framework",
|
|
3462
3462
|
"security-framework-sys",
|
|
3463
3463
|
"webpki-root-certs",
|
|
3464
|
-
"windows-sys 0.
|
|
3464
|
+
"windows-sys 0.52.0",
|
|
3465
3465
|
]
|
|
3466
3466
|
|
|
3467
3467
|
[[package]]
|
|
@@ -3556,7 +3556,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3556
3556
|
|
|
3557
3557
|
[[package]]
|
|
3558
3558
|
name = "sdr-aaronia-rs"
|
|
3559
|
-
version = "0.7.
|
|
3559
|
+
version = "0.7.7"
|
|
3560
3560
|
dependencies = [
|
|
3561
3561
|
"anyhow",
|
|
3562
3562
|
"bitflags 2.13.1",
|
|
@@ -4135,7 +4135,7 @@ dependencies = [
|
|
|
4135
4135
|
"getrandom 0.4.3",
|
|
4136
4136
|
"once_cell",
|
|
4137
4137
|
"rustix",
|
|
4138
|
-
"windows-sys 0.
|
|
4138
|
+
"windows-sys 0.52.0",
|
|
4139
4139
|
]
|
|
4140
4140
|
|
|
4141
4141
|
[[package]]
|
|
@@ -4792,7 +4792,7 @@ version = "0.1.11"
|
|
|
4792
4792
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4793
4793
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
4794
4794
|
dependencies = [
|
|
4795
|
-
"windows-sys 0.
|
|
4795
|
+
"windows-sys 0.52.0",
|
|
4796
4796
|
]
|
|
4797
4797
|
|
|
4798
4798
|
[[package]]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "sdr-aaronia-rs"
|
|
3
3
|
description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
|
|
4
4
|
license = "GPL-3.0-or-later"
|
|
5
|
-
version = "0.7.
|
|
5
|
+
version = "0.7.7"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
|
|
8
8
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aaronia
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.7
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -211,7 +211,7 @@ silently defaulting.
|
|
|
211
211
|
| `read_samples_arrow(count)` | PyArrow `FixedSizeListArray` of `[re, im]` float32 pairs |
|
|
212
212
|
| `read_samples_dual_numpy(count)` | `(rx1, rx2)` NumPy arrays (dual-channel captures) |
|
|
213
213
|
| `set_center_frequency(hz)` / `set_sample_rate(hz)` / `set_reference_level(dbm)` | Live retuning |
|
|
214
|
-
| `cumulative_drops()` |
|
|
214
|
+
| `cumulative_drops()` | Timestamp gaps detected in the stream so far (gap events, not samples) |
|
|
215
215
|
| `take_overrun()` | True once per detected receive-side overrun |
|
|
216
216
|
| `last_timestamp_ns()` | Epoch-ns timestamp of the last received block (HTTP backend; 0 otherwise) |
|
|
217
217
|
|
|
@@ -12,7 +12,7 @@ To use the Seify plugin, enable the `seify` feature in your `Cargo.toml`:
|
|
|
12
12
|
|
|
13
13
|
```toml
|
|
14
14
|
[dependencies]
|
|
15
|
-
sdr-aaronia-rs = { version = "0.
|
|
15
|
+
sdr-aaronia-rs = { version = "0.7", features = ["seify"] }
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Instantiate the device with `AaroniaSeifyDevice::from_args` and use it directly (or via `seify::dev::DynDeviceBackend`). The backend is **not** part of seify's built-in enumeration registry — `seify::enumerate()` will not discover it.
|
|
@@ -136,11 +136,11 @@ Both the native Rust API and the Python/C++ bindings expose critical metrics to
|
|
|
136
136
|
|
|
137
137
|
- **Hardware Timestamps**: You can retrieve the precise hardware timestamp (in nanoseconds) of the last received block using `last_timestamp_ns`.
|
|
138
138
|
- **Overruns**: The `take_overrun()` function checks if the internal buffer has overflown since the last check, allowing you to react to drops on the client side.
|
|
139
|
-
- **Cumulative Drops**:
|
|
139
|
+
- **Cumulative Drops**: `cumulative_drops()` counts the timestamp gaps the client's drop detector has seen in the stream. Each is one gap event, however many samples it spanned.
|
|
140
140
|
|
|
141
141
|
In SoapySDR, you can access these metrics via the `readSensor()` API:
|
|
142
142
|
```python
|
|
143
|
-
# Check for
|
|
143
|
+
# Check for gaps in the stream
|
|
144
144
|
drops = sdr.readSensor("cumulative_drops")
|
|
145
|
-
print(f"
|
|
145
|
+
print(f"Gaps seen: {drops}")
|
|
146
146
|
```
|
|
@@ -36,6 +36,9 @@ selects a backend and presents the same interface either way.
|
|
|
36
36
|
input enumeration and the configuration tree.
|
|
37
37
|
- **Plugs into FutureSDR** with `HttpSource` and `HttpSink` flowgraph
|
|
38
38
|
blocks.
|
|
39
|
+
- **Says whether the link can carry the span** before a capture is
|
|
40
|
+
wasted: `link_budget` measures the path end to end and names the
|
|
41
|
+
widest span on the device's decimation ladder that fits it.
|
|
39
42
|
|
|
40
43
|
## Installation
|
|
41
44
|
|
|
@@ -44,11 +47,11 @@ Add the following to your `Cargo.toml`:
|
|
|
44
47
|
```toml
|
|
45
48
|
[dependencies]
|
|
46
49
|
# By default, includes HTTP, File, native sdr-source trait, and C FFI backend support
|
|
47
|
-
sdr-aaronia-rs = "0.
|
|
50
|
+
sdr-aaronia-rs = "0.7"
|
|
48
51
|
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
|
49
52
|
|
|
50
53
|
# To enable additional backends, opt into their features (e.g. native-sdk, futuresdr)
|
|
51
|
-
# sdr-aaronia-rs = { version = "0.
|
|
54
|
+
# sdr-aaronia-rs = { version = "0.7", features = ["native-sdk", "futuresdr"] }
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
## Quickstart
|
|
@@ -114,7 +114,7 @@ directly and skip the SoapySDR layer.
|
|
|
114
114
|
- `readStream` honours `timeoutUs` and returns partial reads within the
|
|
115
115
|
deadline, per the SoapySDR contract.
|
|
116
116
|
- Retuning mid-stream is safe and requires no Aaronia licence.
|
|
117
|
-
-
|
|
117
|
+
- Gaps detected in the stream are counted by `readSensor("cumulative_drops")`.
|
|
118
118
|
An overrun sets `SOAPY_SDR_END_ABRUPT` on the affected read.
|
|
119
119
|
- TX is hardware-unverified and exists only when the plugin is built
|
|
120
120
|
against the native SDK on Windows or Linux.
|
|
@@ -375,7 +375,7 @@ Contains the actual measurement data.
|
|
|
375
375
|
| `mPacketStartTime` | `double` | Start time of this chunk relative to stream start. |
|
|
376
376
|
| `mPacketEndTime` | `double` | End time of this chunk relative to stream start. |
|
|
377
377
|
| `mPacketFlags` | `quint32` | Packet flags (`DSPPF_*`). |
|
|
378
|
-
| `mSampleSize` | `quint32` |
|
|
378
|
+
| `mSampleSize` | `quint32` | Values per sample, not bytes: 2 for an IQ pair, the bin count for a spectrum. The captures under `tests/` carry 2 (float32 IQ) and 1024 (spectra), the same meaning as the HTTP stream's `sampleSize`. The byte stride of a sample is this count times the width of `mSampleType`. |
|
|
379
379
|
| `mSampleDepth` | `quint32` | Depth of a sample. |
|
|
380
380
|
| `mNumSamples` | `quint32` | Number of samples in this packet. |
|
|
381
381
|
| **Payload** | `binary` | The payload contains the actual sample data, formatted as specified by the header fields. |
|
|
@@ -200,14 +200,16 @@ instead of issuing PUTs directly. No Aaronia licence is involved: the
|
|
|
200
200
|
crate tunes through `/control`, which needs none.
|
|
201
201
|
|
|
202
202
|
**Network saturation at high sample rates.**
|
|
203
|
-
Float32
|
|
203
|
+
Float32 needs roughly 490 MB/s at the 61.44 MS/s top rate. On anything other than
|
|
204
204
|
localhost, use the `I16` wire format via
|
|
205
205
|
`AaroniaConfig::low_bandwidth_mode()` or the `format=I16` SoapySDR
|
|
206
206
|
device argument to halve that. This changes the wire format itself,
|
|
207
207
|
unlike a client-side `CS16` conversion.
|
|
208
208
|
|
|
209
209
|
**Dropped samples.**
|
|
210
|
-
`cumulative_drops()`
|
|
210
|
+
`cumulative_drops()` counts the timestamp gaps the client has detected
|
|
211
|
+
in the stream (one per gap, however many samples it spanned), and
|
|
212
|
+
`take_overrun()` flags the read that follows one.
|
|
211
213
|
Steady growth means the link or the consumer cannot keep pace. Reduce
|
|
212
214
|
the sample rate, switch to `I16`, or read in larger blocks.
|
|
213
215
|
|
|
@@ -231,5 +233,5 @@ let mut source = AaroniaSource::new(config).await?;
|
|
|
231
233
|
|
|
232
234
|
Build with `--features native-sdk`. Aaronia does not ship a macOS SDK,
|
|
233
235
|
so this path is unavailable there. The native-SDK paths are
|
|
234
|
-
hardware-unverified in this project; see the verification table in
|
|
235
|
-
[
|
|
236
|
+
hardware-unverified in this project; see the verification table in
|
|
237
|
+
[VERIFICATION.md](VERIFICATION.md).
|
|
@@ -49,7 +49,7 @@ The Aaronia RTSA Vendor SDK provides low-level, high-performance access to Aaron
|
|
|
49
49
|
* **Real-time Data Access**: Packet-based streaming of IQ samples and spectrum data with configurable decimation
|
|
50
50
|
* **Device Modes**: Verified support for multiple operational modes:
|
|
51
51
|
* **SpectranV6**: `spectranv6/raw`, `spectranv6/iqreceiver`, `spectranv6/iqtransceiver`, `spectranv6/iqtransmitter`, `spectranv6/sweepsa`
|
|
52
|
-
* **SpectranV6 ECO**: `spectranv6eco/raw
|
|
52
|
+
* **SpectranV6 ECO**: `spectranv6eco/rtsa` (its raw pipeline; there is no `spectranv6eco/raw`), `spectranv6eco/iqreceiver`, `spectranv6eco/iqtransceiver`, `spectranv6eco/iqtransmitter`, `spectranv6eco/sweepsa`
|
|
53
53
|
* **Configuration Tree**: Hierarchical configuration system with verified parameters:
|
|
54
54
|
* **Clock Rates** (V6): 46MHz (46.08), 61MHz (61.44), 76MHz (76.80), 92MHz (92.16), 122MHz (122.88), 184MHz (184.32), 245MHz (245.76), 492MHz (491.52) — see the full label table under [Rust Binding Notes](#rust-binding-notes)
|
|
55
55
|
* **Clock Rates** (V6 ECO): fixed at 92.16 MHz. Its top IQ rate is 61.44 MHz, which is that clock over 1.5 — the two are easy to confuse and this document once recorded the rate as the clock
|
|
@@ -99,18 +99,83 @@ When using the HTTP backend over a network link, the wire format heavily impacts
|
|
|
99
99
|
use sdr_aaronia_rs::AaroniaConfig;
|
|
100
100
|
|
|
101
101
|
// Default (Float32): 8 bytes/sample on the wire. Lossless, zero-copy decode.
|
|
102
|
-
// At
|
|
102
|
+
// At the 61.44 MS/s top rate that is ~490 MB/s (best for localhost).
|
|
103
103
|
let _high_fidelity = AaroniaConfig::default()
|
|
104
104
|
.center_frequency(2.4e9);
|
|
105
105
|
|
|
106
106
|
// Low Bandwidth (Int16): 4 bytes/sample. Halves network traffic.
|
|
107
107
|
// Requires setting both the format and the encode scale factor.
|
|
108
|
-
// At
|
|
108
|
+
// At 61.44 MS/s, ~246 MB/s.
|
|
109
109
|
let _low_bandwidth = AaroniaConfig::default()
|
|
110
110
|
.center_frequency(2.4e9)
|
|
111
111
|
.low_bandwidth_mode(); // Sets StreamFormat::Int16 and scale=32767.0
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
### Will the link carry it?
|
|
115
|
+
|
|
116
|
+
The span picks a sample rate off the device's decimation ladder, and at
|
|
117
|
+
4 bytes a sample (Int16/Float16) that rate is a byte rate the whole path
|
|
118
|
+
has to sustain. Measured on a SPECTRAN V6 ECO over gigabit: `--span 10M`
|
|
119
|
+
(15.36 MS/s, 61.4 MB/s) came back contiguous, while `--span 20M`
|
|
120
|
+
(30.72 MS/s, 122.9 MB/s) lost 1.84 s of a 35 s capture — dropped by the
|
|
121
|
+
*server*, upstream of the client, and each gap is a discontinuity that
|
|
122
|
+
breaks digital symbol timing.
|
|
123
|
+
|
|
124
|
+
`link_budget` answers the question before the capture rather than after:
|
|
125
|
+
|
|
126
|
+
```rust
|
|
127
|
+
use sdr_aaronia_rs::link_budget::{max_sustainable_span, required_byte_rate};
|
|
128
|
+
|
|
129
|
+
// What a span costs, via the ladder.
|
|
130
|
+
let rate = sdr_aaronia_rs::iq_sample_rate_for_bandwidth(10e6); // 15.36 MS/s
|
|
131
|
+
assert_eq!(required_byte_rate(rate), Some(61_440_000.0)); // 61.4 MB/s
|
|
132
|
+
|
|
133
|
+
// What a measured path affords, as a span you can pass to --span.
|
|
134
|
+
// `None` would mean no rung fits (or no budget can be computed at all,
|
|
135
|
+
// e.g. for the JSON format) — never a 0.0 a comparison could wave through.
|
|
136
|
+
assert_eq!(max_sustainable_span(75_000_000.0), Some(12_288_000.0)); // 12.288 MHz
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
To measure the path rather than assume it, stream from the server and
|
|
140
|
+
count bytes off the socket. The probe discards an initial settle window
|
|
141
|
+
(`LINK_PROBE_SETTLE`, 500 ms) first, because the RTSA server hands over
|
|
142
|
+
its pre-connect backlog faster than real time — count that and the
|
|
143
|
+
answer comes out *above* the true link rate, which is the one error that
|
|
144
|
+
makes a probe worse than none:
|
|
145
|
+
|
|
146
|
+
```rust,no_run
|
|
147
|
+
# async fn probe() -> sdr_aaronia_rs::Result<()> {
|
|
148
|
+
use std::time::Duration;
|
|
149
|
+
use sdr_aaronia_rs::link_budget::{DEFAULT_LINK_FORMAT, measure_link_throughput};
|
|
150
|
+
|
|
151
|
+
let m = measure_link_throughput("http://localhost:54664", Duration::from_secs(3)).await?;
|
|
152
|
+
println!("{m}"); // rate, window, settle discarded
|
|
153
|
+
println!(
|
|
154
|
+
"widest span: {:?} Hz",
|
|
155
|
+
m.max_sustainable_span_hz(DEFAULT_LINK_FORMAT)
|
|
156
|
+
);
|
|
157
|
+
# Ok(())
|
|
158
|
+
# }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`measure_link_throughput_with` additionally takes the capture's
|
|
162
|
+
`StreamParams` — probe with the same format, input and rate reduction
|
|
163
|
+
the capture will use, or the probe measures a different stream — and the
|
|
164
|
+
settle window, for servers whose connect backlog outlasts the default.
|
|
165
|
+
|
|
166
|
+
It measures what the path *delivered*, which is a floor on what it can
|
|
167
|
+
deliver: point the device at or above the span being planned first, and
|
|
168
|
+
check `ThroughputMeasurement::stream_sample_rate` to confirm the path was
|
|
169
|
+
actually loaded. An unreachable server or an idle mission is an error,
|
|
170
|
+
never a rate — "0 MB/s" would condemn every span on the ladder.
|
|
171
|
+
|
|
172
|
+
The `HttpSource` block runs the same check passively on the stream it is
|
|
173
|
+
already reading, comparing the delivered IQ payload against the rate the
|
|
174
|
+
device reports in its packet headers, and warns once per configuration,
|
|
175
|
+
naming a narrower span from the device's own ladder that fits. The
|
|
176
|
+
verdict is also published as `StreamStats::link_budget` on the shared
|
|
177
|
+
stats handle, and is re-measured after a configuration restart.
|
|
178
|
+
|
|
114
179
|
## Reusable configuration profiles
|
|
115
180
|
|
|
116
181
|
Build your own configuration for specific bands:
|
|
@@ -318,7 +383,7 @@ code you can run. CI builds them, so they stay current:
|
|
|
318
383
|
| Task | Example |
|
|
319
384
|
| --- | --- |
|
|
320
385
|
| HTTP IQ streaming, first samples | [`http_iq_quickstart.rs`](../examples/http_iq_quickstart.rs) |
|
|
321
|
-
| Health checks,
|
|
386
|
+
| Health checks, server info, input enumeration | [`device_control.rs`](../examples/device_control.rs) |
|
|
322
387
|
| Frequency hopping via the `sdr-source` traits | [`channel_hopping.rs`](../examples/channel_hopping.rs) |
|
|
323
388
|
| FutureSDR flowgraph with FM demodulation | [`noaa_scanner.rs`](../examples/noaa_scanner.rs) |
|
|
324
389
|
| Native SDK capture and transmit | [`native_sdk_basic.rs`](../examples/native_sdk_basic.rs), [`native_sdk_transmit.rs`](../examples/native_sdk_transmit.rs) |
|