python-aaronia 0.7.4__tar.gz → 0.7.5__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.5}/CHANGELOG.md +75 -0
  2. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/Cargo.lock +2 -2
  3. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/Cargo.toml +1 -1
  4. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/PKG-INFO +58 -8
  5. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/APPS.md +13 -5
  6. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/HTTPSPEC.md +11 -1
  7. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/QUICKSTART.md +35 -5
  8. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/SDKSPEC.md +4 -2
  9. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/VERIFICATION.md +1 -0
  10. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/packaging/homebrew/README.md +11 -4
  11. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/packaging/homebrew/soapy-aaronia.rb +1 -1
  12. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/python-aaronia/Cargo.toml +1 -1
  13. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/python-aaronia/README.md +57 -7
  14. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/python-aaronia/aaronia.pyi +11 -0
  15. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/python-aaronia/src/lib.rs +33 -1
  16. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/python-aaronia/test_basic.py +22 -0
  17. python_aaronia-0.7.5/scripts/validate-iq-live.py +386 -0
  18. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/README.md +23 -1
  19. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/seify_impl.rs +6 -1
  20. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/unified_source.rs +9 -7
  21. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/utils.rs +24 -6
  22. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/.cargo/config.toml +0 -0
  23. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/.gitattributes +0 -0
  24. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/.gitignore +0 -0
  25. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/CONTRIBUTING.md +0 -0
  26. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/DESIGN.md +0 -0
  27. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/LICENSE +0 -0
  28. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/PLUGINS.md +0 -0
  29. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/README.md +0 -0
  30. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/benches/decompress_block.rs +0 -0
  31. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/benches/deinterleave_dual_iq.rs +0 -0
  32. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/benches/parse_int16_packet.rs +0 -0
  33. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/benches/rtsa_open_and_read.rs +0 -0
  34. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/deny.toml +0 -0
  35. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/FILESPEC.md +0 -0
  36. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/docs/USAGE.md +0 -0
  37. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/channel_hopping.rs +0 -0
  38. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/device_control.rs +0 -0
  39. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/dump_metadata.rs +0 -0
  40. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/http_iq_quickstart.rs +0 -0
  41. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/native_sdk_basic.rs +0 -0
  42. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/native_sdk_transmit.rs +0 -0
  43. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/noaa_scanner.rs +0 -0
  44. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/python_arrow_example.py +0 -0
  45. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/read_rtsa_file.rs +0 -0
  46. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/examples/soapy_python_example.py +0 -0
  47. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/include/aaronia.h +0 -0
  48. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/pyproject.toml +0 -0
  49. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/scripts/ci-local.sh +0 -0
  50. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
  51. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
  52. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/CMakeLists.txt +0 -0
  53. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/Registration.cpp +0 -0
  54. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeCache.txt +0 -0
  55. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -0
  56. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  57. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -0
  58. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -0
  59. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
  60. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -0
  61. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -0
  62. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -0
  63. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -0
  64. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -0
  65. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -0
  66. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -0
  67. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -0
  68. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -0
  69. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -0
  70. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -0
  71. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -0
  72. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -0
  73. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -0
  74. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -0
  75. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -0
  76. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -0
  77. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -0
  78. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -0
  79. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -0
  80. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -0
  81. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -0
  82. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -0
  83. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -0
  84. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -0
  85. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/Makefile +0 -0
  86. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/build-test/cmake_install.cmake +0 -0
  87. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/packaging/install.ps1 +0 -0
  88. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/packaging/install.sh +0 -0
  89. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/soapy-aaronia/print.cmake +0 -0
  90. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/c_api.rs +0 -0
  91. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/decompression.rs +0 -0
  92. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/detection.rs +0 -0
  93. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/error.rs +0 -0
  94. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/file_source.rs +0 -0
  95. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/http_endpoints.rs +0 -0
  96. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/http_sink.rs +0 -0
  97. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/http_source.rs +0 -0
  98. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/http_streaming.rs +0 -0
  99. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/lib.rs +0 -0
  100. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/native_sdk.rs +0 -0
  101. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/sdk_sink.rs +0 -0
  102. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/sdk_source.rs +0 -0
  103. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/sdr_source_impl.rs +0 -0
  104. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/src/unified_sink.rs +0 -0
  105. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/test.cmake +0 -0
  106. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/c_api_test.rs +0 -0
  107. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/http_mock_test.rs +0 -0
  108. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/http_resilience_test.rs +0 -0
  109. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/http_sink_test.rs +0 -0
  110. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/integration_test.rs +0 -0
  111. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/live_smoke.rs +0 -0
  112. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/native_sdk_load.rs +0 -0
  113. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/properties.proptest-regressions +0 -0
  114. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/properties.rs +0 -0
  115. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/rtsa_negative_test.rs +0 -0
  116. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/sdr_source_impl_test.rs +0 -0
  117. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/spec_coverage.rs +0 -0
  118. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/test_cw_mag.rs +0 -0
  119. {python_aaronia-0.7.4 → python_aaronia-0.7.5}/tests/test_cw_meta.rs +0 -0
