python-aaronia 0.7.5__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.5 → python_aaronia-0.7.6}/CHANGELOG.md +85 -0
  2. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/Cargo.lock +2 -2
  3. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/Cargo.toml +1 -1
  4. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/PKG-INFO +1 -1
  5. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/HTTPSPEC.md +6 -1
  6. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/packaging/homebrew/soapy-aaronia.rb +1 -1
  7. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/python-aaronia/Cargo.toml +1 -1
  8. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/file_source.rs +64 -11
  9. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/http_endpoints.rs +398 -0
  10. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/http_source.rs +468 -102
  11. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/lib.rs +3 -2
  12. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/utils.rs +91 -0
  13. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/.cargo/config.toml +0 -0
  14. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/.gitattributes +0 -0
  15. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/.gitignore +0 -0
  16. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/CONTRIBUTING.md +0 -0
  17. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/DESIGN.md +0 -0
  18. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/LICENSE +0 -0
  19. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/PLUGINS.md +0 -0
  20. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/README.md +0 -0
  21. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/benches/decompress_block.rs +0 -0
  22. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/benches/deinterleave_dual_iq.rs +0 -0
  23. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/benches/parse_int16_packet.rs +0 -0
  24. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/benches/rtsa_open_and_read.rs +0 -0
  25. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/deny.toml +0 -0
  26. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/APPS.md +0 -0
  27. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/FILESPEC.md +0 -0
  28. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/QUICKSTART.md +0 -0
  29. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/SDKSPEC.md +0 -0
  30. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/USAGE.md +0 -0
  31. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/docs/VERIFICATION.md +0 -0
  32. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/channel_hopping.rs +0 -0
  33. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/device_control.rs +0 -0
  34. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/dump_metadata.rs +0 -0
  35. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/http_iq_quickstart.rs +0 -0
  36. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/native_sdk_basic.rs +0 -0
  37. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/native_sdk_transmit.rs +0 -0
  38. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/noaa_scanner.rs +0 -0
  39. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/python_arrow_example.py +0 -0
  40. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/read_rtsa_file.rs +0 -0
  41. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/examples/soapy_python_example.py +0 -0
  42. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/include/aaronia.h +0 -0
  43. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/packaging/homebrew/README.md +0 -0
  44. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/pyproject.toml +0 -0
  45. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/python-aaronia/README.md +0 -0
  46. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/python-aaronia/aaronia.pyi +0 -0
  47. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/python-aaronia/src/lib.rs +0 -0
  48. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/python-aaronia/test_basic.py +0 -0
  49. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/scripts/ci-local.sh +0 -0
  50. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/scripts/validate-iq-live.py +0 -0
  51. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
  52. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
  53. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/CMakeLists.txt +0 -0
  54. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/README.md +0 -0
  55. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/Registration.cpp +0 -0
  56. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeCache.txt +0 -0
  57. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -0
  58. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  59. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -0
  60. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -0
  61. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
  62. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -0
  63. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -0
  64. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -0
  65. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -0
  66. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -0
  67. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -0
  68. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -0
  69. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -0
  70. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -0
  71. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -0
  72. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -0
  73. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -0
  74. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -0
  75. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -0
  76. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -0
  77. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -0
  78. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -0
  79. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -0
  80. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -0
  81. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -0
  82. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -0
  83. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -0
  84. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -0
  85. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -0
  86. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -0
  87. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/Makefile +0 -0
  88. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/build-test/cmake_install.cmake +0 -0
  89. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/packaging/install.ps1 +0 -0
  90. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/packaging/install.sh +0 -0
  91. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/soapy-aaronia/print.cmake +0 -0
  92. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/c_api.rs +0 -0
  93. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/decompression.rs +0 -0
  94. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/detection.rs +0 -0
  95. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/error.rs +0 -0
  96. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/http_sink.rs +0 -0
  97. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/http_streaming.rs +0 -0
  98. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/native_sdk.rs +0 -0
  99. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/sdk_sink.rs +0 -0
  100. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/sdk_source.rs +0 -0
  101. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/sdr_source_impl.rs +0 -0
  102. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/seify_impl.rs +0 -0
  103. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/unified_sink.rs +0 -0
  104. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/src/unified_source.rs +0 -0
  105. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/test.cmake +0 -0
  106. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/c_api_test.rs +0 -0
  107. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/http_mock_test.rs +0 -0
  108. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/http_resilience_test.rs +0 -0
  109. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/http_sink_test.rs +0 -0
  110. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/integration_test.rs +0 -0
  111. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/live_smoke.rs +0 -0
  112. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/native_sdk_load.rs +0 -0
  113. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/properties.proptest-regressions +0 -0
  114. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/properties.rs +0 -0
  115. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/rtsa_negative_test.rs +0 -0
  116. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/sdr_source_impl_test.rs +0 -0
  117. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/spec_coverage.rs +0 -0
  118. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/test_cw_mag.rs +0 -0
  119. {python_aaronia-0.7.5 → python_aaronia-0.7.6}/tests/test_cw_meta.rs +0 -0
