python-aaronia 0.9.0__tar.gz → 0.10.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 (92) hide show
  1. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/CHANGELOG.md +187 -1
  2. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/Cargo.lock +2 -2
  3. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/Cargo.toml +1 -1
  4. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/DESIGN.md +11 -11
  5. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/PKG-INFO +29 -26
  6. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/PLUGINS.md +39 -6
  7. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/README.md +33 -13
  8. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/FILESPEC.md +7 -12
  9. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/HTTPSPEC.md +29 -34
  10. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/QUICKSTART.md +25 -23
  11. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/SDKSPEC.md +155 -65
  12. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/USAGE.md +62 -54
  13. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/VERIFICATION.md +2 -1
  14. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/channel_hopping.rs +5 -5
  15. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/dump_metadata.rs +9 -9
  16. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/http_iq_quickstart.rs +6 -6
  17. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/native_sdk_basic.rs +10 -5
  18. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/native_sdk_transmit.rs +7 -7
  19. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/noaa_scanner.rs +39 -34
  20. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/python_arrow_example.py +6 -6
  21. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/read_rtsa_file.rs +3 -3
  22. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/soapy_python_example.py +3 -3
  23. python_aaronia-0.9.0/include/aaronia.h → python_aaronia-0.10.0/include/spectran.h +104 -98
  24. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/python-aaronia/Cargo.toml +1 -1
  25. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/python-aaronia/README.md +28 -25
  26. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/python-aaronia/aaronia.pyi +51 -42
  27. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/python-aaronia/src/lib.rs +110 -101
  28. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/python-aaronia/test_basic.py +26 -24
  29. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/scripts/ci-local.sh +20 -0
  30. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/scripts/fake-rtsa-server.py +1 -1
  31. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/scripts/validate-iq-live.py +10 -2
  32. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/soapy-aaronia/CMakeLists.txt +3 -3
  33. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/soapy-aaronia/README.md +4 -3
  34. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/soapy-aaronia/Registration.cpp +44 -44
  35. python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.cpp → python_aaronia-0.10.0/soapy-aaronia/SpectranSoapyDevice.cpp +116 -113
  36. python_aaronia-0.9.0/soapy-aaronia/AaroniaSoapyDevice.hpp → python_aaronia-0.10.0/soapy-aaronia/SpectranSoapyDevice.hpp +7 -7
  37. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/c_api.rs +478 -398
  38. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/file_source.rs +71 -71
  39. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/http_endpoints.rs +136 -55
  40. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/http_sink.rs +19 -19
  41. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/http_source.rs +90 -85
  42. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/http_streaming.rs +20 -14
  43. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/lib.rs +22 -17
  44. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/link_budget.rs +65 -63
  45. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/native_sdk.rs +300 -109
  46. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/sdk_sink.rs +41 -12
  47. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/sdk_source.rs +84 -173
  48. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/sdr_source_impl.rs +34 -34
  49. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/seify_impl.rs +41 -41
  50. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/unified_sink.rs +23 -23
  51. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/unified_source.rs +352 -303
  52. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/utils.rs +185 -14
  53. python_aaronia-0.10.0/tests/abi_drift.rs +311 -0
  54. python_aaronia-0.10.0/tests/c_api_test.rs +72 -0
  55. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/http_mock_test.rs +159 -5
  56. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/http_resilience_test.rs +15 -15
  57. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/http_sink_test.rs +2 -2
  58. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/integration_test.rs +6 -6
  59. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/live_smoke.rs +38 -22
  60. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/native_sdk_live.rs +57 -53
  61. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/properties.rs +1 -1
  62. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/sdr_source_impl_test.rs +14 -14
  63. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/spec_coverage.rs +1 -1
  64. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/test_cw_meta.rs +3 -3
  65. python_aaronia-0.10.0/tests/wire_contract.rs +232 -0
  66. python_aaronia-0.9.0/tests/c_api_test.rs +0 -72
  67. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/.cargo/config.toml +0 -0
  68. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/.gitattributes +0 -0
  69. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/.gitignore +0 -0
  70. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/CONTRIBUTING.md +0 -0
  71. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/LICENSE +0 -0
  72. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/benches/decompress_block.rs +0 -0
  73. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/benches/deinterleave_dual_iq.rs +0 -0
  74. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/benches/parse_int16_packet.rs +0 -0
  75. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/benches/rtsa_open_and_read.rs +0 -0
  76. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/deny.toml +0 -0
  77. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/docs/APPS.md +0 -0
  78. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/examples/device_control.rs +0 -0
  79. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/packaging/homebrew/README.md +0 -0
  80. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
  81. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/pyproject.toml +0 -0
  82. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/scripts/native-sdk-validate.ps1 +0 -0
  83. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/scripts/sdk-container-test.sh +0 -0
  84. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/soapy-aaronia/packaging/install.ps1 +0 -0
  85. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/soapy-aaronia/packaging/install.sh +0 -0
  86. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/decompression.rs +0 -0
  87. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/detection.rs +0 -0
  88. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/src/error.rs +0 -0
  89. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/native_sdk_load.rs +0 -0
  90. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/properties.proptest-regressions +0 -0
  91. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/rtsa_negative_test.rs +0 -0
  92. {python_aaronia-0.9.0 → python_aaronia-0.10.0}/tests/test_cw_mag.rs +0 -0