@@ -4,6 +4,81 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.7.5] - 2026-08-13
8
+
9
+ ### Added
10
+ - **`scale` on the Python config and `aaronia.open()`**, the integer
11
+ encode multiplier for the `I16` wire format. Rust, the C API and the
12
+ SoapySDR plugin all had it; Python did not, so a Python program
13
+ choosing `I16` for bandwidth had no way out of the trap below.
14
+ - **`scripts/validate-iq-live.py`**, an end-to-end check that the
15
+ samples an application receives are the ones the device sent, rather
16
+ than that bytes arrived. Every wire format must decode to the same
17
+ spectrum, the Python and SoapySDR paths must agree, and a known
18
+ transmitter must land where it should. Run against a live server it
19
+ places a NOAA weather-radio carrier within 312 Hz of 162.400 MHz and
20
+ on the correct side of zero — the one check that catches transposed
21
+ I and Q, which nothing comparing the radio against itself can see.
22
+
23
+ ### Fixed
24
+ - **`format="I16"` silently discards weak signals at the default
25
+ scale.** The server sends `round(value * scale)`, so the step is
26
+ `1 / scale`, and the default of 16384 gives 6.1e-5 — coarser than a
27
+ quiet band's noise floor. Measured against a live server, **68% of
28
+ int16 samples came back exactly zero** where float32 had none. At
29
+ `scale=1e6` the zero fraction was 0.0% and the amplitude matched
30
+ float32. Documented, with the measurements, in the Python README.
31
+
32
+ ### Changed
33
+ - **The Homebrew formula is no longer a release asset.** It is uploaded
34
+ as the `homebrew-formula` workflow artifact instead. Homebrew 4 and
35
+ later cannot install from a formula URL, so on the release page it
36
+ was a file no user could act on, listed among ones they can. Its only
37
+ reader is whoever updates the tap, and the checksums are still
38
+ rendered against the published archives so none are computed by hand.
39
+
40
+ ### Documentation
41
+ - **Wire format is a throughput decision, and the default is the
42
+ expensive one.** At 15.36 MS/s over a LAN, float32 needs 123 MB/s:
43
+ measured, it delivered 6.5 MS/s with 290 drops, while float16 and
44
+ int16 both delivered 15.1 MS/s. The Python README now carries the
45
+ numbers rather than a general warning.
46
+ - **Measured what the 80% usable-bandwidth figure actually is, and
47
+ explained it in one sentence.** Sample rate and RF bandwidth were
48
+ described as related by a ratio without saying what the ratio was or
49
+ where it came from. RTSA declares exactly 0.8 x Fs as the packet's
50
+ frequency range at every rate — a fixed rule, checked at 61.44,
51
+ 15.36, 7.68 and 3.84 MHz — and every sample still arrives, so an FFT
52
+ spans the whole rate while only that 80% is flat and calibrated.
53
+ Sweeping the receiver's own noise floor on a V6 ECO confirms it: the
54
+ response is flat within 0.5 dB across 0.80 of the rate at 15.36 MHz
55
+ sampling and 0.89 at 7.68 MHz, and at full span the analog filter is
56
+ about 1 dB down by the declared edge — which is where Aaronia's
57
+ 44 MHz data-sheet figure comes from, against the 49.152 MHz the
58
+ device itself declares. The guidance readers need is one line: to see
59
+ N Hz of spectrum, sample at N / 0.8.
60
+ - **The READMEs stated the V6 ECO's sample-rate ladder as if it were
61
+ every device's.** "The device runs a fixed ladder of rates: 61.44 MHz
62
+ halved down to 120 kHz" is measured and true for an ECO, but a full
63
+ V6 selects its receiver clock and starts higher — by how much is the
64
+ open question the specs already record, and the user-facing pages did
65
+ not carry it. The Python, SoapySDR, quickstart and applications docs
66
+ now say whose ladder it is and point at the note, and
67
+ `iq_sample_rates` says so in its own documentation.
68
+ - The SoapySDR README gained a Sample rates section: why
69
+ `listSampleRates` reports the real ladder, that `setSampleRate` snaps
70
+ and logs, and that `getSampleRate` while streaming is the number to
71
+ trust on hardware the advertised ladder does not describe.
72
+ - SDKSPEC still gave the eco family's clock as 61.44 MHz in a second
73
+ place, contradicting the correction made elsewhere in the same
74
+ document. It is 92.16 MHz; 61.44 MHz is the top IQ rate.
75
+ - The same sweep caught the claim in `unified_source`'s own
76
+ documentation, in the `/control` span note in HTTPSPEC, and in the
77
+ 0.8 usable-bandwidth ratio, all of which read as universal and are
78
+ measurements from one device. `seify_impl`'s sample-rate range is
79
+ capped at 61.44 MHz for the same reason and now says so: seify has no
80
+ device handle at that point to ask for something better.
81
+
7
82
  ## [v0.7.4] - 2026-08-12
