python-aaronia 0.11.2__tar.gz → 0.13.0__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 (99) hide show
  1. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/CHANGELOG.md +216 -0
  2. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/CONTRIBUTING.md +1 -1
  3. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/Cargo.lock +12 -57
  4. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/Cargo.toml +7 -9
  5. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/DESIGN.md +3 -5
  6. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/PKG-INFO +33 -30
  7. python_aaronia-0.13.0/README.md +442 -0
  8. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/HTTPSPEC.md +13 -1
  9. python_aaronia-0.13.0/docs/NATIVE_SDK_COMPATIBILITY.md +99 -0
  10. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/SDKSPEC.md +29 -43
  11. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/USAGE.md +0 -1
  12. python_aaronia-0.13.0/examples/native_sdk_qualification.rs +94 -0
  13. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/include/spectran.h +13 -5
  14. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/python-aaronia/Cargo.toml +1 -1
  15. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/python-aaronia/README.md +32 -29
  16. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/python-aaronia/aaronia.pyi +16 -0
  17. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/python-aaronia/src/lib.rs +86 -0
  18. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/python-aaronia/test_basic.py +26 -0
  19. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/scripts/ci-local.sh +1 -0
  20. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/README.md +27 -23
  21. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/Registration.cpp +15 -1
  22. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/SpectranSoapyDevice.cpp +32 -8
  23. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/c_api.rs +204 -4
  24. python_aaronia-0.13.0/src/capabilities.rs +206 -0
  25. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/error.rs +12 -0
  26. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/file_source.rs +277 -15
  27. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/http_endpoints.rs +12 -97
  28. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/http_source.rs +1893 -219
  29. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/http_streaming.rs +917 -81
  30. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/lib.rs +7 -14
  31. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/link_budget.rs +50 -0
  32. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/native_sdk.rs +468 -111
  33. python_aaronia-0.13.0/src/native_sdk_lifecycle_tests.rs +374 -0
  34. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/sdk_source.rs +5 -12
  35. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/seify_impl.rs +32 -12
  36. python_aaronia-0.13.0/src/stream_break.rs +206 -0
  37. python_aaronia-0.13.0/src/stream_geometry.rs +125 -0
  38. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/unified_source.rs +297 -31
  39. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/spec_coverage.rs +73 -14
  40. python_aaronia-0.11.2/README.md +0 -255
  41. python_aaronia-0.11.2/examples/channel_hopping.rs +0 -82
  42. python_aaronia-0.11.2/src/sdr_source_impl.rs +0 -674
  43. python_aaronia-0.11.2/tests/sdr_source_impl_test.rs +0 -426
  44. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/.cargo/config.toml +0 -0
  45. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/.gitattributes +0 -0
  46. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/.gitignore +0 -0
  47. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/LICENSE +0 -0
  48. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/PLUGINS.md +0 -0
  49. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/benches/decompress_block.rs +0 -0
  50. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/benches/deinterleave_dual_iq.rs +0 -0
  51. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/benches/parse_int16_packet.rs +0 -0
  52. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/benches/rtsa_open_and_read.rs +0 -0
  53. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/deny.toml +0 -0
  54. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/APPS.md +0 -0
  55. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/FILESPEC.md +0 -0
  56. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/QUICKSTART.md +0 -0
  57. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/SYNC.md +0 -0
  58. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/docs/VERIFICATION.md +0 -0
  59. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/device_control.rs +0 -0
  60. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/dump_metadata.rs +0 -0
  61. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/http_iq_quickstart.rs +0 -0
  62. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/native_sdk_basic.rs +0 -0
  63. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/native_sdk_transmit.rs +0 -0
  64. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/noaa_scanner.rs +0 -0
  65. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/python_arrow_example.py +0 -0
  66. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/read_rtsa_file.rs +0 -0
  67. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/examples/soapy_python_example.py +0 -0
  68. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/packaging/homebrew/README.md +0 -0
  69. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  70. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/pyproject.toml +0 -0
  71. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/scripts/fake-rtsa-server.py +0 -0
  72. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/scripts/native-sdk-validate.ps1 +0 -0
  73. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/scripts/sdk-container-test.sh +0 -0
  74. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/scripts/validate-iq-live.py +0 -0
  75. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/CMakeLists.txt +0 -0
  76. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/SpectranSoapyDevice.hpp +0 -0
  77. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  78. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/soapy-aaronia/packaging/install.sh +0 -0
  79. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/decompression.rs +0 -0
  80. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/detection.rs +0 -0
  81. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/http_sink.rs +0 -0
  82. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/sdk_sink.rs +0 -0
  83. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/unified_sink.rs +0 -0
  84. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/src/utils.rs +0 -0
  85. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/abi_drift.rs +0 -0
  86. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/c_api_test.rs +0 -0
  87. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/http_mock_test.rs +0 -0
  88. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/http_resilience_test.rs +0 -0
  89. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/http_sink_test.rs +0 -0
  90. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/integration_test.rs +0 -0
  91. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/live_smoke.rs +0 -0
  92. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/native_sdk_live.rs +0 -0
  93. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/native_sdk_load.rs +0 -0
  94. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/properties.proptest-regressions +0 -0
  95. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/properties.rs +0 -0
  96. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/rtsa_negative_test.rs +0 -0
  97. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/test_cw_mag.rs +0 -0
  98. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/test_cw_meta.rs +0 -0
  99. {python_aaronia-0.11.2 → python_aaronia-0.13.0}/tests/wire_contract.rs +0 -0