@@ -2,7 +2,193 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [v0.10.0] - 2026-09-08
6
+
7
+ ### Breaking changes
8
+
9
+ **One name per concept, with its unit.** A field, parameter or getter holding a
10
+ bare number now carries its unit (`_hz`, `_dbm`, `_db`, `_s`); a type that
11
+ already carries one, such as `Duration`, does not. `span_frequency` is gone: it
12
+ meant the IQ *sample rate*, while the same word meant alias-free *bandwidth* in
13
+ the link budget. Old names were removed rather than deprecated, since an alias
14
+ that kept working would preserve the ambiguity this release exists to remove.
15
+ The `spanfreq` device key and the `frequencySpan` JSON field are the vendor's
16
+ vocabulary and are unchanged.
17
+
18
+ | Old | New |
19
+ | --- | --- |
20
+ | `AaroniaConfig::center_frequency` (field + builder) | `center_frequency_hz` |
21
+ | `AaroniaConfig::span_frequency` (field + builder) | `sample_rate_hz` |
22
+ | `AaroniaConfig::reference_level` (field + builder) | `reference_level_dbm` |
23
+ | `AaroniaSourceBuilder::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
24
+ | `AaroniaSource::set_center_frequency` / `set_span_frequency` / `set_reference_level` | `set_center_frequency_hz` / `set_sample_rate_hz` / `set_reference_level_dbm` |
25
+ | `SourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
26
+ | `SourceInfo::sample_rate_hz()` (method) | removed — read the field of the same name |
27
+ | `UnifiedSinkConfig::span_frequency`, `trans_gain` | `sample_rate_hz`, `trans_gain_db` |
28
+ | `ThroughputMeasurement::max_sustainable_span_hz` | `max_sustainable_bandwidth` |
29
+ | `ThroughputMeasurement::stream_sample_rate` | `stream_sample_rate_hz` |
30
+ | `LinkBudgetVerdict::sample_rate`, `fit_span_hz` | `sample_rate_hz`, `fit_bandwidth_hz` |
31
+ | `RtsaMetadata` / `StreamingSdrConfig` bare quantities | unit-suffixed (`_hz`, `_s`) |
32
+ | `HttpSourceBuilder::frequency` / `frequency_str` / `sample_rate` / `reference_level` | `center_frequency_hz` / `center_frequency_str` / `sample_rate_hz` / `reference_level_dbm` |
33
+ | `HttpSinkBuilder::frequency`, `sample_rate` | `center_frequency_hz`, `sample_rate_hz` |
34
+ | `HttpSource::new` / `with_advanced_options`, `HttpSink::new` parameters | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
35
+ | `DeviceCapabilities::center_frequency`, `reference_level` | `center_frequency_hz`, `reference_level_dbm` |
36
+ | `PacketMetadata::sample_rate()` | `sample_rate_hz()` |
37
+
38
+ `frequency` became `center_frequency_hz`, not `frequency_hz`: it was always the
39
+ centre frequency. The `_str` variants take a string carrying its own units
40
+ (`"146.52M"`), so they keep no suffix. `DeviceCapabilities` is
41
+ `#[non_exhaustive]`, so callers reach those fields through the API rather than
42
+ by literal.
43
+
44
+ **Rust types name the device: `Aaronia*` becomes `Spectran*`** —
45
+ `SpectranConfig`, `SpectranSource`, `SpectranSourceBuilder`,
46
+ `SpectranSinkBuilder`, `SpectranSeifyDevice`, `SpectranSeifyRxStreamer`,
47
+ `SpectranBackend`, `SpectranSdrSource`.
48
+
49
+ The vendor name stays wherever it is a frozen contract rather than a
50
+ description: the `aaronia_*` C symbols and the `AaroniaSource` /
51
+ `AaroniaFfiError` typedefs, the crate, the PyPI package, the Python module,
52
+ `AaroniaSoapyDevice`, and `driver=aaronia`. That last is the load-bearing one —
53
+ it is typed into GQRX and SDR++ configs already in the world, where breaking it
54
+ reads as "device not found" with nothing pointing at the cause.
55
+
56
+ **Python takes the same vocabulary**, its four exceptions included.
57
+
58
+ | Old Python | New |
59
+ | --- | --- |
60
+ | `aaronia.AaroniaConfig` | `aaronia.SpectranConfig` |
61
+ | `aaronia.AaroniaSource` | `aaronia.SpectranSource` |
62
+ | `aaronia.Aaronia{Connection,Hardware,Timeout}Error`, `AaroniaStreamClosed` | `Spectran…` |
63
+ | `cfg.center_freq` | `cfg.center_frequency_hz` |
64
+ | `cfg.sample_rate` | `cfg.sample_rate_hz` |
65
+ | `cfg.reference_level` | `cfg.reference_level_dbm` |
66
+ | `cfg.read_timeout` | `cfg.read_timeout_s` |
67
+ | `cfg.native_sdk` | `cfg.force_native_sdk` |
68
+ | `src.set_center_frequency()` / `set_sample_rate()` / `set_reference_level()` | `set_center_frequency_hz()` / `set_sample_rate_hz()` / `set_reference_level_dbm()` |
69
+ | `open(freq=, rate=, bandwidth=, ref_level=, read_timeout=)` | `open(center_frequency_hz=, sample_rate_hz=, bandwidth_hz=, reference_level_dbm=, read_timeout_s=)` |
70
+
71
+ `open()`'s keywords changed with them. It is the one-line front door and the
72
+ longer names cost the headline example a wrap, but `open(url, bandwidth=10e6)`
73
+ gives no way to tell Hz from MHz. `sdk=`, `url=`, `file=`, `serial=`, `format=`
74
+ and `scale=` carry no unit and are unchanged.
75
+
76
+ **The whole C ABI moves to `spectran_*`.** Every exported symbol, the header
77
+ (`include/aaronia.h` is now `include/spectran.h`), the opaque typedefs
78
+ (`AaroniaSource` / `AaroniaSourceBuilder` / `AaroniaSink` / `AaroniaSinkBuilder`
79
+ / `AaroniaFfiError` / `CAaroniaSourceType` become `Spectran*`), and the plugin
80
+ class `AaroniaSoapyDevice` become `SpectranSoapyDevice`. The table below lists
81
+ the symbols that also changed shape; the rest changed prefix only, so
82
+ `aaronia_x` is `spectran_x`.
83
+
84
+ What keeps the vendor name: `driver=aaronia`, because it is typed into GQRX and
85
+ SDR++ configuration files already in the world and breaking it reads as "device
86
+ not found"; the crate, the PyPI package and the Python module, which are
87
+ published names; and `AARTSAAPI_*` / `AaroniaRTSAAPI.dll`, which are the
88
+ vendor's own.
89
+
90
+ **The C ABI is renamed with no forwarders**, so a consumer gets an undefined
91
+ symbol at link time and this table is the migration guide. `FfiSourceInfo`'s
92
+ fields moved with the header in the same commit: C reads them positionally, so
93
+ a stale header would go on reading the right bytes under the wrong name.
94
+
95
+ | Old C symbol | New |
96
+ | --- | --- |
97
+ | `aaronia_source_builder_center_frequency` | `spectran_source_builder_center_frequency_hz` |
98
+ | `aaronia_source_builder_span_frequency` | `spectran_source_builder_sample_rate_hz` |
99
+ | `aaronia_source_builder_reference_level` | `spectran_source_builder_reference_level_dbm` |
100
+ | `aaronia_source_set_center_frequency` | `spectran_source_set_center_frequency_hz` |
101
+ | `aaronia_source_set_span_frequency` | `spectran_source_set_sample_rate_hz` |
102
+ | `aaronia_source_set_reference_level` | `spectran_source_set_reference_level_dbm` |
103
+ | `aaronia_sink_builder_center_frequency` | `spectran_sink_builder_center_frequency_hz` |
104
+ | `aaronia_sink_builder_sample_rate` | `spectran_sink_builder_sample_rate_hz` |
105
+ | `aaronia_sink_builder_trans_gain` | `spectran_sink_builder_trans_gain_db` |
106
+ | `aaronia_source_read_sensors` | `spectran_source_get_sensors` |
107
+ | `aaronia_endpoints_client_read_sensors` | `spectran_endpoints_client_get_sensors` |
108
+ | `FfiSourceInfo::center_frequency` / `span_frequency` / `reference_level` | `center_frequency_hz` / `sample_rate_hz` / `reference_level_dbm` |
109
+ | `aaronia_source_get_gps_time(.., double*)` | `spectran_source_get_gps_time_ns(.., int64_t*)` |
110
+
111
+ The two `read_sensors` are a verb change, not a unit one. `read_` advances a
112
+ stream here (`spectran_source_read_samples`); sensors are a snapshot, so they
113
+ join `spectran_source_get_capabilities`.
114
+
115
+ **GPS time changes type as well as name.** `gps_time_ns() -> Option<i64>`, and
116
+ `int64_t*` in C, matching `last_timestamp_ns`. The conversion moved into the
117
+ library because epoch nanoseconds land near `1.7e18`, where an `f64`'s step is
118
+ 256 ns — a single `seconds * 1e9` discards tens of nanoseconds the reading
119
+ still had. The SoapySDR plugin's own whole/fractional split is deleted.
120
+ `GpsState::time` is now `time_s`, and the C function accepts a `NULL`
121
+ out-parameter meaning "is there a fix?".
122
+
123
+ It does **not** improve precision. The device reports `gpstime` as an `f64`,
124
+ whose step at present-day epoch values is about 240 ns, and nothing downstream
125
+ recovers what was never there. `utils::gps_seconds_to_nanos` documents that
126
+ bound and a test pins it against the naive multiply. Across receivers ~240 ns
127
+ is ~70 m of ranging uncertainty, so correlate on the per-packet stream
128
+ timestamps and keep GPS for disciplining and wall-clock labelling.
129
+
130
+ `CaptureControl`'s fields gained units without moving its JSON keys: each is
131
+ now pinned by an explicit `#[serde(rename)]` rather than derived from the Rust
132
+ field name, so a later rename cannot change the wire format by accident.
133
+ `tests/wire_contract.rs` asserts those keys, and asserts the vendor
134
+ `AARTSAAPI_Packet` mirror still matches its C header field for field.
135
+
136
+ ### Added
137
+ - **The stream clock source can be set, not just read.** `device/sclksource`
138
+ selects what disciplines the receiver's clock; a V6 ECO offers `Consumer`,
139
+ `Oscillator`, `GPS`, `PPS`, `10MHz` and three `... Provider` variants. It was
140
+ readable but read-only, and SoapySDR's `setClockSource` merely logged a
141
+ warning telling the operator to change it in RTSA-Suite.
142
+ `SpectranSource::set_clock_source`, `spectran_source_set_clock_source` and the
143
+ plugin's `setClockSource` now write it on both backends — native SDK via
144
+ `ConfigSetString`, HTTP via a `simpleconfig` PUT that is read back to confirm,
145
+ because a `/remoteconfig` PUT naming a block outside the running mission
146
+ answers 200 and changes nothing. A confirmed mismatch is an **error** naming
147
+ the sources the device does offer: being told you are on a reference you are
148
+ not is worse than a failed call. A read-back yielding nothing is reported as
149
+ unconfirmed rather than failed.
150
+
151
+ This is what makes cross-receiver correlation possible: lock a fleet to one
152
+ 10 MHz / PPS / GPS reference and their per-packet timestamps share a timebase.
153
+ What is *not* possible is a commanded synchronous start — the SDK's C API has
154
+ no set-time and no arm-at-time entry point, so multi-device work is a shared
155
+ reference plus post-alignment on timestamps.
156
+
157
+ ### Fixed
158
+ - **A trailing slash in `device_type` no longer produces a malformed open
159
+ string.** The family/mode split asked whether the string contained a slash, so
160
+ `"spectranv6/"` counted as already mode-qualified and went to
161
+ `AARTSAAPI_OpenDevice` verbatim. The mode is now the text after the *first*
162
+ slash, and an empty one means none was given. The ECO was the sharp edge:
163
+ `"spectranv6eco/"` also slipped past the `spectranv6eco/raw` → `iqreceiver`
164
+ remap, so the device was asked for a pipeline it does not have and answered
165
+ with a result code that said nothing about the typo.
166
+
167
+ A `device_type` naming no family (`""`, `"/"`, `"/raw"`) is no longer
168
+ completed into a family-less `"/raw"`; it comes back untouched and is refused
169
+ at both points where a device type reaches the SDK. Both guards are needed
170
+ because enumeration runs first, and an empty family simply enumerates to
171
+ nothing — the old path reported "no devices found" and never mentioned the
172
+ typo. `device_family` and `device_open_mode` stay infallible: a typo in one
173
+ field is not a reason to make four accessors return `Result`.
174
+ - **An over-wide sample rate no longer reaches the hardware before it is
175
+ refused.** `configure_iq_receiver` checked the IQ-mode constraint as its last
176
+ act, so a rate the receiver clock cannot carry was written to
177
+ `main/centerfreq` and `main/spanfreq` first and rejected afterwards — leaving
178
+ the device holding the misconfiguration the check exists to prevent, and per
179
+ the SDK quietly emitting corrupted samples if anything started the stream.
180
+ The check now runs before the first write.
181
+
182
+ It checks against the clock the call *leaves in place*, which is not always
183
+ the one the device holds on entry. Raw mode writes the clock itself, so that
184
+ write is what counts: a V6 left on its 245.76 MHz clock would otherwise pass a
185
+ 150 MS/s request that stops being valid the moment this same call drops the
186
+ clock to 92.16 MHz. Every other mode skips that write, so there the live
187
+ setting is the honest number. The read-back after the writes is kept, and is
188
+ what `receiver_clock_hz()` reports. Native SDK only; the HTTP backend already
189
+ validated at the API boundary.
190
+
191
+ ## [v0.9.0] - 2026-09-08
6
192
 