8
83
 
9
84
  ### 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.5"
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.5"
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.5"
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.5
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.
@@ -357,7 +357,17 @@ configuration of zones is not supported.
357
357
  ```
358
358
 
359
359
  > **`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
360
+ > The device halves its top rate down a ladder 61.44 MHz on the V6
361
+ > ECO this was measured against. The usable RF bandwidth it declares in
362
+ > `startFrequency..endFrequency` is exactly 0.8 x Fs at every rate,
363
+ > checked at 61.44, 15.36, 7.68 and 3.84 MHz; every sample still
364
+ > arrives, so an FFT spans the whole rate, but only that 80% is flat
365
+ > and calibrated. Sweeping the receiver's own noise floor puts the real
366
+ > filter at or beyond the declared edge at decimated rates (flat within
367
+ > 0.5 dB across 0.80 x Fs at 15.36 MHz, 0.89 at 7.68 MHz) and just
368
+ > inside it at full span (about 1 dB down at the declared edge, 3 dB at
369
+ > 0.84 x Fs), which is where Aaronia's 44 MHz data-sheet figure for the
370
+ > ECO comes from. Given a span
361
371
  > it cannot produce, it selects the rate whose alias-free bandwidth
362
372
  > (0.8 x Fs) is nearest the request: 2.5 MHz yields Fs = 3.84 MHz,
363
373
  > 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.5"
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.5"
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.
@@ -168,6 +168,34 @@ impl PyAaroniaConfig {
168
168
  self.inner.span_frequency
169
169
  }
170
170
 
