python-aaronia 0.6.1__tar.gz → 0.6.2__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.6.1 → python_aaronia-0.6.2}/CHANGELOG.md +34 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/Cargo.lock +2 -2
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/Cargo.toml +1 -1
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/PKG-INFO +1 -1
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/HTTPSPEC.md +38 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/SDKSPEC.md +58 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/VERIFICATION.md +13 -1
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/lib.rs +2 -2
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/native_sdk.rs +236 -10
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/unified_sink.rs +5 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/utils.rs +64 -9
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/.cargo/config.toml +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/.gitattributes +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/.gitignore +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/CONTRIBUTING.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/DESIGN.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/LICENSE +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/PLUGINS.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/README.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/deny.toml +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/APPS.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/QUICKSTART.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/docs/USAGE.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/device_control.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/include/aaronia.h +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/pyproject.toml +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/python-aaronia/README.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/python-aaronia/aaronia.pyi +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/python-aaronia/src/lib.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/soapy-aaronia/AaroniaSoapyDevice.cpp +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/soapy-aaronia/README.md +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/soapy-aaronia/Registration.cpp +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/c_api.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/decompression.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/detection.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/error.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/file_source.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/http_endpoints.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/http_sink.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/http_source.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/http_streaming.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/sdk_source.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/sdr_source_impl.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/seify_impl.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/src/unified_source.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/http_resilience_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/integration_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/live_smoke.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/properties.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.6.1 → python_aaronia-0.6.2}/tests/test_cw_meta.rs +0 -0
|
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.6.2] - 2026-08-12
|
|
8
|
+
|
|
7
9
|
## [v0.6.1] - 2026-08-12
|
|
8
10
|
|
|
9
11
|
### Fixed
|
|
@@ -30,8 +32,40 @@ All notable changes to this project will be documented in this file.
|
|
|
30
32
|
guesses, so the relationships now live in one tested place. Wanting
|
|
31
33
|
8 MHz of spectrum needs 10 MHz of sampling, and
|
|
32
34
|
`iq_sample_rate_for_bandwidth` returns the 15.36 MHz that provides it.
|
|
35
|
+
`iq_sample_rates_for_clock` covers devices whose receiver clock is not
|
|
36
|
+
the default: a V6 ECO has a fixed clock and gives the measured ladder,
|
|
37
|
+
while a full V6 can select a faster one and reach further. Aaronia's
|
|
38
|
+
samples set `device/receiverclock` to "92MHz" or "245MHz"; only the
|
|
39
|
+
default has been checked against hardware.
|
|
40
|
+
|
|
41
|
+
### Added (native SDK)
|
|
42
|
+
- **Device-family auto-detection.** `detect_device_family` and
|
|
43
|
+
`open_detected_device` try each known family in turn, so an ECO owner
|
|
44
|
+
no longer has to know that the default `spectranv6` will not find
|
|
45
|
+
their device and that `spectranv6eco` is the string they needed.
|
|
46
|
+
- **`read_spectra`**, with the stream index taken from the open mode
|
|
47
|
+
rather than assumed. `spectranv6/raw` carries spectra on stream 2 and
|
|
48
|
+
IQ on stream 0; every other mode uses stream 0. Hardware-unverified.
|
|
49
|
+
- **`receiver_clock_hz`** on the native source, and
|
|
50
|
+
`spectranv6eco/rtsa` added to the known open modes. The clock sets the
|
|
51
|
+
rate ladder's ceiling, so callers that need to know which rates exist
|
|
52
|
+
can now ask instead of assuming.
|
|
33
53
|
|
|
34
54
|
### Fixed (native SDK)
|
|
55
|
+
- **The V6 ECO's fixed receiver clock was recorded as 61.44 MHz.** It is
|
|
56
|
+
92.16 MHz: an ECO streams at 61.44 MHz sampling, measured against real
|
|
57
|
+
hardware, and the constraint checked at configuration time is
|
|
58
|
+
`span * 1.5 <= clock`. The old value rejected every span above
|
|
59
|
+
40.96 MHz, including the device's own maximum.
|
|
60
|
+
- **Dual-channel capture selected the wrong mode and would have
|
|
61
|
+
returned corrupted samples.** `RxChannel::Rx1And2` wrote
|
|
62
|
+
`device/receiverchannel = "Rx1+Rx2"`, which delivers the two inputs as
|
|
63
|
+
two independent streams at indices 0 and 1. This crate reads a single
|
|
64
|
+
stream and deinterleaves it, which is the contract of the other mode,
|
|
65
|
+
`"Rx12"`. On a two-input V6 the result would have been Rx1's samples
|
|
66
|
+
split into two bogus channels, with no error anywhere. It now writes
|
|
67
|
+
`"Rx12"`. Aaronia's `RawIQ2RX` and `RawIQ2RXInterleave` samples
|
|
68
|
+
demonstrate one mode each. Still hardware-unverified.
|
|
35
69
|
- **Sweep mode set the wrong resolution-bandwidth key.** It sent
|
|
36
70
|
`main/rbw`, which no Aaronia sample uses; the key is `main/rbwfreq`.
|
|
37
71
|
Checked against Aaronia's published `SweepSpectrumEco` sample, which
|
|
@@ -3027,7 +3027,7 @@ dependencies = [
|
|
|
3027
3027
|
|
|
3028
3028
|
[[package]]
|
|
3029
3029
|
name = "python-aaronia"
|
|
3030
|
-
version = "0.6.
|
|
3030
|
+
version = "0.6.2"
|
|
3031
3031
|
dependencies = [
|
|
3032
3032
|
"arrow",
|
|
3033
3033
|
"num-complex",
|
|
@@ -3556,7 +3556,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3556
3556
|
|
|
3557
3557
|
[[package]]
|
|
3558
3558
|
name = "sdr-aaronia-rs"
|
|
3559
|
-
version = "0.6.
|
|
3559
|
+
version = "0.6.2"
|
|
3560
3560
|
dependencies = [
|
|
3561
3561
|
"anyhow",
|
|
3562
3562
|
"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.6.
|
|
5
|
+
version = "0.6.2"
|
|
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.6.
|
|
3
|
+
Version: 0.6.2
|
|
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+)
|
|
@@ -590,6 +590,44 @@ Data captured using antennas with location or directional information.
|
|
|
590
590
|
| `azimuth` | Azimuth of directional antenna |
|
|
591
591
|
| `declination` | Declination of directional antenna |
|
|
592
592
|
|
|
593
|
+
## What the native SDK samples tell us about this API
|
|
594
|
+
|
|
595
|
+
Aaronia's [C++ SDK samples](https://github.com/Aaronia-Open-source/RTSA-API-Samples)
|
|
596
|
+
drive the same hardware through a different transport, so some of what
|
|
597
|
+
they establish applies here and some does not.
|
|
598
|
+
|
|
599
|
+
### GPS needs enabling, over HTTP too
|
|
600
|
+
|
|
601
|
+
The `device/gpsmode` and `device/sclksource` keys the `GPSTime` sample
|
|
602
|
+
writes are the same keys this config tree exposes, so the prerequisite
|
|
603
|
+
is identical. A device ships with `gpsmode` set to `Disabled` and takes
|
|
604
|
+
its stream clock from whatever `sclksource` names, which on the system
|
|
605
|
+
tested here was `10MHz` rather than GPS. Timestamps are not
|
|
606
|
+
GPS-disciplined until both change, and nothing reports an error
|
|
607
|
+
meanwhile.
|
|
608
|
+
|
|
609
|
+
Observed on a V6 ECO: `gpsmode` offers `Disabled`, `Location`, `Time`
|
|
610
|
+
and `Location and Time`; `sclksource` offers `Consumer`, `Oscillator`,
|
|
611
|
+
`GPS`, `PPS`, `10MHz` and three `... Provider` variants.
|
|
612
|
+
|
|
613
|
+
### The rate ladder carries over
|
|
614
|
+
|
|
615
|
+
`decimation0` takes the same labels as the SDK's `main/decimation`,
|
|
616
|
+
`"Full"` through `"1 / 512"`, verified by writing one over HTTP. Both
|
|
617
|
+
transports drive the same divider.
|
|
618
|
+
|
|
619
|
+
### Two things that do not carry over
|
|
620
|
+
|
|
621
|
+
- **Receiver-channel selection.** There is no `device/receiverchannel`
|
|
622
|
+
here. The tree exposes per-channel settings instead — `centerfreq0`
|
|
623
|
+
and `centerfreq1`, `decimation0` and `decimation1`, `rfchsource0` and
|
|
624
|
+
`rfchsource1` — and which channels reach the stream is a property of
|
|
625
|
+
the RTSA mission graph. The SDK's `Rx12` versus `Rx1+Rx2` distinction
|
|
626
|
+
has no equivalent.
|
|
627
|
+
- **Stream indices.** The SDK separates IQ from spectra by packet stream
|
|
628
|
+
index. HTTP selects data with the `input=` parameter and reports the
|
|
629
|
+
kind in each packet's `payload` field.
|
|
630
|
+
|
|
593
631
|
## Authorization & Licensing
|
|
594
632
|
|
|
595
633
|
### HTTP Streaming vs Remote Configuration
|
|
@@ -876,3 +876,61 @@ the Aaronia Software License Agreement governs use of the SDK itself (see
|
|
|
876
876
|
[Licensing Considerations](#licensing-considerations)); this community document
|
|
877
877
|
describes only the public interface. "Aaronia", "RTSA", and "Spectran" are the
|
|
878
878
|
property of Aaronia AG.
|
|
879
|
+
|
|
880
|
+
## Facts taken from Aaronia's published samples
|
|
881
|
+
|
|
882
|
+
The following come from
|
|
883
|
+
[Aaronia-Open-source/RTSA-API-Samples](https://github.com/Aaronia-Open-source/RTSA-API-Samples),
|
|
884
|
+
read in full. They are vendor code rather than vendor documentation, but
|
|
885
|
+
they are the closest thing to an authoritative statement of how the API
|
|
886
|
+
is meant to be driven, and this crate's native-SDK paths cannot be
|
|
887
|
+
tested here.
|
|
888
|
+
|
|
889
|
+
### Both receivers, two different modes
|
|
890
|
+
|
|
891
|
+
`device/receiverchannel` takes four values, and the last two are not
|
|
892
|
+
interchangeable:
|
|
893
|
+
|
|
894
|
+
| Value | Delivery |
|
|
895
|
+
| --- | --- |
|
|
896
|
+
| `Rx1`, `Rx2` | One input, one stream |
|
|
897
|
+
| `Rx12` | Both inputs interleaved into **one** stream: four floats per sample, `[I0, Q0, I1, Q1]`, read from stream 0 |
|
|
898
|
+
| `Rx1+Rx2` | Both inputs as **two independent streams**, fetched and consumed separately at indices 0 and 1 |
|
|
899
|
+
|
|
900
|
+
`RawIQ2RXInterleave` uses the first, `RawIQ2RX` the second. This crate
|
|
901
|
+
reads a single stream and deinterleaves it, so it writes `Rx12`.
|
|
902
|
+
|
|
903
|
+
### V6 against V6 ECO
|
|
904
|
+
|
|
905
|
+
| | Full V6 | V6 ECO |
|
|
906
|
+
| --- | --- | --- |
|
|
907
|
+
| Family string | `spectranv6` | `spectranv6eco` |
|
|
908
|
+
| `device/receiverchannel` | Set explicitly | Never set: one receiver |
|
|
909
|
+
| `device/receiverclock` | Set, `"92MHz"` or `"245MHz"` | Never set: fixed |
|
|
910
|
+
| Spectrum packets | Stream index 2 | Stream index 0 |
|
|
911
|
+
| Raw-mode open string | `spectranv6/raw` | `spectranv6eco/rtsa` |
|
|
912
|
+
|
|
913
|
+
The clock matters beyond configuration: with `span * 1.5 <=
|
|
914
|
+
receiverclock`, a V6 on the fast clock reaches roughly 163 MHz of span
|
|
915
|
+
where the ECO's fixed clock allows 61.44 MHz.
|
|
916
|
+
|
|
917
|
+
### Sample rates
|
|
918
|
+
|
|
919
|
+
`main/decimation` accepts either the label (`"1 / 64"`) or the index
|
|
920
|
+
(`6`). Combined with the clock, that gives the rate ladder: the top rate
|
|
921
|
+
is `receiverclock / 1.5` and each step halves it.
|
|
922
|
+
|
|
923
|
+
### Transmitting
|
|
924
|
+
|
|
925
|
+
The samples flag the first packet `SEGMENT_START | STREAM_START`, the
|
|
926
|
+
last `SEGMENT_END | STREAM_END`, and everything between `0`. The
|
|
927
|
+
transceiver samples additionally send a zero-length packet carrying only
|
|
928
|
+
`STREAM_START`, timestamped at the master stream clock, before real
|
|
929
|
+
data, to improve startup synchronisation.
|
|
930
|
+
|
|
931
|
+
### Keys this crate does not use
|
|
932
|
+
|
|
933
|
+
`device/outputformat` (`"iq"` or `"spectra"`), `main/demodcenterfreq`
|
|
934
|
+
and `main/demodspanfreq`, `main/centerfreqtx` and `main/centerfreqrx`
|
|
935
|
+
for independent transceiver tuning, `calibration/preamp`, and the
|
|
936
|
+
read-only `boostusbbytessecond` throughput reading.
|
|
@@ -21,7 +21,10 @@ SDK are marked unverified.
|
|
|
21
21
|
| SoapySDR plugin RX | HTTP | Verified manually (~9.7 Msps via `SoapySDRUtil`); no automated live test, as a `soapysdr` dev-dependency would make `cargo test` unbuildable without system SoapySDR |
|
|
22
22
|
| Python bindings RX | HTTP | Verified manually (NumPy and Arrow); no automated live test |
|
|
23
23
|
| TX (`UnifiedSink`, `aaronia_sink_*`, SoapySDR TX) | Native SDK | **Hardware-unverified**. No TX-licensed device available |
|
|
24
|
-
| Dual-channel RX (`Rx1And2`, `read_samples_dual`) | Native SDK | **Hardware-unverified**. Requires a full V6 |
|
|
24
|
+
| Dual-channel RX (`Rx1And2`, `read_samples_dual`) | Native SDK | **Hardware-unverified**. Requires a full V6. Selects `Rx12`, the interleaved single-stream mode, matching how this crate reads |
|
|
25
|
+
| Spectra reads (`read_spectra`) | Native SDK | **Hardware-unverified**. Packet layout and stream index follow Aaronia's `RawSpectrum` sample |
|
|
26
|
+
| Device-family detection (`open_detected_device`) | Native SDK | **Hardware-unverified**. Enumerates each known family in turn |
|
|
27
|
+
| Sample-rate ladder and usable bandwidth | Both | **Live-verified at the default clock** on a V6 ECO, rung by rung. Faster receiver clocks are inferred from the documented constraint, not measured |
|
|
25
28
|
| GPS hardware time | Native SDK | **Hardware-unverified** |
|
|
26
29
|
| Native SDK capture generally | Native SDK | **Hardware-unverified**; compiled and unit-tested in a Linux VM each release |
|
|
27
30
|
| HTTP TX push (`/sample`) | HTTP | Endpoint exercised live; RF output not measured |
|
|
@@ -40,6 +43,15 @@ cargo test --all-features --test live_smoke -- --ignored --nocapture
|
|
|
40
43
|
Contributions that convert an unverified row, particularly from users
|
|
41
44
|
with a full V6 or a TX licence, are welcome.
|
|
42
45
|
|
|
46
|
+
Native-SDK entries carry a second qualifier worth stating plainly: they
|
|
47
|
+
are not merely untested, they run on a platform this project cannot
|
|
48
|
+
build on locally. They are compile-checked for Linux and Windows and
|
|
49
|
+
their logic is drawn from Aaronia's published samples, which caught
|
|
50
|
+
three real defects, but nothing has executed them against a device.
|
|
51
|
+
|
|
52
|
+
If you have a full V6 or a TX licence, closing one of these rows is the
|
|
53
|
+
single most valuable contribution you can make to this crate.
|
|
54
|
+
|
|
43
55
|
## Related
|
|
44
56
|
|
|
45
57
|
- [QUICKSTART.md](QUICKSTART.md) — setting up an RTSA-Suite mission.
|
|
@@ -134,6 +134,6 @@ pub use unified_source::{
|
|
|
134
134
|
/// Utilities for DB/linear conversions and string parsing
|
|
135
135
|
pub use utils::{
|
|
136
136
|
IQ_CLOCK_HZ, USABLE_BANDWIDTH_RATIO, db_to_linear, format_frequency, format_sample_rate,
|
|
137
|
-
iq_sample_rate_for_bandwidth, iq_sample_rates,
|
|
138
|
-
parse_frequency, parse_sample_rate, usable_bandwidth_hz,
|
|
137
|
+
iq_sample_rate_for_bandwidth, iq_sample_rates, iq_sample_rates_for_clock, linear_to_db,
|
|
138
|
+
nearest_iq_sample_rate, parse_frequency, parse_sample_rate, usable_bandwidth_hz,
|
|
139
139
|
};
|
|
@@ -725,6 +725,43 @@ impl NativeSdkClient {
|
|
|
725
725
|
}
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
/// The device families this SDK knows about, in the order worth
|
|
729
|
+
/// trying.
|
|
730
|
+
pub const DEVICE_FAMILIES: [&'static str; 2] = ["spectranv6", "spectranv6eco"];
|
|
731
|
+
|
|
732
|
+
/// Find which family actually has a device attached.
|
|
733
|
+
///
|
|
734
|
+
/// `AARTSAAPI_EnumDevice` matches one family at a time, so a caller
|
|
735
|
+
/// configured for `spectranv6` finds nothing on a machine holding
|
|
736
|
+
/// only a V6 ECO, and the failure reads as "no device" rather than
|
|
737
|
+
/// "wrong family". This tries each family in turn and returns the
|
|
738
|
+
/// first that enumerates a device, so the common case needs no
|
|
739
|
+
/// configuration at all.
|
|
740
|
+
///
|
|
741
|
+
/// # Safety
|
|
742
|
+
/// `handle` must be an open library handle.
|
|
743
|
+
pub unsafe fn detect_device_family(
|
|
744
|
+
&self,
|
|
745
|
+
handle: &mut AARTSAAPI_Handle,
|
|
746
|
+
) -> Result<Option<&'static str>> {
|
|
747
|
+
for family in Self::DEVICE_FAMILIES {
|
|
748
|
+
match unsafe { self.enum_device(handle, family, 0) } {
|
|
749
|
+
Ok(Some(_)) => {
|
|
750
|
+
info!("Detected device family {family}");
|
|
751
|
+
return Ok(Some(family));
|
|
752
|
+
}
|
|
753
|
+
// An empty family is the normal answer for the one that
|
|
754
|
+
// is not attached; keep looking.
|
|
755
|
+
Ok(None) => continue,
|
|
756
|
+
Err(e) => {
|
|
757
|
+
debug!("Enumerating {family} failed, trying the next: {e}");
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
Ok(None)
|
|
763
|
+
}
|
|
764
|
+
|
|
728
765
|
pub unsafe fn enum_device(
|
|
729
766
|
&self,
|
|
730
767
|
handle: &mut AARTSAAPI_Handle,
|
|
@@ -1240,6 +1277,27 @@ pub(crate) fn split_packet(packet_samples: usize, remaining: usize) -> (usize, u
|
|
|
1240
1277
|
(to_caller, packet_samples - to_caller)
|
|
1241
1278
|
}
|
|
1242
1279
|
|
|
1280
|
+
/// What one spectra packet described, alongside the values appended to
|
|
1281
|
+
/// the caller's buffer.
|
|
1282
|
+
///
|
|
1283
|
+
/// A packet holds `frames` consecutive spectra of `bins_per_frame`
|
|
1284
|
+
/// values each, laid out one after another, so frame `i` occupies
|
|
1285
|
+
/// `[i * bins_per_frame ..][.. bins_per_frame]` of what was appended.
|
|
1286
|
+
/// Values are dBm.
|
|
1287
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
1288
|
+
pub struct SpectraRead {
|
|
1289
|
+
/// Number of spectra in the packet.
|
|
1290
|
+
pub frames: usize,
|
|
1291
|
+
/// Bins in each spectrum.
|
|
1292
|
+
pub bins_per_frame: usize,
|
|
1293
|
+
/// Frequency of the first bin, in Hz.
|
|
1294
|
+
pub start_frequency_hz: f64,
|
|
1295
|
+
/// Spacing between bins, in Hz.
|
|
1296
|
+
pub step_frequency_hz: f64,
|
|
1297
|
+
/// Packet start time, on the device's clock.
|
|
1298
|
+
pub start_time: f64,
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1243
1301
|
// === High-Level Stream Manager ===
|
|
1244
1302
|
|
|
1245
1303
|
/// The open-mode supplied to `AARTSAAPI_OpenDevice`. Tracking which family /
|
|
@@ -1251,6 +1309,9 @@ pub enum DeviceOpenMode {
|
|
|
1251
1309
|
Raw,
|
|
1252
1310
|
/// `spectranv6eco/iqreceiver` — IQ receiver on the ECO platform.
|
|
1253
1311
|
EcoIqReceiver,
|
|
1312
|
+
/// `spectranv6eco/rtsa` — the ECO's raw pipeline, which is what its
|
|
1313
|
+
/// spectrum sample opens. There is no `spectranv6eco/raw`.
|
|
1314
|
+
EcoRtsa,
|
|
1254
1315
|
/// `spectranv6/sweepsa` — sweep spectrum analyzer on the V6 platform.
|
|
1255
1316
|
Sweepsa,
|
|
1256
1317
|
/// `spectranv6eco/sweepsa` — sweep spectrum analyzer on the ECO platform.
|
|
@@ -1267,6 +1328,9 @@ impl DeviceOpenMode {
|
|
|
1267
1328
|
pub fn from_open_string(s: &str) -> Self {
|
|
1268
1329
|
match s {
|
|
1269
1330
|
"spectranv6/raw" => Self::Raw,
|
|
1331
|
+
// The ECO's equivalent of raw mode is called rtsa; it has no
|
|
1332
|
+
// "spectranv6eco/raw".
|
|
1333
|
+
"spectranv6eco/rtsa" => Self::EcoRtsa,
|
|
1270
1334
|
"spectranv6eco/iqreceiver" => Self::EcoIqReceiver,
|
|
1271
1335
|
"spectranv6/sweepsa" => Self::Sweepsa,
|
|
1272
1336
|
"spectranv6eco/sweepsa" => Self::EcoSweepsa,
|
|
@@ -1281,6 +1345,22 @@ impl DeviceOpenMode {
|
|
|
1281
1345
|
pub fn supports_raw_only_keys(&self) -> bool {
|
|
1282
1346
|
matches!(self, Self::Raw)
|
|
1283
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
/// Which stream index carries spectra in this mode.
|
|
1350
|
+
///
|
|
1351
|
+
/// Not a property of the device family, which is how this reads at
|
|
1352
|
+
/// first glance. In `spectranv6/raw` the same device delivers IQ on
|
|
1353
|
+
/// stream 0 and spectra on stream 2, selected by
|
|
1354
|
+
/// `device/outputformat`; Aaronia's `RawIQ` and `RawSpectrum`
|
|
1355
|
+
/// samples differ in exactly that. Every other mode, including the
|
|
1356
|
+
/// ECO's `rtsa` and both `sweepsa` variants, carries spectra on
|
|
1357
|
+
/// stream 0.
|
|
1358
|
+
pub fn spectra_stream_index(&self) -> i32 {
|
|
1359
|
+
match self {
|
|
1360
|
+
Self::Raw => 2,
|
|
1361
|
+
_ => 0,
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1284
1364
|
}
|
|
1285
1365
|
|
|
1286
1366
|
pub struct NativeSdkSource {
|
|
@@ -1305,6 +1385,11 @@ pub struct NativeSdkSource {
|
|
|
1305
1385
|
/// time-gaps into both outputs (each call consumes whole packets
|
|
1306
1386
|
/// the other path never sees), so the second path errors instead.
|
|
1307
1387
|
read_mode: Option<ReadMode>,
|
|
1388
|
+
/// Receiver clock in Hz, learned when the IQ receiver is configured.
|
|
1389
|
+
/// The sample-rate ladder is derived from it, so callers that need
|
|
1390
|
+
/// to know which rates exist should read it rather than assume the
|
|
1391
|
+
/// default. `None` until the device has been configured.
|
|
1392
|
+
receiver_clock_hz: Option<f64>,
|
|
1308
1393
|
}
|
|
1309
1394
|
|
|
1310
1395
|
/// Which of the two packet-consuming read paths a streaming session
|
|
@@ -1399,6 +1484,7 @@ impl NativeSdkSource {
|
|
|
1399
1484
|
sample_buffer: VecDeque::new(),
|
|
1400
1485
|
dual_sample_buffer: VecDeque::new(),
|
|
1401
1486
|
read_mode: None,
|
|
1487
|
+
receiver_clock_hz: None,
|
|
1402
1488
|
})
|
|
1403
1489
|
}
|
|
1404
1490
|
}
|
|
@@ -1595,6 +1681,40 @@ impl NativeSdkSource {
|
|
|
1595
1681
|
}
|
|
1596
1682
|
}
|
|
1597
1683
|
|
|
1684
|
+
/// Open whichever device is present, without the caller naming the
|
|
1685
|
+
/// family.
|
|
1686
|
+
///
|
|
1687
|
+
/// `mode` is the suffix, for example `"raw"` or `"iqreceiver"`. The
|
|
1688
|
+
/// family is detected first, then joined to it. Note the two
|
|
1689
|
+
/// families do not offer identical modes: the V6 has `raw`, while
|
|
1690
|
+
/// the ECO's equivalent is `rtsa`, so pass a mode the detected
|
|
1691
|
+
/// family supports or use [`Self::open_device`] directly.
|
|
1692
|
+
///
|
|
1693
|
+
/// # Safety
|
|
1694
|
+
/// Same contract as [`Self::open_device`].
|
|
1695
|
+
pub unsafe fn open_detected_device(
|
|
1696
|
+
&mut self,
|
|
1697
|
+
mode: &str,
|
|
1698
|
+
serial_number: &[WideChar],
|
|
1699
|
+
) -> Result<()> {
|
|
1700
|
+
unsafe {
|
|
1701
|
+
let family = {
|
|
1702
|
+
let handle = self
|
|
1703
|
+
.handle
|
|
1704
|
+
.as_mut()
|
|
1705
|
+
.ok_or_else(|| Error::Sdk("SDK not initialized".to_string()))?;
|
|
1706
|
+
self.client.detect_device_family(handle)?.ok_or_else(|| {
|
|
1707
|
+
Error::Sdk(format!(
|
|
1708
|
+
"no Aaronia device found in any known family ({})",
|
|
1709
|
+
NativeSdkClient::DEVICE_FAMILIES.join(", ")
|
|
1710
|
+
))
|
|
1711
|
+
})?
|
|
1712
|
+
};
|
|
1713
|
+
let open_string = format!("{family}/{mode}");
|
|
1714
|
+
self.open_device(&open_string, serial_number)
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1598
1718
|
/// Returns the currently configured open mode, if a device is open.
|
|
1599
1719
|
pub fn open_mode(&self) -> Option<DeviceOpenMode> {
|
|
1600
1720
|
self.open_mode.clone()
|
|
@@ -1887,13 +2007,18 @@ impl NativeSdkSource {
|
|
|
1887
2007
|
crate::utils::DEFAULT_RECEIVER_CLOCK_HZ
|
|
1888
2008
|
}
|
|
1889
2009
|
} else {
|
|
1890
|
-
// Eco devices report no receiverclock key
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
|
|
1894
|
-
|
|
2010
|
+
// Eco devices report no receiverclock key: the clock is
|
|
2011
|
+
// fixed. It is 92.16 MHz, not the 61.44 MHz this used to
|
|
2012
|
+
// assume. A V6 ECO streams at 61.44 MHz sampling, measured
|
|
2013
|
+
// over HTTP against real hardware, and the constraint
|
|
2014
|
+
// checked below is `span * 1.5 <= clock`, so the clock
|
|
2015
|
+
// cannot be lower than 92.16 MHz. With the old value this
|
|
2016
|
+
// rejected every span above 40.96 MHz, including the
|
|
2017
|
+
// device's own maximum.
|
|
2018
|
+
crate::utils::DEFAULT_RECEIVER_CLOCK_HZ
|
|
1895
2019
|
};
|
|
1896
2020
|
|
|
2021
|
+
self.receiver_clock_hz = Some(actual_clock_hz);
|
|
1897
2022
|
crate::utils::validate_iq_mode(span_freq, actual_clock_hz)?;
|
|
1898
2023
|
|
|
1899
2024
|
info!("IQ Receiver configuration completed");
|
|
@@ -1979,6 +2104,101 @@ impl NativeSdkSource {
|
|
|
1979
2104
|
pub const READ_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
|
|
1980
2105
|
pub const READ_POLL_DEADLINE: std::time::Duration = std::time::Duration::from_millis(500);
|
|
1981
2106
|
|
|
2107
|
+
/// The receiver clock in Hz, once the device has been configured.
|
|
2108
|
+
///
|
|
2109
|
+
/// Worth reading rather than assuming: the sample-rate ladder runs
|
|
2110
|
+
/// from `clock / 1.5` downward in halves, so a V6 on the 245 MHz
|
|
2111
|
+
/// clock reaches rates a V6 ECO cannot. Pass this to
|
|
2112
|
+
/// [`crate::utils::iq_sample_rates_for_clock`] to get the rates this
|
|
2113
|
+
/// device actually supports.
|
|
2114
|
+
pub fn receiver_clock_hz(&self) -> Option<f64> {
|
|
2115
|
+
self.receiver_clock_hz
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/// Read one packet of spectra, appending its values to `out`.
|
|
2119
|
+
///
|
|
2120
|
+
/// Returns `None` when no packet arrived within
|
|
2121
|
+
/// [`Self::READ_POLL_DEADLINE`], matching the IQ read.
|
|
2122
|
+
///
|
|
2123
|
+
/// The stream index comes from the open mode rather than a constant:
|
|
2124
|
+
/// `spectranv6/raw` carries spectra on stream 2 while its IQ is on
|
|
2125
|
+
/// stream 0, and every other mode uses stream 0. See
|
|
2126
|
+
/// [`DeviceOpenMode::spectra_stream_index`]. The device must also be
|
|
2127
|
+
/// producing spectra in the first place, which in raw mode means
|
|
2128
|
+
/// `device/outputformat` set to `"spectra"`.
|
|
2129
|
+
///
|
|
2130
|
+
/// > Hardware-unverified. The packet layout, `num` frames of `size`
|
|
2131
|
+
/// > bins, follows Aaronia's `RawSpectrum` sample; no spectra-capable
|
|
2132
|
+
/// > device has been available to run it against.
|
|
2133
|
+
///
|
|
2134
|
+
/// # Safety
|
|
2135
|
+
/// The device must be open and streaming.
|
|
2136
|
+
pub unsafe fn read_spectra(&mut self, out: &mut Vec<f32>) -> Result<Option<SpectraRead>> {
|
|
2137
|
+
unsafe {
|
|
2138
|
+
if !self.stream_active {
|
|
2139
|
+
return Err(Error::Sdk("Streaming not active".to_string()));
|
|
2140
|
+
}
|
|
2141
|
+
let stream = self
|
|
2142
|
+
.open_mode
|
|
2143
|
+
.as_ref()
|
|
2144
|
+
.map(DeviceOpenMode::spectra_stream_index)
|
|
2145
|
+
.unwrap_or(0);
|
|
2146
|
+
let device = self
|
|
2147
|
+
.device
|
|
2148
|
+
.as_mut()
|
|
2149
|
+
.ok_or_else(|| Error::Sdk("No device opened".to_string()))?;
|
|
2150
|
+
|
|
2151
|
+
let started = std::time::Instant::now();
|
|
2152
|
+
let packet_opt = loop {
|
|
2153
|
+
match self.client.get_packet(device, stream, 0)? {
|
|
2154
|
+
Some(p) => break Some(p),
|
|
2155
|
+
None => {
|
|
2156
|
+
if started.elapsed() >= Self::READ_POLL_DEADLINE {
|
|
2157
|
+
break None;
|
|
2158
|
+
}
|
|
2159
|
+
std::thread::sleep(Self::READ_POLL_INTERVAL);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
let Some(packet) = packet_opt else {
|
|
2165
|
+
return Ok(None);
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
// Consume on every path out from here: `get_packet` always
|
|
2169
|
+
// returns the head of the queue, so leaving one behind makes
|
|
2170
|
+
// every later read return the same packet forever.
|
|
2171
|
+
let result = (|| {
|
|
2172
|
+
if packet.fp32.is_null() || packet.num <= 0 || packet.size == 0 {
|
|
2173
|
+
return Ok(None);
|
|
2174
|
+
}
|
|
2175
|
+
let frames = packet.num as usize;
|
|
2176
|
+
let bins = packet.size as usize;
|
|
2177
|
+
// Bound the product before allocating: a corrupt header
|
|
2178
|
+
// must not size a multi-gigabyte copy.
|
|
2179
|
+
const MAX_VALUES: usize = 1 << 26;
|
|
2180
|
+
let total = frames.checked_mul(bins).filter(|n| *n <= MAX_VALUES);
|
|
2181
|
+
let Some(total) = total else {
|
|
2182
|
+
return Err(Error::Sdk(format!(
|
|
2183
|
+
"spectra packet claims {frames} frames of {bins} bins, which exceeds the {MAX_VALUES}-value limit"
|
|
2184
|
+
)));
|
|
2185
|
+
};
|
|
2186
|
+
out.reserve(total);
|
|
2187
|
+
out.extend_from_slice(std::slice::from_raw_parts(packet.fp32, total));
|
|
2188
|
+
Ok(Some(SpectraRead {
|
|
2189
|
+
frames,
|
|
2190
|
+
bins_per_frame: bins,
|
|
2191
|
+
start_frequency_hz: packet.start_frequency,
|
|
2192
|
+
step_frequency_hz: packet.step_frequency,
|
|
2193
|
+
start_time: packet.start_time,
|
|
2194
|
+
}))
|
|
2195
|
+
})();
|
|
2196
|
+
|
|
2197
|
+
self.client.consume_packets(device, stream, 1)?;
|
|
2198
|
+
result
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
1982
2202
|
/// Read up to `max_samples` complex IQ pairs from the device.
|
|
1983
2203
|
///
|
|
1984
2204
|
/// **Magnitude scaling differs by open mode.** Comparing the official
|
|
@@ -2494,11 +2714,16 @@ impl Drop for NativeSdkSource {
|
|
|
2494
2714
|
/// `device/receiverchannel` config item.
|
|
2495
2715
|
///
|
|
2496
2716
|
/// The full SPECTRAN V6 has two RF inputs; the V6 ECO has one. The config
|
|
2497
|
-
/// strings
|
|
2498
|
-
///
|
|
2499
|
-
///
|
|
2500
|
-
///
|
|
2501
|
-
///
|
|
2717
|
+
/// strings come from the official RTSA-API-Samples, which use four:
|
|
2718
|
+
/// `"Rx1"`, `"Rx2"`, `"Rx12"` and `"Rx1+Rx2"`. The last two both enable
|
|
2719
|
+
/// both inputs but differ in delivery — `"Rx12"` interleaves them into
|
|
2720
|
+
/// one stream, `"Rx1+Rx2"` produces two independent streams — so they
|
|
2721
|
+
/// are not interchangeable. This crate reads one stream and
|
|
2722
|
+
/// deinterleaves it, so [`RxChannel::Rx1And2`] writes `"Rx12"`.
|
|
2723
|
+
///
|
|
2724
|
+
/// `"Rx1"` is what [`NativeSdkSource::configure_iq_receiver`] has
|
|
2725
|
+
/// always written and is the only variant verified against real
|
|
2726
|
+
/// hardware (see [`NativeSdkSource::set_receiver_channel`]).
|
|
2502
2727
|
///
|
|
2503
2728
|
/// The enum itself lives in [`crate::utils`] so cross-platform
|
|
2504
2729
|
/// configuration code can name a channel without the `native-sdk`
|
|
@@ -3132,6 +3357,7 @@ mod tests {
|
|
|
3132
3357
|
sample_buffer: VecDeque::new(),
|
|
3133
3358
|
dual_sample_buffer: VecDeque::new(),
|
|
3134
3359
|
read_mode: None,
|
|
3360
|
+
receiver_clock_hz: None,
|
|
3135
3361
|
};
|
|
3136
3362
|
|
|
3137
3363
|
assert!(!source.is_streaming());
|
|
@@ -189,6 +189,11 @@ impl UnifiedSink {
|
|
|
189
189
|
)),
|
|
190
190
|
allow(unused_variables)
|
|
191
191
|
)]
|
|
192
|
+
/// Aaronia's `IQTransceiverSweep` sample precedes real data with a
|
|
193
|
+
/// zero-length packet carrying only `STREAM_START`, timestamped at
|
|
194
|
+
/// the master stream clock, "to improve startup synch". Passing an
|
|
195
|
+
/// empty slice here does the same thing. Untested against hardware,
|
|
196
|
+
/// like the rest of this path, but it is what the vendor does.
|
|
192
197
|
pub fn write_samples(
|
|
193
198
|
&mut self,
|
|
194
199
|
channel: i32,
|
|
@@ -39,9 +39,18 @@ pub enum RxChannel {
|
|
|
39
39
|
Rx1,
|
|
40
40
|
/// Second RF input (full V6 only; hardware-unverified).
|
|
41
41
|
Rx2,
|
|
42
|
-
/// Both inputs
|
|
43
|
-
/// hardware-unverified). Read
|
|
44
|
-
///
|
|
42
|
+
/// Both inputs interleaved into a single stream (full V6 only;
|
|
43
|
+
/// hardware-unverified). Read with `read_samples_dual` on the
|
|
44
|
+
/// native SDK source.
|
|
45
|
+
///
|
|
46
|
+
/// The device offers two ways to run both inputs and they are not
|
|
47
|
+
/// interchangeable. `"Rx12"`, used here, interleaves the pair into
|
|
48
|
+
/// one stream: four floats per sample, `[I0, Q0, I1, Q1]`, read
|
|
49
|
+
/// from stream 0. `"Rx1+Rx2"` instead delivers two independent
|
|
50
|
+
/// streams that must be fetched and consumed separately, at indices
|
|
51
|
+
/// 0 and 1. Aaronia's `RawIQ2RXInterleave` and `RawIQ2RX` samples
|
|
52
|
+
/// show one each. This crate reads a single stream and
|
|
53
|
+
/// deinterleaves it, so it requires the former.
|
|
45
54
|
Rx1And2,
|
|
46
55
|
}
|
|
47
56
|
|
|
@@ -51,7 +60,10 @@ impl RxChannel {
|
|
|
51
60
|
match self {
|
|
52
61
|
Self::Rx1 => "Rx1",
|
|
53
62
|
Self::Rx2 => "Rx2",
|
|
54
|
-
|
|
63
|
+
// Not "Rx1+Rx2": that selects two separate streams, while
|
|
64
|
+
// this crate reads one interleaved stream. See the variant
|
|
65
|
+
// documentation above.
|
|
66
|
+
Self::Rx1And2 => "Rx12",
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
}
|
|
@@ -80,7 +92,7 @@ pub fn deinterleave_dual_iq<'a>(
|
|
|
80
92
|
if stride < 4 {
|
|
81
93
|
return Err(Error::Sdk(format!(
|
|
82
94
|
"packet stride {} cannot carry two interleaved IQ channels (need >= 4); \
|
|
83
|
-
is device/receiverchannel set to
|
|
95
|
+
is device/receiverchannel set to Rx12?",
|
|
84
96
|
stride
|
|
85
97
|
)));
|
|
86
98
|
}
|
|
@@ -283,7 +295,13 @@ pub fn receiver_clock_for_label(label: &str) -> f64 {
|
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
///
|
|
298
|
+
/// Highest IQ sample rate available with the default receiver clock.
|
|
299
|
+
///
|
|
300
|
+
/// This is the [`DEFAULT_RECEIVER_CLOCK_HZ`] divided by the 1.5 factor
|
|
301
|
+
/// that [`validate_iq_mode`] enforces, and it matches the maximum
|
|
302
|
+
/// measured on a SPECTRAN V6 ECO. A full V6 can select a faster
|
|
303
|
+
/// receiver clock, so use [`iq_sample_rates_for_clock`] when the clock
|
|
304
|
+
/// is known rather than assuming this ceiling.
|
|
287
305
|
pub const IQ_CLOCK_HZ: f64 = 61_440_000.0;
|
|
288
306
|
|
|
289
307
|
/// Fraction of the sample rate that survives the anti-alias filter.
|
|
@@ -301,9 +319,27 @@ pub const USABLE_BANDWIDTH_RATIO: f64 = 0.8;
|
|
|
301
319
|
/// it got what it asked for will compute every derived frequency
|
|
302
320
|
/// wrongly.
|
|
303
321
|
pub fn iq_sample_rates() -> [f64; 10] {
|
|
322
|
+
iq_sample_rates_for_clock(DEFAULT_RECEIVER_CLOCK_HZ)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// The IQ sample rates available at a given receiver clock, highest
|
|
326
|
+
/// first.
|
|
327
|
+
///
|
|
328
|
+
/// The top rate is `receiver_clock_hz / 1.5`, the most that
|
|
329
|
+
/// [`validate_iq_mode`] permits, and each step halves it.
|
|
330
|
+
///
|
|
331
|
+
/// **Measured only at the default clock.** A V6 ECO has a fixed
|
|
332
|
+
/// receiver clock and produced exactly the ladder this returns for
|
|
333
|
+
/// [`DEFAULT_RECEIVER_CLOCK_HZ`], verified rung by rung. A full V6 can
|
|
334
|
+
/// select other clocks — Aaronia's own samples use `"92MHz"` and
|
|
335
|
+
/// `"245MHz"` — and the rates there follow the same constraint but have
|
|
336
|
+
/// not been confirmed against hardware. Read `device/receiverclock`
|
|
337
|
+
/// from the device and pass it here rather than assuming.
|
|
338
|
+
pub fn iq_sample_rates_for_clock(receiver_clock_hz: f64) -> [f64; 10] {
|
|
339
|
+
let top = receiver_clock_hz / 1.5;
|
|
304
340
|
let mut rates = [0.0; 10];
|
|
305
341
|
for (n, rate) in rates.iter_mut().enumerate() {
|
|
306
|
-
*rate =
|
|
342
|
+
*rate = top / f64::from(1u32 << n);
|
|
307
343
|
}
|
|
308
344
|
rates
|
|
309
345
|
}
|
|
@@ -396,6 +432,22 @@ mod ladder_tests {
|
|
|
396
432
|
use super::*;
|
|
397
433
|
|
|
398
434
|
/// The ten rates the device reports in its own decimation enum.
|
|
435
|
+
/// A V6 with a faster receiver clock reaches higher rates. Aaronia's
|
|
436
|
+
/// samples select "245MHz", which the constraint puts at 163.84 MHz
|
|
437
|
+
/// of span. Inferred from the constraint, not measured.
|
|
438
|
+
#[test]
|
|
439
|
+
fn faster_clock_raises_the_ceiling() {
|
|
440
|
+
let fast = iq_sample_rates_for_clock(245_760_000.0);
|
|
441
|
+
assert!((fast[0] - 163_840_000.0).abs() < 1.0);
|
|
442
|
+
assert!(
|
|
443
|
+
fast[0] > iq_sample_rates()[0],
|
|
444
|
+
"a faster clock must reach further"
|
|
445
|
+
);
|
|
446
|
+
for pair in fast.windows(2) {
|
|
447
|
+
assert!((pair[0] / pair[1] - 2.0).abs() < 1e-9);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
399
451
|
#[test]
|
|
400
452
|
fn ladder_matches_the_device() {
|
|
401
453
|
let rates = iq_sample_rates();
|
|
@@ -457,7 +509,10 @@ mod tests {
|
|
|
457
509
|
// per the official RTSA-API-Samples.
|
|
458
510
|
assert_eq!(RxChannel::Rx1.as_config_str(), "Rx1");
|
|
459
511
|
assert_eq!(RxChannel::Rx2.as_config_str(), "Rx2");
|
|
460
|
-
|
|
512
|
+
// "Rx12" interleaves both inputs into one stream, which is
|
|
513
|
+
// what this crate reads. "Rx1+Rx2" would deliver two separate
|
|
514
|
+
// streams and silently break the deinterleave.
|
|
515
|
+
assert_eq!(RxChannel::Rx1And2.as_config_str(), "Rx12");
|
|
461
516
|
}
|
|
462
517
|
|
|
463
518
|
#[test]
|
|
@@ -499,7 +554,7 @@ mod tests {
|
|
|
499
554
|
Err(e) => e,
|
|
500
555
|
Ok(_) => panic!("stride 2 must be rejected"),
|
|
501
556
|
};
|
|
502
|
-
assert!(err.to_string().contains("
|
|
557
|
+
assert!(err.to_string().contains("Rx12"), "got: {err}");
|
|
503
558
|
}
|
|
504
559
|
|
|
505
560
|
#[test]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|