python-aaronia 0.7.4__tar.gz → 0.7.6__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 (119) hide show
  1. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/CHANGELOG.md +160 -0
  2. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/Cargo.lock +2 -2
  3. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/Cargo.toml +1 -1
  4. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/PKG-INFO +58 -8
  5. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/APPS.md +13 -5
  6. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/HTTPSPEC.md +17 -2
  7. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/QUICKSTART.md +35 -5
  8. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/SDKSPEC.md +4 -2
  9. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/VERIFICATION.md +1 -0
  10. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/packaging/homebrew/README.md +11 -4
  11. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/packaging/homebrew/soapy-aaronia.rb +1 -1
  12. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/python-aaronia/Cargo.toml +1 -1
  13. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/python-aaronia/README.md +57 -7
  14. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/python-aaronia/aaronia.pyi +11 -0
  15. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/python-aaronia/src/lib.rs +33 -1
  16. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/python-aaronia/test_basic.py +22 -0
  17. python_aaronia-0.7.6/scripts/validate-iq-live.py +386 -0
  18. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/README.md +23 -1
  19. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/file_source.rs +64 -11
  20. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/http_endpoints.rs +398 -0
  21. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/http_source.rs +468 -102
  22. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/lib.rs +3 -2
  23. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/seify_impl.rs +6 -1
  24. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/unified_source.rs +9 -7
  25. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/utils.rs +115 -6
  26. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/.cargo/config.toml +0 -0
  27. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/.gitattributes +0 -0
  28. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/.gitignore +0 -0
  29. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/CONTRIBUTING.md +0 -0
  30. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/DESIGN.md +0 -0
  31. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/LICENSE +0 -0
  32. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/PLUGINS.md +0 -0
  33. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/README.md +0 -0
  34. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/benches/decompress_block.rs +0 -0
  35. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/benches/deinterleave_dual_iq.rs +0 -0
  36. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/benches/parse_int16_packet.rs +0 -0
  37. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/benches/rtsa_open_and_read.rs +0 -0
  38. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/deny.toml +0 -0
  39. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/FILESPEC.md +0 -0
  40. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/docs/USAGE.md +0 -0
  41. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/channel_hopping.rs +0 -0
  42. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/device_control.rs +0 -0
  43. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/dump_metadata.rs +0 -0
  44. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/http_iq_quickstart.rs +0 -0
  45. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/native_sdk_basic.rs +0 -0
  46. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/native_sdk_transmit.rs +0 -0
  47. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/noaa_scanner.rs +0 -0
  48. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/python_arrow_example.py +0 -0
  49. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/read_rtsa_file.rs +0 -0
  50. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/examples/soapy_python_example.py +0 -0
  51. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/include/aaronia.h +0 -0
  52. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/pyproject.toml +0 -0
  53. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/scripts/ci-local.sh +0 -0
  54. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
  55. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
  56. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/CMakeLists.txt +0 -0
  57. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/Registration.cpp +0 -0
  58. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeCache.txt +0 -0
  59. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -0
  60. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  61. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -0
  62. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -0
  63. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
  64. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -0
  65. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -0
  66. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -0
  67. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -0
  68. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -0
  69. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -0
  70. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -0
  71. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -0
  72. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -0
  73. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -0
  74. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -0
  75. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -0
  76. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -0
  77. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -0
  78. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -0
  79. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -0
  80. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -0
  81. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -0
  82. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -0
  83. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -0
  84. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -0
  85. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -0
  86. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -0
  87. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -0
  88. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -0
  89. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/Makefile +0 -0
  90. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/build-test/cmake_install.cmake +0 -0
  91. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/packaging/install.ps1 +0 -0
  92. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/packaging/install.sh +0 -0
  93. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/soapy-aaronia/print.cmake +0 -0
  94. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/c_api.rs +0 -0
  95. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/decompression.rs +0 -0
  96. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/detection.rs +0 -0
  97. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/error.rs +0 -0
  98. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/http_sink.rs +0 -0
  99. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/http_streaming.rs +0 -0
  100. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/native_sdk.rs +0 -0
  101. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/sdk_sink.rs +0 -0
  102. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/sdk_source.rs +0 -0
  103. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/sdr_source_impl.rs +0 -0
  104. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/src/unified_sink.rs +0 -0
  105. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/test.cmake +0 -0
  106. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/c_api_test.rs +0 -0
  107. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/http_mock_test.rs +0 -0
  108. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/http_resilience_test.rs +0 -0
  109. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/http_sink_test.rs +0 -0
  110. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/integration_test.rs +0 -0
  111. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/live_smoke.rs +0 -0
  112. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/native_sdk_load.rs +0 -0
  113. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/properties.proptest-regressions +0 -0
  114. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/properties.rs +0 -0
  115. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/rtsa_negative_test.rs +0 -0
  116. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/sdr_source_impl_test.rs +0 -0
  117. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/spec_coverage.rs +0 -0
  118. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/test_cw_mag.rs +0 -0
  119. {python_aaronia-0.7.4 → python_aaronia-0.7.6}/tests/test_cw_meta.rs +0 -0
