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.
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/CHANGELOG.md +162 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/CONTRIBUTING.md +1 -1
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/Cargo.lock +2 -47
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/Cargo.toml +3 -9
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/DESIGN.md +3 -5
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/PKG-INFO +1 -1
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/README.md +158 -5
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/HTTPSPEC.md +13 -1
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/USAGE.md +0 -1
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/include/spectran.h +6 -5
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/c_api.rs +71 -4
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/file_source.rs +277 -15
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_source.rs +1893 -219
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_streaming.rs +911 -81
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/lib.rs +5 -14
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/link_budget.rs +50 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/native_sdk.rs +19 -2
- python_aaronia-0.12.0/src/stream_break.rs +206 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/unified_source.rs +41 -2
- python_aaronia-0.11.2/examples/channel_hopping.rs +0 -82
- python_aaronia-0.11.2/src/sdr_source_impl.rs +0 -674
- python_aaronia-0.11.2/tests/sdr_source_impl_test.rs +0 -426
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.cargo/config.toml +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.gitattributes +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/.gitignore +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/LICENSE +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/PLUGINS.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/deny.toml +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/APPS.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/QUICKSTART.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/SDKSPEC.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/SYNC.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/docs/VERIFICATION.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/device_control.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/pyproject.toml +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/README.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/aaronia.pyi +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/src/lib.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/fake-rtsa-server.py +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/native-sdk-validate.ps1 +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/sdk-container-test.sh +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/README.md +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/Registration.cpp +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/SpectranSoapyDevice.cpp +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/SpectranSoapyDevice.hpp +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/decompression.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/detection.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/error.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_endpoints.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/http_sink.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/sdk_source.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/seify_impl.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/unified_sink.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/src/utils.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/abi_drift.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_resilience_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/integration_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/live_smoke.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/native_sdk_live.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/properties.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.11.2 → python_aaronia-0.12.0}/tests/test_cw_meta.rs +0 -0
- {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`),
|
|
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.
|
|
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.
|
|
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.
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 |
|
|
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
|
|
89
|
-
sdr-aaronia-rs = "0.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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. */
|