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.
Files changed (94) hide show
  1. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/CHANGELOG.md +296 -0
  2. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/Cargo.lock +2 -2
  3. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/Cargo.toml +1 -1
  4. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/DESIGN.md +11 -11
  5. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/PKG-INFO +29 -26
  6. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/PLUGINS.md +39 -6
  7. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/README.md +34 -13
  8. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/FILESPEC.md +7 -12
  9. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/HTTPSPEC.md +29 -34
  10. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/QUICKSTART.md +25 -23
  11. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/SDKSPEC.md +172 -66
  12. python_aaronia-0.11.0/docs/SYNC.md +118 -0
  13. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/USAGE.md +62 -54
  14. python_aaronia-0.11.0/docs/VERIFICATION.md +73 -0
  15. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/channel_hopping.rs +5 -5
  16. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/dump_metadata.rs +9 -9
  17. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/http_iq_quickstart.rs +6 -6
  18. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/native_sdk_basic.rs +10 -5
  19. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/native_sdk_transmit.rs +12 -10
  20. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/noaa_scanner.rs +39 -34
  21. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/python_arrow_example.py +6 -6
  22. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/read_rtsa_file.rs +3 -3
  23. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/soapy_python_example.py +3 -3
  24. python_aaronia-0.9.0/include/aaronia.h → python_aaronia-0.11.0/include/spectran.h +116 -98
  25. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/Cargo.toml +1 -1
  26. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/README.md +28 -25
  27. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/aaronia.pyi +69 -42
  28. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/src/lib.rs +135 -101
  29. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/python-aaronia/test_basic.py +26 -24
  30. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/ci-local.sh +20 -0
  31. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/fake-rtsa-server.py +1 -1
  32. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/validate-iq-live.py +10 -2
  33. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/CMakeLists.txt +3 -3
  34. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/README.md +43 -6
  35. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/Registration.cpp +54 -45
  36. python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.cpp → python_aaronia-0.11.0/soapy-aaronia/SpectranSoapyDevice.cpp +241 -130
  37. python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.hpp → python_aaronia-0.11.0/soapy-aaronia/SpectranSoapyDevice.hpp +22 -7
  38. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/c_api.rs +600 -400
  39. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/file_source.rs +71 -71
  40. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_endpoints.rs +339 -57
  41. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_sink.rs +19 -19
  42. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_source.rs +90 -85
  43. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/http_streaming.rs +20 -14
  44. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/lib.rs +26 -17
  45. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/link_budget.rs +65 -63
  46. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/native_sdk.rs +550 -122
  47. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdk_sink.rs +52 -20
  48. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdk_source.rs +84 -173
  49. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/sdr_source_impl.rs +103 -46
  50. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/seify_impl.rs +41 -41
  51. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/unified_sink.rs +35 -28
  52. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/unified_source.rs +763 -309
  53. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/utils.rs +281 -14
  54. python_aaronia-0.11.0/tests/abi_drift.rs +311 -0
  55. python_aaronia-0.11.0/tests/c_api_test.rs +107 -0
  56. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_mock_test.rs +159 -5
  57. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_resilience_test.rs +15 -15
  58. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/http_sink_test.rs +2 -2
  59. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/integration_test.rs +6 -6
  60. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/live_smoke.rs +131 -22
  61. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/native_sdk_live.rs +150 -53
  62. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/properties.rs +1 -1
  63. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/sdr_source_impl_test.rs +14 -14
  64. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/spec_coverage.rs +1 -1
  65. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/test_cw_meta.rs +3 -3
  66. python_aaronia-0.11.0/tests/wire_contract.rs +232 -0
  67. python_aaronia-0.9.0/docs/VERIFICATION.md +0 -79
  68. python_aaronia-0.9.0/tests/c_api_test.rs +0 -72
  69. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.cargo/config.toml +0 -0
  70. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.gitattributes +0 -0
  71. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/.gitignore +0 -0
  72. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/CONTRIBUTING.md +0 -0
  73. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/LICENSE +0 -0
  74. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/decompress_block.rs +0 -0
  75. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/deinterleave_dual_iq.rs +0 -0
  76. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/parse_int16_packet.rs +0 -0
  77. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/benches/rtsa_open_and_read.rs +0 -0
  78. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/deny.toml +0 -0
  79. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/docs/APPS.md +0 -0
  80. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/examples/device_control.rs +0 -0
  81. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/packaging/homebrew/README.md +0 -0
  82. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  83. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/pyproject.toml +0 -0
  84. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/native-sdk-validate.ps1 +0 -0
  85. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/scripts/sdk-container-test.sh +0 -0
  86. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  87. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/soapy-aaronia/packaging/install.sh +0 -0
  88. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/decompression.rs +0 -0
  89. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/detection.rs +0 -0
  90. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/src/error.rs +0 -0
  91. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/native_sdk_load.rs +0 -0
  92. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/properties.proptest-regressions +0 -0
  93. {python_aaronia-0.9.0 → python_aaronia-0.11.0}/tests/rtsa_negative_test.rs +0 -0
  94. {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.9.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.9.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.9.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, `AaroniaSource`, so callers can work with Aaronia hardware without depending on the underlying transport.
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[AaroniaSourceBuilder]
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
- `AaroniaSource::new(config)` selects a backend using strict, deterministic rules:
35
+ `SpectranSource::new(config)` selects a backend using strict, deterministic rules:
36
36
 
37
37
  | Configuration | Backend | Fallback | Platforms |
38
38
  |---|---|---|---|
39
- | `AaroniaConfig::from_file("recording.rtsa")` | File | None — fails if the file is missing | All |
40
- | `AaroniaConfig::from_http("http://192.168.1.100")` | HTTP | None — fails if unreachable | All |
41
- | `AaroniaConfig::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) |
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 `AaroniaConfig::stream_format()` / `stream_scale()`.
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, `AaroniaSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
72
+ 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**: `AaroniaSource::set_center_frequency` 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.)
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 `AaroniaSource::pending_overrun`, which `take_overrun()` reads and clears. `single_channel_pump` and `hop_pump` (`sdr_source_impl.rs`) call `take_overrun()` once per emitted `IqPacket`, so a drop detected anywhere since the last read surfaces as `IqPacket::overrun = true` on the next packet. This is a per-call signal, not a precise per-sample one, since drop timing is lost once chunks merge into the flat `sample_buffer`.
82
+ The HTTP reader task (spawned by `init_http_source` in `unified_source.rs`) runs a `DropDetector` over each packet's `start_time`/`end_time` metadata. A timestamp gap larger than the tolerance latches `SpectranSource::pending_overrun`, which `take_overrun()` reads and clears. `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 (`AaroniaSdrSource::start`) is wrapped in `catch_unwind`: a panic inside the pump loop is logged rather than silently unwinding the 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.9.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("http://localhost:54664", freq=2.44e9, bandwidth=10e6) as src:
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. `bandwidth`
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 `rate=` instead to name one exactly. Use
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(freq=2.44e9, rate=15.36e6, format="I16")
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, freq=2.44e9, rate=15.36e6)
89
- src = aaronia.open(sdk=True, serial="C2-P-03000105", freq=2.44e9)
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.set_center_frequency(2.41e9) # live retune, no teardown
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 an `AaroniaConfig` and pass it to
98
- `AaroniaSource.start_streaming()`; `open()` is a shorthand for the
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 `reference_level` for more
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, freq=2.44e9, rate=15.36e6, format="I16", scale=1e6)
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 (`AaroniaConfig`)
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
- | `native_sdk` | `True` pins the source to the native SDK; missing SDK is an error |
178
- | `center_freq` | Center frequency, Hz |
179
- | `sample_rate` | IQ sample rate, Hz (the Aaronia "span") |
180
- | `reference_level` | Reference level, dBm |
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
- | `read_timeout` | Seconds a blocking read waits before `AaroniaTimeoutError` (default `30.0`) |
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.read_timeout` seconds (default 30)
198
- elapse, which raises `AaroniaTimeoutError`.
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 `AaroniaStreamClosed`.
207
- - **Typed exceptions.** `AaroniaConnectionError` (unreachable endpoint),
208
- `AaroniaTimeoutError`, `AaroniaHardwareError` (device and SDK errors)
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
- `AaroniaStreamClosed` subclasses `AaroniaConnectionError` and means
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
- | `set_center_frequency(hz)` / `set_sample_rate(hz)` / `set_reference_level(dbm)` | Live retuning |
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, *, freq, rate, bandwidth, ref_level, file, format, scale, read_timeout)` | Configure, connect and start streaming in one call |
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.8", features = ["seify"] }
15
+ sdr-aaronia-rs = { version = "0.10", features = ["seify"] }
16
16
  ```
17
17
 
18
- Instantiate the device with `AaroniaSeifyDevice::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.
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
- `AaroniaSeifyDevice` owns a tokio runtime. Drop it from synchronous
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
- use sdr_aaronia_rs::seify_impl::AaroniaSeifyDevice;
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 = AaroniaSeifyDevice::from_args(&args).expect("Failed to open Aaronia device");
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
+