@@ -4,6 +4,166 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.7.6] - 2026-08-15
8
+
9
+ All of this is about the FutureSDR `HttpSource` block (the `futuresdr`
10
+ feature), whose streaming path turned out to be losing most of the
11
+ stream. Found and measured against a live SPECTRAN V6 ECO running a
12
+ 49 MHz survey for a trunking decoder.
13
+
14
+ ### Fixed
15
+ - **The sample buffer guillotined every packet bigger than itself.**
16
+ The capacity trim runs as each packet lands, and with capacity fixed
17
+ at `buffer_size * 2` a device sending 49k-sample packets into a
18
+ 16384-sample capacity lost ~75% of every packet before the consumer
19
+ was offered any of it. Measured live: 61.4 MS/s at the device,
20
+ 0.33 MS/s reaching the pipeline, and digital decode unable to hold
21
+ frame sync. The capacity now floors at four times the largest packet
22
+ observed, learned from the stream; the floor only ever raises the
23
+ configured value.
24
+ - **The initial tune could claim success while the device never
25
+ moved.** `/control` answers `success=true` whether or not any block
26
+ applies the command — the same device has been measured both
27
+ honouring and ignoring the identical full-tuple payload in different
28
+ mission states. The block's start-up tune now writes the real field
29
+ names (`centerfreq0`, `decimation0`, `reflevel0`) via `/remoteconfig`
30
+ to the block discovered by walking the config tree, then reads them
31
+ back and warns about anything that did not take. Where no such block
32
+ exists (an IQ-demodulator mission exposes different fields), it falls
33
+ back to the old `/control` capture command and says the result is
34
+ unverified. The tune is one-shot: stream restarts after an external
35
+ retune no longer re-push the source's stale target over it.
36
+ - **Launching an app no longer overwrites the operator's gain.** The
37
+ builder's reference level was a bare number pushed on every start, so
38
+ merely starting a flowgraph reset the device to the builder default.
39
+ It is now optional and left untouched unless the caller set one.
40
+ - **`work()` spun.** The runtime ran it whenever the output port had
41
+ any room — measured at 106,000–390,000 calls a second averaging 14
42
+ free samples each — burning ~60% of a core and starving the
43
+ downstream block. The output port now requires a worthwhile block of
44
+ room before `work()` runs; call rate dropped to ~14/s.
45
+
46
+ ### Changed
47
+ - **The HTTP socket is drained by a dedicated task**, not inside
48
+ `work()`. The socket used to be read only while the scheduler happened
49
+ to run the block, which capped throughput at a tenth of what `curl`
50
+ pulls from the same endpoint. A background task now pushes chunks
51
+ into a bounded channel (~10 MB), whose fill is the backpressure point:
52
+ consumer falls behind → channel fills → reader blocks → TCP flow
53
+ control stops the server. Measured at a 49 MHz span: 1.8 → ~7–9 MS/s.
54
+ The honest ceiling is the link (~57 MB/s ≈ 14 MS/s as float32);
55
+ 61.44 MS/s over this transport is 2 Gbit/s and not reachable.
56
+ - Buffer-overflow drops are now counted and logged geometrically
57
+ rather than once per occurrence, which at wide span was 5040 log
58
+ lines in 25 seconds for one piece of information.
59
+ - File playback decodes little-endian cf32 with a zero-copy read
60
+ straight into the sample buffer; the explicit per-sample decode
61
+ remains for big-endian hosts, and a round-trip test pins the two to
62
+ identical output.
63
+
64
+ ### Fixed (in review, before this release shipped)
65
+ - The refill loop kept iterating through its 50 ms idle sleeps when the
66
+ channel was empty, holding samples already in the buffer for up to
67
+ 800 ms before producing them. It now flushes immediately and sleeps
68
+ only when there is nothing to flush.
69
+ - A parse error mid-stream reconnected without reaping the reader task,
70
+ which on a stalled socket keeps holding a server connection — on the
71
+ free licence, the one connection the reconnect needs.
72
+ - Two comments overstated the `/control` finding as "this endpoint does
73
+ not carry these fields on a Spectran V6". Measured today, the same
74
+ device honours the identical full-tuple payload — the accurate claim
75
+ is that `success=true` proves nothing either way, which is the reason
76
+ the verified `/remoteconfig` path exists. Where no `centerfreq0`
77
+ block is found, the initial tune now falls back to `/control` rather
78
+ than silently not tuning (an IQ-demodulator mission exposes
79
+ `centerfreq`, not `centerfreq0`).
80
+
81
+ ### Added
82
+ - `iq_sample_rate_for_decimation_index`, `decimation_index_for_rate`
83
+ and `decimation_index_for_bandwidth` in `utils`: the RTSA "Span"
84
+ enum (`Full` … `1 / 512`) mapped onto the sample-rate ladder and
85
+ back, so a requested span becomes the enum index the device actually
86
+ takes.
87
+ - `HttpEndpointsClient::apply_capture_config` — retune via
88
+ `/remoteconfig` with read-back confirmation — plus
89
+ `find_block_name_with_field`, which discovers the receiver block by
90
+ the field the write will target instead of assuming its category.
91
+
92
+ ## [v0.7.5] - 2026-08-13
93
+
94
+ ### Added
95
+ - **`scale` on the Python config and `aaronia.open()`**, the integer
96
+ encode multiplier for the `I16` wire format. Rust, the C API and the
97
+ SoapySDR plugin all had it; Python did not, so a Python program
98
+ choosing `I16` for bandwidth had no way out of the trap below.
99
+ - **`scripts/validate-iq-live.py`**, an end-to-end check that the
100
+ samples an application receives are the ones the device sent, rather
101
+ than that bytes arrived. Every wire format must decode to the same
102
+ spectrum, the Python and SoapySDR paths must agree, and a known
103
+ transmitter must land where it should. Run against a live server it
104
+ places a NOAA weather-radio carrier within 312 Hz of 162.400 MHz and
105
+ on the correct side of zero — the one check that catches transposed
106
+ I and Q, which nothing comparing the radio against itself can see.
107
+
108
+ ### Fixed
109
+ - **`format="I16"` silently discards weak signals at the default
110
+ scale.** The server sends `round(value * scale)`, so the step is
111
+ `1 / scale`, and the default of 16384 gives 6.1e-5 — coarser than a
112
+ quiet band's noise floor. Measured against a live server, **68% of
113
+ int16 samples came back exactly zero** where float32 had none. At
114
+ `scale=1e6` the zero fraction was 0.0% and the amplitude matched
115
+ float32. Documented, with the measurements, in the Python README.
116
+
117
+ ### Changed
118
+ - **The Homebrew formula is no longer a release asset.** It is uploaded
119
+ as the `homebrew-formula` workflow artifact instead. Homebrew 4 and
120
+ later cannot install from a formula URL, so on the release page it
121
+ was a file no user could act on, listed among ones they can. Its only
122
+ reader is whoever updates the tap, and the checksums are still
123
+ rendered against the published archives so none are computed by hand.
124
+
125
+ ### Documentation
126
+ - **Wire format is a throughput decision, and the default is the
127
+ expensive one.** At 15.36 MS/s over a LAN, float32 needs 123 MB/s:
128
+ measured, it delivered 6.5 MS/s with 290 drops, while float16 and
129
+ int16 both delivered 15.1 MS/s. The Python README now carries the
130
+ numbers rather than a general warning.
131
+ - **Measured what the 80% usable-bandwidth figure actually is, and
132
+ explained it in one sentence.** Sample rate and RF bandwidth were
133
+ described as related by a ratio without saying what the ratio was or
134
+ where it came from. RTSA declares exactly 0.8 x Fs as the packet's
135
+ frequency range at every rate — a fixed rule, checked at 61.44,
136
+ 15.36, 7.68 and 3.84 MHz — and every sample still arrives, so an FFT
137
+ spans the whole rate while only that 80% is flat and calibrated.
138
+ Sweeping the receiver's own noise floor on a V6 ECO confirms it: the
139
+ response is flat within 0.5 dB across 0.80 of the rate at 15.36 MHz
140
+ sampling and 0.89 at 7.68 MHz, and at full span the analog filter is
141
+ about 1 dB down by the declared edge — which is where Aaronia's
142
+ 44 MHz data-sheet figure comes from, against the 49.152 MHz the
143
+ device itself declares. The guidance readers need is one line: to see
144
+ N Hz of spectrum, sample at N / 0.8.
145
+ - **The READMEs stated the V6 ECO's sample-rate ladder as if it were
146
+ every device's.** "The device runs a fixed ladder of rates: 61.44 MHz
147
+ halved down to 120 kHz" is measured and true for an ECO, but a full
148
+ V6 selects its receiver clock and starts higher — by how much is the
149
+ open question the specs already record, and the user-facing pages did
150
+ not carry it. The Python, SoapySDR, quickstart and applications docs
151
+ now say whose ladder it is and point at the note, and
152
+ `iq_sample_rates` says so in its own documentation.
153
+ - The SoapySDR README gained a Sample rates section: why
154
+ `listSampleRates` reports the real ladder, that `setSampleRate` snaps
155
+ and logs, and that `getSampleRate` while streaming is the number to
156
+ trust on hardware the advertised ladder does not describe.
157
+ - SDKSPEC still gave the eco family's clock as 61.44 MHz in a second
158
+ place, contradicting the correction made elsewhere in the same
159
+ document. It is 92.16 MHz; 61.44 MHz is the top IQ rate.
160
+ - The same sweep caught the claim in `unified_source`'s own
161
+ documentation, in the `/control` span note in HTTPSPEC, and in the
162
+ 0.8 usable-bandwidth ratio, all of which read as universal and are
163
+ measurements from one device. `seify_impl`'s sample-rate range is
164
+ capped at 61.44 MHz for the same reason and now says so: seify has no
165
+ device handle at that point to ask for something better.
166
+
7
167
  ## [v0.7.4] - 2026-08-12