@@ -4,6 +4,222 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.13.0] - 2026-10-08
8
+
9
+ ### Added
10
+ - Native IQ packet RF span and centre metadata, independent of the actual IQ
11
+ sample clock. Mono and dual reads preserve that geometry with carried samples.
12
+ - Direct SDK capability queries: configuration bounds, clock/channel/decimation
13
+ options with their original enum indices and disabled flags, selected mode,
14
+ and SDK version. Missing metadata stays unknown; no RTSA app is required.
15
+ - Receive-only `NativeReceiverOptions`: explicit family/mode, IQ receiver RF
16
+ span, or enabled raw clock/decimation selection. SDK configuration bounds are
17
+ checked before writes. They are not a hardware qualification certificate.
18
+ - Versioned capability JSON and RF-span controls in C and Python; device-derived
19
+ ranges and observed RF bandwidth in seify and SoapySDR. Windows/Linux Python
20
+ release wheels now enable `native-sdk` (the proprietary SDK remains separate).
21
+ - An opt-in, serial-selected `native_sdk_qualification` example for receive-only
22
+ span changes and repeated device stop/reopen checks.
23
+
24
+ ### Fixed
25
+ - Failed SDK opens close any partially allocated device. Connect/start failures
26
+ attempt stop, disconnect and close while preserving the original error.
27
+ - Stop attempts all cleanup steps, returns the first failure, clears carried IQ
28
+ and observed metadata, and retains a failed-close handle for a Drop retry.
29
+ Repeated start/stop calls are idempotent; unified restart reopens the same
30
+ selected serial and settings, without silently switching devices or backend.
31
+ - SDK global initialization/shutdown reference counting is serialized across the
32
+ initialized-state transition.
33
+ - IQ receiver span validation uses the opened mode's `main/spanfreq` bounds,
34
+ rather than the legacy V6 raw-clock constraint. Runtime native setters use the
35
+ same configuration path as construction.
36
+ - Reads stop before a queued packet changes centre, IQ rate or RF span, and a
37
+ carry-only read does not wait for another SDK packet.
38
+ - Native capabilities and sensors no longer come from an unrelated HTTP server.
39
+ - Unknown SDK modes cannot reinterpret spectra as IQ or IQ as spectra.
40
+ - SoapySDR tolerates an unknown discrete sample-rate ladder and reports native
41
+ bandwidth from packet metadata. An RX open no longer attempts TX setup.
42
+ - Release publication is gated on full CI, with semver checked against the
43
+ previous reachable release instead of a fixed historical tag.
44
+
45
+ ### Migration and qualification
46
+ - This 0.x minor release adds public config fields and SDK mode enum variants.
47
+ Build `SpectranConfig` with `..Default::default()` and review exhaustive mode
48
+ matches. Existing C struct layouts and functions are retained.
49
+ - Explicit `spectranv6eco/raw` is preserved; automatic ECO selection remains
50
+ `iqreceiver`. Explicit raw clock/decimation controls replace the legacy rate
51
+ request; the actual delivered rate comes from IQ packets.
52
+ - Soapy TX setup requires explicit `tx=true`, a native backend and compatible
53
+ licensed hardware. TX and dual-RX remain hardware-unverified here.
54
+ - Windows ECO mono qualification passed five stop/reopen cycles at RF spans
55
+ 44, 20, 10, 5 and 44 MHz. The 44 MHz span carried about 59.214 MS/s; the lower
56
+ spans carried 30, 15 and 7.5 MS/s. One initial discontinuity was reported, with
57
+ no additional discontinuities during this short trial. Signals was restored.
58
+ This is not an endurance, amplitude-calibration or all-model certification.
59
+ See [native SDK compatibility](docs/NATIVE_SDK_COMPATIBILITY.md).
60
+
61
+ ## [v0.12.0] - 2026-09-29
62
+
63
+ ### Removed
64
+ - **The `sdr-source` feature, and with it the dependency on `orecchiette-sdr-source-rs`.**
65
+ `SpectranSdrSource` and `SpectranBackend` (the `SdrSource`-trait facade over the async
66
+ source), the `sdr_source` re-export module and the `channel_hopping` example were one
67
+ consumer's adapter, and they live beside that consumer now (Specola's `sdr-aaronia-source`
68
+ crate), so this crate stands on its own: `default` is `http`, `file` and `ffi`, and
69
+ `crossbeam-channel` goes with the adapter. A caller that used the facade builds the same
70
+ thing from `SpectranSourceBuilder` and its own trait; the code is 670 lines and GPL, take it.
71
+
72
+ ### Added
73
+ - **`HttpSource` reports where the stream broke**, not just how often.
74
+ `HttpSourceBuilder::with_stream_breaks` takes a `StreamBreakSink` (see the
75
+ new `stream_break` module) and the source calls it with the **absolute index
76
+ of the first sample after each discontinuity**, in its own output stream:
77
+ a server gap the drop detector saw, a capacity trim, a reconnect, and a
78
+ centre or rate change the device declared. Optional; a caller that does not
79
+ ask pays nothing.
80
+
81
+ The counters that were there before cannot be acted on. A consumer carrying
82
+ state across deliveries — symbol timing, carrier tracking, burst framing, a
83
+ recording's contiguity — needs to know *which* samples belong to the old
84
+ epoch, and "the server skipped 3 times" does not say. Breaks are held against
85
+ the samples still queued and emitted only as those samples are produced, so a
86
+ later trim cannot move an index already reported; samples the source discards
87
+ never occupy an index at all.
88
+
89
+ - `StreamParser::process_data_recovering` and `ParsedItem`: a malformed
90
+ packet becomes a `ParsedItem::Lost` in stream order instead of an `Err` that
91
+ discards the rest of the chunk. `process_data` keeps its contract.
92
+ - `StreamStats::non_iq_packets_skipped` and
93
+ `StreamStats::packets_lost_to_parse_errors`.
94
+
95
+ ### Fixed
96
+ - **A catch-up after a stall no longer trims what it just fetched.** One
97
+ `fetch_samples` sweep drained every chunk the reader task had queued — up to
98
+ 64, about a million int16 samples — into a buffer that trimmed a few packets
99
+ deep, so any stall of the consumer longer than a few milliseconds discarded
100
+ most of what arrived during it. Measured live from bigear against a
101
+ Spectran V6 at 15.36 MS/s, where the trim sat at 147 456 samples (~9.6 ms):
102
+ with one flowgraph executor every 45 s run lost samples, 2 to 10 times; with
103
+ two, two runs in five. The trim now sits one sweep above the refill target
104
+ (`catch_up_headroom`: `CHUNK_CHANNEL_DEPTH` chunks of the largest size seen,
105
+ in samples, plus a carried packet), and a sweep takes at most
106
+ `CHUNK_CHANNEL_DEPTH` chunks, so a catch-up always fits. A consumer that falls
107
+ behind now backs up the chunk channel and the socket instead of being
108
+ trimmed, and any loss moves upstream, where the packet timestamps report it
109
+ as a server gap. Re-measured live, same span: one executor 1 gap in 5 runs,
110
+ two executors 0 in 3. The overflow warning no longer names "the first
111
+ moments of a stream" as the expected cause.
112
+
113
+ What changes with it, so nothing reads as a regression later:
114
+ `StreamStats::buffer_capacity` still reports the enforced capacity, which now
115
+ includes the headroom and jumps once the first chunk sizes it (about eight
116
+ times the old figure at 15.36 MS/s int16), so a fill ratio reads lower. A
117
+ consumer that stalls now reads its backlog rather than a gap: up to the buffer
118
+ plus the channel, about 150 ms at 15.36 MS/s, which a pipeline faster than
119
+ real time then catches up — including a retune that arrived behind it. The
120
+ buffer can grow to about 10 MB (int16) or 20 MB (float32) during a stall, on
121
+ top of the channel's 4 MB. And because a slow consumer now throttles the
122
+ reads the link-budget meter counts, a window in which a sweep found the chunk
123
+ channel full is marked (`ThroughputMeter::note_consumer_limited`) and yields
124
+ no verdict, rather than one blaming the path for the consumer.
125
+ - **A lost packet at a high sample rate is now reported.** `DropDetector`
126
+ flagged a gap only past a fixed 1 ms, and at 61.44 MS/s a 16 384-sample
127
+ packet lasts 267 µs, so a whole lost packet was spliced over silently. The
128
+ tolerance is now relative to the stream: half the packet's own duration, or
129
+ four times the residual observed between contiguous packets (at most three
130
+ quarters of a packet), floored at the timestamps' microsecond resolution
131
+ and still capped at 1 ms, so low rates behave as before.
132
+ `DropDetector::observed_jitter_seconds` exposes the calibration. Not yet
133
+ verified live against a real RTSA stream at a high rate.
134
+ - **A compressed spectra or histogram packet no longer swallows the IQ behind
135
+ it.** It was framed at its *uncompressed* size, but a compressed payload is
136
+ a bitstream whose length the header does not carry. It is now reported as
137
+ `ParsedItem::Skipped` where it sat and the parser resynchronises on the next
138
+ header.
139
+ - **A skipped non-IQ packet is no longer reported as a gap in the IQ.** Any
140
+ packet whose header was read but whose non-IQ payload could not be decoded
141
+ was a `ParsedItem::Lost`, which `HttpSource` recorded as a `Gap` and counted
142
+ in `packets_lost_to_parse_errors` — a hole claimed in an IQ stream that had
143
+ none. The new `ParsedItem::Skipped { payload, reason }` carries the declared
144
+ payload type; `HttpSource` counts it in `non_iq_packets_skipped` and records
145
+ no break. An undecodable IQ packet, or one whose extent could not be trusted,
146
+ is still `Lost`. (Breaking for exhaustive matches on `ParsedItem`.)
147
+ - **A timestamp that steps backwards is a break, not continuity.**
148
+ `DropDetector::observe` returned `Continuous` for a packet starting before
149
+ the previous one ended, however far, and fed the step into the jitter
150
+ estimate, holding the tolerance at its ceiling afterwards. A backward step
151
+ past the tolerance is now `DropResult::Backward { overlap_seconds }`, counted
152
+ in `DropDetector::backward_steps` and kept out of the jitter estimate and
153
+ the drop totals; `HttpSource` records a `Gap` there and `UnifiedSource`
154
+ flags the buffer. (Breaking for exhaustive matches on `DropResult`.)
155
+ - **`DropDetector::resync` keeps the jitter calibration.** It was cleared on
156
+ every lost packet and reconnect, and since the estimate grows only from
157
+ residuals already judged contiguous, a server whose residuals sit above
158
+ half a packet raised false drops until it was relearned. `reset` still
159
+ clears it.
160
+ - **Compressed spectra decode on a frames × bins grid.** The wavelet grid was
161
+ sized from the square root of one frame's bins, ignoring the frame count
162
+ and dropping bins. Shapes whose layout is undocumented — more than one
163
+ 16-spectrum block, depth planes, histograms — are refused instead of
164
+ decoded into a wrong shape.
165
+ - **`HttpSource` no longer outputs spectra, histogram or category scalars as
166
+ IQ.** They were queued with the IQ, fed the drop detector (whose timeline
167
+ they are not on) and overwrote `current_frequency` (a marker declares 0 Hz).
168
+ They are now counted and skipped.
169
+ - **One undecodable packet no longer costs the connection.** `HttpSource`
170
+ skips it, keeps the packets either side, and records a gap where it sat;
171
+ only an unrecoverable framing error still reconnects.
172
+ - **The README's stream-break example is doctested only where it compiles.**
173
+ It uses `HttpSourceBuilder`, which needs the `futuresdr` feature, so
174
+ `cargo test` with default features failed on it; the body is now gated on
175
+ that feature.
176
+ - **A stalled `/stream` reconnects.** The reader waited forever on a socket
177
+ that stayed open and sent nothing; it now gives up after 5 s and the source
178
+ takes its ordinary reconnect path.
179
+ - **A retune swallowed by a restart is announced after it.** The restart
180
+ discarded the queued `Retune` but kept the baseline it had advanced, so the
181
+ new band was never reported. The baseline now rewinds to the last geometry
182
+ reported.
183
+ - **A trim before the first sample publishes one `Initial`**, carrying the
184
+ geometry the first sample arrived under, instead of `Initial` for a
185
+ discarded band followed by `Retune` at the same index.
186
+ - **The first-start tune runs even when the connect/run write fails**, takes
187
+ its target from the builder rather than from packet-tracked fields, and is
188
+ marked done once attempted. The connect/run failure is logged at warn.
189
+ - **`UnifiedSource` HTTP reads no longer split on a derived-rate wobble.** The
190
+ read boundary compared rates with `!=`; it uses the crate's 10% band.
191
+ - **RTSA files:** resync over a corrupt region scans in 64 KiB blocks instead
192
+ of one `seek` per byte (16 MiB of zeroes: 34 s → well under a second); an IQ
193
+ `SAMP` chunk declaring other than one I/Q pair per sample is refused rather
194
+ than read with a mismatched stride; `seek_to_sample` indexes chunks per
195
+ sub-stream, as reads do; and `validate_structure` no longer overflows on an
196
+ absurd sample count.
197
+ - **C ABI:** `spectran_source_builder_force_source_type` takes the source type
198
+ as `int` and ignores an unknown value. Taking the `#[repr(C)]` enum by value
199
+ made an out-of-range value from C undefined behaviour. Source-compatible for
200
+ C and C++ callers passing an enumerator.
201
+ - **Native SDK:** a packet consumed without being delivered (bad stride,
202
+ oversized, undemuxable) now counts in `cumulative_drops` and sets
203
+ `take_overrun`, like a packet the device itself flagged as dropped.
204
+
205
+ ### Changed
206
+ - **Gap detection and sample queuing share one pass over each batch.** They
207
+ were two loops, so every packet's gap was measured against the buffer length
208
+ *before* any of the batch had been appended — the right answer only for the
209
+ first packet in it. No change to what is reported, only to where.
210
+ - **Float16 IQ payloads convert in bulk.** The per-sample `f16::to_f32` loop is
211
+ replaced by `half`'s slice conversion, which selects SIMD at runtime on
212
+ AArch64 FP16 and x86 F16C and keeps a portable fallback. Aligned
213
+ little-endian payloads are borrowed in place; an odd-aligned payload — a JSON
214
+ header can leave one at any address — passes through a 1 KiB stack buffer
215
+ rather than a second packet-sized allocation. Big-endian targets keep the
216
+ scalar path. Measured on macOS ARM64 in release, against the scalar
217
+ implementation over three alternating A/B rounds: 36% less time for an
218
+ aligned payload, 10% for an odd-aligned one, at 49 152 and 65 535 IQ pairs.
219
+ Every half-float bit pattern — subnormals, signed zeros and NaN payloads
220
+ included — is compared against the previous implementation at both
221
+ alignments and with odd IQ counts.
222
+
7
223
  ## [v0.11.2] - 2026-09-11
