python-aaronia 0.10.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/CHANGELOG.md +133 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/Cargo.lock +2 -2
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/Cargo.toml +1 -1
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/PKG-INFO +1 -1
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/README.md +1 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/SDKSPEC.md +18 -2
- python_aaronia-0.11.1/docs/SYNC.md +118 -0
- python_aaronia-0.11.1/docs/VERIFICATION.md +73 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/native_sdk_transmit.rs +5 -3
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/include/spectran.h +12 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/python-aaronia/aaronia.pyi +18 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/python-aaronia/src/lib.rs +25 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/scripts/ci-local.sh +1 -1
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/README.md +39 -3
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/Registration.cpp +11 -2
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/SpectranSoapyDevice.cpp +125 -17
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/SpectranSoapyDevice.hpp +15 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/c_api.rs +122 -2
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/http_endpoints.rs +203 -2
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/lib.rs +4 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/native_sdk.rs +251 -14
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/sdk_sink.rs +11 -8
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/sdr_source_impl.rs +69 -12
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/unified_sink.rs +12 -5
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/unified_source.rs +568 -19
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/utils.rs +114 -18
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/c_api_test.rs +36 -1
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/http_resilience_test.rs +32 -7
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/live_smoke.rs +93 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/native_sdk_live.rs +93 -0
- python_aaronia-0.10.0/docs/VERIFICATION.md +0 -80
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/.cargo/config.toml +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/.gitattributes +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/.gitignore +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/CONTRIBUTING.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/DESIGN.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/LICENSE +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/PLUGINS.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/deny.toml +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/APPS.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/HTTPSPEC.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/QUICKSTART.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/docs/USAGE.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/device_control.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/pyproject.toml +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/python-aaronia/README.md +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/scripts/fake-rtsa-server.py +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/scripts/native-sdk-validate.ps1 +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/scripts/sdk-container-test.sh +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/decompression.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/detection.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/error.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/file_source.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/http_sink.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/http_source.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/http_streaming.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/link_budget.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/sdk_source.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/src/seify_impl.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/abi_drift.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/integration_test.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/properties.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/test_cw_meta.rs +0 -0
- {python_aaronia-0.10.0 → python_aaronia-0.11.1}/tests/wire_contract.rs +0 -0
|
@@ -2,6 +2,139 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [v0.11.1] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **A stream gap now ends the block it precedes, instead of hiding inside
|
|
11
|
+
one.** Read assembly noted a dropped chunk and carried on concatenating,
|
|
12
|
+
so a returned block held samples from both sides of the hole while
|
|
13
|
+
`take_overrun` said only "somewhere in there". A consumer resetting its
|
|
14
|
+
filters once, before the block, still ran the discontinuity through
|
|
15
|
+
them — and for an FM discriminator that is a phase step, i.e. a
|
|
16
|
+
frequency spike that reads downstream as a sync edge.
|
|
17
|
+
|
|
18
|
+
A gap has a position, and the only position a consumer can act on is a
|
|
19
|
+
read boundary. The post-gap chunk is now stashed and the block ends,
|
|
20
|
+
exactly as it already did for a retune, with the flag carried to the
|
|
21
|
+
read that begins there. A gap on a read's first chunk still reports
|
|
22
|
+
against that read, since nothing precedes it to split from.
|
|
23
|
+
|
|
24
|
+
This also reaches reconnects, which are discontinuities for the same
|
|
25
|
+
reason: the stream stopped and resumed, so the samples either side are
|
|
26
|
+
not adjacent. Reads continue across a reconnect as before, they just
|
|
27
|
+
no longer splice across it.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## [v0.11.0] - 2026-09-09
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **A read never spans a retune, and packets carry the frequency they
|
|
34
|
+
were captured at.** The centre frequency now travels through the
|
|
35
|
+
sample channel with the samples it belongs to, so
|
|
36
|
+
`read_samples`/`read_samples_deadline` stop at a frequency boundary
|
|
37
|
+
and `SpectranSource::capture_frequency_hz` reports what the stream
|
|
38
|
+
said, not what was requested.
|
|
39
|
+
|
|
40
|
+
Reading the frequency from shared state at consume time cannot work:
|
|
41
|
+
the reader task runs ahead of the consumer, so a buffer parsed before
|
|
42
|
+
a retune would be tagged with the frequency after it. Measured on a
|
|
43
|
+
V6 ECO over RTSA HTTP at 61.44 MSPS, a retune is carried by the signal
|
|
44
|
+
within 24 ms median and 39 ms worst case, and during that window the
|
|
45
|
+
device is still delivering the old centre.
|
|
46
|
+
|
|
47
|
+
Callers already handle short reads — `read_samples_numpy` slices what
|
|
48
|
+
it got, and a SoapySDR `readStream` returning fewer than `numElems` is
|
|
49
|
+
ordinary — so the visible change is that a buffer taken across a
|
|
50
|
+
retune is no longer a mixture of two frequencies. `RETUNE_SETTLE` in
|
|
51
|
+
the `SdrSource` facade drops from 75 ms to 20 ms with it: the
|
|
52
|
+
frequency check rejects stale samples exactly, so the drain is only an
|
|
53
|
+
efficiency measure. A 48-tune sweep of every FPV band falls from 28.2 s
|
|
54
|
+
to 5.9 s.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **The GPS mode can be set, not just the clock source.** `device/gpsmode`
|
|
58
|
+
decides whether GPS supplies location, time, both, or nothing, and the device
|
|
59
|
+
ships on `Disabled` — in which state no fix is ever reported and
|
|
60
|
+
`gps_time_ns` returns `None` forever, reading as broken rather than
|
|
61
|
+
unconfigured. `SpectranSource::set_gps_mode` writes it on both backends,
|
|
62
|
+
with the same confirm-by-read-back rule as the clock source.
|
|
63
|
+
- **`clock_source()`, `clock_sources()`, `gps_mode()`, `gps_modes()`** on
|
|
64
|
+
`SpectranSource`, so reading the current reference no longer means fetching
|
|
65
|
+
the whole capability tree. These read over HTTP; the writes work on both
|
|
66
|
+
backends.
|
|
67
|
+
- **Dual-channel RX reaches SoapySDR.** A full V6's two inputs were readable
|
|
68
|
+
from Rust, C and Python but not through the plugin, which reported one
|
|
69
|
+
channel and refused any other. Open with `rx_channel=Rx1And2` and
|
|
70
|
+
`getNumChannels(RX)` reports 2; `setupStream(RX, …, {0, 1})` puts
|
|
71
|
+
`readStream` on the paired read, filling both buffers with the same count.
|
|
72
|
+
The count follows the request, not the model — dual capture is configured
|
|
73
|
+
before the device opens, so a V6 opened single-channel has one channel to
|
|
74
|
+
offer. Still hardware-unverified: the development device is a single-channel
|
|
75
|
+
ECO.
|
|
76
|
+
- **`read_samples_dual_deadline`** (Rust) and
|
|
77
|
+
**`spectran_source_read_samples_dual_timeout`** (C), the dual counterparts of
|
|
78
|
+
the existing deadline-bounded reads. `readStream` must honour the
|
|
79
|
+
application's `timeoutUs`, which the blocking dual read ignored.
|
|
80
|
+
- **The device's master stream clock is readable from a source.**
|
|
81
|
+
`SpectranSource::master_stream_time_ns`,
|
|
82
|
+
`spectran_source_get_master_stream_time_ns`, Python
|
|
83
|
+
`master_stream_time_ns()`, and SoapySDR `getHardwareTime("master")`. It was
|
|
84
|
+
reachable only through the TX sink before. Unlike `last_timestamp_ns` it
|
|
85
|
+
answers before the first packet, so a caller that must not stamp data to
|
|
86
|
+
1970 has something to read; it is also the clock two receivers are aligned
|
|
87
|
+
on. Native-SDK backend only.
|
|
88
|
+
- **Python gained `master_stream_time_ns()` and `gps_time_ns()`**, which the
|
|
89
|
+
Rust and C surfaces already had.
|
|
90
|
+
- **[docs/SYNC.md](docs/SYNC.md)** — what the hardware does and does not offer
|
|
91
|
+
for multi-device timing. The vendor API has no set-time, no arm-at-time and
|
|
92
|
+
no trigger in any of its 34 functions, so UHD-style commanded capture is not
|
|
93
|
+
possible; a shared reference plus post-alignment on timestamps is, and that
|
|
94
|
+
is written down with its resolution floor (~240 ns, set by the vendor's own
|
|
95
|
+
`double`). Wired into the doctests, so its snippets cannot rot.
|
|
96
|
+
|
|
97
|
+
### Breaking changes
|
|
98
|
+
- **`get_master_stream_time()` is now `master_stream_time_ns()`**, on
|
|
99
|
+
`NativeSdkSource`, `SdkSink` and `UnifiedSink`, returning `i64` nanoseconds
|
|
100
|
+
instead of `f64` seconds. One unit per dimension, matching every other clock
|
|
101
|
+
the crate reports. `TxBurst` still carries the vendor's `f64` seconds — it is
|
|
102
|
+
written straight into the packet header — so convert with the new
|
|
103
|
+
`utils::epoch_nanos_to_seconds`.
|
|
104
|
+
- **`utils::gps_seconds_to_nanos` is now `utils::epoch_seconds_to_nanos`.**
|
|
105
|
+
Two of the vendor's clocks arrive as `float64` epoch seconds, not one, and
|
|
106
|
+
the old name claimed otherwise.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
- **A timed-out read no longer discards the staging buffer.**
|
|
110
|
+
`spectran_source_read_samples_timeout` returned early on timeout, past the
|
|
111
|
+
point where it hands the buffer back, so the next read reallocated it — the
|
|
112
|
+
allocation that path exists to avoid.
|
|
113
|
+
- **An unrecognised `rx_channel=` is reported, not silently taken as `Rx1`.**
|
|
114
|
+
A typo used to yield a single-channel stream indistinguishable from a working
|
|
115
|
+
dual request.
|
|
116
|
+
- **Reading the wrong payload for the open mode is refused instead of answered.**
|
|
117
|
+
Neither the SDK nor the packet says what a stream carries, so asking a
|
|
118
|
+
spectrum pipeline for IQ returned dBm bins reinterpreted as voltages, and
|
|
119
|
+
asking an IQ pipeline for spectra returned complex pairs reinterpreted as
|
|
120
|
+
two-bin frames whose bin spacing was the whole span. Both look like data.
|
|
121
|
+
Measured on a V6 ECO's `rtsa`: 200k "samples" spanning -130 to -59 with a
|
|
122
|
+
mean of -77 and not one value near zero — a dBm distribution, not a voltage
|
|
123
|
+
one. `read_samples`, `read_samples_dual` and `read_spectra` now check the
|
|
124
|
+
open mode and say which payload it carries.
|
|
125
|
+
|
|
126
|
+
This also corrects a doc claim: `spectranv6eco/rtsa` was documented as
|
|
127
|
+
yielding IQ at "~0.4 MS/s regardless of the requested rate". It yields no IQ
|
|
128
|
+
at all; that figure was the spectra misread. `spectranv6/raw` genuinely does
|
|
129
|
+
both, IQ on stream 0 and spectra on stream 2, and is unaffected.
|
|
130
|
+
- **Capability lists no longer advertise options the device will refuse.** The
|
|
131
|
+
config tree carries a bitmask of enum entries the device is currently
|
|
132
|
+
rejecting, and it was ignored: a V6 ECO with no GPS antenna offered `GPS` and
|
|
133
|
+
`GPS Provider` as clock sources, and `set_clock_source("GPS")` then failed
|
|
134
|
+
against a list that said it would not. Those entries are filtered out, so what
|
|
135
|
+
`clock_sources()` and `gps_modes()` return is what the device accepts.
|
|
136
|
+
Verified live: the six sources advertised are exactly the six it takes.
|
|
137
|
+
|
|
5
138
|
## [v0.10.0] - 2026-09-08
|
|
6
139
|
|
|
7
140
|
### Breaking changes
|
|
@@ -3092,7 +3092,7 @@ dependencies = [
|
|
|
3092
3092
|
|
|
3093
3093
|
[[package]]
|
|
3094
3094
|
name = "python-aaronia"
|
|
3095
|
-
version = "0.
|
|
3095
|
+
version = "0.11.1"
|
|
3096
3096
|
dependencies = [
|
|
3097
3097
|
"arrow",
|
|
3098
3098
|
"num-complex",
|
|
@@ -3621,7 +3621,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3621
3621
|
|
|
3622
3622
|
[[package]]
|
|
3623
3623
|
name = "sdr-aaronia-rs"
|
|
3624
|
-
version = "0.
|
|
3624
|
+
version = "0.11.1"
|
|
3625
3625
|
dependencies = [
|
|
3626
3626
|
"anyhow",
|
|
3627
3627
|
"bitflags 2.13.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "sdr-aaronia-rs"
|
|
3
3
|
description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
|
|
4
4
|
license = "GPL-3.0-or-later"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.11.1"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
|
|
8
8
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aaronia
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
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+)
|
|
@@ -236,6 +236,7 @@ Start here:
|
|
|
236
236
|
- [Quickstart](docs/QUICKSTART.md) — configuring an RTSA-Suite mission, first samples in Rust, Python and SoapySDR, and troubleshooting for common setup failures.
|
|
237
237
|
- [Usage](docs/USAGE.md) — worked examples for each part of the API, plus the `AARONIA_SDK_PATH` and `AARONIA_USER_AGENT` environment variables.
|
|
238
238
|
- [Using existing SDR apps](docs/APPS.md) — SDR++, GQRX, GNU Radio, SoapySDR from Python.
|
|
239
|
+
- [Synchronising receivers](docs/SYNC.md) — what the hardware can and cannot do for multi-device timing, and the recipe that works.
|
|
239
240
|
|
|
240
241
|
Reference:
|
|
241
242
|
|
|
@@ -258,7 +258,16 @@ The Aaronia RTSA SDK (`aaroniartsaapi.h`) exposes a C-style API for maximum comp
|
|
|
258
258
|
* **`AARTSAAPI_AvailPackets(AARTSAAPI_Device * dhandle, int32_t channel, int32_t * num)`**: Gets the number of available packets in a specified data `channel`.
|
|
259
259
|
* **`AARTSAAPI_GetPacket(AARTSAAPI_Device * dhandle, int32_t channel, int32_t index, AARTSAAPI_Packet * packet)`**: Retrieves a specific packet from the output queue.
|
|
260
260
|
* **`AARTSAAPI_ConsumePackets(AARTSAAPI_Device * dhandle, int32_t channel, int32_t num)`**: Consumes (removes) a number of packets from the data channel. Essential to prevent blocking and data drops.
|
|
261
|
-
* **`AARTSAAPI_GetMasterStreamTime(AARTSAAPI_Device * dhandle, double * stime)`**:
|
|
261
|
+
* **`AARTSAAPI_GetMasterStreamTime(AARTSAAPI_Device * dhandle, double * stime)`**:
|
|
262
|
+
Gets the current master stream time, seconds since the epoch. The
|
|
263
|
+
header declares the out-parameter as a C++ *reference* (`double &`),
|
|
264
|
+
which is a pointer in the ABI — hence `*mut f64` here. Exposed as
|
|
265
|
+
`NativeSdkSource::master_stream_time_ns` and
|
|
266
|
+
`SpectranSource::master_stream_time_ns`, converted to nanoseconds so
|
|
267
|
+
it reads in the same unit as every other clock the crate reports.
|
|
268
|
+
This is the device's own timebase: it paces streams against it, TX
|
|
269
|
+
burst times are expressed in it, and it is what a multi-device
|
|
270
|
+
capture is aligned on — see [SYNC.md](SYNC.md).
|
|
262
271
|
* **`AARTSAAPI_SendPacket(AARTSAAPI_Device * dhandle, int32_t channel, const AARTSAAPI_Packet * packet)`**: Sends a packet to an inbound channel (for transmission modes).
|
|
263
272
|
|
|
264
273
|
### File I/O API
|
|
@@ -461,7 +470,7 @@ values an `f64`'s step is about **238 ns**, which is the real resolution of
|
|
|
461
470
|
any GPS timestamp this device gives — no conversion downstream can improve
|
|
462
471
|
on it.
|
|
463
472
|
|
|
464
|
-
`utils::
|
|
473
|
+
`utils::epoch_seconds_to_nanos` converts to `i64` nanoseconds by handling the
|
|
465
474
|
whole and fractional seconds separately. That is not pedantry: epoch
|
|
466
475
|
nanoseconds are around `1.7e18`, where an `f64`'s step is 256 ns, so a single
|
|
467
476
|
`seconds * 1e9` multiply discards tens of nanoseconds the reading still had.
|
|
@@ -541,6 +550,13 @@ never sees), so the second path errors instead of corrupting silently.
|
|
|
541
550
|
`stop_streaming` clears both carry buffers and the latch, so a
|
|
542
551
|
restarted session starts clean.
|
|
543
552
|
|
|
553
|
+
The SoapySDR plugin exposes the same capture as two RX channels:
|
|
554
|
+
`rx_channel=Rx1And2` at open time makes `getNumChannels(RX)` report 2,
|
|
555
|
+
and `setupStream(RX, …, {0, 1})` switches `readStream` onto the paired
|
|
556
|
+
read. The channel count follows the request rather than the model,
|
|
557
|
+
because dual capture must be configured before the device is opened —
|
|
558
|
+
a full V6 opened single-channel really does have one channel to offer.
|
|
559
|
+
|
|
544
560
|
> **Hardware-unverified:** like the rest of the `Rx2`/`Rx1+Rx2` paths,
|
|
545
561
|
> the interleave layout follows the packet contract (`stride` = floats
|
|
546
562
|
> from sample to sample), not a live dual-channel capture — the
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Synchronising two or more receivers
|
|
2
|
+
|
|
3
|
+
The short version: **the SPECTRAN V6 is a shared-reference machine, not a
|
|
4
|
+
commanded-time one.** You cannot tell it to start capturing at a stated
|
|
5
|
+
instant. You can lock several devices to one reference, timestamp
|
|
6
|
+
everything they produce, and align the streams afterwards. That is
|
|
7
|
+
enough for RDF, TDOA and coherent multi-channel work; it is not enough
|
|
8
|
+
for the UHD-style `set_start_time()` pattern, and no amount of API
|
|
9
|
+
wrapping will make it so.
|
|
10
|
+
|
|
11
|
+
## What the hardware offers
|
|
12
|
+
|
|
13
|
+
| Primitive | Where |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Common frequency reference (10 MHz, PPS, GPS) | `device/sclksource` — `set_clock_source()` |
|
|
16
|
+
| GPS discipline and time of day | `device/gpsmode` — `set_gps_mode()`, `gps_time_ns()` |
|
|
17
|
+
| Per-packet hardware timestamps | `AARTSAAPI_Packet::startTime`/`endTime` — `last_timestamp_ns()` |
|
|
18
|
+
| The device's own stream clock | `AARTSAAPI_GetMasterStreamTime` — `master_stream_time_ns()` |
|
|
19
|
+
| Two phase-coherent RX inputs (full V6) | `device/receiverchannel` — `read_samples_dual()` |
|
|
20
|
+
|
|
21
|
+
A V6 ECO with no GPS antenna accepts six clock sources: `Consumer`,
|
|
22
|
+
`Oscillator`, `PPS`, `10MHz`, `Oscillator Provider` and `PPS Provider`.
|
|
23
|
+
The `Provider` variants make that device the reference for others rather
|
|
24
|
+
than a consumer of one. `GPS` and `GPS Provider` appear in the tree but
|
|
25
|
+
are masked out when no antenna is attached, and `clock_sources()`
|
|
26
|
+
filters them for that reason — the list reports what the device will
|
|
27
|
+
accept, not what it can spell.
|
|
28
|
+
|
|
29
|
+
## What it does not offer
|
|
30
|
+
|
|
31
|
+
The vendor API is 34 functions. There is no set-time, no arm-at-time and
|
|
32
|
+
no trigger among them: the closest thing to time control is
|
|
33
|
+
`AARTSAAPI_GetMasterStreamTime`, which reads. Nothing writes a clock,
|
|
34
|
+
and nothing schedules a capture.
|
|
35
|
+
|
|
36
|
+
Transmission is the exception, and it proves the rule — a `TxBurst`
|
|
37
|
+
carries `startTime`/`endTime` and the device schedules it. The
|
|
38
|
+
capability exists in the FPGA; the API simply does not expose it for
|
|
39
|
+
receive.
|
|
40
|
+
|
|
41
|
+
So a two-receiver capture cannot be started simultaneously by command.
|
|
42
|
+
Both devices are started as promptly as software allows, and the offset
|
|
43
|
+
between them is measured rather than commanded.
|
|
44
|
+
|
|
45
|
+
## The recipe
|
|
46
|
+
|
|
47
|
+
1. **Discipline every device from one reference.** One device (or an
|
|
48
|
+
external source) provides; the rest consume:
|
|
49
|
+
|
|
50
|
+
```rust,no_run
|
|
51
|
+
# async fn discipline() -> sdr_aaronia_rs::Result<()> {
|
|
52
|
+
use sdr_aaronia_rs::{SpectranConfig, SpectranSource};
|
|
53
|
+
|
|
54
|
+
let mut provider =
|
|
55
|
+
SpectranSource::new(SpectranConfig::from_http("http://ref.local:54664")).await?;
|
|
56
|
+
let mut consumer =
|
|
57
|
+
SpectranSource::new(SpectranConfig::from_http("http://sat.local:54664")).await?;
|
|
58
|
+
|
|
59
|
+
provider.set_clock_source("PPS Provider").await?;
|
|
60
|
+
consumer.set_clock_source("PPS").await?;
|
|
61
|
+
# Ok(())
|
|
62
|
+
# }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Confirm by reading back — `set_clock_source` already does, and errors
|
|
66
|
+
if the device did not take it. This is what makes the sample clocks
|
|
67
|
+
agree in *rate*. It does not make them agree in *phase*.
|
|
68
|
+
|
|
69
|
+
2. **Start the streams.** In whatever order; the offset between them is
|
|
70
|
+
not controlled and does not need to be.
|
|
71
|
+
|
|
72
|
+
3. **Timestamp everything.** Each packet carries device time.
|
|
73
|
+
`last_timestamp_ns()` reports the most recent one, and every
|
|
74
|
+
SoapySDR buffer comes back flagged `SOAPY_SDR_HAS_TIME`.
|
|
75
|
+
|
|
76
|
+
4. **Align afterwards.** Compute the offset between the streams from
|
|
77
|
+
their timestamps, then refine it by cross-correlating a common
|
|
78
|
+
signal. The timestamps get you to within the resolution below; the
|
|
79
|
+
correlation gets you the rest of the way, and is what actually
|
|
80
|
+
determines the answer in TDOA work.
|
|
81
|
+
|
|
82
|
+
For two channels on a *single* full V6, none of this applies: `Rx1` and
|
|
83
|
+
`Rx2` share one tuner and one converter, arrive interleaved in one
|
|
84
|
+
packet, and are sample-aligned by construction. Read them with
|
|
85
|
+
`read_samples_dual()` — see [SDKSPEC.md](SDKSPEC.md#receiver-channel-selection-and-dual-channel-capture).
|
|
86
|
+
|
|
87
|
+
## The resolution floor
|
|
88
|
+
|
|
89
|
+
The device reports both its GPS time and its master stream time as
|
|
90
|
+
`float64` seconds since the epoch. At present-day epoch values an `f64`
|
|
91
|
+
steps about **238 ns**, so those two clocks are quantised at roughly
|
|
92
|
+
that — before any conversion this crate performs, and not something it
|
|
93
|
+
can improve on.
|
|
94
|
+
|
|
95
|
+
For TDOA, 240 ns of timing uncertainty is about **70 m** of ranging
|
|
96
|
+
uncertainty. That is the floor for *timestamp-based* alignment. It is
|
|
97
|
+
not the floor for the technique: cross-correlating a common signal
|
|
98
|
+
resolves far below one sample period, and the timestamps only have to be
|
|
99
|
+
good enough to identify which samples to correlate. Build the geometry
|
|
100
|
+
on the correlation, and use the timestamps to get there.
|
|
101
|
+
|
|
102
|
+
The per-packet stream timestamps are the better of the two clocks for
|
|
103
|
+
this. GPS time is for disciplining and for wall-clock labelling.
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
Hardware-unverified. The clock-source writes are exercised against a
|
|
108
|
+
single V6 ECO; nothing here has been run across two devices, and the
|
|
109
|
+
crate has never seen a full V6. See
|
|
110
|
+
[VERIFICATION.md](VERIFICATION.md).
|
|
111
|
+
|
|
112
|
+
## Related
|
|
113
|
+
|
|
114
|
+
- [SDKSPEC.md](SDKSPEC.md) — the native SDK surface, including the
|
|
115
|
+
packet layout and dual-channel capture.
|
|
116
|
+
- [USAGE.md](USAGE.md) — worked examples for each part of the API.
|
|
117
|
+
- [VERIFICATION.md](VERIFICATION.md) — what has been tested against
|
|
118
|
+
hardware.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Hardware verification status
|
|
2
|
+
|
|
3
|
+
The development device is a SPECTRAN V6 ECO — single RX channel, no TX
|
|
4
|
+
licence, no GPS antenna — driven over HTTP through RTSA-Suite PRO from
|
|
5
|
+
macOS and, since 0.8.2, through the native SDK on the Windows machine it
|
|
6
|
+
is attached to.
|
|
7
|
+
|
|
8
|
+
**Live-verified** means an `#[ignore]`d test asserts it against hardware,
|
|
9
|
+
so it cannot regress unnoticed. **Manual** means it was seen working but
|
|
10
|
+
nothing asserts it.
|
|
11
|
+
|
|
12
|
+
| Capability | HTTP | Native SDK |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| IQ streaming (F32 / F16 / I16 / JSON) | **Live** | **Live** |
|
|
15
|
+
| Spectra streaming | **Live** | **Live** — the ECO's `rtsa` pipeline, 512 frames x 88 bins per packet |
|
|
16
|
+
| Retuning, rate and reference level mid-stream | **Live** | **Live** |
|
|
17
|
+
| Long-run stability, drop and overrun counters | **Live** | **Live** |
|
|
18
|
+
| Sample-rate ladder and usable bandwidth | **Live** | **Live** |
|
|
19
|
+
| Device capability and health reporting | **Live** | Partial — the raw SDK's health tree reads zeros, so sensors are HTTP-only by design |
|
|
20
|
+
| Clock-source and GPS-mode writes | **Live** | Unverified |
|
|
21
|
+
| Auto-reconnect and connect retry | Mock-tested | n/a |
|
|
22
|
+
| End-to-end IQ correctness (`validate-iq-live.py`) | **Live** | — |
|
|
23
|
+
| seify backend | **Live** | **Live** |
|
|
24
|
+
| Python bindings | Manual | Manual |
|
|
25
|
+
| SoapySDR plugin | Manual | Manual |
|
|
26
|
+
| `.rtsa` file playback | **Verified against real captures** | — |
|
|
27
|
+
| TX (`UnifiedSink`, `spectran_sink_*`) | Endpoint only, no RF measured | Unverified |
|
|
28
|
+
| Dual-channel RX (Rust, C, Python, SoapySDR) | — | Unverified — needs a full V6 |
|
|
29
|
+
| GPS time (`gps_time_ns`) | — | Unverified — needs a GPS antenna |
|
|
30
|
+
| Master stream clock (`master_stream_time_ns`) | — | Unverified |
|
|
31
|
+
|
|
32
|
+
## What is not verified, and why
|
|
33
|
+
|
|
34
|
+
**TX, dual-channel RX, GPS time and multi-device sync** need hardware
|
|
35
|
+
this project does not have: a TX licence, a full V6, a GPS antenna, or a
|
|
36
|
+
second device. If you
|
|
37
|
+
have any of them, exercising one of these paths is the most valuable
|
|
38
|
+
contribution you can make here — the first native-SDK run against a real
|
|
39
|
+
device found five defects that compile-checking never would.
|
|
40
|
+
|
|
41
|
+
**Native-SDK clock and GPS-mode writes** share their shape with the
|
|
42
|
+
HTTP path, which is live-verified, but are not themselves exercised.
|
|
43
|
+
|
|
44
|
+
**Drops.** The live drop test does not assert that drops *occur* — that
|
|
45
|
+
is a property of the link, not the crate, and a fast enough link never
|
|
46
|
+
drops anything. It checks the stream stays coherent and the counters
|
|
47
|
+
agree; the gap logic itself is unit-tested.
|
|
48
|
+
|
|
49
|
+
## Running them
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cargo test --all-features --test live_smoke -- --ignored --test-threads=1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The native-SDK set needs the machine the device is attached to, with
|
|
56
|
+
RTSA-Suite PRO closed — it holds the device:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cargo test --release --features native-sdk --test native_sdk_live -- --ignored --test-threads=1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`--test-threads=1` matters: one live test writes device state, and a
|
|
63
|
+
concurrent streaming test would see the reference change underneath it.
|
|
64
|
+
|
|
65
|
+
Both suites were re-run in full for 0.10.0 — 21 and 14 tests, all
|
|
66
|
+
passing — along with a SoapySDR probe through the renamed C ABI.
|
|
67
|
+
|
|
68
|
+
## Related
|
|
69
|
+
|
|
70
|
+
- [QUICKSTART.md](QUICKSTART.md) — setting up an RTSA-Suite mission.
|
|
71
|
+
- [SYNC.md](SYNC.md) — what multi-device timing the hardware supports.
|
|
72
|
+
- [USAGE.md](USAGE.md) — worked examples for each part of the API.
|
|
73
|
+
- [../CHANGELOG.md](../CHANGELOG.md) — what changed in each release.
|
|
@@ -67,10 +67,12 @@ async fn main() -> anyhow::Result<()> {
|
|
|
67
67
|
|
|
68
68
|
// Read the current master stream time from the device
|
|
69
69
|
// This is required to correctly schedule our packets on the device's FPGA
|
|
70
|
-
let
|
|
70
|
+
let now_ns = sdk_sink.master_stream_time_ns()?;
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
|
|
72
|
+
// Burst times go into the vendor's packet header, which is seconds:
|
|
73
|
+
// convert once, here. Pre-buffer by scheduling the first packet
|
|
74
|
+
// 200 ms in the future.
|
|
75
|
+
let mut current_time = sdr_aaronia_rs::utils::epoch_nanos_to_seconds(now_ns) + 0.2;
|
|
74
76
|
|
|
75
77
|
let duration_per_burst = SAMPLES_PER_BURST as f64 / config.sample_rate_hz;
|
|
76
78
|
|
|
@@ -187,6 +187,12 @@ intptr_t spectran_source_read_samples_timeout(SpectranSource* source, FfiComplex
|
|
|
187
187
|
// the native-SDK backend): fills rx1/rx2 with equal numbers of
|
|
188
188
|
// time-aligned samples; returns the pair count or -1.
|
|
189
189
|
intptr_t spectran_source_read_samples_dual(SpectranSource* source, FfiComplex* rx1, FfiComplex* rx2, uintptr_t len);
|
|
190
|
+
// Deadline-bounded dual read, the pair to
|
|
191
|
+
// spectran_source_read_samples_timeout: waits at most timeout_us and
|
|
192
|
+
// returns the pairs collected within the deadline; returns -3 only when
|
|
193
|
+
// the deadline passes with zero pairs. timeout_us == 0 drains without
|
|
194
|
+
// waiting. Both buffers receive the same count, so they stay aligned.
|
|
195
|
+
intptr_t spectran_source_read_samples_dual_timeout(SpectranSource* source, FfiComplex* rx1, FfiComplex* rx2, uintptr_t len, uint64_t timeout_us);
|
|
190
196
|
bool spectran_source_take_overrun(SpectranSource* source);
|
|
191
197
|
uint64_t spectran_source_get_cumulative_drops(SpectranSource* source);
|
|
192
198
|
int64_t spectran_source_get_last_timestamp_ns(SpectranSource* source);
|
|
@@ -196,6 +202,12 @@ int64_t spectran_source_get_last_timestamp_ns(SpectranSource* source);
|
|
|
196
202
|
// library now, so callers no longer split whole/fractional seconds
|
|
197
203
|
// themselves. Resolution is ~240 ns, set by the vendor's own double.
|
|
198
204
|
bool spectran_source_get_gps_time_ns(SpectranSource* source, int64_t* out_gps_time_ns);
|
|
205
|
+
// The device's master stream clock, nanoseconds since the Unix epoch:
|
|
206
|
+
// the timebase it paces streams against, readable before the first
|
|
207
|
+
// packet (unlike get_last_timestamp_ns, which is 0 until one arrives).
|
|
208
|
+
// Native-SDK backend only; false over HTTP and file playback.
|
|
209
|
+
// out_time_ns may be NULL to probe only whether the clock is readable.
|
|
210
|
+
bool spectran_source_get_master_stream_time_ns(SpectranSource* source, int64_t* out_time_ns);
|
|
199
211
|
SpectranFfiError spectran_source_start_streaming(SpectranSource* source);
|
|
200
212
|
SpectranFfiError spectran_source_stop_streaming(SpectranSource* source);
|
|
201
213
|
SpectranFfiError spectran_source_set_center_frequency_hz(SpectranSource* source, double hz);
|
|
@@ -143,6 +143,12 @@ class SpectranSource:
|
|
|
143
143
|
def read_samples_numpy(self, count: int) -> npt.NDArray[np.complex64]:
|
|
144
144
|
"""Read up to ``count`` IQ samples into a NumPy ``complex64`` array.
|
|
145
145
|
|
|
146
|
+
A read never spans a retune: if the centre frequency changes
|
|
147
|
+
part-way through, the read returns the samples captured before
|
|
148
|
+
it and the rest arrive on the next call. Expect a short array
|
|
149
|
+
around a :meth:`set_center_frequency_hz`, rather than one whose
|
|
150
|
+
samples come from two different frequencies.
|
|
151
|
+
|
|
146
152
|
Raises ``SpectranTimeoutError`` if no data arrives within
|
|
147
153
|
``config.read_timeout_s``, ``SpectranConnectionError`` if the
|
|
148
154
|
stream is closed, and ``ValueError`` if ``count`` exceeds the
|
|
@@ -181,6 +187,18 @@ class SpectranSource:
|
|
|
181
187
|
"""Epoch-nanosecond timestamp of the most recent block.
|
|
182
188
|
HTTP backend only; 0 otherwise."""
|
|
183
189
|
|
|
190
|
+
def master_stream_time_ns(self) -> Optional[int]:
|
|
191
|
+
"""The device's master stream clock, in epoch nanoseconds.
|
|
192
|
+
|
|
193
|
+
The timebase the device paces streams against, readable before
|
|
194
|
+
the first block arrives. Native-SDK backend only; None otherwise."""
|
|
195
|
+
|
|
196
|
+
def gps_time_ns(self) -> Optional[int]:
|
|
197
|
+
"""The latest GPS time in epoch nanoseconds, or None without a fix.
|
|
198
|
+
|
|
199
|
+
Native-SDK backend only, and only once ``device/gpsmode`` is
|
|
200
|
+
enabled — the device ships with GPS off."""
|
|
201
|
+
|
|
184
202
|
class BlockIterator:
|
|
185
203
|
"""Iterator returned by :meth:`SpectranSource.blocks`."""
|
|
186
204
|
|
|
@@ -601,6 +601,31 @@ impl PySpectranSource {
|
|
|
601
601
|
.ok_or_else(|| SpectranHardwareError::new_err("Not streaming"))?;
|
|
602
602
|
Ok(source.last_timestamp_ns())
|
|
603
603
|
}
|
|
604
|
+
|
|
605
|
+
/// The device's master stream clock (epoch ns) — the timebase it
|
|
606
|
+
/// paces streams against. Unlike `last_timestamp_ns` it is readable
|
|
607
|
+
/// before the first block arrives. Native-SDK backend only; `None`
|
|
608
|
+
/// otherwise.
|
|
609
|
+
fn master_stream_time_ns(&mut self) -> PyResult<Option<i64>> {
|
|
610
|
+
let source = self
|
|
611
|
+
.source
|
|
612
|
+
.as_mut()
|
|
613
|
+
.ok_or_else(|| SpectranHardwareError::new_err("Not streaming"))?;
|
|
614
|
+
Ok(source.master_stream_time_ns())
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// The latest GPS time (epoch ns), or `None` without a valid fix.
|
|
618
|
+
/// Native-SDK backend only, and only once `device/gpsmode` has been
|
|
619
|
+
/// enabled — the device ships with GPS off, in which state this
|
|
620
|
+
/// stays `None`. Quantised at roughly 240 ns by the device's own
|
|
621
|
+
/// reading.
|
|
622
|
+
fn gps_time_ns(&mut self) -> PyResult<Option<i64>> {
|
|
623
|
+
let source = self
|
|
624
|
+
.source
|
|
625
|
+
.as_mut()
|
|
626
|
+
.ok_or_else(|| SpectranHardwareError::new_err("Not streaming"))?;
|
|
627
|
+
Ok(source.gps_time_ns())
|
|
628
|
+
}
|
|
604
629
|
}
|
|
605
630
|
|
|
606
631
|
/// Iterator over fixed-size sample blocks, returned by
|
|
@@ -120,6 +120,39 @@ receiver clock and reaches higher, by how much is unsettled — see [the
|
|
|
120
120
|
note in HTTPSPEC](../docs/HTTPSPEC.md#unresolved-the-full-v6s-top-rate).
|
|
121
121
|
There, `getSampleRate` while streaming is the number to trust.
|
|
122
122
|
|
|
123
|
+
## Dual-channel RX
|
|
124
|
+
|
|
125
|
+
A full SPECTRAN V6 has two RF inputs. Ask for both at open time and the
|
|
126
|
+
plugin advertises two RX channels:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
sdr = SoapySDR.Device("driver=aaronia,sdk=1,rx_channel=Rx1And2")
|
|
130
|
+
sdr.getNumChannels(SOAPY_SDR_RX) # 2
|
|
131
|
+
stream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32, [0, 1])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`readStream` then fills both buffers with the same number of samples,
|
|
135
|
+
index-aligned in time. Channel 0 is Rx1, channel 1 is Rx2.
|
|
136
|
+
|
|
137
|
+
It has to be requested before the device opens, so a V6 opened without
|
|
138
|
+
it reports one channel — the count describes this session, not the
|
|
139
|
+
model. The request needs the native-SDK backend; over HTTP it is
|
|
140
|
+
ignored with a warning.
|
|
141
|
+
|
|
142
|
+
Valid channel sets are `{0}`, `{0, 1}` and `{1, 0}` — the list order
|
|
143
|
+
decides which receiver each buffer gets. `{1}` is not one: the SDK
|
|
144
|
+
interleaves both receivers into a single packet, so Rx2 never arrives
|
|
145
|
+
without Rx1. Open with `rx_channel=Rx2` for a single-channel stream from
|
|
146
|
+
the second input.
|
|
147
|
+
|
|
148
|
+
Both channels share one tuner: centre frequency, sample rate and
|
|
149
|
+
reference level are device-wide, and setting them on either channel sets
|
|
150
|
+
them for both.
|
|
151
|
+
|
|
152
|
+
> **Hardware-unverified.** The development device is a single-channel V6
|
|
153
|
+
> ECO, so this path follows the packet contract rather than a live
|
|
154
|
+
> capture. See [VERIFICATION.md](../docs/VERIFICATION.md).
|
|
155
|
+
|
|
123
156
|
## What the probe reports
|
|
124
157
|
|
|
125
158
|
Over the HTTP backend, `SoapySDRUtil --probe` reports the attached
|
|
@@ -166,6 +199,11 @@ it does offer. `listClockSources` reports the device's own vocabulary.
|
|
|
166
199
|
- `hasHardwareTime("GPS")` reports a value rather than a capability —
|
|
167
200
|
true only on the native-SDK backend with a valid fix.
|
|
168
201
|
`getHardwareTime("GPS")` returns epoch nanoseconds.
|
|
202
|
+
- `getHardwareTime("master")` returns the device's own stream clock in
|
|
203
|
+
epoch nanoseconds — the timebase it paces streams against, and the one
|
|
204
|
+
to align two receivers on. Native-SDK backend only. Unlike the default
|
|
205
|
+
it is readable before the first packet, so a probe gets a real answer.
|
|
206
|
+
See [SYNC.md](../docs/SYNC.md).
|
|
169
207
|
- The single gain element, `REF`, is the Aaronia reference level in dBm.
|
|
170
208
|
It is not an amplifier gain: raising it reduces sensitivity. Range and
|
|
171
209
|
step come from the device — −55…+23 dBm in 0.5 dB steps on a V6 ECO.
|
|
@@ -174,8 +212,6 @@ it does offer. `listClockSources` reports the device's own vocabulary.
|
|
|
174
212
|
|
|
175
213
|
## Known limitations
|
|
176
214
|
|
|
177
|
-
-
|
|
178
|
-
antenna input; simultaneous dual-channel reads are available through
|
|
179
|
-
the crate's Rust, Python and C APIs, not the SoapySDR interface.
|
|
215
|
+
- Dual RX is hardware-unverified — see [Dual-channel RX](#dual-channel-rx).
|
|
180
216
|
- Enumeration advertises a default localhost candidate without probing
|
|
181
217
|
it, because `find()` must not block on the network.
|
|
@@ -167,8 +167,17 @@ static SoapySDR::Device *makeAaronia(const SoapySDR::Kwargs &args) {
|
|
|
167
167
|
// itself streams channel 0; Rx2 selects the second antenna input.
|
|
168
168
|
if (args.count("rx_channel") != 0) {
|
|
169
169
|
const std::string &ch = args.at("rx_channel");
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// Warn rather than silently fall back to Rx1: a typo used to
|
|
171
|
+
// hand back a single-channel Rx1 stream that looked exactly
|
|
172
|
+
// like a working dual request.
|
|
173
|
+
if (ch != "Rx1" && ch != "Rx2" && ch != "Rx1And2") {
|
|
174
|
+
SoapySDR::logf(SOAPY_SDR_WARNING,
|
|
175
|
+
"aaronia: ignoring rx_channel=%s; expected Rx1, Rx2 or Rx1And2",
|
|
176
|
+
ch.c_str());
|
|
177
|
+
} else {
|
|
178
|
+
int32_t sel = ch == "Rx2" ? 1 : (ch == "Rx1And2" ? 2 : 0);
|
|
179
|
+
spectran_source_builder_receiver_channel(builder, sel);
|
|
180
|
+
}
|
|
172
181
|
}
|
|
173
182
|
// read_timeout=<seconds>: only affects the crate's own blocking
|
|
174
183
|
// reads. readStream always passes SoapySDR's per-call timeoutUs, so
|