python-aaronia 0.11.2__tar.gz → 0.12.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 (93) hide show
  1. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/CHANGELOG.md +162 -0
  2. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/CONTRIBUTING.md +1 -1
  3. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/Cargo.lock +2 -47
  4. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/Cargo.toml +3 -9
  5. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/DESIGN.md +3 -5
  6. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/PKG-INFO +1 -1
  7. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/README.md +158 -5
  8. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/HTTPSPEC.md +13 -1
  9. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/USAGE.md +0 -1
  10. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/include/spectran.h +6 -5
  11. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/Cargo.toml +1 -1
  12. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/c_api.rs +71 -4
  13. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/file_source.rs +277 -15
  14. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_source.rs +1893 -219
  15. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_streaming.rs +911 -81
  16. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/lib.rs +5 -14
  17. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/link_budget.rs +50 -0
  18. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/native_sdk.rs +19 -2
  19. python_aaronia-0.12.0/src/stream_break.rs +206 -0
  20. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/unified_source.rs +41 -2
  21. python_aaronia-0.11.2/examples/channel_hopping.rs +0 -82
  22. python_aaronia-0.11.2/src/sdr_source_impl.rs +0 -674
  23. python_aaronia-0.11.2/tests/sdr_source_impl_test.rs +0 -426
  24. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.cargo/config.toml +0 -0
  25. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.gitattributes +0 -0
  26. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.gitignore +0 -0
  27. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/LICENSE +0 -0
  28. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/PLUGINS.md +0 -0
  29. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/decompress_block.rs +0 -0
  30. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/deinterleave_dual_iq.rs +0 -0
  31. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/parse_int16_packet.rs +0 -0
  32. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/rtsa_open_and_read.rs +0 -0
  33. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/deny.toml +0 -0
  34. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/APPS.md +0 -0
  35. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/FILESPEC.md +0 -0
  36. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/QUICKSTART.md +0 -0
  37. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/SDKSPEC.md +0 -0
  38. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/SYNC.md +0 -0
  39. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/VERIFICATION.md +0 -0
  40. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/device_control.rs +0 -0
  41. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/dump_metadata.rs +0 -0
  42. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/http_iq_quickstart.rs +0 -0
  43. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/native_sdk_basic.rs +0 -0
  44. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/native_sdk_transmit.rs +0 -0
  45. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/noaa_scanner.rs +0 -0
  46. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/python_arrow_example.py +0 -0
  47. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/read_rtsa_file.rs +0 -0
  48. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/soapy_python_example.py +0 -0
  49. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/packaging/homebrew/README.md +0 -0
  50. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  51. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/pyproject.toml +0 -0
  52. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/README.md +0 -0
  53. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/aaronia.pyi +0 -0
  54. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/src/lib.rs +0 -0
  55. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/test_basic.py +0 -0
  56. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/ci-local.sh +0 -0
  57. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/fake-rtsa-server.py +0 -0
  58. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/native-sdk-validate.ps1 +0 -0
  59. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/sdk-container-test.sh +0 -0
  60. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/validate-iq-live.py +0 -0
  61. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/CMakeLists.txt +0 -0
  62. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/README.md +0 -0
  63. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/Registration.cpp +0 -0
  64. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/SpectranSoapyDevice.cpp +0 -0
  65. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/SpectranSoapyDevice.hpp +0 -0
  66. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  67. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/packaging/install.sh +0 -0
  68. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/decompression.rs +0 -0
  69. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/detection.rs +0 -0
  70. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/error.rs +0 -0
  71. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_endpoints.rs +0 -0
  72. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_sink.rs +0 -0
  73. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/sdk_sink.rs +0 -0
  74. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/sdk_source.rs +0 -0
  75. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/seify_impl.rs +0 -0
  76. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/unified_sink.rs +0 -0
  77. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/utils.rs +0 -0
  78. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/abi_drift.rs +0 -0
  79. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/c_api_test.rs +0 -0
  80. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_mock_test.rs +0 -0
  81. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_resilience_test.rs +0 -0
  82. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_sink_test.rs +0 -0
  83. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/integration_test.rs +0 -0
  84. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/live_smoke.rs +0 -0
  85. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/native_sdk_live.rs +0 -0
  86. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/native_sdk_load.rs +0 -0
  87. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/properties.proptest-regressions +0 -0
  88. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/properties.rs +0 -0
  89. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/rtsa_negative_test.rs +0 -0
  90. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/spec_coverage.rs +0 -0
  91. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/test_cw_mag.rs +0 -0
  92. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/test_cw_meta.rs +0 -0
  93. {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/wire_contract.rs +0 -0
@@ -4,6 +4,168 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.12.0] - 2026-09-29
8
+
9
+ ### Removed
10
+ - **The `sdr-source` feature, and with it the dependency on `orecchiette-sdr-source-rs`.**
11
+ `SpectranSdrSource` and `SpectranBackend` (the `SdrSource`-trait facade over the async
12
+ source), the `sdr_source` re-export module and the `channel_hopping` example were one
13
+ consumer's adapter, and they live beside that consumer now (Specola's `sdr-aaronia-source`
14
+ crate), so this crate stands on its own: `default` is `http`, `file` and `ffi`, and
15
+ `crossbeam-channel` goes with the adapter. A caller that used the facade builds the same
16
+ thing from `SpectranSourceBuilder` and its own trait; the code is 670 lines and GPL, take it.
17
+
18
+ ### Added
19
+ - **`HttpSource` reports where the stream broke**, not just how often.
20
+ `HttpSourceBuilder::with_stream_breaks` takes a `StreamBreakSink` (see the
21
+ new `stream_break` module) and the source calls it with the **absolute index
22
+ of the first sample after each discontinuity**, in its own output stream:
23
+ a server gap the drop detector saw, a capacity trim, a reconnect, and a
24
+ centre or rate change the device declared. Optional; a caller that does not
25
+ ask pays nothing.
26
+
27
+ The counters that were there before cannot be acted on. A consumer carrying
28
+ state across deliveries — symbol timing, carrier tracking, burst framing, a
29
+ recording's contiguity — needs to know *which* samples belong to the old
30
+ epoch, and "the server skipped 3 times" does not say. Breaks are held against
31
+ the samples still queued and emitted only as those samples are produced, so a
32
+ later trim cannot move an index already reported; samples the source discards
33
+ never occupy an index at all.
34
+
35
+ - `StreamParser::process_data_recovering` and `ParsedItem`: a malformed
36
+ packet becomes a `ParsedItem::Lost` in stream order instead of an `Err` that
37
+ discards the rest of the chunk. `process_data` keeps its contract.
38
+ - `StreamStats::non_iq_packets_skipped` and
39
+ `StreamStats::packets_lost_to_parse_errors`.
40
+
41
+ ### Fixed
42
+ - **A catch-up after a stall no longer trims what it just fetched.** One
43
+ `fetch_samples` sweep drained every chunk the reader task had queued — up to
44
+ 64, about a million int16 samples — into a buffer that trimmed a few packets
45
+ deep, so any stall of the consumer longer than a few milliseconds discarded
46
+ most of what arrived during it. Measured live from bigear against a
47
+ Spectran V6 at 15.36 MS/s, where the trim sat at 147 456 samples (~9.6 ms):
48
+ with one flowgraph executor every 45 s run lost samples, 2 to 10 times; with
49
+ two, two runs in five. The trim now sits one sweep above the refill target
50
+ (`catch_up_headroom`: `CHUNK_CHANNEL_DEPTH` chunks of the largest size seen,
51
+ in samples, plus a carried packet), and a sweep takes at most
52
+ `CHUNK_CHANNEL_DEPTH` chunks, so a catch-up always fits. A consumer that falls
53
+ behind now backs up the chunk channel and the socket instead of being
54
+ trimmed, and any loss moves upstream, where the packet timestamps report it
55
+ as a server gap. Re-measured live, same span: one executor 1 gap in 5 runs,
56
+ two executors 0 in 3. The overflow warning no longer names "the first
57
+ moments of a stream" as the expected cause.
58
+
59
+ What changes with it, so nothing reads as a regression later:
60
+ `StreamStats::buffer_capacity` still reports the enforced capacity, which now
61
+ includes the headroom and jumps once the first chunk sizes it (about eight
62
+ times the old figure at 15.36 MS/s int16), so a fill ratio reads lower. A
63
+ consumer that stalls now reads its backlog rather than a gap: up to the buffer
64
+ plus the channel, about 150 ms at 15.36 MS/s, which a pipeline faster than
65
+ real time then catches up — including a retune that arrived behind it. The
66
+ buffer can grow to about 10 MB (int16) or 20 MB (float32) during a stall, on
67
+ top of the channel's 4 MB. And because a slow consumer now throttles the
68
+ reads the link-budget meter counts, a window in which a sweep found the chunk
69
+ channel full is marked (`ThroughputMeter::note_consumer_limited`) and yields
70
+ no verdict, rather than one blaming the path for the consumer.
71
+ - **A lost packet at a high sample rate is now reported.** `DropDetector`
72
+ flagged a gap only past a fixed 1 ms, and at 61.44 MS/s a 16 384-sample
73
+ packet lasts 267 µs, so a whole lost packet was spliced over silently. The
74
+ tolerance is now relative to the stream: half the packet's own duration, or
75
+ four times the residual observed between contiguous packets (at most three
76
+ quarters of a packet), floored at the timestamps' microsecond resolution
77
+ and still capped at 1 ms, so low rates behave as before.
78
+ `DropDetector::observed_jitter_seconds` exposes the calibration. Not yet
79
+ verified live against a real RTSA stream at a high rate.
80
+ - **A compressed spectra or histogram packet no longer swallows the IQ behind
81
+ it.** It was framed at its *uncompressed* size, but a compressed payload is
82
+ a bitstream whose length the header does not carry. It is now reported as
83
+ `ParsedItem::Skipped` where it sat and the parser resynchronises on the next
84
+ header.
85
+ - **A skipped non-IQ packet is no longer reported as a gap in the IQ.** Any
86
+ packet whose header was read but whose non-IQ payload could not be decoded
87
+ was a `ParsedItem::Lost`, which `HttpSource` recorded as a `Gap` and counted
88
+ in `packets_lost_to_parse_errors` — a hole claimed in an IQ stream that had
89
+ none. The new `ParsedItem::Skipped { payload, reason }` carries the declared
90
+ payload type; `HttpSource` counts it in `non_iq_packets_skipped` and records
91
+ no break. An undecodable IQ packet, or one whose extent could not be trusted,
92
+ is still `Lost`. (Breaking for exhaustive matches on `ParsedItem`.)
93
+ - **A timestamp that steps backwards is a break, not continuity.**
94
+ `DropDetector::observe` returned `Continuous` for a packet starting before
95
+ the previous one ended, however far, and fed the step into the jitter
96
+ estimate, holding the tolerance at its ceiling afterwards. A backward step
97
+ past the tolerance is now `DropResult::Backward { overlap_seconds }`, counted
98
+ in `DropDetector::backward_steps` and kept out of the jitter estimate and
99
+ the drop totals; `HttpSource` records a `Gap` there and `UnifiedSource`
100
+ flags the buffer. (Breaking for exhaustive matches on `DropResult`.)
101
+ - **`DropDetector::resync` keeps the jitter calibration.** It was cleared on
102
+ every lost packet and reconnect, and since the estimate grows only from
103
+ residuals already judged contiguous, a server whose residuals sit above
104
+ half a packet raised false drops until it was relearned. `reset` still
105
+ clears it.
106
+ - **Compressed spectra decode on a frames × bins grid.** The wavelet grid was
107
+ sized from the square root of one frame's bins, ignoring the frame count
108
+ and dropping bins. Shapes whose layout is undocumented — more than one
109
+ 16-spectrum block, depth planes, histograms — are refused instead of
110
+ decoded into a wrong shape.
111
+ - **`HttpSource` no longer outputs spectra, histogram or category scalars as
112
+ IQ.** They were queued with the IQ, fed the drop detector (whose timeline
113
+ they are not on) and overwrote `current_frequency` (a marker declares 0 Hz).
114
+ They are now counted and skipped.
115
+ - **One undecodable packet no longer costs the connection.** `HttpSource`
116
+ skips it, keeps the packets either side, and records a gap where it sat;
117
+ only an unrecoverable framing error still reconnects.
118
+ - **The README's stream-break example is doctested only where it compiles.**
119
+ It uses `HttpSourceBuilder`, which needs the `futuresdr` feature, so
120
+ `cargo test` with default features failed on it; the body is now gated on
121
+ that feature.
122
+ - **A stalled `/stream` reconnects.** The reader waited forever on a socket
123
+ that stayed open and sent nothing; it now gives up after 5 s and the source
124
+ takes its ordinary reconnect path.
125
+ - **A retune swallowed by a restart is announced after it.** The restart
126
+ discarded the queued `Retune` but kept the baseline it had advanced, so the
127
+ new band was never reported. The baseline now rewinds to the last geometry
128
+ reported.
129
+ - **A trim before the first sample publishes one `Initial`**, carrying the
130
+ geometry the first sample arrived under, instead of `Initial` for a
131
+ discarded band followed by `Retune` at the same index.
132
+ - **The first-start tune runs even when the connect/run write fails**, takes
133
+ its target from the builder rather than from packet-tracked fields, and is
134
+ marked done once attempted. The connect/run failure is logged at warn.
135
+ - **`UnifiedSource` HTTP reads no longer split on a derived-rate wobble.** The
136
+ read boundary compared rates with `!=`; it uses the crate's 10% band.
137
+ - **RTSA files:** resync over a corrupt region scans in 64 KiB blocks instead
138
+ of one `seek` per byte (16 MiB of zeroes: 34 s → well under a second); an IQ
139
+ `SAMP` chunk declaring other than one I/Q pair per sample is refused rather
140
+ than read with a mismatched stride; `seek_to_sample` indexes chunks per
141
+ sub-stream, as reads do; and `validate_structure` no longer overflows on an
142
+ absurd sample count.
143
+ - **C ABI:** `spectran_source_builder_force_source_type` takes the source type
144
+ as `int` and ignores an unknown value. Taking the `#[repr(C)]` enum by value
145
+ made an out-of-range value from C undefined behaviour. Source-compatible for
146
+ C and C++ callers passing an enumerator.
147
+ - **Native SDK:** a packet consumed without being delivered (bad stride,
148
+ oversized, undemuxable) now counts in `cumulative_drops` and sets
149
+ `take_overrun`, like a packet the device itself flagged as dropped.
150
+
151
+ ### Changed
152
+ - **Gap detection and sample queuing share one pass over each batch.** They
153
+ were two loops, so every packet's gap was measured against the buffer length
154
+ *before* any of the batch had been appended — the right answer only for the
155
+ first packet in it. No change to what is reported, only to where.
156
+ - **Float16 IQ payloads convert in bulk.** The per-sample `f16::to_f32` loop is
157
+ replaced by `half`'s slice conversion, which selects SIMD at runtime on
158
+ AArch64 FP16 and x86 F16C and keeps a portable fallback. Aligned
159
+ little-endian payloads are borrowed in place; an odd-aligned payload — a JSON
160
+ header can leave one at any address — passes through a 1 KiB stack buffer
161
+ rather than a second packet-sized allocation. Big-endian targets keep the
162
+ scalar path. Measured on macOS ARM64 in release, against the scalar
163
+ implementation over three alternating A/B rounds: 36% less time for an
164
+ aligned payload, 10% for an odd-aligned one, at 49 152 and 65 535 IQ pairs.
165
+ Every half-float bit pattern — subnormals, signed zeros and NaN payloads
166
+ included — is compared against the previous implementation at both
167
+ alignments and with odd IQ counts.
168
+
7
169
  ## [v0.11.2] - 2026-09-11