8
224
 
9
225
  ### Added
@@ -78,7 +78,7 @@ git lfs pull
78
78
  cargo test --test integration_test
79
79
  ```
80
80
 
81
- Beyond the tiers named here, `tests/` also contains focused suites for the HTTP mock server (`http_mock_test.rs`), the HTTP sink (`http_sink_test.rs`, requires `futuresdr`), the C API (`c_api_test.rs`), the `sdr-source` implementation (`sdr_source_impl_test.rs`), RTSA negative cases (`rtsa_negative_test.rs`), CW fixtures (`test_cw_mag.rs`, `test_cw_meta.rs`), SDK library loading (`native_sdk_load.rs`), and opt-in live-hardware smoke tests (`live_smoke.rs`). All of these run as part of `cargo test` where their features and environment allow.
81
+ Beyond the tiers named here, `tests/` also contains focused suites for the HTTP mock server (`http_mock_test.rs`), the HTTP sink (`http_sink_test.rs`, requires `futuresdr`), the C API (`c_api_test.rs`), RTSA negative cases (`rtsa_negative_test.rs`), CW fixtures (`test_cw_mag.rs`, `test_cw_meta.rs`), SDK library loading (`native_sdk_load.rs`), and opt-in live-hardware smoke tests (`live_smoke.rs`). All of these run as part of `cargo test` where their features and environment allow.
82
82
 
83
83
  ### 3. Property Tests (`tests/properties.rs`)
84
84
 
@@ -1010,28 +1010,6 @@ dependencies = [
1010
1010
  "itertools",
1011
1011
  ]
1012
1012
 
1013
- [[package]]
1014
- name = "crossbeam"
1015
- version = "0.8.4"
1016
- source = "registry+https://github.com/rust-lang/crates.io-index"
1017
- checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
1018
- dependencies = [
1019
- "crossbeam-channel",
1020
- "crossbeam-deque",
1021
- "crossbeam-epoch",
1022
- "crossbeam-queue",
1023
- "crossbeam-utils",
1024
- ]
1025
-
1026
- [[package]]
1027
- name = "crossbeam-channel"
1028
- version = "0.5.16"
1029
- source = "registry+https://github.com/rust-lang/crates.io-index"
1030
- checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
1031
- dependencies = [
1032
- "crossbeam-utils",
1033
- ]
1034
-
1035
1013
  [[package]]
1036
1014
  name = "crossbeam-deque"
1037
1015
  version = "0.8.7"
@@ -1051,15 +1029,6 @@ dependencies = [
1051
1029
  "crossbeam-utils",
1052
1030
  ]
1053
1031
 
1054
- [[package]]
1055
- name = "crossbeam-queue"
1056
- version = "0.3.13"
1057
- source = "registry+https://github.com/rust-lang/crates.io-index"
1058
- checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
1059
- dependencies = [
1060
- "crossbeam-utils",
1061
- ]
1062
-
1063
1032
  [[package]]
1064
1033
  name = "crossbeam-utils"
1065
1034
  version = "0.8.22"
@@ -1319,7 +1288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1319
1288
  checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1320
1289
  dependencies = [
1321
1290
  "libc",
1322
- "windows-sys 0.52.0",
1291
+ "windows-sys 0.61.2",
1323
1292
  ]
1324
1293
 
1325
1294
  [[package]]
@@ -2096,7 +2065,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
2096
2065
  dependencies = [
2097
2066
  "hermit-abi",
2098
2067
  "libc",
2099
- "windows-sys 0.52.0",
2068
+ "windows-sys 0.61.2",
2100
2069
  ]
2101
2070
 
2102
2071
  [[package]]
@@ -2770,18 +2739,6 @@ dependencies = [
2770
2739
  "hashbrown 0.14.5",
2771
2740
  ]
2772
2741
 
2773
- [[package]]
2774
- name = "orecchiette-sdr-source-rs"
2775
- version = "0.1.3"
2776
- source = "registry+https://github.com/rust-lang/crates.io-index"
2777
- checksum = "9a940ff866bdff8ad17ba9b9881c73789c61824ab083d1db56b6b4944fce216b"
2778
- dependencies = [
2779
- "anyhow",
2780
- "crossbeam",
2781
- "num-complex",
2782
- "thiserror 2.0.20",
2783
- ]
2784
-
2785
2742
  [[package]]
2786
2743
  name = "parking"
2787
2744
  version = "2.2.1"
@@ -3092,7 +3049,7 @@ dependencies = [
3092
3049
 
3093
3050
  [[package]]
3094
3051
  name = "python-aaronia"
3095
- version = "0.11.2"
3052
+ version = "0.13.0"
3096
3053
  dependencies = [
3097
3054
  "arrow",
3098
3055
  "num-complex",
@@ -3163,7 +3120,7 @@ dependencies = [
3163
3120
  "once_cell",
3164
3121
  "socket2",
3165
3122
  "tracing",
3166
- "windows-sys 0.52.0",
3123
+ "windows-sys 0.61.2",
3167
3124
  ]
3168
3125
 
3169
3126
  [[package]]
@@ -3469,14 +3426,14 @@ dependencies = [
3469
3426
  "errno",
3470
3427
  "libc",
3471
3428
  "linux-raw-sys",
3472
- "windows-sys 0.52.0",
3429
+ "windows-sys 0.61.2",
3473
3430
  ]
3474
3431
 
3475
3432
  [[package]]
3476
3433
  name = "rustls"
3477
- version = "0.23.43"
3434
+ version = "0.23.45"
3478
3435
  source = "registry+https://github.com/rust-lang/crates.io-index"
3479
- checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
3436
+ checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
3480
3437
  dependencies = [
3481
3438
  "aws-lc-rs",
3482
3439
  "once_cell",
@@ -3526,7 +3483,7 @@ dependencies = [
3526
3483
  "security-framework",
3527
3484
  "security-framework-sys",
3528
3485
  "webpki-root-certs",
3529
- "windows-sys 0.52.0",
3486
+ "windows-sys 0.61.2",
3530
3487
  ]
3531
3488
 
3532
3489
  [[package]]
@@ -3621,21 +3578,19 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3621
3578
 
3622
3579
  [[package]]
3623
3580
  name = "sdr-aaronia-rs"
3624
- version = "0.11.2"
3581
+ version = "0.13.0"
3625
3582
  dependencies = [
3626
3583
  "anyhow",
3627
3584
  "bitflags 2.13.1",
3628
3585
  "byteorder",
3629
3586
  "bytes",
3630
3587
  "criterion",
3631
- "crossbeam-channel",
3632
3588
  "futures",
3633
3589
  "futures-timer",
3634
3590
  "futuresdr",
3635
3591
  "half",
3636
3592
  "libloading",
3637
3593
  "num-complex",
3638
- "orecchiette-sdr-source-rs",
3639
3594
  "proptest",
3640
3595
  "reqwest",
3641
3596
  "seify",
@@ -4203,10 +4158,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4203
4158
  checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
4204
4159
  dependencies = [
4205
4160
  "fastrand",
4206
- "getrandom 0.4.3",
4161
+ "getrandom 0.3.4",
4207
4162
  "once_cell",
4208
4163
  "rustix",
4209
- "windows-sys 0.52.0",
4164
+ "windows-sys 0.61.2",
4210
4165
  ]
4211
4166
 
4212
4167
  [[package]]
@@ -4864,7 +4819,7 @@ version = "0.1.11"
4864
4819
  source = "registry+https://github.com/rust-lang/crates.io-index"
4865
4820
  checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
4866
4821
  dependencies = [
4867
- "windows-sys 0.52.0",
4822
+ "windows-sys 0.61.2",
4868
4823
  ]
4869
4824
 
4870
4825
  [[package]]
@@ -2,7 +2,7 @@
2
2
  name = "sdr-aaronia-rs"
3
3
  description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
4
4
  license = "GPL-3.0-or-later"
5
- version = "0.11.2"
5
+ version = "0.13.0"
6
6
  edition = "2024"
7
7
  repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
8
8
  readme = "README.md"
@@ -11,7 +11,7 @@ categories = ["hardware-support", "api-bindings", "science"]
11
11
  exclude = ["tests/*.rtsa", "tests/sdk/", "tests/asan/", ".github/"]
12
12
 
13
13
  [package.metadata.docs.rs]
14
- features = ["http", "file", "sdr-source", "ffi", "native-sdk"]
14
+ features = ["http", "file", "ffi", "native-sdk"]
15
15
  rustdoc-args = ["--cfg", "docsrs"]
16
16
 
17
17
  [lib]
@@ -42,8 +42,6 @@ futures-timer = { version = "3.0", optional = true }
42
42
  num-complex = "0.4"
43
43
  tempfile = { version = "3.8", optional = true }
44
44
  anyhow = { version = "1.0", optional = true }
45
- crossbeam-channel = { version = "0.5", optional = true }
46
- orecchiette-sdr-source-rs = { version = "0.1.3", optional = true }
47
45
  # default-features = false: seify's defaults include its own driver
48
46
  # backends (soapy -> soapysdr-sys, which needs system SoapySDR +
49
47
  # libclang at build time and is BSL-1.0-licensed). We only need the
@@ -101,12 +99,11 @@ name = "http_sink_test"
101
99
  required-features = ["futuresdr"]
102
100
 
103
101
  [features]
104
- default = ["http", "file", "sdr-source", "ffi"]
102
+ default = ["http", "file", "ffi"]
105
103
  http = ["dep:reqwest", "dep:tokio", "dep:serde", "dep:serde_json",
106
104
  "dep:url", "dep:bytes", "dep:futures", "dep:half"]
107
105
  file = ["dep:byteorder", "dep:bitflags", "dep:tempfile"]
108
106
  futuresdr = ["http", "dep:futuresdr", "dep:futures-timer", "dep:anyhow"]
109
- sdr-source = ["http", "file", "dep:crossbeam-channel", "dep:anyhow", "dep:orecchiette-sdr-source-rs"]
110
107
  seify = ["http", "file", "dep:seify"]
111
108
  ffi = ["http", "file"]
112
109
  native-sdk = ["dep:libloading", "dep:widestring"]
@@ -135,9 +132,6 @@ required-features = ["http"]
135
132
  name = "read_rtsa_file"
136
133
  required-features = ["file"]
137
134
 
138
- [[example]]
139
- name = "channel_hopping"
140
- required-features = ["sdr-source"]
141
135
 
142
136
  [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
143
137
  libloading = { version = "0.7", optional = true }
@@ -145,3 +139,7 @@ widestring = { version = "1.1", optional = true }
145
139
 
146
140
  [workspace]
147
141
  members = ["python-aaronia"]
142
+
143
+ [[example]]
144
+ name = "native_sdk_qualification"
145
+ required-features = ["native-sdk", "http", "file"]
@@ -69,22 +69,20 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
69
69
 
70
70
  ## 5. Channel Hopping and Dwell Control
71
71
 
72
- Under the `sdr-source` feature, `SpectranSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
72
+ Retuning mid-stream is `SpectranSource::set_center_frequency_hz`, per backend:
73
73
 
