python-aaronia 0.9.0__tar.gz → 0.11.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.9.0 → python_aaronia-0.11.0}/CHANGELOG.md +296 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/Cargo.lock +2 -2
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/Cargo.toml +1 -1
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/DESIGN.md +11 -11
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/PKG-INFO +29 -26
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/PLUGINS.md +39 -6
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/README.md +34 -13
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/FILESPEC.md +7 -12
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/HTTPSPEC.md +29 -34
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/QUICKSTART.md +25 -23
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/SDKSPEC.md +172 -66
- python_aaronia-0.11.0/docs/SYNC.md +118 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/USAGE.md +62 -54
- python_aaronia-0.11.0/docs/VERIFICATION.md +73 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/channel_hopping.rs +5 -5
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/dump_metadata.rs +9 -9
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/http_iq_quickstart.rs +6 -6
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/native_sdk_basic.rs +10 -5
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/native_sdk_transmit.rs +12 -10
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/noaa_scanner.rs +39 -34
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/python_arrow_example.py +6 -6
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/read_rtsa_file.rs +3 -3
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/soapy_python_example.py +3 -3
- python_aaronia-0.9.0/include/aaronia.h → python_aaronia-0.11.0/include/spectran.h +116 -98
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/README.md +28 -25
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/aaronia.pyi +69 -42
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/src/lib.rs +135 -101
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/test_basic.py +26 -24
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/ci-local.sh +20 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/fake-rtsa-server.py +1 -1
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/validate-iq-live.py +10 -2
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/CMakeLists.txt +3 -3
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/README.md +43 -6
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/Registration.cpp +54 -45
- python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.cpp → python_aaronia-0.11.0/soapy-aaronia/SpectranSoapyDevice.cpp +241 -130
- python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.hpp → python_aaronia-0.11.0/soapy-aaronia/SpectranSoapyDevice.hpp +22 -7
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/c_api.rs +600 -400
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/file_source.rs +71 -71
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_endpoints.rs +339 -57
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_sink.rs +19 -19
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_source.rs +90 -85
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_streaming.rs +20 -14
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/lib.rs +26 -17
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/link_budget.rs +65 -63
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/native_sdk.rs +550 -122
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdk_sink.rs +52 -20
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdk_source.rs +84 -173
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdr_source_impl.rs +103 -46
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/seify_impl.rs +41 -41
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/unified_sink.rs +35 -28
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/unified_source.rs +763 -309
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/utils.rs +281 -14
- python_aaronia-0.11.0/tests/abi_drift.rs +311 -0
- python_aaronia-0.11.0/tests/c_api_test.rs +107 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_mock_test.rs +159 -5
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_resilience_test.rs +15 -15
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_sink_test.rs +2 -2
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/integration_test.rs +6 -6
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/live_smoke.rs +131 -22
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/native_sdk_live.rs +150 -53
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/properties.rs +1 -1
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/sdr_source_impl_test.rs +14 -14
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/spec_coverage.rs +1 -1
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/test_cw_meta.rs +3 -3
- python_aaronia-0.11.0/tests/wire_contract.rs +232 -0
- python_aaronia-0.9.0/docs/VERIFICATION.md +0 -79
- python_aaronia-0.9.0/tests/c_api_test.rs +0 -72
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.cargo/config.toml +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.gitattributes +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.gitignore +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/CONTRIBUTING.md +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/LICENSE +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/deny.toml +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/APPS.md +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/device_control.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/pyproject.toml +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/native-sdk-validate.ps1 +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/sdk-container-test.sh +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/decompression.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/detection.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/error.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/test_cw_mag.rs +0 -0
|
@@ -4,6 +4,302 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.11.0] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **A read never spans a retune, and packets carry the frequency they
|
|
11
|
+
were captured at.** The centre frequency now travels through the
|
|
12
|
+
sample channel with the samples it belongs to, so
|
|
13
|
+
`read_samples`/`read_samples_deadline` stop at a frequency boundary
|
|
14
|
+
and `SpectranSource::capture_frequency_hz` reports what the stream
|
|
15
|
+
said, not what was requested.
|
|
16
|
+
|
|
17
|
+
Reading the frequency from shared state at consume time cannot work:
|
|
18
|
+
the reader task runs ahead of the consumer, so a buffer parsed before
|
|
19
|
+
a retune would be tagged with the frequency after it. Measured on a
|
|
20
|
+
V6 ECO over RTSA HTTP at 61.44 MSPS, a retune is carried by the signal
|
|
21
|
+
within 24 ms median and 39 ms worst case, and during that window the
|
|
22
|
+
device is still delivering the old centre.
|
|
23
|
+
|
|
24
|
+
Callers already handle short reads — `read_samples_numpy` slices what
|
|
25
|
+
it got, and a SoapySDR `readStream` returning fewer than `numElems` is
|
|
26
|
+
ordinary — so the visible change is that a buffer taken across a
|
|
27
|
+
retune is no longer a mixture of two frequencies. `RETUNE_SETTLE` in
|
|
28
|
+
the `SdrSource` facade drops from 75 ms to 20 ms with it: the
|
|
29
|
+
frequency check rejects stale samples exactly, so the drain is only an
|
|
30
|
+
efficiency measure. A 48-tune sweep of every FPV band falls from 28.2 s
|
|
31
|
+
to 5.9 s.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **The GPS mode can be set, not just the clock source.** `device/gpsmode`
|
|
35
|
+
decides whether GPS supplies location, time, both, or nothing, and the device
|
|
36
|
+
ships on `Disabled` — in which state no fix is ever reported and
|
|
37
|
+
`gps_time_ns` returns `None` forever, reading as broken rather than
|
|
38
|
+
unconfigured. `SpectranSource::set_gps_mode` writes it on both backends,
|
|
39
|
+
with the same confirm-by-read-back rule as the clock source.
|
|
40
|
+
- **`clock_source()`, `clock_sources()`, `gps_mode()`, `gps_modes()`** on
|
|
41
|
+
`SpectranSource`, so reading the current reference no longer means fetching
|
|
42
|
+
the whole capability tree. These read over HTTP; the writes work on both
|
|
43
|
+
backends.
|
|
44
|
+
- **Dual-channel RX reaches SoapySDR.** A full V6's two inputs were readable
|
|
45
|
+
from Rust, C and Python but not through the plugin, which reported one
|
|
46
|
+
channel and refused any other. Open with `rx_channel=Rx1And2` and
|
|
47
|
+
`getNumChannels(RX)` reports 2; `setupStream(RX, …, {0, 1})` puts
|
|
48
|
+
`readStream` on the paired read, filling both buffers with the same count.
|
|
49
|
+
The count follows the request, not the model — dual capture is configured
|
|
50
|
+
before the device opens, so a V6 opened single-channel has one channel to
|
|
51
|
+
offer. Still hardware-unverified: the development device is a single-channel
|
|
52
|
+
ECO.
|
|
53
|
+
- **`read_samples_dual_deadline`** (Rust) and
|
|
54
|
+
**`spectran_source_read_samples_dual_timeout`** (C), the dual counterparts of
|
|
55
|
+
the existing deadline-bounded reads. `readStream` must honour the
|
|
56
|
+
application's `timeoutUs`, which the blocking dual read ignored.
|
|
57
|
+
- **The device's master stream clock is readable from a source.**
|
|
58
|
+
`SpectranSource::master_stream_time_ns`,
|
|
59
|
+
`spectran_source_get_master_stream_time_ns`, Python
|
|
60
|
+
`master_stream_time_ns()`, and SoapySDR `getHardwareTime("master")`. It was
|
|
61
|
+
reachable only through the TX sink before. Unlike `last_timestamp_ns` it
|
|
62
|
+
answers before the first packet, so a caller that must not stamp data to
|
|
63
|
+
1970 has something to read; it is also the clock two receivers are aligned
|
|
64
|
+
on. Native-SDK backend only.
|
|
65
|
+
- **Python gained `master_stream_time_ns()` and `gps_time_ns()`**, which the
|
|
66
|
+
Rust and C surfaces already had.
|
|
67
|
+
- **[docs/SYNC.md](docs/SYNC.md)** — what the hardware does and does not offer
|
|
68
|
+
for multi-device timing. The vendor API has no set-time, no arm-at-time and
|
|
69
|
+
no trigger in any of its 34 functions, so UHD-style commanded capture is not
|
|
70
|
+
possible; a shared reference plus post-alignment on timestamps is, and that
|
|
71
|
+
is written down with its resolution floor (~240 ns, set by the vendor's own
|
|
72
|
+
`double`). Wired into the doctests, so its snippets cannot rot.
|
|
73
|
+
|
|
74
|
+
### Breaking changes
|
|
75
|
+
- **`get_master_stream_time()` is now `master_stream_time_ns()`**, on
|
|
76
|
+
`NativeSdkSource`, `SdkSink` and `UnifiedSink`, returning `i64` nanoseconds
|
|
77
|
+
instead of `f64` seconds. One unit per dimension, matching every other clock
|
|
78
|
+
the crate reports. `TxBurst` still carries the vendor's `f64` seconds — it is
|
|
79
|
+
written straight into the packet header — so convert with the new
|
|
80
|
+
`utils::epoch_nanos_to_seconds`.
|
|
81
|
+
- **`utils::gps_seconds_to_nanos` is now `utils::epoch_seconds_to_nanos`.**
|
|
82
|
+
Two of the vendor's clocks arrive as `float64` epoch seconds, not one, and
|
|
83
|
+
the old name claimed otherwise.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- **A timed-out read no longer discards the staging buffer.**
|
|
87
|
+
`spectran_source_read_samples_timeout` returned early on timeout, past the
|
|
88
|
+
point where it hands the buffer back, so the next read reallocated it — the
|
|
89
|
+
allocation that path exists to avoid.
|
|
90
|
+
- **An unrecognised `rx_channel=` is reported, not silently taken as `Rx1`.**
|
|
91
|
+
A typo used to yield a single-channel stream indistinguishable from a working
|
|
92
|
+
dual request.
|
|
93
|
+
- **Reading the wrong payload for the open mode is refused instead of answered.**
|
|
94
|
+
Neither the SDK nor the packet says what a stream carries, so asking a
|
|
95
|
+
spectrum pipeline for IQ returned dBm bins reinterpreted as voltages, and
|
|
96
|
+
asking an IQ pipeline for spectra returned complex pairs reinterpreted as
|
|
97
|
+
two-bin frames whose bin spacing was the whole span. Both look like data.
|
|
98
|
+
Measured on a V6 ECO's `rtsa`: 200k "samples" spanning -130 to -59 with a
|
|
99
|
+
mean of -77 and not one value near zero — a dBm distribution, not a voltage
|
|
100
|
+
one. `read_samples`, `read_samples_dual` and `read_spectra` now check the
|
|
101
|
+
open mode and say which payload it carries.
|
|
102
|
+
|
|
103
|
+
This also corrects a doc claim: `spectranv6eco/rtsa` was documented as
|
|
104
|
+
yielding IQ at "~0.4 MS/s regardless of the requested rate". It yields no IQ
|
|
105
|
+
at all; that figure was the spectra misread. `spectranv6/raw` genuinely does
|
|
106
|
+
both, IQ on stream 0 and spectra on stream 2, and is unaffected.
|
|
107
|
+
- **Capability lists no longer advertise options the device will refuse.** The
|
|
108
|
+
config tree carries a bitmask of enum entries the device is currently
|
|
109
|
+
rejecting, and it was ignored: a V6 ECO with no GPS antenna offered `GPS` and
|
|
110
|
+
`GPS Provider` as clock sources, and `set_clock_source("GPS")` then failed
|
|
111
|
+
against a list that said it would not. Those entries are filtered out, so what
|
|
112
|
+
`clock_sources()` and `gps_modes()` return is what the device accepts.
|
|
113
|
+
Verified live: the six sources advertised are exactly the six it takes.
|
|
114
|
+
|
|
115
|
+
## [v0.10.0] - 2026-09-08
|
|
116
|
+
|
|
117
|
+
### Breaking changes
|
|
118
|
+
|
|
119
|
+
**One name per concept, with its unit.** A field, parameter or getter holding a
|
|
120
|
+
bare number now carries its unit (`_hz`, `_dbm`, `_db`, `_s`); a type that
|
|
121
|
+
already carries one, such as `Duration`, does not. `span_frequency` is gone: it
|
|
122
|
+
meant the IQ *sample rate*, while the same word meant alias-free *bandwidth* in
|
|
123
|
+
the link budget. Old names were removed rather than deprecated, since an alias
|
|
124
|
+
that kept working would preserve the ambiguity this release exists to remove.
|
|
125
|
+
The `spanfreq` device key and the `frequencySpan` JSON field are the vendor's
|
|
126
|
+
vocabulary and are unchanged.
|
|
127
|
+
|
|
128
|
+
| Old | New |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `AaroniaConfig::center_frequency` (field + builder) | `center_frequency_hz` |
|
|
131
|
+
| `AaroniaConfig::span_frequency` (field + builder) | `sample_rate_hz` |
|
|
132
|
+
| `AaroniaConfig::reference_level` (field + builder) | `reference_level_dbm` |
|
|
133
|
+
| `AaroniaSourceBuilder::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
|
|
134
|
+
| `AaroniaSource::set_center_frequency` / `set_span_frequency` / `set_reference_level` | `set_center_frequency_hz` / `set_sample_rate_hz` / `set_reference_level_dbm` |
|
|
135
|
+
| `SourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
|
|
136
|
+
| `SourceInfo::sample_rate_hz()` (method) | removed — read the field of the same name |
|
|
137
|
+
| `UnifiedSinkConfig::span_frequency`, `trans_gain` | `sample_rate_hz`, `trans_gain_db` |
|
|
138
|
+
| `ThroughputMeasurement::max_sustainable_span_hz` | `max_sustainable_bandwidth` |
|
|
139
|
+
| `ThroughputMeasurement::stream_sample_rate` | `stream_sample_rate_hz` |
|
|
140
|
+
| `LinkBudgetVerdict::sample_rate`, `fit_span_hz` | `sample_rate_hz`, `fit_bandwidth_hz` |
|
|
141
|
+
| `RtsaMetadata` / `StreamingSdrConfig` bare quantities | unit-suffixed (`_hz`, `_s`) |
|
|
142
|
+
| `HttpSourceBuilder::frequency` / `frequency_str` / `sample_rate` / `reference_level` | `center_frequency_hz` / `center_frequency_str` / `sample_rate_hz` / `reference_level_dbm` |
|
|
143
|
+
| `HttpSinkBuilder::frequency`, `sample_rate` | `center_frequency_hz`, `sample_rate_hz` |
|
|
144
|
+
| `HttpSource::new` / `with_advanced_options`, `HttpSink::new` parameters | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
|
|
145
|
+
| `DeviceCapabilities::center_frequency`, `reference_level` | `center_frequency_hz`, `reference_level_dbm` |
|
|
146
|
+
| `PacketMetadata::sample_rate()` | `sample_rate_hz()` |
|
|
147
|
+
|
|
148
|
+
`frequency` became `center_frequency_hz`, not `frequency_hz`: it was always the
|
|
149
|
+
centre frequency. The `_str` variants take a string carrying its own units
|
|
150
|
+
(`"146.52M"`), so they keep no suffix. `DeviceCapabilities` is
|
|
151
|
+
`#[non_exhaustive]`, so callers reach those fields through the API rather than
|
|
152
|
+
by literal.
|
|
153
|
+
|
|
154
|
+
**Rust types name the device: `Aaronia*` becomes `Spectran*`** —
|
|
155
|
+
`SpectranConfig`, `SpectranSource`, `SpectranSourceBuilder`,
|
|
156
|
+
`SpectranSinkBuilder`, `SpectranSeifyDevice`, `SpectranSeifyRxStreamer`,
|
|
157
|
+
`SpectranBackend`, `SpectranSdrSource`.
|
|
158
|
+
|
|
159
|
+
The vendor name stays wherever it is a frozen contract rather than a
|
|
160
|
+
description: the `aaronia_*` C symbols and the `AaroniaSource` /
|
|
161
|
+
`AaroniaFfiError` typedefs, the crate, the PyPI package, the Python module,
|
|
162
|
+
`AaroniaSoapyDevice`, and `driver=aaronia`. That last is the load-bearing one —
|
|
163
|
+
it is typed into GQRX and SDR++ configs already in the world, where breaking it
|
|
164
|
+
reads as "device not found" with nothing pointing at the cause.
|
|
165
|
+
|
|
166
|
+
**Python takes the same vocabulary**, its four exceptions included.
|
|
167
|
+
|
|
168
|
+
| Old Python | New |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `aaronia.AaroniaConfig` | `aaronia.SpectranConfig` |
|
|
171
|
+
| `aaronia.AaroniaSource` | `aaronia.SpectranSource` |
|
|
172
|
+
| `aaronia.Aaronia{Connection,Hardware,Timeout}Error`, `AaroniaStreamClosed` | `Spectran…` |
|
|
173
|
+
| `cfg.center_freq` | `cfg.center_frequency_hz` |
|
|
174
|
+
| `cfg.sample_rate` | `cfg.sample_rate_hz` |
|
|
175
|
+
| `cfg.reference_level` | `cfg.reference_level_dbm` |
|
|
176
|
+
| `cfg.read_timeout` | `cfg.read_timeout_s` |
|
|
177
|
+
| `cfg.native_sdk` | `cfg.force_native_sdk` |
|
|
178
|
+
| `src.set_center_frequency()` / `set_sample_rate()` / `set_reference_level()` | `set_center_frequency_hz()` / `set_sample_rate_hz()` / `set_reference_level_dbm()` |
|
|
179
|
+
| `open(freq=, rate=, bandwidth=, ref_level=, read_timeout=)` | `open(center_frequency_hz=, sample_rate_hz=, bandwidth_hz=, reference_level_dbm=, read_timeout_s=)` |
|
|
180
|
+
|
|
181
|
+
`open()`'s keywords changed with them. It is the one-line front door and the
|
|
182
|
+
longer names cost the headline example a wrap, but `open(url, bandwidth=10e6)`
|
|
183
|
+
gives no way to tell Hz from MHz. `sdk=`, `url=`, `file=`, `serial=`, `format=`
|
|
184
|
+
and `scale=` carry no unit and are unchanged.
|
|
185
|
+
|
|
186
|
+
**The whole C ABI moves to `spectran_*`.** Every exported symbol, the header
|
|
187
|
+
(`include/aaronia.h` is now `include/spectran.h`), the opaque typedefs
|
|
188
|
+
(`AaroniaSource` / `AaroniaSourceBuilder` / `AaroniaSink` / `AaroniaSinkBuilder`
|
|
189
|
+
/ `AaroniaFfiError` / `CAaroniaSourceType` become `Spectran*`), and the plugin
|
|
190
|
+
class `AaroniaSoapyDevice` become `SpectranSoapyDevice`. The table below lists
|
|
191
|
+
the symbols that also changed shape; the rest changed prefix only, so
|
|
192
|
+
`aaronia_x` is `spectran_x`.
|
|
193
|
+
|
|
194
|
+
What keeps the vendor name: `driver=aaronia`, because it is typed into GQRX and
|
|
195
|
+
SDR++ configuration files already in the world and breaking it reads as "device
|
|
196
|
+
not found"; the crate, the PyPI package and the Python module, which are
|
|
197
|
+
published names; and `AARTSAAPI_*` / `AaroniaRTSAAPI.dll`, which are the
|
|
198
|
+
vendor's own.
|
|
199
|
+
|
|
200
|
+
**The C ABI is renamed with no forwarders**, so a consumer gets an undefined
|
|
201
|
+
symbol at link time and this table is the migration guide. `FfiSourceInfo`'s
|
|
202
|
+
fields moved with the header in the same commit: C reads them positionally, so
|
|
203
|
+
a stale header would go on reading the right bytes under the wrong name.
|
|
204
|
+
|
|
205
|
+
| Old C symbol | New |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `aaronia_source_builder_center_frequency` | `spectran_source_builder_center_frequency_hz` |
|
|
208
|
+
| `aaronia_source_builder_span_frequency` | `spectran_source_builder_sample_rate_hz` |
|
|
209
|
+
| `aaronia_source_builder_reference_level` | `spectran_source_builder_reference_level_dbm` |
|
|
210
|
+
| `aaronia_source_set_center_frequency` | `spectran_source_set_center_frequency_hz` |
|
|
211
|
+
| `aaronia_source_set_span_frequency` | `spectran_source_set_sample_rate_hz` |
|
|
212
|
+
| `aaronia_source_set_reference_level` | `spectran_source_set_reference_level_dbm` |
|
|
213
|
+
| `aaronia_sink_builder_center_frequency` | `spectran_sink_builder_center_frequency_hz` |
|
|
214
|
+
| `aaronia_sink_builder_sample_rate` | `spectran_sink_builder_sample_rate_hz` |
|
|
215
|
+
| `aaronia_sink_builder_trans_gain` | `spectran_sink_builder_trans_gain_db` |
|
|
216
|
+
| `aaronia_source_read_sensors` | `spectran_source_get_sensors` |
|
|
217
|
+
| `aaronia_endpoints_client_read_sensors` | `spectran_endpoints_client_get_sensors` |
|
|
218
|
+
| `FfiSourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
|
|
219
|
+
| `aaronia_source_get_gps_time(.., double*)` | `spectran_source_get_gps_time_ns(.., int64_t*)` |
|
|
220
|
+
|
|
221
|
+
The two `read_sensors` are a verb change, not a unit one. `read_` advances a
|
|
222
|
+
stream here (`spectran_source_read_samples`); sensors are a snapshot, so they
|
|
223
|
+
join `spectran_source_get_capabilities`.
|
|
224
|
+
|
|
225
|
+
**GPS time changes type as well as name.** `gps_time_ns() -> Option<i64>`, and
|
|
226
|
+
`int64_t*` in C, matching `last_timestamp_ns`. The conversion moved into the
|
|
227
|
+
library because epoch nanoseconds land near `1.7e18`, where an `f64`'s step is
|
|
228
|
+
256 ns — a single `seconds * 1e9` discards tens of nanoseconds the reading
|
|
229
|
+
still had. The SoapySDR plugin's own whole/fractional split is deleted.
|
|
230
|
+
`GpsState::time` is now `time_s`, and the C function accepts a `NULL`
|
|
231
|
+
out-parameter meaning "is there a fix?".
|
|
232
|
+
|
|
233
|
+
It does **not** improve precision. The device reports `gpstime` as an `f64`,
|
|
234
|
+
whose step at present-day epoch values is about 240 ns, and nothing downstream
|
|
235
|
+
recovers what was never there. `utils::gps_seconds_to_nanos` documents that
|
|
236
|
+
bound and a test pins it against the naive multiply. Across receivers ~240 ns
|
|
237
|
+
is ~70 m of ranging uncertainty, so correlate on the per-packet stream
|
|
238
|
+
timestamps and keep GPS for disciplining and wall-clock labelling.
|
|
239
|
+
|
|
240
|
+
`CaptureControl`'s fields gained units without moving its JSON keys: each is
|
|
241
|
+
now pinned by an explicit `#[serde(rename)]` rather than derived from the Rust
|
|
242
|
+
field name, so a later rename cannot change the wire format by accident.
|
|
243
|
+
`tests/wire_contract.rs` asserts those keys, and asserts the vendor
|
|
244
|
+
`AARTSAAPI_Packet` mirror still matches its C header field for field.
|
|
245
|
+
|
|
246
|
+
### Added
|
|
247
|
+
- **The stream clock source can be set, not just read.** `device/sclksource`
|
|
248
|
+
selects what disciplines the receiver's clock; a V6 ECO offers `Consumer`,
|
|
249
|
+
`Oscillator`, `GPS`, `PPS`, `10MHz` and three `... Provider` variants. It was
|
|
250
|
+
readable but read-only, and SoapySDR's `setClockSource` merely logged a
|
|
251
|
+
warning telling the operator to change it in RTSA-Suite.
|
|
252
|
+
`SpectranSource::set_clock_source`, `spectran_source_set_clock_source` and the
|
|
253
|
+
plugin's `setClockSource` now write it on both backends — native SDK via
|
|
254
|
+
`ConfigSetString`, HTTP via a `simpleconfig` PUT that is read back to confirm,
|
|
255
|
+
because a `/remoteconfig` PUT naming a block outside the running mission
|
|
256
|
+
answers 200 and changes nothing. A confirmed mismatch is an **error** naming
|
|
257
|
+
the sources the device does offer: being told you are on a reference you are
|
|
258
|
+
not is worse than a failed call. A read-back yielding nothing is reported as
|
|
259
|
+
unconfirmed rather than failed.
|
|
260
|
+
|
|
261
|
+
This is what makes cross-receiver correlation possible: lock a fleet to one
|
|
262
|
+
10 MHz / PPS / GPS reference and their per-packet timestamps share a timebase.
|
|
263
|
+
What is *not* possible is a commanded synchronous start — the SDK's C API has
|
|
264
|
+
no set-time and no arm-at-time entry point, so multi-device work is a shared
|
|
265
|
+
reference plus post-alignment on timestamps.
|
|
266
|
+
|
|
267
|
+
### Fixed
|
|
268
|
+
- **A trailing slash in `device_type` no longer produces a malformed open
|
|
269
|
+
string.** The family/mode split asked whether the string contained a slash, so
|
|
270
|
+
`"spectranv6/"` counted as already mode-qualified and went to
|
|
271
|
+
`AARTSAAPI_OpenDevice` verbatim. The mode is now the text after the *first*
|
|
272
|
+
slash, and an empty one means none was given. The ECO was the sharp edge:
|
|
273
|
+
`"spectranv6eco/"` also slipped past the `spectranv6eco/raw` → `iqreceiver`
|
|
274
|
+
remap, so the device was asked for a pipeline it does not have and answered
|
|
275
|
+
with a result code that said nothing about the typo.
|
|
276
|
+
|
|
277
|
+
A `device_type` naming no family (`""`, `"/"`, `"/raw"`) is no longer
|
|
278
|
+
completed into a family-less `"/raw"`; it comes back untouched and is refused
|
|
279
|
+
at both points where a device type reaches the SDK. Both guards are needed
|
|
280
|
+
because enumeration runs first, and an empty family simply enumerates to
|
|
281
|
+
nothing — the old path reported "no devices found" and never mentioned the
|
|
282
|
+
typo. `device_family` and `device_open_mode` stay infallible: a typo in one
|
|
283
|
+
field is not a reason to make four accessors return `Result`.
|
|
284
|
+
- **An over-wide sample rate no longer reaches the hardware before it is
|
|
285
|
+
refused.** `configure_iq_receiver` checked the IQ-mode constraint as its last
|
|
286
|
+
act, so a rate the receiver clock cannot carry was written to
|
|
287
|
+
`main/centerfreq` and `main/spanfreq` first and rejected afterwards — leaving
|
|
288
|
+
the device holding the misconfiguration the check exists to prevent, and per
|
|
289
|
+
the SDK quietly emitting corrupted samples if anything started the stream.
|
|
290
|
+
The check now runs before the first write.
|
|
291
|
+
|
|
292
|
+
It checks against the clock the call *leaves in place*, which is not always
|
|
293
|
+
the one the device holds on entry. Raw mode writes the clock itself, so that
|
|
294
|
+
write is what counts: a V6 left on its 245.76 MHz clock would otherwise pass a
|
|
295
|
+
150 MS/s request that stops being valid the moment this same call drops the
|
|
296
|
+
clock to 92.16 MHz. Every other mode skips that write, so there the live
|
|
297
|
+
setting is the honest number. The read-back after the writes is kept, and is
|
|
298
|
+
what `receiver_clock_hz()` reports. Native SDK only; the HTTP backend already
|
|
299
|
+
validated at the API boundary.
|
|
300
|
+
|
|
301
|
+
## [v0.9.0] - 2026-09-08
|
|
302
|
+
|
|
7
303
|
### Added
|
|
8
304
|
- **SoapySDR: device sensors.** The plugin exposed one sensor
|
|
9
305
|
(`cumulative_drops`); it now surfaces the device's live telemetry from
|
|
@@ -3092,7 +3092,7 @@ dependencies = [
|
|
|
3092
3092
|
|
|
3093
3093
|
[[package]]
|
|
3094
3094
|
name = "python-aaronia"
|
|
3095
|
-
version = "0.
|
|
3095
|
+
version = "0.11.0"
|
|
3096
3096
|
dependencies = [
|
|
3097
3097
|
"arrow",
|
|
3098
3098
|
"num-complex",
|
|
@@ -3621,7 +3621,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3621
3621
|
|
|
3622
3622
|
[[package]]
|
|
3623
3623
|
name = "sdr-aaronia-rs"
|
|
3624
|
-
version = "0.
|
|
3624
|
+
version = "0.11.0"
|
|
3625
3625
|
dependencies = [
|
|
3626
3626
|
"anyhow",
|
|
3627
3627
|
"bitflags 2.13.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "sdr-aaronia-rs"
|
|
3
3
|
description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
|
|
4
4
|
license = "GPL-3.0-or-later"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.11.0"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
|
|
8
8
|
readme = "README.md"
|
|
@@ -4,7 +4,7 @@ This document describes the architecture of the `sdr-aaronia-rs` crate, which pr
|
|
|
4
4
|
|
|
5
5
|
## 1. Overview
|
|
6
6
|
|
|
7
|
-
The Aaronia ecosystem offers three ways to access Spectran hardware: the native RTSA-Suite PRO SDK, the HTTP streaming API, and recorded RTSA files. This crate wraps all three behind a single deterministic facade, `
|
|
7
|
+
The Aaronia ecosystem offers three ways to access Spectran hardware: the native RTSA-Suite PRO SDK, the HTTP streaming API, and recorded RTSA files. This crate wraps all three behind a single deterministic facade, `SpectranSource`, so callers can work with Aaronia hardware without depending on the underlying transport.
|
|
8
8
|
|
|
9
9
|
## 2. Architecture
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ The crate is built around an automatic source-detection router that instantiates
|
|
|
13
13
|
```mermaid
|
|
14
14
|
graph TB
|
|
15
15
|
subgraph "sdr-aaronia-rs Unified API"
|
|
16
|
-
API[
|
|
16
|
+
API[SpectranSourceBuilder]
|
|
17
17
|
Detection[Deterministic Auto-Detection]
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -32,13 +32,13 @@ graph TB
|
|
|
32
32
|
|
|
33
33
|
## 3. Source Selection
|
|
34
34
|
|
|
35
|
-
`
|
|
35
|
+
`SpectranSource::new(config)` selects a backend using strict, deterministic rules:
|
|
36
36
|
|
|
37
37
|
| Configuration | Backend | Fallback | Platforms |
|
|
38
38
|
|---|---|---|---|
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
39
|
+
| `SpectranConfig::from_file("recording.rtsa")` | File | None — fails if the file is missing | All |
|
|
40
|
+
| `SpectranConfig::from_http("http://192.168.1.100")` | HTTP | None — fails if unreachable | All |
|
|
41
|
+
| `SpectranConfig::default()` (nothing specified) | Native SDK, when the `native-sdk` feature is enabled and the SDK library is found | Connects to the default HTTP endpoint (`http://localhost:54664`) otherwise. Since `native-sdk` is not a default feature, a stock build always resolves to HTTP. | All (SDK itself is Windows/Linux only) |
|
|
42
42
|
| Force option, e.g. `config.force_native_sdk()` | Forced type | None — fails if unavailable | Depends on forced type |
|
|
43
43
|
|
|
44
44
|
## 4. Backend Implementations
|
|
@@ -58,7 +58,7 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
|
|
|
58
58
|
|
|
59
59
|
- Implements the V9 RTSA HTTP specification.
|
|
60
60
|
- Supports Basic Auth and token-based authentication.
|
|
61
|
-
- **Formats**: supports the `Json`, `Int16`, `Float16`, and `Float32` wire formats, maintaining a persistent chunked-transfer buffer across packet boundaries. The default is `Float32` (binary, lossless); `Int16` is opt-in via `
|
|
61
|
+
- **Formats**: supports the `Json`, `Int16`, `Float16`, and `Float32` wire formats, maintaining a persistent chunked-transfer buffer across packet boundaries. The default is `Float32` (binary, lossless); `Int16` is opt-in via `SpectranConfig::stream_format()` / `stream_scale()`.
|
|
62
62
|
- Beyond streaming, `HttpEndpointsClient` exposes device control and health telemetry (as a generic configuration tree) within a headless Rust process.
|
|
63
63
|
|
|
64
64
|
### 4.3 RTSA Files
|
|
@@ -69,21 +69,21 @@ 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, `
|
|
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:
|
|
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
|
-
- **HTTP**: `
|
|
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
|
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.
|
|
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 `
|
|
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`.
|
|
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 (`
|
|
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
87
|
|
|
88
88
|
## 7. FutureSDR Integration
|
|
89
89
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aaronia
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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+)
|
|
@@ -63,21 +63,24 @@ fix for each failure.
|
|
|
63
63
|
```python
|
|
64
64
|
import aaronia
|
|
65
65
|
|
|
66
|
-
with aaronia.open(
|
|
66
|
+
with aaronia.open(
|
|
67
|
+
"http://localhost:54664", center_frequency_hz=2.44e9, bandwidth_hz=10e6
|
|
68
|
+
) as src:
|
|
67
69
|
for block in src.blocks(65536): # numpy complex64 arrays
|
|
68
70
|
process(block)
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
`aaronia.open()` connects and starts streaming in one call. `
|
|
73
|
+
`aaronia.open()` connects and starts streaming in one call. `bandwidth_hz`
|
|
72
74
|
asks for that much usable spectrum and picks a sample rate the hardware
|
|
73
|
-
can actually run; pass `
|
|
75
|
+
can actually run; pass `sample_rate_hz=` instead to name one exactly.
|
|
76
|
+
Use
|
|
74
77
|
`file="capture.rtsa"` in place of the URL to play back a recording.
|
|
75
78
|
|
|
76
79
|
Iterating with `blocks()` ends when the stream closes. To read on your
|
|
77
80
|
own schedule, or for Apache Arrow:
|
|
78
81
|
|
|
79
82
|
```python
|
|
80
|
-
src = aaronia.open(
|
|
83
|
+
src = aaronia.open(center_frequency_hz=2.44e9, sample_rate_hz=15.36e6, format="I16")
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
`sdk=True` opens the device through the native SDK instead of a server
|
|
@@ -85,17 +88,17 @@ src = aaronia.open(freq=2.44e9, rate=15.36e6, format="I16")
|
|
|
85
88
|
the SDK is missing — a capture never quietly comes from another backend:
|
|
86
89
|
|
|
87
90
|
```python
|
|
88
|
-
src = aaronia.open(sdk=True,
|
|
89
|
-
src = aaronia.open(sdk=True, serial="C2-P-03000105",
|
|
91
|
+
src = aaronia.open(sdk=True, center_frequency_hz=2.44e9, sample_rate_hz=15.36e6)
|
|
92
|
+
src = aaronia.open(sdk=True, serial="C2-P-03000105", center_frequency_hz=2.44e9)
|
|
90
93
|
samples = src.read_samples_numpy(65536) # numpy complex64 array
|
|
91
94
|
batch = src.read_samples_arrow(65536) # pyarrow FixedSizeListArray of [re, im]
|
|
92
|
-
src.
|
|
95
|
+
src.set_center_frequency_hz(2.41e9) # live retune, no teardown
|
|
93
96
|
print(src.cumulative_drops(), src.take_overrun(), src.last_timestamp_ns())
|
|
94
97
|
src.stop_streaming()
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
For full control, build
|
|
98
|
-
`
|
|
100
|
+
For full control, build a `SpectranConfig` and pass it to
|
|
101
|
+
`SpectranSource.start_streaming()`; `open()` is a shorthand for the
|
|
99
102
|
common fields.
|
|
100
103
|
|
|
101
104
|
The
|
|
@@ -154,18 +157,18 @@ most of the capture was dropped. Either half-width format fits.
|
|
|
154
157
|
quantisation step is `1 / scale`, and the default of 16384 gives a step
|
|
155
158
|
of 6.1e-5. A quiet band's noise floor is smaller than that — on the
|
|
156
159
|
same server, **68% of `I16` samples came back exactly zero** while
|
|
157
|
-
`F32` had none. Pass `scale=`, or lower `
|
|
160
|
+
`F32` had none. Pass `scale=`, or lower `reference_level_dbm` for more
|
|
158
161
|
gain:
|
|
159
162
|
|
|
160
163
|
```python
|
|
161
|
-
aaronia.open(url,
|
|
164
|
+
aaronia.open(url, center_frequency_hz=2.44e9, sample_rate_hz=15.36e6, format="I16", scale=1e6)
|
|
162
165
|
```
|
|
163
166
|
|
|
164
167
|
At `scale=1e6` the zero fraction measured 0.0% and the amplitude
|
|
165
168
|
matched `F32`. `F16` needs no such tuning, which makes it the simpler
|
|
166
169
|
choice when the link is the constraint.
|
|
167
170
|
|
|
168
|
-
## Configuration (`
|
|
171
|
+
## Configuration (`SpectranConfig`)
|
|
169
172
|
|
|
170
173
|
Every field is readable and writable.
|
|
171
174
|
|
|
@@ -174,14 +177,14 @@ Every field is readable and writable.
|
|
|
174
177
|
| `http_base_url` | RTSA-Suite HTTP server URL; pins the HTTP backend |
|
|
175
178
|
| `file_path` | Path to a recorded `.rtsa` file; pins the file backend |
|
|
176
179
|
| `device_serial` | Device selection for the native-SDK backend |
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
180
|
+
| `force_native_sdk` | `True` pins the source to the native SDK; missing SDK is an error |
|
|
181
|
+
| `center_frequency_hz` | Center frequency, Hz |
|
|
182
|
+
| `sample_rate_hz` | IQ sample rate (Fs), Hz |
|
|
183
|
+
| `reference_level_dbm` | Reference level, dBm |
|
|
181
184
|
| `format` | HTTP wire format: `"F32"`, `"F16"` or `"I16"`. `I16` is the low-bandwidth network mode |
|
|
182
185
|
| `scale` | Integer encode multiplier for `I16` (see below). None uses the server default |
|
|
183
186
|
| `receiver_channel` | `"Rx1"` (default), `"Rx2"`, or `"Rx1And2"` (native SDK, full V6) |
|
|
184
|
-
| `
|
|
187
|
+
| `read_timeout_s` | Seconds a blocking read waits before `SpectranTimeoutError` (default `30.0`) |
|
|
185
188
|
| `auto_reconnect` | Reconnect the HTTP stream after a drop (default `True`) |
|
|
186
189
|
|
|
187
190
|
Unknown `format`/`receiver_channel` strings raise `ValueError` instead of
|
|
@@ -194,8 +197,8 @@ silently defaulting.
|
|
|
194
197
|
indefinitely. This is not zero-copy; one copy is the accurate count.
|
|
195
198
|
- **Blocking calls release the GIL.** Other Python threads keep running;
|
|
196
199
|
`KeyboardInterrupt` is delivered between calls. Reads block until
|
|
197
|
-
`count` samples arrive or `cfg.
|
|
198
|
-
elapse, which raises `
|
|
200
|
+
`count` samples arrive or `cfg.read_timeout_s` seconds (default 30)
|
|
201
|
+
elapse, which raises `SpectranTimeoutError`.
|
|
199
202
|
- **Connecting retries transient failures**, up to 4 attempts within a
|
|
200
203
|
10 second budget, so a cold `*.local` hostname or a server that is
|
|
201
204
|
still starting does not fail on the first attempt.
|
|
@@ -203,12 +206,12 @@ silently defaulting.
|
|
|
203
206
|
enabled, which is the default. The reader reopens the stream,
|
|
204
207
|
re-applies the current tuning, and flags the first read after the gap
|
|
205
208
|
through `take_overrun()`. After five failed attempts the stream ends
|
|
206
|
-
and reads raise `
|
|
207
|
-
- **Typed exceptions.** `
|
|
208
|
-
`
|
|
209
|
+
and reads raise `SpectranStreamClosed`.
|
|
210
|
+
- **Typed exceptions.** `SpectranConnectionError` (unreachable endpoint),
|
|
211
|
+
`SpectranTimeoutError`, `SpectranHardwareError` (device and SDK errors)
|
|
209
212
|
and `ValueError` (invalid configuration), mapped from the Rust error
|
|
210
213
|
enum with the full cause chain in the message.
|
|
211
|
-
`
|
|
214
|
+
`SpectranStreamClosed` subclasses `SpectranConnectionError` and means
|
|
212
215
|
the stream finished rather than failed; `blocks()` ends on it, while
|
|
213
216
|
a timeout or transport failure still raises.
|
|
214
217
|
- **Dual-channel** reads (`receiver_channel = "Rx1And2"` with
|
|
@@ -227,7 +230,7 @@ silently defaulting.
|
|
|
227
230
|
| `read_samples_numpy(count)` | NumPy `complex64` array |
|
|
228
231
|
| `read_samples_arrow(count)` | PyArrow `FixedSizeListArray` of `[re, im]` float32 pairs |
|
|
229
232
|
| `read_samples_dual_numpy(count)` | `(rx1, rx2)` NumPy arrays (dual-channel captures) |
|
|
230
|
-
| `
|
|
233
|
+
| `set_center_frequency_hz(hz)` / `set_sample_rate_hz(hz)` / `set_reference_level_dbm(dbm)` | Live retuning |
|
|
231
234
|
| `cumulative_drops()` | Timestamp gaps detected in the stream so far (gap events, not samples) |
|
|
232
235
|
| `take_overrun()` | True once per detected receive-side overrun |
|
|
233
236
|
| `last_timestamp_ns()` | Epoch-ns timestamp of the last received block (HTTP backend; 0 otherwise) |
|
|
@@ -236,7 +239,7 @@ silently defaulting.
|
|
|
236
239
|
|
|
237
240
|
| Function | Purpose |
|
|
238
241
|
| --- | --- |
|
|
239
|
-
| `open(url=None, *,
|
|
242
|
+
| `open(url=None, *, center_frequency_hz, sample_rate_hz, bandwidth_hz, reference_level_dbm, file, format, scale, read_timeout_s)` | Configure, connect and start streaming in one call |
|
|
240
243
|
| `sample_rates()` | The V6 ECO's sample rates, highest first (see [Sample rates](#sample-rates)) |
|
|
241
244
|
| `sample_rate_for_bandwidth(hz)` | Lowest rate covering that much spectrum |
|
|
242
245
|
| `diagnose(url)` | `(ok, message, fix)` for each setup check; what `aaronia-doctor` prints |
|
|
@@ -12,10 +12,10 @@ To use the Seify plugin, enable the `seify` feature in your `Cargo.toml`:
|
|
|
12
12
|
|
|
13
13
|
```toml
|
|
14
14
|
[dependencies]
|
|
15
|
-
sdr-aaronia-rs = { version = "0.
|
|
15
|
+
sdr-aaronia-rs = { version = "0.10", features = ["seify"] }
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Instantiate the device with `
|
|
18
|
+
Instantiate the device with `SpectranSeifyDevice::from_args` and use it directly (or via `seify::dev::DynDeviceBackend`). The backend is **not** part of seify's built-in enumeration registry — `seify::enumerate()` will not discover it.
|
|
19
19
|
|
|
20
20
|
`url=` selects the HTTP backend and `file=` playback. `sdk=true` (or
|
|
21
21
|
`serial=<device serial>`) selects the Aaronia native SDK; that needs the
|
|
@@ -23,13 +23,15 @@ crate built with both features — `features = ["seify", "native-sdk"]` —
|
|
|
23
23
|
on Windows or Linux with RTSA-Suite PRO installed. Without the feature
|
|
24
24
|
the request is a clean error, never a fallback to HTTP.
|
|
25
25
|
|
|
26
|
-
`
|
|
26
|
+
`SpectranSeifyDevice` owns a tokio runtime. Drop it from synchronous
|
|
27
27
|
code: dropping it inside an `async` context (a `#[tokio::test]`, a task)
|
|
28
28
|
is a tokio panic, "Cannot drop a runtime in a context where blocking is
|
|
29
29
|
not allowed".
|
|
30
30
|
|
|
31
|
-
```rust
|
|
32
|
-
|
|
31
|
+
```rust,no_run
|
|
32
|
+
# #[cfg(feature = "seify")]
|
|
33
|
+
# fn demo() {
|
|
34
|
+
use sdr_aaronia_rs::seify_impl::SpectranSeifyDevice;
|
|
33
35
|
use seify::{Args, RxDevice, RxStreamer, DeviceInfo};
|
|
34
36
|
use seify::dev::DynDeviceBackend;
|
|
35
37
|
|
|
@@ -38,7 +40,7 @@ let mut args = Args::new();
|
|
|
38
40
|
args.set("url", "http://localhost:54664");
|
|
39
41
|
|
|
40
42
|
// Open the device
|
|
41
|
-
let dev =
|
|
43
|
+
let dev = SpectranSeifyDevice::from_args(&args).expect("Failed to open Aaronia device");
|
|
42
44
|
|
|
43
45
|
// Start streaming (CF32 complex floats)
|
|
44
46
|
let rx = dev.rx_device().expect("Failed to get RX device");
|
|
@@ -48,6 +50,8 @@ streamer.activate_at(None).expect("Failed to activate stream");
|
|
|
48
50
|
let mut buffer = [num_complex::Complex32::new(0.0, 0.0); 1024];
|
|
49
51
|
let read = streamer.read(&mut [&mut buffer], 1_000_000).expect("Read failed");
|
|
50
52
|
println!("Read {} samples", read);
|
|
53
|
+
# }
|
|
54
|
+
# fn main() {}
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
> **Note on Bandwidth:** Seify's `RxStreamer` trait natively expects `Complex32` (CF32) buffers, so data will be transferred as 32-bit floats.
|
|
@@ -227,3 +231,32 @@ print(sdr.listBandwidths(SoapySDR.SOAPY_SDR_RX, 0))
|
|
|
227
231
|
`setBandwidth` maps the request to the nearest sample-rate rung and
|
|
228
232
|
drives `setSampleRate`; the two stay consistent, so setting either
|
|
229
233
|
updates the other.
|
|
234
|
+
|
|
235
|
+
### Clock source
|
|
236
|
+
|
|
237
|
+
`device/sclksource` selects what disciplines the receiver's clock. A V6 ECO
|
|
238
|
+
offers `Consumer`, `Oscillator`, `GPS`, `PPS`, `10MHz` and three `... Provider`
|
|
239
|
+
variants; `listClockSources` reports whatever the device actually offers.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
print(sdr.listClockSources()) # ['Consumer', 'Oscillator', 'GPS', 'PPS', '10MHz', ...]
|
|
243
|
+
sdr.setClockSource("10MHz") # lock to a house 10 MHz reference
|
|
244
|
+
print(sdr.getClockSource())
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`setClockSource` now writes the device; it previously only reported the current
|
|
248
|
+
source and told you to change it in RTSA-Suite. Over HTTP the write is read back
|
|
249
|
+
to confirm, because a `/remoteconfig` PUT naming a block that is not in the
|
|
250
|
+
running mission answers 200 and changes nothing.
|
|
251
|
+
|
|
252
|
+
If the device does not take the source — usually because it does not offer it —
|
|
253
|
+
the plugin logs a `SOAPY_SDR_ERROR` naming the sources it *does* offer, and does
|
|
254
|
+
not cache the requested value. `getClockSource()` therefore keeps reporting what
|
|
255
|
+
the device is actually running on, never what you asked for.
|
|
256
|
+
|
|
257
|
+
**Why this matters for multi-device work.** Point several receivers at one
|
|
258
|
+
10 MHz / PPS / GPS reference and their per-packet hardware timestamps share a
|
|
259
|
+
timebase, so captures can be correlated afterwards. There is no commanded
|
|
260
|
+
synchronous start: the SDK exposes no set-time and no arm-at-time call, so you
|
|
261
|
+
align on timestamps in post rather than arming devices at an instant.
|
|
262
|
+
|