@@ -4,6 +4,91 @@ 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
+
7
92
  ## [v0.7.5] - 2026-08-13
8
93
 
9
94
  ### Added
@@ -3027,7 +3027,7 @@ dependencies = [
3027
3027
 
3028
3028
  [[package]]
3029
3029
  name = "python-aaronia"
3030
- version = "0.7.5"
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.5"
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.5"
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.5
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+)
@@ -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:
@@ -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.5"
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.5"
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"
@@ -743,17 +743,42 @@ pub fn rtsa_epoch_seconds(raw: f64) -> f64 {
743
743
  /// method call plus a `BufReader` bounds check, which dominates on large
744
744
  /// sequential reads.
745
745
  fn read_iq_f32<R: Read>(reader: &mut R, count: usize) -> Result<Vec<Complex32>> {
746
- let mut bytes = vec![0u8; count * 8];
747
- reader.read_exact(&mut bytes)?;
748
- Ok(bytes
749
- .chunks_exact(8)
750
- .map(|c| {
751
- Complex32::new(
752
- f32::from_le_bytes([c[0], c[1], c[2], c[3]]),
753
- f32::from_le_bytes([c[4], c[5], c[6], c[7]]),
754
- )
755
- })
756
- .collect())
746
+ // The wire format is little-endian f32 pairs, and `Complex32` is
747
+ // `#[repr(C)] { re: f32, im: f32 }`. On a little-endian host those bytes
748
+ // are already the in-memory representation, so `from_le_bytes` is a no-op
749
+ // and reading straight into the sample buffer removes both the separate
750
+ // byte allocation and the whole per-sample decode pass. The native-SDK
751
+ // path reinterprets its buffer the same way; this is the file mirror of
752
+ // it. On a big-endian host the bytes must be swapped, so that path keeps
753
+ // the explicit decode.
754
+ const _: () = assert!(std::mem::size_of::<Complex32>() == 2 * std::mem::size_of::<f32>());
755
+
756
+ #[cfg(target_endian = "little")]
757
+ {
758
+ let mut samples = vec![Complex32::new(0.0, 0.0); count];
759
+ // SAFETY: `samples` owns `count * 8` contiguous bytes (the repr and
760
+ // size are asserted above), and `read_exact` writes exactly that many
761
+ // before it returns; any bit pattern is a valid pair of `f32`.
762
+ let byte_view =
763
+ unsafe { std::slice::from_raw_parts_mut(samples.as_mut_ptr() as *mut u8, count * 8) };
764
+ reader.read_exact(byte_view)?;
765
+ Ok(samples)
766
+ }
767
+
768
+ #[cfg(not(target_endian = "little"))]
769
+ {
770
+ let mut bytes = vec![0u8; count * 8];
771
+ reader.read_exact(&mut bytes)?;
772
+ Ok(bytes
773
+ .chunks_exact(8)
774
+ .map(|c| {
775
+ Complex32::new(
776
+ f32::from_le_bytes([c[0], c[1], c[2], c[3]]),
777
+ f32::from_le_bytes([c[4], c[5], c[6], c[7]]),
778
+ )
779
+ })
780
+ .collect())
781
+ }
757
782
  }
758
783
 
759
784
  /// Bulk-read `count` little-endian i16 IQ pairs, each component scaled by
@@ -2901,6 +2926,34 @@ impl DsftChunk {
2901
2926
 
2902
2927
  #[cfg(test)]
2903
2928
  mod tests {
2929
+ /// The zero-copy little-endian `read_iq_f32` must decode identically to
2930
+ /// the explicit `from_le_bytes` version. A raw reinterpret is exactly
2931
+ /// where an endianness or layout mistake hides while still producing
2932
+ /// plausible-looking floats.
2933
+ #[test]
2934
+ fn read_iq_f32_round_trips() {
2935
+ let pairs = [
2936
+ (0.0f32, 0.0f32),
2937
+ (1.0, -1.0),
2938
+ (1.5e-3, -2.7e4),
2939
+ (f32::MIN_POSITIVE, 12345.678),
2940
+ ];
2941
+ let mut bytes = Vec::new();
2942
+ for (re, im) in pairs {
2943
+ bytes.extend_from_slice(&re.to_le_bytes());
2944
+ bytes.extend_from_slice(&im.to_le_bytes());
2945
+ }
2946
+
2947
+ let mut cursor = std::io::Cursor::new(bytes);
2948
+ let got = read_iq_f32(&mut cursor, pairs.len()).expect("decode");
2949
+
2950
+ assert_eq!(got.len(), pairs.len());
2951
+ for (g, (re, im)) in got.iter().zip(pairs) {
2952
+ assert_eq!(g.re, re, "re mismatch");
2953
+ assert_eq!(g.im, im, "im mismatch");
2954
+ }
2955
+ }
2956
+
2904
2957
  use super::*;
2905
2958
  use std::io::Cursor;
2906
2959