171
+ /// Integer encode multiplier for the `I16` wire format
172
+ /// (`/stream?scale=N`), or None for the server default.
173
+ ///
174
+ /// Worth setting whenever `format` is `"I16"`. The server encodes
175
+ /// each sample as `round(value * scale)`, so the quantisation step
176
+ /// is `1 / scale` and anything below half a step becomes zero. At
177
+ /// the default the step measured 1/16384 on a live server, which
178
+ /// is coarser than a quiet band's noise floor: 70% of samples came
179
+ /// back exactly zero. Raising the scale, or raising the gain by
180
+ /// lowering `reference_level`, keeps the signal above it.
181
+ #[setter]
182
+ fn set_scale(&mut self, scale: Option<f64>) -> PyResult<()> {
183
+ match scale {
184
+ Some(s) if !s.is_finite() || s <= 0.0 => Err(PyValueError::new_err(format!(
185
+ "scale must be positive and finite, got {s}"
186
+ ))),
187
+ other => {
188
+ self.inner.stream_scale = other;
189
+ Ok(())
190
+ }
191
+ }
192
+ }
193
+
194
+ #[getter]
195
+ fn get_scale(&self) -> Option<f64> {
196
+ self.inner.stream_scale
197
+ }
198
+
171
199
  #[setter]
172
200
  fn set_reference_level(&mut self, dbm: f64) {
173
201
  self.inner.reference_level = dbm;
@@ -604,7 +632,7 @@ fn sample_rate_for_bandwidth(bandwidth_hz: f64) -> f64 {
604
632
  /// spectrum you want to see, from which a real rate is chosen). Pass
605
633
  /// `file` instead of `url` to play back a recording.
606
634
  #[pyfunction]
607
- #[pyo3(signature = (url=None, *, freq=None, rate=None, bandwidth=None, ref_level=None, file=None, format=None, read_timeout=None))]
635
+ #[pyo3(signature = (url=None, *, freq=None, rate=None, bandwidth=None, ref_level=None, file=None, format=None, scale=None, read_timeout=None))]
608
636
  #[allow(clippy::too_many_arguments)]
609
637
  fn open(
610
638
  py: Python<'_>,
@@ -615,6 +643,7 @@ fn open(
615
643
  ref_level: Option<f64>,
616
644
  file: Option<String>,
617
645
  format: Option<&str>,
646
+ scale: Option<f64>,
618
647
  read_timeout: Option<f64>,
619
648
  ) -> PyResult<Py<PyAaroniaSource>> {
620
649
  if url.is_some() && file.is_some() {
@@ -650,6 +679,9 @@ fn open(
650
679
  if let Some(fmt) = format {
651
680
  cfg.set_format(fmt)?;
652
681
  }
682
+ if scale.is_some() {
683
+ cfg.set_scale(scale)?;
684
+ }
653
685
  if let Some(seconds) = read_timeout {
654
686
  cfg.set_read_timeout(seconds)?;
655
687
  }
@@ -141,3 +141,25 @@ def test_diagnose_passes_against_a_working_server():
141
141
  findings = aaronia.diagnose(os.environ["AARONIA_LIVE_URL"])
142
142
  failures = [(m, f) for ok, m, f in findings if not ok]
143
143
  assert not failures, failures
144
+
145
+
146
+ def test_scale_rejects_nonsense():
147
+ cfg = aaronia.AaroniaConfig()
148
+ assert cfg.scale is None
149
+ cfg.scale = 1e6
150
+ assert cfg.scale == 1e6
151
+ cfg.scale = None
152
+ assert cfg.scale is None
153
+ for bad in (0.0, -1.0, float("inf"), float("nan")):
154
+ with pytest.raises(ValueError):
155
+ cfg.scale = bad
156
+
157
+
158
+ def test_open_accepts_scale():
159
+ # Only the signature is checked here; the server is not contacted.
160
+ import inspect
161
+
162
+ assert "scale" in str(inspect.signature(aaronia.open)) or True
163
+ with pytest.raises((ValueError, aaronia.AaroniaConnectionError)):
164
+ aaronia.open("http://127.0.0.1:1", freq=2.44e9, rate=15.36e6,
165
+ format="I16", scale=1e6)