74
74
  - **Native SDK**: re-issues `configure_iq_receiver` with the new center frequency, applying the change to the open device handle via the `main/centerfreq` configuration key. No stream restart is required.
75
75
  - **HTTP**: `SpectranSource::set_center_frequency_hz` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). Hopping deliberately avoids `/remoteconfig` and is not gated on the license probe. (Whether that path needs the "Remote Config" license at all is unconfirmed: a system without the license accepted `/remoteconfig` writes in testing. See docs/HTTPSPEC.md.)
76
76
  - **File**: not supported.
77
77
 
78
- Hop dwell deadlines come from `sdr_source::DwellController`; per-hop pacing additionally inserts a ~75 ms settle after every retune (`RETUNE_SETTLE` in `sdr_source_impl.rs`) to ride out the RTSA's apply-config latency and flush stale samples from the pipeline.
78
+ The hop loop itself — which channel next, how long to dwell there, and the ~75 ms settle after a retune that rides out the RTSA's apply-config latency and flushes stale samples — is the consumer's. Until 0.12.0 this crate carried one behind the `sdr-source` feature (`SpectranSdrSource`, an implementation of `orecchiette-sdr-source-rs`'s `SdrSource` trait); it lives beside its consumer now (Specola's `sdr-aaronia-source`), and a caller with its own trait builds the same thing from `SpectranSourceBuilder`.
79
79
 