8
170
 
9
171
  ### 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"
@@ -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.12.0"
3096
3053
  dependencies = [
3097
3054
  "arrow",
3098
3055
  "num-complex",
@@ -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.12.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",
@@ -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.12.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 }
@@ -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.12.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+)
@@ -58,6 +58,17 @@ Consumers: ├── Native Rust API
58
58
  └── Python Bindings (PyO3: python-aaronia) ──► NumPy / Arrow
59
59
  ```
60
60
 
61
+ ## Listening to an analyser that is not yours
62
+
63
+ A start writes to the device before it reads a sample — the RTSA block's connect/run
64
+ switches, a tune to the requested centre and rate, a `/control` start — and the end of a
65
+ stream writes a stop. On an analyser shared with other users, none of that is yours to do.
66
+ `HttpSourceBuilder::listen_only(true)` skips all of it: the stream is read as the device is
67
+ running it, the requested centre and rate only describe what to expect, the reference level
68
+ is not pushed, no reconnect ever tunes, and the device is left running at the end. The
69
+ stall fix below was verified live in this mode against a shared SPECTRAN V6 streaming its
70
+ own 30.72 MS/s, the device's `/remoteconfig` identical before and after.
71
+
61
72
  ## Link requirements
62
73
 
63
74
  The server streams IQ at a fixed number of bytes per sample, so the span
@@ -68,7 +79,7 @@ waterfall and defeat any digital demodulator.
68
79
  | Real-time bandwidth | Sample rate | Needs (4 B/sample) | Link |
69
80
  |---|---|---|---|
70
81
  | up to 12.2 MHz | 15.36 MS/s | 61.4 MB/s | gigabit |
71
- | up to 24.5 MHz | 30.72 MS/s | 122.9 MB/s | gigabit measured 1024 skips; prefer 2.5GbE |
82
+ | up to 24.5 MHz | 30.72 MS/s | 122.9 MB/s | **2.5GbE** — a 1000baseT hop at a 1500-byte MTU carries ~118 MB/s of TCP payload (measured 114–116.5); 10GbE measured 123.2 and carried 90 s at 30.72 MS/s without a gap |
72
83
  | up to 49.1 MHz | 61.44 MS/s | 245.8 MB/s | **2.5GbE** |
73
84
 
74
85
  **An ECO 100 needs 2.5GbE.** Its 44 MHz of real-time bandwidth only fits on
@@ -79,18 +90,161 @@ format: stay on `int16` or `float16`.
79
90
  Treat the table as a starting point. `link_budget` measures *your* path end
80
91
  to end and names the widest span that fits.
81
92
 
93
+ Arithmetic can rule a link out; only a measurement rules it in. A 1000baseT
94
+ hop at the usual 1500-byte MTU carries about 118 MB/s of TCP payload however
95
+ the client reads, so 30.72 MS/s at 4 bytes a sample is 4 % short before a
96
+ sample is processed (jumbo frames on every hop would lift the ceiling to
97
+ ~124 MB/s, 0.7 % above the need — untested). Measured 2026-09-28 through a USB
98
+ gigabit adapter: 114–116.5 MB/s off the socket with nothing behind it, 5–7 %
99
+ short, and the whole receiver delivered exactly that — 28.5–29.0 MS/s with
100
+ about 35 server skips a second and no client-side trim — whatever its
101
+ buffering (a deeper chunk channel, a larger reservoir and a full-batch
102
+ consumer were each tried and changed nothing). The same stream with the
103
+ client on a Wi-Fi 7 link read 123.3 MB/s in each one-second window once it
104
+ had ramped, the first five seconds at 103 and hundreds of skips before it
105
+ settled; wireless varies with the air, and a station-to-station Wi-Fi 7 path
106
+ measured 75 MB/s (`link_budget`). Over a direct 10GbE link the same receiver
107
+ ran 90 s at 30.72 MS/s with no gap at all, the socket reading 123.0–123.5 MB/s
108
+ from its first second.
109
+
110
+ ## HTTP sample conversion
111
+
112
+ Float16 IQ payloads use `half`'s bulk conversion, which selects SIMD at
113
+ runtime on supported CPUs (AArch64 FP16 or x86 F16C) and retains a portable
114
+ fallback. On little-endian targets, aligned payloads are borrowed directly;
115
+ odd-aligned payloads pass through a 1 KiB stack buffer. Big-endian targets
116
+ retain scalar little-endian decoding. Tests compare every half-float bit
117
+ pattern with the preceding scalar implementation, including subnormals,
118
+ signed zeros, and NaN payloads, at both alignments and with odd IQ counts.
119
+
120
+ On the local macOS ARM64 host, release-mode conversion time fell as follows.
121
+ Each result compares median elapsed times from three alternating A/B rounds,
122
+ with 20,000 conversions per round after 2,000 warm-up calls:
123
+
124
+ | IQ pairs per payload | Aligned payload | Odd-aligned payload |
125
+ | --- | ---: | ---: |
126
+ | 49,152 | 35.6% less time | 9.5% less time |
127
+ | 65,535 | 36.3% less time | 9.8% less time |
128
+
129
+ These measurements include output allocation, but exclude HTTP framing,
130
+ networking, and application DSP; they are not Raspberry Pi measurements.
131
+ This change applies to Float16 IQ, not the default Int16 capture format.
132
+ Run the same comparison without concurrent builds or other benchmarks:
133
+
134
+ ```bash
135
+ cargo test --release --lib --no-default-features --features http \
136
+ float16_bulk_throughput_meter -- --ignored --nocapture
137
+ ```
138
+
139
+ ## Stream continuity
140
+
141
+ A source hands its consumer a flat run of samples, and that run *claims* to be
142
+ continuous. Six things break the claim, and `HttpSource` knows about all six:
143
+
144
+ | event | cause |
145
+ | --- | --- |
146
+ | server gap | the RTSA server dropped what it could not send; seen as a jump between one packet's `endTime` and the next's `startTime` larger than a tolerance measured from the stream (half a packet, or above the observed timestamp jitter, capped at 1 ms — see `DropDetector`; not yet verified live at high rates) |
147
+ | backward timestamp | a packet's `startTime` precedes the previous packet's `endTime` by more than the same tolerance: nothing is missing, but the two do not follow one another |
148
+ | undecodable packet | an IQ packet, or one whose header could not be read, framed but could not be decoded; it is skipped and its neighbours kept, and the hole is a gap where it sat |
149
+ | capacity trim | the buffer reached its memory bound and the oldest samples were discarded. The trim sits one fetch sweep above the refill target, so a consumer that falls behind backs up the socket instead, and any loss then shows as a server gap; it is reached only by a JSON stream, whose samples have no fixed width, or if the headroom's assumptions — chunk and packet sizes, each learned before it is parsed — stop holding |
150
+ | reconnect | the stream ended, errored, or delivered nothing for 5 s, and was reopened; the parser resets and the drop detector forgets the last timestamp (keeping its jitter calibration) |
151
+ | retune | the device's centre frequency or sample rate changed mid-stream |
152
+
153
+ `HttpSource` outputs IQ only. Spectra, histogram and category packets on a
154
+ mixed mission are skipped and counted in `StreamStats::non_iq_packets_skipped`;
155
+ they feed neither the output, the drop detector nor the frequency tracking. That
156
+ includes the ones the parser cannot decode — a compressed spectra packet carries
157
+ no byte length to frame it by — which cost no IQ and so mark no gap.
158
+
159
+ Counters for these have always been available through `StreamStats`. What a
160
+ counter cannot say is *which* samples belong to the old epoch — and that is the
161
+ only thing a consumer can act on, because symbol timing, carrier tracking and
162
+ burst framing are all carried across the boundary between one delivery and the
163
+ next. `link_budget`'s own warning applies here: every dropped sample is an
164
+ unsignalled phase discontinuity that breaks digital symbol lock.
165
+
166
+ `with_stream_breaks` reports each one against the **absolute index of the first
167
+ sample after it**, counted in this source's output stream:
168
+
169
+ ```rust,no_run
170
+ # // `HttpSourceBuilder` is the FutureSDR block: compiled only with that feature.
171
+ # #[cfg(feature = "futuresdr")]
172
+ # fn main() -> Result<(), sdr_aaronia_rs::Error> {
173
+ use std::sync::Arc;
174
+ use sdr_aaronia_rs::{HttpSourceBuilder, RecordingBreakSink, StreamDiscontinuity};
175
+
176
+ let breaks = Arc::new(RecordingBreakSink::new());
177
+ let source = HttpSourceBuilder::new("http://atc.local:54664")
178
+ .center_frequency_hz(146.52e6)
179
+ .sample_rate_hz(1e6)
180
+ .with_stream_breaks(breaks.clone())
181
+ .build()?;
182
+
183
+ // …later, after the flowgraph has run:
184
+ for (at_sample, cause) in breaks.breaks() {
185
+ match cause {
186
+ StreamDiscontinuity::Gap => println!("reset decoder state at {at_sample}"),
187
+ StreamDiscontinuity::Initial { center_hz, rate_hz }
188
+ | StreamDiscontinuity::Retune { center_hz, rate_hz } => {
189
+ println!("samples from {at_sample} are {center_hz} Hz at {rate_hz} Sa/s")
190
+ }
191
+ }
192
+ }
193
+ # Ok(())
194
+ # }
195
+ # #[cfg(not(feature = "futuresdr"))]
196
+ # fn main() {}
197
+ ```
198
+
199
+ Implement `StreamBreakSink` yourself to push into whatever queue the consumer
200
+ already reads; `RecordingBreakSink` is the built-in one, for tests and for
201
+ looking at a finished run.
202
+
203
+ Two properties make the index usable. A break is recorded **before** the
204
+ samples it precedes are published, so by the time a consumer can see sample
205
+ `i`, every break at or before `i` is already in the sink. And samples the
206
+ source discards — trimmed, or cleared on a reconnect — never occupy an index,
207
+ so the coordinate is simply how many samples the block has produced. Breaks are
208
+ held against the samples still queued and emitted only as those are produced,
209
+ which is what makes a reported index final: a trim that arrives later rebases
210
+ what is still pending rather than invalidating what was already said.
211
+
212
+ A `Retune` is measured against the geometry **last announced**, not against the
213
+ previous packet. A baseline that moved with every packet would let a device
214
+ creeping by less than a tolerance per packet travel arbitrarily far without
215
+ ever reporting a change, since each step would be judged against the step
216
+ before it rather than against what the consumer believes. Centre moves of more
217
+ than 1 Hz and rate moves outside the crate's 10% band count; the rate ladder
218
+ steps in powers of two, so a real change clears that band by a wide margin
219
+ while a rate the parser had to infer from `samples / duration` wobbles well
220
+ inside it. A packet declaring no usable rate at all is ignored for this
221
+ purpose rather than taken as a new baseline. A restart that clears the buffer
222
+ also discards any announcement still queued in it, so the baseline goes back to
223
+ the last geometry actually *reported* — a retune the restart swallowed is then
224
+ announced after it.
225
+
226
+ `Initial` is emitted at sample 0 of every run, whether or not it differs from
227
+ what was requested. The server serves the nearest rung of its own rate ladder
228
+ and the span its mission is configured for, so the first packet is exactly
229
+ where a disagreement with the request appears — and a consumer comparing
230
+ packets only against each other would never see it. Breaks at one index are
231
+ reported as one boundary — at most a gap, then at most one geometry — so when
232
+ a trim before the first sample discards the band the run opened on, the
233
+ consumer gets a single `Initial` naming the geometry its first sample actually
234
+ arrived under.
235
+
82
236
  ## Installation
83
237
 
84
238
  Add the following to your `Cargo.toml`:
85
239
 
86
240
  ```toml
