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.
Files changed (120) hide show
  1. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/CHANGELOG.md +310 -0
  2. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/CONTRIBUTING.md +1 -1
  3. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/Cargo.lock +11 -11
  4. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/Cargo.toml +1 -1
  5. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/PKG-INFO +2 -2
  6. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/PLUGINS.md +4 -4
  7. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/README.md +5 -2
  8. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/APPS.md +1 -1
  9. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/FILESPEC.md +1 -1
  10. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/QUICKSTART.md +6 -4
  11. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/SDKSPEC.md +1 -1
  12. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/USAGE.md +68 -3
  13. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/include/aaronia.h +3 -1
  14. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/packaging/homebrew/soapy-aaronia.rb +1 -1
  15. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/Cargo.toml +1 -1
  16. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/README.md +1 -1
  17. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/aaronia.pyi +1 -1
  18. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/src/lib.rs +1 -1
  19. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/README.md +2 -1
  20. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/c_api.rs +39 -14
  21. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/decompression.rs +11 -1
  22. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/file_source.rs +96 -20
  23. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_endpoints.rs +179 -55
  24. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_sink.rs +4 -1
  25. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_source.rs +1342 -132
  26. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/http_streaming.rs +660 -184
  27. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/lib.rs +12 -0
  28. python_aaronia-0.7.7/src/link_budget.rs +1490 -0
  29. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/native_sdk.rs +162 -12
  30. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdk_source.rs +33 -3
  31. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdr_source_impl.rs +9 -7
  32. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/seify_impl.rs +5 -2
  33. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/unified_sink.rs +7 -0
  34. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/unified_source.rs +95 -19
  35. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/utils.rs +34 -14
  36. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/c_api_test.rs +1 -1
  37. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.cargo/config.toml +0 -0
  38. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.gitattributes +0 -0
  39. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/.gitignore +0 -0
  40. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/DESIGN.md +0 -0
  41. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/LICENSE +0 -0
  42. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/decompress_block.rs +0 -0
  43. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/deinterleave_dual_iq.rs +0 -0
  44. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/parse_int16_packet.rs +0 -0
  45. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/benches/rtsa_open_and_read.rs +0 -0
  46. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/deny.toml +0 -0
  47. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/HTTPSPEC.md +0 -0
  48. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/docs/VERIFICATION.md +0 -0
  49. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/channel_hopping.rs +0 -0
  50. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/device_control.rs +0 -0
  51. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/dump_metadata.rs +0 -0
  52. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/http_iq_quickstart.rs +0 -0
  53. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/native_sdk_basic.rs +0 -0
  54. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/native_sdk_transmit.rs +0 -0
  55. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/noaa_scanner.rs +0 -0
  56. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/python_arrow_example.py +0 -0
  57. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/read_rtsa_file.rs +0 -0
  58. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/examples/soapy_python_example.py +0 -0
  59. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/packaging/homebrew/README.md +0 -0
  60. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/pyproject.toml +0 -0
  61. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/python-aaronia/test_basic.py +0 -0
  62. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/scripts/ci-local.sh +0 -0
  63. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/scripts/validate-iq-live.py +0 -0
  64. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
  65. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
  66. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/CMakeLists.txt +0 -0
  67. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/Registration.cpp +0 -0
  68. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeCache.txt +0 -0
  69. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -0
  70. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  71. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -0
  72. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -0
  73. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
  74. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -0
  75. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -0
  76. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -0
  77. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -0
  78. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -0
  79. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -0
  80. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -0
  81. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -0
  82. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -0
  83. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -0
  84. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -0
  85. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -0
  86. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -0
  87. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -0
  88. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -0
  89. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -0
  90. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -0
  91. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -0
  92. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -0
  93. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -0
  94. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -0
  95. {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
  96. {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
  97. {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
  98. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -0
  99. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/Makefile +0 -0
  100. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/build-test/cmake_install.cmake +0 -0
  101. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/packaging/install.ps1 +0 -0
  102. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/packaging/install.sh +0 -0
  103. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/soapy-aaronia/print.cmake +0 -0
  104. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/detection.rs +0 -0
  105. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/error.rs +0 -0
  106. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/src/sdk_sink.rs +0 -0
  107. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/test.cmake +0 -0
  108. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_mock_test.rs +0 -0
  109. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_resilience_test.rs +0 -0
  110. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/http_sink_test.rs +0 -0
  111. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/integration_test.rs +0 -0
  112. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/live_smoke.rs +0 -0
  113. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/native_sdk_load.rs +0 -0
  114. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/properties.proptest-regressions +0 -0
  115. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/properties.rs +0 -0
  116. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/rtsa_negative_test.rs +0 -0
  117. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/sdr_source_impl_test.rs +0 -0
  118. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/spec_coverage.rs +0 -0
  119. {python_aaronia-0.7.6 → python_aaronia-0.7.7}/tests/test_cw_mag.rs +0 -0
  120. {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
- Three benchmark harnesses track the hot paths across releases. Benchmarks are run on demand to investigate performance implications of a PR.
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.61.2",
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.15"
1672
+ version = "0.4.19"
1673
1673
  source = "registry+https://github.com/rust-lang/crates.io-index"
1674
- checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
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.61.2",
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.6"
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.61.2",
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.61.2",
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.61.2",
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.6"
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.61.2",
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.61.2",
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.6"
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.6
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()` | Total server-reported dropped samples |
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.6", features = ["seify"] }
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**: The `cumulative_drops()` function reports the total number of blocks dropped by the Aaronia server itself due to network backpressure.
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 server-side drops
143
+ # Check for gaps in the stream
144
144
  drops = sdr.readSensor("cumulative_drops")
145
- print(f"Dropped blocks: {drops}")
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.6"
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.6", features = ["native-sdk", "futuresdr"] }
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
- - Dropped blocks are reported through `readSensor("cumulative_drops")`.
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` | Size of an individual sample. This crate treats it as the byte stride of one sample when seeking within IQ payloads (8 for float32 IQ); for spectra the vendor dump annotates it as the bin count the two interpretations have not been reconciled against vendor documentation. |
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 requires roughly 740 MB/s at 92 MSPS. On anything other than
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()` and `take_overrun()` report server-side gaps.
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 the
235
- [README](../README.md).
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`, `spectranv6eco/iqreceiver`, `spectranv6eco/iqtransceiver`, `spectranv6eco/iqtransmitter`, `spectranv6eco/sweepsa`
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 92 MSPS, requires ~740 MB/s network throughput (best for localhost).
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 92 MSPS, requires ~370 MB/s.
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, input enumeration, recording control, license probing | [`device_control.rs`](../examples/device_control.rs) |
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) |