7
193
  ### Added
8
194
  - **SoapySDR: device sensors.** The plugin exposed one sensor
@@ -3092,7 +3092,7 @@ dependencies = [
3092
3092
 
3093
3093
  [[package]]
3094
3094
  name = "python-aaronia"
3095
- version = "0.9.0"
3095
+ version = "0.10.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.10.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.10.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.10.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
+
@@ -5,14 +5,14 @@
5
5
  [![CI](https://github.com/isaacbentley/sdr-aaronia-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/isaacbentley/sdr-aaronia-rs/actions/workflows/ci.yml)
6
6
  [![License: GPL-3.0-or-later](https://img.shields.io/github/license/isaacbentley/sdr-aaronia-rs.svg)](https://choosealicense.com/licenses/gpl-3.0/)
7
7
 
8
- One API for Aaronia SPECTRAN analyzers and SDRs, whether the samples
8
+ One API for Aaronia SPECTRAN analyzers, whether the samples
9
9
  come from the native SDK, an RTSA-Suite HTTP server, or a recorded file.
10
10
  Python bindings and a SoapySDR plugin come from the same engine.
11
11
 
12
12
  *Disclaimer: This project is not affiliated with Aaronia AG. Aaronia, SPECTRAN, and RTSA-Suite PRO are trademarks of Aaronia AG.*
13
13
 
14
14
  Working with a SPECTRAN usually means choosing a transport first and
15
- then writing against whatever API that transport exposes. `AaroniaSource`
15
+ then writing against whatever API that transport exposes. `SpectranSource`
16
16
  removes the choice: point it at a file, a URL, or nothing at all, and it
17
17
  selects a backend and presents the same interface either way.
18
18
 
@@ -40,6 +40,24 @@ selects a backend and presents the same interface either way.
40
40
  wasted: `link_budget` measures the path end to end and names the
41
41
  widest span on the device's decimation ladder that fits it.
42
42
 
43
+ Three backends feed one engine that a range of consumers read from:
44
+
45
+ ```text
46
+ ┌── Native SDK (C FFI)
47
+ Backends: ├── HTTP Streaming (REST + binary chunked)
48
+ └── Offline .rtsa Files (binary parser)
49
+ │
50
+ ▼
51
+ Engine: [ SpectranSource / Unified Source ]
52
+ │
53
+ ▼
54
+ Consumers: ├── Native Rust API
55
+ ├── FutureSDR Block (HttpSource / HttpSink)
56
+ ├── Seify Driver (seify_impl.rs)
57
+ ├── C ABI (c_api.rs) ──► SoapySDR (C++) ──► GQRX / SDR++ / GNU Radio
58
+ └── Python Bindings (PyO3: python-aaronia) ──► NumPy / Arrow
59
+ ```
60
+
43
61
  ## Link requirements
44
62
 
45
63
  The server streams IQ at a fixed number of bytes per sample, so the span
@@ -80,17 +98,17 @@ tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
80
98
  Set the RF parameters and read:
81
99
 
82
100
  ```rust,no_run
83
- use sdr_aaronia_rs::{AaroniaSource, AaroniaConfig};
101
+ use sdr_aaronia_rs::{SpectranSource, SpectranConfig};
84
102
  use anyhow::Result;
85
103
 
86
104
  #[tokio::main]
87
105
  async fn main() -> Result<()> {
88
- let config = AaroniaConfig::default()
89
- .center_frequency(446.0e6) // 446 MHz
90
- .span_frequency(10.0e6) // 10 MHz span
91
- .reference_level(-30.0); // -30 dBm
106
+ let config = SpectranConfig::default()
107
+ .center_frequency_hz(446.0e6) // 446 MHz
108
+ .sample_rate_hz(10.0e6) // 10 MS/s (Fs), not RF bandwidth
109
+ .reference_level_dbm(-30.0); // -30 dBm
92
110
 
93
- let mut source = AaroniaSource::new(config).await?;
111
+ let mut source = SpectranSource::new(config).await?;
94
112
 
95
113
  let mut buffer = Vec::with_capacity(1024);
96
114
  let n = source.read_samples(&mut buffer, 1024).await?;
@@ -126,7 +144,9 @@ pip install python-aaronia
126
144
  ```python
127
145
  import aaronia
128
146
 
129
- with aaronia.open("http://localhost:54664", freq=2.44e9, bandwidth=10e6) as src:
147
+ with aaronia.open(
148
+ "http://localhost:54664", center_frequency_hz=2.44e9, bandwidth_hz=10e6
149
+ ) as src:
130
150
  for block in src.blocks(65536): # numpy complex64 arrays
131
151
  process(block)
132
152
  ```
@@ -157,7 +177,7 @@ which is a real wire-format change rather than a client-side conversion.
157
177
  ### seify (Rust-native)
158
178
 
159
179
  Enable the `seify` feature and construct the device with
160
- `AaroniaSeifyDevice::from_args`. It is not part of seify's built-in
180
+ `SpectranSeifyDevice::from_args`. It is not part of seify's built-in
161
181
  enumeration, so it will not appear in `seify::enumerate()`. See
162
182
  [PLUGINS.md](PLUGINS.md).
163
183
 
@@ -177,12 +197,12 @@ responses are retried; 4xx and configuration errors fail on the first
177
197
  attempt. This matters for `*.local` hostnames, which refuse the first
178
198
  connection from a cold process while mDNS resolves.
179
199
 
180
- `AaroniaConfig::read_timeout` (default 30 s) bounds `read_samples`.
200
+ `SpectranConfig::read_timeout` (default 30 s) bounds `read_samples`.
181
201
  `read_samples_deadline`, and therefore the SoapySDR and seify paths, uses
182
202
  its caller's per-call deadline instead.
183
203
 
184
204
  A dropped HTTP stream, from an RTSA restart or a network interruption,
185
- reconnects automatically. This is `AaroniaConfig::auto_reconnect`,
205
+ reconnects automatically. This is `SpectranConfig::auto_reconnect`,
186
206
  enabled by default. The reader reopens the stream, re-applies the
187
207
  current tuning (a restarted server returns to its mission's frequency),
188
208
  and flags the first packet after the gap as an overrun so callers know
@@ -200,7 +220,7 @@ Functionality is grouped behind Cargo features so unused dependencies stay out o
200
220
  | `file` | Buffered RTSA file parsing. | **Yes** |
201
221
  | `native-sdk` | Links the proprietary Aaronia C++ SDK. Windows/Linux only. | No |
202
222
  | `futuresdr` | Enables the FutureSDR block API: `HttpSource`, `HttpSink`, and their builders. Implies `http`. | No |
203
- | `sdr-source` | Integrates `AaroniaSdrSource` implementing the native `SdrSource` traits. | **Yes** |
223
+ | `sdr-source` | Integrates `SpectranSdrSource` implementing the native `SdrSource` traits. | **Yes** |
204
224
  | `ffi` | Builds the C-API export layer. | **Yes** |
205
225
 
206
226
  ## Testing & Contributing