87
241
  [dependencies]
88
- # By default, includes HTTP, File, native sdr-source trait, and C FFI backend support
89
- sdr-aaronia-rs = "0.8"
242
+ # By default, includes HTTP, File and C FFI backend support
243
+ sdr-aaronia-rs = "0.12"
90
244
  tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
91
245
 
92
246
  # To enable additional backends, opt into their features (e.g. native-sdk, futuresdr)
93
- # sdr-aaronia-rs = { version = "0.8", features = ["native-sdk", "futuresdr"] }
247
+ # sdr-aaronia-rs = { version = "0.12", features = ["native-sdk", "futuresdr"] }
94
248
  ```
95
249
 
96
250
  HTTP reads use one timeout budget for the entire requested block and return partial data at a timeout, frequency change, sample-rate change, or detected gap. `capture_frequency_hz()` and `capture_sample_rate_hz()` describe the returned samples even when newer packets are queued. File tuning setters preserve the recording's metadata.
@@ -222,7 +376,6 @@ Functionality is grouped behind Cargo features so unused dependencies stay out o
222
376
  | `file` | Buffered RTSA file parsing. | **Yes** |
223
377
  | `native-sdk` | Links the proprietary Aaronia C++ SDK. Windows/Linux only. | No |
224
378
  | `futuresdr` | Enables the FutureSDR block API: `HttpSource`, `HttpSink`, and their builders. Implies `http`. | No |
225
- | `sdr-source` | Integrates `SpectranSdrSource` implementing the native `SdrSource` traits. | **Yes** |
226
379
  | `ffi` | Builds the C-API export layer. | **Yes** |
227
380
 
228
381
  ## Testing & Contributing
@@ -124,7 +124,7 @@ The stream server supports multiple data formats for high-performance streaming:
124
124
  | `minPower` | Minimum power in dBm | `integer` (e.g., `-95`, `-2`, `-165`); this crate parses it as `i32` |
125
125
  | `maxPower` | Maximum power in dBm | `integer` (e.g., `5`, `2`); this crate parses it as `i32` |
126
126
  | `sampleFrequency` | Sample rate in Hz | `double` (e.g., `100000000.0`) |
127
- | `compression` | Compression scheme of the binary payload; drives the wavelet decompression path when present | `integer` |
127
+ | `compression` | Compression factor of the binary payload (0 = uncompressed). Not in the v9 endpoints document, and the server accepts `compression=N` only with `format=rtsa`. A compressed payload's byte length is not in the header, so a `spectra` or `histogram` packet with `compression > 0` on the JSON-plus-binary wire **cannot be framed**: `StreamParser` reports it lost and resynchronises on the next header rather than consuming an assumed length | `integer` |
128
128
  | `startFrequency` | Start of a frequency range | `double` (e.g., `2400250000`, `2402250128`) |
129
129
  | `endFrequency` | End of a frequency range | `double` (e.g., `2487750000`, `2489750128`) |
130
130
  | `sampleDepth` | Number of sample sets per sample, e.g., bins in a histogram | `integer` (e.g., `1`, `2`, `256`) |
@@ -197,6 +197,18 @@ announces the loss; it shows up only as a gap between the timestamps of
197
197
  two adjacent packets. A slow consumer, a slow link, or a rate the
198
198
  network cannot carry all end here, so reducing the wire format
199
199
  (`format=int16`) is the fix rather than a larger client-side buffer.
200
+
201
+ How large a jump counts as a gap is measured from the stream itself.
202
+ `DropDetector` flags a gap larger than half the shorter of the two
203
+ packets' own durations — a lost packet opens a gap of a whole one — or
204
+ four times the residual it has seen between contiguous packets, if that
205
+ is larger (up to three quarters of a packet), never less than 4 µs (the
206
+ server prints `startTime` to the microsecond) and never more than 1 ms.
207
+ A fixed 1 ms was the old rule, and above roughly 16 MS/s with 16 384-sample
208
+ packets a whole lost packet is shorter than that and went unreported.
209
+ The calibration is unit-tested on synthetic timestamps; **live
210
+ verification against a real RTSA stream at a high rate is still
211
+ pending.**
200
212
  Narrowing the span works too, since it moves the device down its
201
213
  decimation ladder. `rate_reduction=n` does not help here either — it is
202
214
  time compression for frame-based payloads (see the parameter note above).
@@ -460,7 +460,6 @@ code you can run. CI builds them, so they stay current:
460
460
  | --- | --- |
461
461
  | HTTP IQ streaming, first samples | [`http_iq_quickstart.rs`](../examples/http_iq_quickstart.rs) |
462
462
  | Health checks, server info, input enumeration | [`device_control.rs`](../examples/device_control.rs) |
463
- | Frequency hopping via the `sdr-source` traits | [`channel_hopping.rs`](../examples/channel_hopping.rs) |
464
463
  | FutureSDR flowgraph with FM demodulation | [`noaa_scanner.rs`](../examples/noaa_scanner.rs) |
465
464
  | Native SDK capture and transmit | [`native_sdk_basic.rs`](../examples/native_sdk_basic.rs), [`native_sdk_transmit.rs`](../examples/native_sdk_transmit.rs) |
466
465
  | RTSA file playback and metadata inspection | [`read_rtsa_file.rs`](../examples/read_rtsa_file.rs), [`dump_metadata.rs`](../examples/dump_metadata.rs) |
@@ -25,9 +25,9 @@ typedef enum SpectranFfiError {
25
25
 
26
26
  // --- C-compatible SourceType --- //
27
27
  typedef enum CSpectranSourceType {
28
- NativeSdk,
29
- Http,
30
- File,
28
+ NativeSdk = 0,
29
+ Http = 1,
30
+ File = 2,
31
31
  } CSpectranSourceType;
32
32
 
33
33
  // --- C-compatible Complex struct --- //
@@ -137,9 +137,10 @@ void spectran_source_builder_device_serial(SpectranSourceBuilder* builder, const
137
137
 
138
138
  /* Pin the source to one backend instead of auto-detecting. Forcing
139
139
  * NativeSdk makes a missing SDK a build error rather than a silent
140
- * fallback to localhost HTTP. */
140
+ * fallback to localhost HTTP. `source_type` is a CSpectranSourceType
141
+ * value, passed as int; a value outside the enum is ignored. */
141
142
  void spectran_source_builder_force_source_type(SpectranSourceBuilder *builder,
142
- CSpectranSourceType source_type);
143
+ int source_type);
143
144
 
144
145
  /* Whether the native SDK library is present, by the search the builder
145
146
  * uses; does not load it. */
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "python-aaronia"
3
- version = "0.11.2"
3
+ version = "0.12.0"
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"