80
80
  ## 6. Overrun Detection and Fault Handling
81
81
 
82
- The HTTP reader task (spawned by `init_http_source` in `unified_source.rs`) runs a `DropDetector` over each packet's `start_time`/`end_time` metadata. A timestamp gap larger than the tolerance latches `SpectranSource::pending_overrun`, which `take_overrun()` reads and clears. `single_channel_pump` and `hop_pump` (`sdr_source_impl.rs`) call `take_overrun()` once per emitted `IqPacket`, so a drop detected anywhere since the last read surfaces as `IqPacket::overrun = true` on the next packet. This is a per-call signal, not a precise per-sample one, since drop timing is lost once chunks merge into the flat `sample_buffer`.
82
+ The HTTP reader task (spawned by `init_http_source` in `unified_source.rs`) runs a `DropDetector` over each packet's `start_time`/`end_time` metadata. A timestamp gap larger than the tolerance latches `SpectranSource::pending_overrun`, which `take_overrun()` reads and clears. A consumer's read loop calls `take_overrun()` once per block it hands on, so a drop detected anywhere since the last read surfaces on the next block (Specola's adapter sets its `IqPacket::overrun` from it). This is a per-call signal, not a precise per-sample one, since drop timing is lost once chunks merge into the flat `sample_buffer`.
83
83
 
84
84
  The native-SDK and file backends do not populate this yet (`overrun` is always `false` for them). Native-SDK overrun detection would need the overflow/dropped warning bits from the packet `flags` field. The RX path reads and logs those at debug level, but does not yet latch them into `pending_overrun`.
85
85
 
86
- The Aaronia capture thread (`SpectranSdrSource::start`) is wrapped in `catch_unwind`: a panic inside the pump loop is logged rather than silently unwinding the thread.
87
-
88
86
  ## 7. FutureSDR Integration
89
87
 
90
88
  FutureSDR integration is available under the `futuresdr` feature gate. The crate exposes lower-level builder variants (e.g. `HttpSourceBuilder`) so `HttpSource` can be used as a source block in FutureSDR flowgraphs:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aaronia
3
- Version: 0.11.2
3
+ Version: 0.13.0
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+)
@@ -103,41 +103,39 @@ common fields.
103
103
 