8
168
 
9
169
  ### Fixed
@@ -3027,7 +3027,7 @@ dependencies = [
3027
3027
 
3028
3028
  [[package]]
3029
3029
  name = "python-aaronia"
3030
- version = "0.7.4"
3030
+ version = "0.7.6"
3031
3031
  dependencies = [
3032
3032
  "arrow",
3033
3033
  "num-complex",
@@ -3556,7 +3556,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3556
3556
 
3557
3557
  [[package]]
3558
3558
  name = "sdr-aaronia-rs"
3559
- version = "0.7.4"
3559
+ version = "0.7.6"
3560
3560
  dependencies = [
3561
3561
  "anyhow",
3562
3562
  "bitflags 2.13.1",
@@ -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.4"
5
+ version = "0.7.6"
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.4
3
+ Version: 0.7.6
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+)
@@ -88,17 +88,66 @@ above depends on.
88
88
 
89
89
  ## Sample rates
90
90
 
91
- The device runs a fixed ladder of rates: 61.44 MHz halved down to
92
- 120 kHz. Ask for anything else and it quietly uses the nearest rung,
93
- leaving your program computing against a rate that is not in use.
91
+ The device runs a ladder of rates rather than a continuous range: each
92
+ rung is half the one above it. Ask for anything else and it quietly
93
+ uses the nearest rung, leaving your program computing against a rate
94
+ that is not in use.
94
95
 
95
96
  ```python
96
97
  aaronia.sample_rates() # every rate, highest first
97
98
  aaronia.sample_rate_for_bandwidth(8e6) # 15.36e6: the lowest rate covering 8 MHz
98
99
  ```
99
100
 
100
- A rate carries only 80% of itself as alias-free bandwidth, which is why
101
- 8 MHz of spectrum needs 15.36 MHz of sampling.
101
+ **Sample rate is not RF bandwidth.** You get every sample, so an FFT of
102
+ them spans the full rate but only the middle 80% is flat and
103
+ calibrated. That is not an approximation: RTSA reports exactly 0.8 x Fs
104
+ as the packet's frequency range at every rate. Outside it, data still
105
+ arrives, attenuated and uncalibrated.
106
+
107
+ So **to see N Hz of spectrum, sample at N / 0.8**, which is what
108
+ `sample_rate_for_bandwidth()` computes. Aaronia's data sheet quotes a
109
+ more conservative figure still — 44 MHz for the ECO against the
110
+ 49.152 MHz it declares at full span — because the analog filter is
111
+ already about 1 dB down at that edge. The
112
+ [quickstart](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/QUICKSTART.md#4-troubleshooting)
113
+ has the measurements.
114
+
115
+ `sample_rates()` returns the ladder for a SPECTRAN V6 ECO — 61.44 MHz
116
+ down to 120 kHz — which is measured, rung by rung. A full V6 has a
117
+ selectable receiver clock and can go higher, and exactly how much
118
+ higher is not settled; see
119
+ [the note in HTTPSPEC](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/HTTPSPEC.md#unresolved-the-full-v6s-top-rate).
120
+ On that hardware, take the rate the device reports over the computed
121
+ ladder: it arrives in the stream metadata, and `diagnose()` prints it.
122
+
123
+ ## Choosing a wire format
124
+
125
+ `format` decides what crosses the network, and it matters more than it
126
+ looks. Measured against a live server at 15.36 MS/s over a LAN:
127
+
128
+ | format | bytes/sample | delivered | drops |
129
+ | --- | --- | --- | --- |
130
+ | `F32` (default) | 8 | 6.5 MS/s | 290 |
131
+ | `F16` | 4 | 15.1 MS/s | 9 |
132
+ | `I16` | 4 | 15.1 MS/s | 12 |
133
+
134
+ `F32` needs 123 MB/s at that rate and the link could not carry it, so
135
+ most of the capture was dropped. Either half-width format fits.
136
+
137
+ `I16` has one trap: the server sends `round(value * scale)`, so the
138
+ quantisation step is `1 / scale`, and the default of 16384 gives a step
139
+ of 6.1e-5. A quiet band's noise floor is smaller than that — on the
140
+ same server, **68% of `I16` samples came back exactly zero** while
141
+ `F32` had none. Pass `scale=`, or lower `reference_level` for more
142
+ gain:
143
+
144
+ ```python
145
+ aaronia.open(url, freq=2.44e9, rate=15.36e6, format="I16", scale=1e6)
146
+ ```
147
+
148
+ At `scale=1e6` the zero fraction measured 0.0% and the amplitude
149
+ matched `F32`. `F16` needs no such tuning, which makes it the simpler
150
+ choice when the link is the constraint.
102
151
 
103
152
  ## Configuration (`AaroniaConfig`)
104
153
 
@@ -113,6 +162,7 @@ Every field is readable and writable.
113
162
  | `sample_rate` | IQ sample rate, Hz (the Aaronia "span") |
114
163
  | `reference_level` | Reference level, dBm |
115
164
  | `format` | HTTP wire format: `"F32"`, `"F16"` or `"I16"`. `I16` is the low-bandwidth network mode |
165
+ | `scale` | Integer encode multiplier for `I16` (see below). None uses the server default |
116
166
  | `receiver_channel` | `"Rx1"` (default), `"Rx2"`, or `"Rx1And2"` (native SDK, full V6) |
117
167
  | `read_timeout` | Seconds a blocking read waits before `AaroniaTimeoutError` (default `30.0`) |
118
168
  | `auto_reconnect` | Reconnect the HTTP stream after a drop (default `True`) |
@@ -169,8 +219,8 @@ silently defaulting.
169
219
 
170
220
  | Function | Purpose |
171
221
  | --- | --- |
172
- | `open(url=None, *, freq, rate, bandwidth, ref_level, file, format, read_timeout)` | Configure, connect and start streaming in one call |
173
- | `sample_rates()` | Every sample rate the hardware can run |
222
+ | `open(url=None, *, freq, rate, bandwidth, ref_level, file, format, scale, read_timeout)` | Configure, connect and start streaming in one call |
223
+ | `sample_rates()` | The V6 ECO's sample rates, highest first (see [Sample rates](#sample-rates)) |
174
224
  | `sample_rate_for_bandwidth(hz)` | Lowest rate covering that much spectrum |
175
225
  | `diagnose(url)` | `(ok, message, fix)` for each setup check; what `aaronia-doctor` prints |
176
226
 
@@ -50,9 +50,12 @@ driver=aaronia,url=http://localhost:54664
50
50
  ```
51
51
 
52
52
  Select the sample rate from GQRX's list. The plugin advertises the
53
- device's real ladder: 61.44 MHz halved down to 120 kHz, which the RTSA
54
- GUI labels Full through 1/512. The hardware produces no other rate, so a
55
- request in between snaps to the nearest rung and logs a warning.
53
+ device's real ladder rather than round numbers it cannot produce: on a
54
+ V6 ECO that is 61.44 MHz halved down to 120 kHz, which the RTSA GUI
55
+ labels Full through 1/512. Nothing in between exists, so a request
56
+ between rungs snaps to the nearest and logs a warning. A full V6 has a
57
+ selectable receiver clock and reaches higher; the plugin advertises the
58
+ ECO ladder, so check the rate the device reports if you are on one.
56
59
 
57
60
  The single gain element is `REF`, the Aaronia reference level in dBm. It
58
61
  is not an amplifier gain: raising it reduces sensitivity. Start near
@@ -101,8 +104,13 @@ directly and skip the SoapySDR layer.
101
104
 
102
105
  ## General notes
103
106
 
104
- - Sample rate is the Aaronia "span", not the usable RF bandwidth. The
105
- alias-free span is smaller, roughly 49 MHz within a 61.44 MHz capture.
107
+ - Sample rate is the Aaronia "span", not the usable RF bandwidth. Every
108
+ sample reaches the application, so the display spans the full rate,
109
+ but only the middle 80% is flat and calibrated — RTSA reports exactly
110
+ 0.8 x Fs as the packet's frequency range. Set the rate whose 80%
111
+ covers the span you want to see: 61.44 MHz of sampling gives roughly
112
+ 49 MHz of calibrated spectrum. The edges are real data, just rolled
113
+ off.
106
114
  - `readStream` honours `timeoutUs` and returns partial reads within the
107
115
  deadline, per the SoapySDR contract.
108
116
  - Retuning mid-stream is safe and requires no Aaronia licence.
@@ -302,7 +302,12 @@ from `/remoteconfig` first if you need to target one block.
302
302
 
303
303
  These are one-off requests. Aaronia's own words: if they cannot be
304
304
  executed, or conflict with the local configuration, the behaviour is
305
- undefined — nothing reports the conflict.
305
+ undefined — nothing reports the conflict. That is not theoretical: the
306
+ same device has been measured both honouring and ignoring an identical
307
+ full-tuple capture command in different mission states, answering
308
+ `success=true` both times. A retune that must be provable goes through
309
+ `/remoteconfig` and reads the value back
310
+ (`HttpEndpointsClient::apply_capture_config` does exactly this).
306
311
 
307
312
  **The settings each `type` accepts**, per Aaronia support (January
308
313
  2024), which goes well beyond the published specification:
@@ -357,7 +362,17 @@ configuration of zones is not supported.
357
362
  ```
358
363
 
359
364
  > **`frequencySpan` is a usable-bandwidth request, not a sample rate.**
360
- > The device runs at 61.44 MHz divided by a power of two. Given a span
365
+ > The device halves its top rate down a ladder 61.44 MHz on the V6
366
+ > ECO this was measured against. The usable RF bandwidth it declares in
367
+ > `startFrequency..endFrequency` is exactly 0.8 x Fs at every rate,
368
+ > checked at 61.44, 15.36, 7.68 and 3.84 MHz; every sample still
369
+ > arrives, so an FFT spans the whole rate, but only that 80% is flat
370
+ > and calibrated. Sweeping the receiver's own noise floor puts the real
371
+ > filter at or beyond the declared edge at decimated rates (flat within
372
+ > 0.5 dB across 0.80 x Fs at 15.36 MHz, 0.89 at 7.68 MHz) and just
373
+ > inside it at full span (about 1 dB down at the declared edge, 3 dB at
374
+ > 0.84 x Fs), which is where Aaronia's 44 MHz data-sheet figure for the
375
+ > ECO comes from. Given a span
361
376
  > it cannot produce, it selects the rate whose alias-free bandwidth
362
377
  > (0.8 x Fs) is nearest the request: 2.5 MHz yields Fs = 3.84 MHz,
363
378
  > 1.3 MHz yields 1.92 MHz, and 10 MHz yields 15.36 MHz. A value that is
@@ -141,11 +141,41 @@ different:
141
141
  | Where you see it | Example | Meaning |
142
142
  | --- | --- | --- |
143
143
  | `span_frequency`, and `sampleFrequency` in packet metadata | 15.36 MHz | The sample rate, Fs |
144
- | `startFrequency`..`endFrequency` in packet metadata | 12.288 MHz | Usable alias-free bandwidth, which measures 0.8 x Fs |
145
- | The Span control in the RTSA GUI | `1 / 4` | Decimation of the 61.44 MHz clock, so Fs = 61.44 / 4 |
146
-
147
- The device runs at 61.44 MHz divided by a power of two, ten rates from
148
- 61.44 MHz down to 120 kHz, shown in the GUI as Full through `1 / 512`.
144
+ | `startFrequency`..`endFrequency` in packet metadata | 12.288 MHz | Usable RF bandwidth: exactly 0.8 x Fs, at every rate |
145
+ | The Span control in the RTSA GUI | `1 / 4` | Decimation of the top rate, so Fs = 61.44 / 4 on an ECO |
146
+
147
+ **You get every sample, but only the middle 80% is calibrated.** An FFT
148
+ of the samples spans the full Fs. RTSA reports 0.8 x Fs as the packet's
149
+ frequency range, at every rate — a fixed rule, not a per-rate
150
+ measurement — and that is the part the anti-alias filter keeps flat and
151
+ the calibration covers. Data outside it still arrives, attenuated and
152
+ uncalibrated.
153
+
154
+ So **to see N Hz of spectrum, sample at N / 0.8**: 8 MHz of signal needs
155
+ 10 MHz of sampling, and the lowest rung providing it is 15.36 MHz.
156
+ `iq_sample_rate_for_bandwidth` does that arithmetic;
157
+ `aaronia.sample_rate_for_bandwidth` is the Python equivalent.
158
+
159
+ The 80% figure holds up when measured. Averaging the receiver's own
160
+ noise floor on a V6 ECO, the response is flat to within 0.5 dB across
161
+ 0.80 x Fs at 15.36 MHz sampling and 0.89 x Fs at 7.68 MHz — at or
162
+ beyond what is declared. At full span it is tighter: the analog filter
163
+ is about 1 dB down by the declared edge and 3 dB down at 0.84 x Fs,
164
+ which is why Aaronia's data sheet quotes 44 MHz of real-time bandwidth
165
+ for the ECO rather than the 49.152 MHz the device declares. Take the
166
+ declared span as the working figure and the data sheet as the
167
+ guaranteed one.
168
+
169
+ That sweep sees the antenna as well as the receiver, so the full-span
170
+ roll-off is an upper bound on how good the filter is, not a
171
+ measurement of it alone. It is enough to show the declared 80% is
172
+ physically grounded rather than an arbitrary number, which is what the
173
+ figure is used for here.
174
+
175
+ The device halves its top rate down a ten-rung ladder, shown in the GUI
176
+ as Full through `1 / 512`. On a V6 ECO that is 61.44 MHz down to
177
+ 120 kHz, measured rung by rung. A full V6 has a selectable receiver
178
+ clock and starts higher.
149
179
 
150
180
  **Pass one of those rates and you get it exactly.** Anything else is
151
181
  silently adjusted, and not to the nearest rate: the server reads the
@@ -275,9 +275,11 @@ actually 92.16 MHz, etc. Use `receiver_clock_for_label` to convert.
275
275
  | `245MHz` | 245.76 MHz | README + ConfigTree |
276
276
  | `492MHz` | 491.52 MHz | ConfigTree |
277
277
 
278
- The eco family runs at a fixed `61.44 MHz` clock and does not expose the
278
+ The eco family runs at a fixed clock and does not expose the
279
279
  `device/receiverclock` config key — `configure_iq_receiver` skips the
280
- write on that family (see `DeviceOpenMode::EcoIqReceiver`).
280
+ write on that family (see `DeviceOpenMode::EcoIqReceiver`) and uses
281
+ `DEFAULT_RECEIVER_CLOCK_HZ`, 92.16 MHz. Its top IQ rate is 61.44 MHz,
282
+ which is that clock over 1.5; do not confuse the two.
281
283
 
282
284
  ### `read_samples` polling cadence
283
285
 
@@ -25,6 +25,7 @@ SDK are marked unverified.
25
25
  | Spectra reads (`read_spectra`) | Native SDK | **Hardware-unverified**. Packet layout and stream index follow Aaronia's `RawSpectrum` sample |
26
26
  | Device-family detection (`open_detected_device`) | Native SDK | **Hardware-unverified**. Enumerates each known family in turn |
27
27
  | Sample-rate ladder and usable bandwidth | Both | **Live-verified at the default clock** on a V6 ECO, rung by rung. Faster receiver clocks are inferred from the documented constraint, not measured |
28
+ | End-to-end IQ correctness | HTTP | **Live-verified** by `scripts/validate-iq-live.py`: every wire format decodes to the same spectrum, the Python and SoapySDR paths agree, and a NOAA weather-radio carrier lands within 312 Hz of its known frequency on the correct side of zero |
28
29
  | GPS hardware time | Native SDK | **Hardware-unverified** |
29
30
  | Native SDK capture generally | Native SDK | **Hardware-unverified**; compiled and unit-tested in a Linux VM each release |
30
31
  | HTTP TX push (`/sample`) | HTTP | Endpoint exercised live; RF output not measured |
@@ -23,12 +23,19 @@ Homebrew pulls in SoapySDR, drops the module into
23
23
 
24
24
  ## Each release
25
25
 
26
- The release workflow renders this file against the published archives
27
- and attaches the result to the release as `soapy-aaronia.rb`, with the
28
- version and both SHA-256 checksums filled in. Copy that file into the
29
- tap repository as `Formula/soapy-aaronia.rb` and commit. Nothing needs
26
+ The release workflow renders this file against the published archives,
27
+ with the version and both SHA-256 checksums filled in, and uploads it
28
+ as the **`homebrew-formula` workflow artifact** not a release asset.
29
+ Download it from the release run's Artifacts section, copy it into the
30
+ tap repository as `Formula/soapy-aaronia.rb`, and commit. Nothing needs
30
31
  hashing by hand.
31
32
 
33
+ It is deliberately not attached to the release page: Homebrew 4 and
34
+ later cannot install from a formula URL, so there it would be a file no
35
+ user could act on, sitting among ones they can. Once the tap exists,
36
+ the better move is to have the workflow commit to it directly and skip
37
+ this step.
38
+
32
39
  Before pushing, `brew audit --strict --online soapy-aaronia` checks the
33
40
  formula, and `brew install --build-from-source` proves it works.
34
41
 
@@ -12,7 +12,7 @@
12
12
  class SoapyAaronia < Formula
13
13
  desc "SoapySDR plugin for Aaronia SPECTRAN V6 spectrum analyzers"
14
14
  homepage "https://github.com/isaacbentley/sdr-aaronia-rs"
15
- version "0.7.4"
15
+ version "0.7.6"
16
16
  license "GPL-3.0-or-later"
17
17
 
18
18
  # Prebuilt modules: the plugin statically links its Rust core, so there
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "python-aaronia"
3
- version = "0.7.4"
3
+ version = "0.7.6"
4
4
  edition = "2024"
5
5
  description = "Python bindings for sdr-aaronia-rs (Aaronia SPECTRAN V6 SDR source)"
6
6
  license = "GPL-3.0-or-later"
@@ -73,17 +73,66 @@ above depends on.
73
73
 
74
74
  ## Sample rates
75
75
 
76
- The device runs a fixed ladder of rates: 61.44 MHz halved down to
77
- 120 kHz. Ask for anything else and it quietly uses the nearest rung,
78
- leaving your program computing against a rate that is not in use.
76
+ The device runs a ladder of rates rather than a continuous range: each
77
+ rung is half the one above it. Ask for anything else and it quietly
78
+ uses the nearest rung, leaving your program computing against a rate
79
+ that is not in use.
79
80
 
80
81
  ```python
81
82
  aaronia.sample_rates() # every rate, highest first
82
83
  aaronia.sample_rate_for_bandwidth(8e6) # 15.36e6: the lowest rate covering 8 MHz
83
84
  ```
84
85
 
85
- A rate carries only 80% of itself as alias-free bandwidth, which is why
86
- 8 MHz of spectrum needs 15.36 MHz of sampling.
86
+ **Sample rate is not RF bandwidth.** You get every sample, so an FFT of
87
+ them spans the full rate but only the middle 80% is flat and
88
+ calibrated. That is not an approximation: RTSA reports exactly 0.8 x Fs
89
+ as the packet's frequency range at every rate. Outside it, data still
90
+ arrives, attenuated and uncalibrated.
91
+
92
+ So **to see N Hz of spectrum, sample at N / 0.8**, which is what
93
+ `sample_rate_for_bandwidth()` computes. Aaronia's data sheet quotes a
94
+ more conservative figure still — 44 MHz for the ECO against the
95
+ 49.152 MHz it declares at full span — because the analog filter is
96
+ already about 1 dB down at that edge. The
97
+ [quickstart](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/QUICKSTART.md#4-troubleshooting)
98
+ has the measurements.
99
+
100
+ `sample_rates()` returns the ladder for a SPECTRAN V6 ECO — 61.44 MHz
101
+ down to 120 kHz — which is measured, rung by rung. A full V6 has a
102
+ selectable receiver clock and can go higher, and exactly how much
103
+ higher is not settled; see
104
+ [the note in HTTPSPEC](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/HTTPSPEC.md#unresolved-the-full-v6s-top-rate).
105
+ On that hardware, take the rate the device reports over the computed
106
+ ladder: it arrives in the stream metadata, and `diagnose()` prints it.
107
+
108
+ ## Choosing a wire format
109
+
110
+ `format` decides what crosses the network, and it matters more than it
111
+ looks. Measured against a live server at 15.36 MS/s over a LAN:
112
+
113
+ | format | bytes/sample | delivered | drops |
114
+ | --- | --- | --- | --- |
115
+ | `F32` (default) | 8 | 6.5 MS/s | 290 |
116
+ | `F16` | 4 | 15.1 MS/s | 9 |
117
+ | `I16` | 4 | 15.1 MS/s | 12 |
118
+
119
+ `F32` needs 123 MB/s at that rate and the link could not carry it, so
120
+ most of the capture was dropped. Either half-width format fits.
121
+
122
+ `I16` has one trap: the server sends `round(value * scale)`, so the
123
+ quantisation step is `1 / scale`, and the default of 16384 gives a step
124
+ of 6.1e-5. A quiet band's noise floor is smaller than that — on the
125
+ same server, **68% of `I16` samples came back exactly zero** while
126
+ `F32` had none. Pass `scale=`, or lower `reference_level` for more
127
+ gain:
128
+
129
+ ```python
130
+ aaronia.open(url, freq=2.44e9, rate=15.36e6, format="I16", scale=1e6)
131
+ ```
132
+
133
+ At `scale=1e6` the zero fraction measured 0.0% and the amplitude
134
+ matched `F32`. `F16` needs no such tuning, which makes it the simpler
135
+ choice when the link is the constraint.
87
136
 
88
137
  ## Configuration (`AaroniaConfig`)
89
138
 
@@ -98,6 +147,7 @@ Every field is readable and writable.
98
147
  | `sample_rate` | IQ sample rate, Hz (the Aaronia "span") |
99
148
  | `reference_level` | Reference level, dBm |
100
149
  | `format` | HTTP wire format: `"F32"`, `"F16"` or `"I16"`. `I16` is the low-bandwidth network mode |
150
+ | `scale` | Integer encode multiplier for `I16` (see below). None uses the server default |
101
151
  | `receiver_channel` | `"Rx1"` (default), `"Rx2"`, or `"Rx1And2"` (native SDK, full V6) |
102
152
  | `read_timeout` | Seconds a blocking read waits before `AaroniaTimeoutError` (default `30.0`) |
103
153
  | `auto_reconnect` | Reconnect the HTTP stream after a drop (default `True`) |
@@ -154,7 +204,7 @@ silently defaulting.
154
204
 
155
205
  | Function | Purpose |
156
206
  | --- | --- |
157
- | `open(url=None, *, freq, rate, bandwidth, ref_level, file, format, read_timeout)` | Configure, connect and start streaming in one call |
158
- | `sample_rates()` | Every sample rate the hardware can run |
207
+ | `open(url=None, *, freq, rate, bandwidth, ref_level, file, format, scale, read_timeout)` | Configure, connect and start streaming in one call |
208
+ | `sample_rates()` | The V6 ECO's sample rates, highest first (see [Sample rates](#sample-rates)) |
159
209
  | `sample_rate_for_bandwidth(hz)` | Lowest rate covering that much spectrum |
160
210
  | `diagnose(url)` | `(ok, message, fix)` for each setup check; what `aaronia-doctor` prints |
@@ -65,6 +65,16 @@ class AaroniaConfig:
65
65
  """IQ sample rate in Hz (the Aaronia "span")."""
66
66
  reference_level: float
67
67
  """Reference level in dBm."""
68
+ scale: Optional[float]
69
+ """Integer encode multiplier for the ``I16`` wire format
70
+ (``/stream?scale=N``), or None for the server default.
71
+
72
+ The server encodes ``round(value * scale)``, so the quantisation
73
+ step is ``1 / scale``. At the default the step is 1/16384, which is
74
+ coarser than a quiet band's noise floor — measured on a live
75
+ server, 70% of samples came back exactly zero. Raise it, or raise
76
+ the gain by lowering ``reference_level``. Must be positive and
77
+ finite."""
68
78
 
69
79
  # Transport behaviour. `format` and `receiver_channel` read back as
70
80
  # None when unset but only accept a string: assigning None raises
@@ -185,6 +195,7 @@ def open(
185
195
  ref_level: Optional[float] = None,
186
196
  file: Optional[str] = None,
187
197
  format: Optional[WireFormat] = None,
198
+ scale: Optional[float] = None,
188
199
  read_timeout: Optional[float] = None,
189
200
  ) -> AaroniaSource:
190
201
  """Open a source and start streaming, in one call.