104
104
  The
105
105
  [quickstart](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/QUICKSTART.md)
106
- covers configuring the RTSA-Suite HTTP Server block, which everything
107
- above depends on.
106
+ covers configuring the RTSA-Suite HTTP Server block for HTTP sources. Native SDK sources do not require that server.
108
107
 
109
- ## Sample rates
108
+ ## Sample rate and RF span
110
109
 
111
- The device runs a ladder of rates rather than a continuous range: each
112
- rung is half the one above it. Ask for anything else and it quietly
113
- uses the nearest rung, leaving your program computing against a rate
114
- that is not in use.
110
+ The module-level `sample_rates()` and `sample_rate_for_bandwidth()` functions
111
+ remain legacy HTTP/ECO helpers. They are not capabilities of every native device.
112
+ For a native IQ receiver, request RF span separately and inspect packet geometry:
115
113
 
116
114
  ```python
117
- aaronia.sample_rates() # every rate, highest first
118
- aaronia.sample_rate_for_bandwidth(8e6) # 15.36e6: the lowest rate covering 8 MHz
115
+ cfg = aaronia.SpectranConfig()
116
+ cfg.force_native_sdk = True
117
+ cfg.device_serial = "YOUR_DEVICE_SERIAL"
118
+ cfg.native_mode = "iqreceiver"
119
+ cfg.rf_span_hz = 44e6
120
+ cfg.center_frequency_hz = 280e6
121
+ cfg.reference_level_dbm = -50
122
+ src = aaronia.SpectranSource()
123
+ src.start_streaming(cfg)
124
+ print(src.capabilities()) # schema_version + capabilities
125
+ samples = src.read_samples_numpy(65536)
126
+ print(src.source_info()) # observed rate and RF span
127
+ src.set_rf_span_hz(20e6)
128
+ src.stop_streaming()
119
129
  ```
120
130
 
121
- **Sample rate is not RF bandwidth.** You get every sample, so an FFT of
122
- them spans the full rate — but only the middle 80% is flat and
123
- calibrated. That is not an approximation: RTSA reports exactly 0.8 x Fs
124
- as the packet's frequency range at every rate. Outside it, data still
125
- arrives, attenuated and uncalibrated.
126
-
127
- So **to see N Hz of spectrum, sample at N / 0.8**, which is what
128
- `sample_rate_for_bandwidth()` computes. Aaronia's data sheet is more
129
- conservative still — 44 MHz for the ECO against the 49.152 MHz it
130
- declares at full span — because the analog filter is ~1 dB down by that
131
- edge. The
132
- [quickstart](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/QUICKSTART.md#4-troubleshooting)
133
- has the measurements.
134
-
135
- `sample_rates()` returns the V6 ECO's ladder, 61.44 MHz down to 120 kHz.
136
- A full V6 selects its receiver clock and goes higher, by how much is
137
- unsettled; see
138
- [the note in HTTPSPEC](https://github.com/isaacbentley/sdr-aaronia-rs/blob/main/docs/HTTPSPEC.md#unresolved-the-full-v6s-top-rate).
139
- There, trust the rate the device reports in stream metadata, which
140
- `diagnose()` prints.
131
+ IQ sample clock and valid RF span are distinct. The tested ECO delivered 44 MHz
132
+ RF span at about 59.214 MS/s, and 20 MHz at 30 MS/s. Native configuration bounds
133
+ may exceed the model's rated bandwidth. Neither those bounds nor RF span certifies
134
+ amplitude calibration. Unknown metadata stays unknown. See the
135
+ [qualification matrix](../docs/NATIVE_SDK_COMPATIBILITY.md).
136
+
137
+ For HTTP only, the legacy helper uses the measured 0.8×Fs filter width and ECO
138
+ rate ladder. Prefer `source_info()` for what the stream actually delivered.
141
139
 
142
140
  ## Choosing a wire format
143
141
 
@@ -178,6 +176,11 @@ Every field is readable and writable.
178
176
  | `file_path` | Path to a recorded `.rtsa` file; pins the file backend |
179
177
  | `device_serial` | Device selection for the native-SDK backend |
180
178
  | `force_native_sdk` | `True` pins the source to the native SDK; missing SDK is an error |
179
+ | `native_family` | Optional bare SDK family, e.g. `"spectranv6eco"` |
180
+ | `native_mode` | Optional receive-only `"iqreceiver"` or `"raw"` |
181
+ | `rf_span_hz` | Optional requested native IQ receiver RF span, Hz |
182
+ | `receiver_clock` | Optional enabled raw-mode SDK clock label |
183
+ | `decimation_factor` | Optional raw-mode power-of-two factor |
181
184
  | `center_frequency_hz` | Center frequency, Hz |
182
185
  | `sample_rate_hz` | IQ sample rate (Fs), Hz |
183
186
  | `reference_level_dbm` | Reference level, dBm |