python-aaronia 0.7.7__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.gitignore +6 -8
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/CHANGELOG.md +146 -1
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/Cargo.lock +2 -2
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/Cargo.toml +1 -1
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/PKG-INFO +1 -1
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/README.md +24 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/APPS.md +7 -2
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/HTTPSPEC.md +96 -5
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/USAGE.md +69 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/include/aaronia.h +55 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/AaroniaSoapyDevice.cpp +142 -24
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/AaroniaSoapyDevice.hpp +20 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/CMakeLists.txt +14 -5
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/README.md +80 -11
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/Registration.cpp +35 -7
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/c_api.rs +293 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_endpoints.rs +818 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_source.rs +261 -4
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/link_budget.rs +34 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/native_sdk.rs +1 -1
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdk_source.rs +4 -2
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/unified_sink.rs +24 -4
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/unified_source.rs +33 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/utils.rs +133 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/live_smoke.rs +84 -0
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeCache.txt +0 -439
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeCXXCompiler.cmake +0 -103
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CMakeSystem.cmake +0 -15
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -954
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/a.out +0 -0
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/4.4.2/CompilerIdCXX/apple-sdk.cpp +0 -1
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeConfigureLog.yaml +0 -1904
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeDirectoryInformation.cmake +0 -16
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/CMakeRuleHashes.txt +0 -3
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/InstallScripts.json +0 -7
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/Makefile.cmake +0 -61
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/Makefile2 +0 -157
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/TargetDirectories.txt +0 -8
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/DependInfo.cmake +0 -24
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/build.make +0 -131
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/cmake_clean.cmake +0 -13
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.make +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/compiler_depend.ts +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/depend.make +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/flags.make +0 -12
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/link.txt +0 -1
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/aaroniaSupport.dir/progress.make +0 -4
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/cmake.check_cache +0 -1
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/progress.marks +0 -1
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/DependInfo.cmake +0 -22
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/build.make +0 -94
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/cmake_clean.cmake +0 -9
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.make +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/compiler_depend.ts +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/CMakeFiles/sdr_aaronia_rs_static.dir/progress.make +0 -2
- python_aaronia-0.7.7/soapy-aaronia/build-test/Makefile +0 -271
- python_aaronia-0.7.7/soapy-aaronia/build-test/cmake_install.cmake +0 -78
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.cargo/config.toml +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/.gitattributes +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/CONTRIBUTING.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/DESIGN.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/LICENSE +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/PLUGINS.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/deny.toml +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/QUICKSTART.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/SDKSPEC.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/docs/VERIFICATION.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/device_control.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/pyproject.toml +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/README.md +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/aaronia.pyi +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/src/lib.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/soapy-aaronia/print.cmake +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/decompression.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/detection.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/error.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/file_source.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_sink.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/http_streaming.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/lib.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/sdr_source_impl.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/src/seify_impl.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/test.cmake +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_resilience_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/integration_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/properties.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.7.7 → python_aaronia-0.8.0}/tests/test_cw_meta.rs +0 -0
|
@@ -10,15 +10,13 @@ tests/sdk/
|
|
|
10
10
|
# Local agent configuration/memory
|
|
11
11
|
.agents/
|
|
12
12
|
|
|
13
|
-
# CMake build
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
branch_diff.txt
|
|
13
|
+
# Local CMake build trees for the SoapySDR plugin. Globbed, not a single
|
|
14
|
+
# `build/`: a second tree under any other name (build-test, build-e2e)
|
|
15
|
+
# was committed once already — 33 files of CMakeCache and compiler probe
|
|
16
|
+
# binaries carrying absolute paths from the machine that made them.
|
|
17
|
+
soapy-aaronia/build*/
|
|
19
18
|
|
|
20
|
-
# Python bindings dev environment and
|
|
19
|
+
# Python bindings dev environment and build output
|
|
21
20
|
python-aaronia/venv/
|
|
22
21
|
python-aaronia/**/__pycache__/
|
|
23
|
-
soapy-aaronia/build/
|
|
24
22
|
dist/
|
|
@@ -2,7 +2,152 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [v0.8.0] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
**Breaking:** `StreamStats` gained a `device_health` field, so an
|
|
8
|
+
exhaustive struct literal over it no longer compiles. It is
|
|
9
|
+
`#[non_exhaustive]` now, along with the new `DeviceCapabilities` and
|
|
10
|
+
`DeviceHealthSummary`: these are reports a consumer reads rather than
|
|
11
|
+
builds, and the attribute makes every future field addition free. Only
|
|
12
|
+
the `futuresdr` feature exposes `StreamStats`; the default feature set is
|
|
13
|
+
unaffected.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **The SoapySDR probe now reports what the device says about itself.**
|
|
17
|
+
`SoapySDRUtil --probe` published constants compiled in for one model:
|
|
18
|
+
`hardware=Spectran V6` whatever was attached, a 10 Hz–6 GHz frequency
|
|
19
|
+
range, and a −100…+10 dB gain range. A SPECTRAN V6 ECO declares
|
|
20
|
+
5.5 MHz–8 GHz and −55…+23 dBm — too low at one end for any tune to
|
|
21
|
+
succeed, two whole GHz short at the other, and a gain slider spanning
|
|
22
|
+
values the device clamps. The plugin now reads `/remoteconfig` and
|
|
23
|
+
`/healthstatus` once at construction and publishes the model, serial,
|
|
24
|
+
firmware version, both ranges with their declared steps, and the
|
|
25
|
+
sample-rate ladder.
|
|
26
|
+
|
|
27
|
+
The ladder is the part that needed care: `status/iqsamples` is the
|
|
28
|
+
device's native undecimated rate but it is a *measurement*
|
|
29
|
+
(61 411 246 Hz for a nominal 61 440 000), so it is snapped to the exact
|
|
30
|
+
`receiver_clock / 1.5` rung it names — new `utils::snap_to_ladder_top`
|
|
31
|
+
— before being halved once per rung `decimation0` offers. A rate
|
|
32
|
+
advertised to an application has to be one the device can be set to.
|
|
33
|
+
`getSampleRateRange` is now taken from the ends of that same ladder, so
|
|
34
|
+
it can no longer disagree with `listSampleRates`; its old 10 kHz floor
|
|
35
|
+
sat below the slowest rung the hardware has.
|
|
36
|
+
|
|
37
|
+
New API: `http_endpoints::DeviceCapabilities` and `ValueRange`,
|
|
38
|
+
`HttpEndpointsClient::get_device_capabilities()`,
|
|
39
|
+
`AaroniaSource::device_capabilities()`, and the C entry points
|
|
40
|
+
`aaronia_source_get_capabilities` / `aaronia_source_capabilities_free`.
|
|
41
|
+
Every field is optional and independently so: a device answering about
|
|
42
|
+
frequency but not gain still gets its frequency range published, and
|
|
43
|
+
the file and native-SDK backends — which have no equivalent surface to
|
|
44
|
+
ask — fall back throughout. Nothing here is a guess; a field that
|
|
45
|
+
cannot be read stays absent.
|
|
46
|
+
- **A stream gap now says whether the device caused it.** `HttpSource`
|
|
47
|
+
reads `/healthstatus` on each gap report and checks the device block's
|
|
48
|
+
own loss counters — `status/errors`, `status/usboverflows`,
|
|
49
|
+
`status/dsboverflows`, all per-second rates, so the reading describes
|
|
50
|
+
the moment of the gap rather than the run so far. Nonzero and the loss
|
|
51
|
+
starts at the device, where no amount of network headroom will recover
|
|
52
|
+
it; zero and the samples went missing downstream, in the server's 8 MB
|
|
53
|
+
outbound buffer or on the wire. The read is detached rather than
|
|
54
|
+
awaited: the control-plane timeout is 30 s, and stalling `work()` that
|
|
55
|
+
long would back the chunk channel up into the very loss being
|
|
56
|
+
diagnosed. `HttpEndpointsClient::get_device_health()` exposes the same
|
|
57
|
+
reduction of the health tree, and each reading is published as
|
|
58
|
+
`StreamStats::device_health`.
|
|
59
|
+
|
|
60
|
+
What it cannot settle is whether another client is on the same server
|
|
61
|
+
block. Nothing in the HTTP surface counts connections, so clearing the
|
|
62
|
+
device narrows a gap to a set of causes that includes contention
|
|
63
|
+
without singling it out — which is why the warning names it rather
|
|
64
|
+
than asserting it.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- **The SoapySDR plugin reported the clock source as `Internal`, a name
|
|
68
|
+
the device does not use, on hardware running off an external 10 MHz
|
|
69
|
+
reference.** `device/sclksource` offers `Consumer`, `Oscillator`,
|
|
70
|
+
`GPS`, `PPS`, `10MHz` and three `… Provider` variants; the measured V6
|
|
71
|
+
ECO is on `10MHz`. An operator who wired a house reference up for
|
|
72
|
+
frequency accuracy was told the device was free-running.
|
|
73
|
+
`listClockSources` and `getClockSource` now answer from the device.
|
|
74
|
+
`setClockSource` still only reads: it accepts the current source as
|
|
75
|
+
the no-op it is, and warns for anything else naming what the device is
|
|
76
|
+
actually on. `listAntennas` likewise takes its name from
|
|
77
|
+
`device/devicemode` — `RX1` on this device, and correct rather than
|
|
78
|
+
coincidental on a V6 running an RX2 mode.
|
|
79
|
+
- **The SoapySDR plugin could link a stale Rust static library.** The
|
|
80
|
+
CMake rule produced `libsdr_aaronia_rs.a` through an
|
|
81
|
+
`add_custom_command(OUTPUT …)` with no `DEPENDS`, so CMake treated the
|
|
82
|
+
archive as up to date the moment it existed and skipped cargo
|
|
83
|
+
entirely. Editing Rust sources and rebuilding relinked the module
|
|
84
|
+
against the old archive — silently, producing a module that looked
|
|
85
|
+
fine and did not contain the change. It is a custom *target* now,
|
|
86
|
+
whose command runs every build and which cargo no-ops when nothing
|
|
87
|
+
moved, which is what the comment there always claimed happened.
|
|
88
|
+
- **`SoapySDRUtil --probe` reported `Timestamps: NO` on a device that
|
|
89
|
+
timestamps every buffer.** `hasHardwareTime("")` answered with the last
|
|
90
|
+
stream timestamp, so it read as "no capability" until a packet had
|
|
91
|
+
arrived — and a probe never streams. It is a capability query now:
|
|
92
|
+
every RTSA packet header carries a start time, `readStream` has always
|
|
93
|
+
returned `SOAPY_SDR_HAS_TIME` with correct epoch nanoseconds, and an
|
|
94
|
+
application deciding at setup whether to record timestamps no longer
|
|
95
|
+
reads the probe as "cannot". `hasHardwareTime("GPS")` is unchanged and
|
|
96
|
+
still reports a value, since a GPS fix may genuinely not exist.
|
|
97
|
+
- **The SoapySDR plugin advertised a TX channel on every device,
|
|
98
|
+
including receivers that cannot transmit at all.** `SoapySDRUtil
|
|
99
|
+
--probe` on a V6 ECO over HTTP reported `1 Tx`, a full TX channel
|
|
100
|
+
section and `Full-duplex: YES`; an application that believed it failed
|
|
101
|
+
on the first write. `getNumChannels` did guard on holding a sink, but
|
|
102
|
+
`aaronia_sink_build` allocates unconditionally — it succeeds on a build
|
|
103
|
+
carrying no TX code at all, and only `aaronia_sink_initialize` fails —
|
|
104
|
+
so the guard was never false. A sink is now built only when the new
|
|
105
|
+
`aaronia_sink_supported()` reports this binary carries the native-SDK
|
|
106
|
+
TX path *and* the source is that backend, which means a `serial=` open
|
|
107
|
+
with no `url=`/`file=` overriding it. The probe now reads `0 Tx` and
|
|
108
|
+
`Full-duplex: NO`.
|
|
109
|
+
|
|
110
|
+
Still not a device capability check: a native-SDK build opened by
|
|
111
|
+
serial against a V6 ECO would advertise TX, since the ECO has no
|
|
112
|
+
transmitter and nothing here asks the SDK.
|
|
113
|
+
- **Restored a clean CI run.** Three clippy lints had been failing
|
|
114
|
+
`cargo clippy --workspace --all-features --all-targets` on Linux since
|
|
115
|
+
v0.7.7 — a collapsible `if let` in `UnifiedSink::initialize`, a needless
|
|
116
|
+
borrow in `native_sdk`, and a `field_reassign_with_default` in a
|
|
117
|
+
`sdk_source` unit test. All three sit in code gated to Windows and
|
|
118
|
+
Linux, so a macOS `cargo clippy` compiles none of it and reports
|
|
119
|
+
success; only CI's `--all-features` run sees them.
|
|
120
|
+
|
|
121
|
+
### Documentation
|
|
122
|
+
- **What several clients on one HTTP Server block cost, measured.** The
|
|
123
|
+
block accepts any number of concurrent `/stream` clients and serves
|
|
124
|
+
each one a full copy, so *n* clients cost the server *n* times the
|
|
125
|
+
egress. Two clients at 15.36 MS/s ran contiguous at 61.8 MB/s each;
|
|
126
|
+
five saturated the 2.5GbE path at 293.9 MB/s — the same ceiling a
|
|
127
|
+
single fast stream hits — and the loss fell on an arbitrary two of the
|
|
128
|
+
five, moving to a different pair on a repeat run. Two consequences a
|
|
129
|
+
client cannot escape: a clean stream is not evidence of being alone,
|
|
130
|
+
and a gap is not evidence of company. `/info`, `/healthstatus`,
|
|
131
|
+
`/remoteconfig` and the `/stream` response headers were all checked and
|
|
132
|
+
none counts connections.
|
|
133
|
+
- **Corrected the free-licence claim in `docs/HTTPSPEC.md`.** It read
|
|
134
|
+
that running this crate alongside a second client would meet the
|
|
135
|
+
one-connection limit. Five simultaneous clients were served on a system
|
|
136
|
+
holding one HTTP Server block licence, with no error and no refusal:
|
|
137
|
+
the limit is on block instances in the mission graph, not on
|
|
138
|
+
connections to one block.
|
|
139
|
+
- **The link a device actually needs, measured.** `link_budget`'s module
|
|
140
|
+
docs gain a 2.5GbE table beside the gigabit one, and the README a
|
|
141
|
+
requirements section. A 44 MHz real-time-bandwidth device — an ECO 100 —
|
|
142
|
+
has to run the 61.44 MS/s rung, and that rung costs 245.8 MB/s at 4 bytes
|
|
143
|
+
a sample, which gigabit cannot carry; 2.5 Gbps Ethernet is the floor for
|
|
144
|
+
those devices, and the wire format has to stay at 4 bytes a sample
|
|
145
|
+
because 8 asks 491.5 MB/s and loses 41 % of the stream. Two controls came
|
|
146
|
+
out of the same measurement: the path saturates at 292 MB/s, 93 % of
|
|
147
|
+
2.5GbE line rate, so the wire is the limit and not the server; and two
|
|
148
|
+
configurations asking the same 245.8 MB/s by different routes — 8 bytes a
|
|
149
|
+
sample at 30.72 MS/s, 4 bytes at 61.44 — deliver the same 244 MB/s,
|
|
150
|
+
confirming that only the byte rate matters.
|
|
6
151
|
|
|
7
152
|
## [v0.7.7] - 2026-09-06
|
|
8
153
|
|
|
@@ -3027,7 +3027,7 @@ dependencies = [
|
|
|
3027
3027
|
|
|
3028
3028
|
[[package]]
|
|
3029
3029
|
name = "python-aaronia"
|
|
3030
|
-
version = "0.
|
|
3030
|
+
version = "0.8.0"
|
|
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.
|
|
3559
|
+
version = "0.8.0"
|
|
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.
|
|
5
|
+
version = "0.8.0"
|
|
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.8.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+)
|
|
@@ -40,6 +40,30 @@ 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
|
+
## Link requirements
|
|
44
|
+
|
|
45
|
+
The server streams IQ at a fixed number of bytes per sample, so the span
|
|
46
|
+
you ask for sets a byte rate the whole path has to sustain. Miss it and the
|
|
47
|
+
*server* drops what it cannot send — unsignalled gaps that look fine in a
|
|
48
|
+
waterfall and defeat any digital demodulator.
|
|
49
|
+
|
|
50
|
+
| Real-time bandwidth | Sample rate | Needs (4 B/sample) | Link |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| up to 12.2 MHz | 15.36 MS/s | 61.4 MB/s | gigabit |
|
|
53
|
+
| up to 24.5 MHz | 30.72 MS/s | 122.9 MB/s | gigabit measured 1024 skips; prefer 2.5GbE |
|
|
54
|
+
| up to 49.1 MHz | 61.44 MS/s | 245.8 MB/s | **2.5GbE** |
|
|
55
|
+
|
|
56
|
+
**A 44 MHz device — an ECO 100 — needs a 2.5 Gbps Ethernet link.** Only the
|
|
57
|
+
top rung covers 44 MHz of real-time bandwidth (30.72 MS/s affords 24.5), and
|
|
58
|
+
that rung costs 245.8 MB/s, which gigabit cannot carry. Measured over
|
|
59
|
+
2.5GbE: 244.3 MB/s delivered against a 245.8 MB/s requirement, on a path
|
|
60
|
+
that saturates at 292 MB/s. Keep to a 4-byte wire format there — `float32`
|
|
61
|
+
doubles the requirement to 491.5 MB/s and loses 41 % of the stream.
|
|
62
|
+
|
|
63
|
+
`link_budget` measures *your* path end to end and names the widest span that
|
|
64
|
+
fits it, so treat the table as a starting point rather than a substitute for
|
|
65
|
+
measuring.
|
|
66
|
+
|
|
43
67
|
## Installation
|
|
44
68
|
|
|
45
69
|
Add the following to your `Cargo.toml`:
|
|
@@ -114,7 +114,12 @@ directly and skip the SoapySDR layer.
|
|
|
114
114
|
- `readStream` honours `timeoutUs` and returns partial reads within the
|
|
115
115
|
deadline, per the SoapySDR contract.
|
|
116
116
|
- Retuning mid-stream is safe and requires no Aaronia licence.
|
|
117
|
+
- The frequency range, gain range and sample-rate list an application
|
|
118
|
+
shows come from the device itself over the HTTP backend, so they match
|
|
119
|
+
the model attached rather than a compiled-in default.
|
|
117
120
|
- Gaps detected in the stream are counted by `readSensor("cumulative_drops")`.
|
|
118
121
|
An overrun sets `SOAPY_SDR_END_ABRUPT` on the affected read.
|
|
119
|
-
- TX is hardware-unverified and
|
|
120
|
-
against the native SDK on Windows or Linux
|
|
122
|
+
- TX is hardware-unverified, and a TX channel is reported only when the
|
|
123
|
+
plugin was built against the native SDK on Windows or Linux *and* the
|
|
124
|
+
device was opened by `serial=`. Over `url=` or `file=` the probe shows
|
|
125
|
+
`0 Tx`.
|
|
@@ -189,6 +189,62 @@ the network cannot carry all end here, so reducing the wire format
|
|
|
189
189
|
(`format=int16`) or the rate (`rate_reduction=n`) is the fix rather
|
|
190
190
|
than a larger client-side buffer.
|
|
191
191
|
|
|
192
|
+
### Several clients on one server block
|
|
193
|
+
|
|
194
|
+
The HTTP Server block accepts any number of concurrent `/stream`
|
|
195
|
+
clients, silently, and serves **each one a full copy** of the stream. It
|
|
196
|
+
does not split the data and it does not refuse the second connection, so
|
|
197
|
+
*n* clients cost the server *n* times the egress. Measured against a
|
|
198
|
+
V6 ECO at 15.36 MS/s (`format=int16`, 61.4 MB/s a client), counting
|
|
199
|
+
bytes off the socket over 6 s after a 500 ms settle:
|
|
200
|
+
|
|
201
|
+
| clients | per client | aggregate | result |
|
|
202
|
+
|---------|------------|------------|------------------------------------|
|
|
203
|
+
| 1 | 61.8 MB/s | 61.8 MB/s | contiguous |
|
|
204
|
+
| 2 | 61.8 MB/s | 123.5 MB/s | contiguous, both clients |
|
|
205
|
+
| 5 | 52–62 MB/s | 293.9 MB/s | saturated; 2 of 5 clients lost data |
|
|
206
|
+
|
|
207
|
+
The five-client rows are the ones to read. The aggregate pins at
|
|
208
|
+
293.9 MB/s — the same ~292 MB/s ceiling this 2.5GbE path saturates at
|
|
209
|
+
with a single fast stream — so the constraint is the path, not a
|
|
210
|
+
per-client limit. And the loss is **not shared out fairly**: three of the
|
|
211
|
+
five clients ran contiguous at the full 61.7 MB/s while the other two ran
|
|
212
|
+
short with hundreds of timestamp gaps each. Repeating the run moved the
|
|
213
|
+
loss to a different pair.
|
|
214
|
+
|
|
215
|
+
Two consequences for a client:
|
|
216
|
+
|
|
217
|
+
- **A clean stream is not evidence of being alone.** You may be one of
|
|
218
|
+
the connections the server is keeping up with while it starves another.
|
|
219
|
+
- **A gap is not evidence of company either.** A slow link, a slow
|
|
220
|
+
consumer and a second client all arrive as the same 8 MB outbound
|
|
221
|
+
buffer overflowing.
|
|
222
|
+
|
|
223
|
+
#### Nothing reports the number of clients
|
|
224
|
+
|
|
225
|
+
There is no endpoint, field or header that counts connections. Checked
|
|
226
|
+
against a live RTSA-Suite PRO:
|
|
227
|
+
|
|
228
|
+
| Surface | What it carries |
|
|
229
|
+
| :--- | :--- |
|
|
230
|
+
| `/info` | `name`, `title`, `uuid`, `port`, `mission`, `features` — no counts |
|
|
231
|
+
| `/healthstatus` | the device block only; the HTTP Server block publishes no health at all |
|
|
232
|
+
| `/remoteconfig` | the device block only, same as above |
|
|
233
|
+
| `/stream` response headers | `Access-Control-Allow-Origin`, `Transfer-Encoding`, `Date`, `DateMS` |
|
|
234
|
+
|
|
235
|
+
So a client cannot ask. What it can do is rule the *device* out: at the
|
|
236
|
+
moment of a gap, `/healthstatus` reports the device block's own loss
|
|
237
|
+
counters — `status/errors`, `status/usboverflows`, `status/dsboverflows`,
|
|
238
|
+
all per-second rates — and if they are zero the samples went missing
|
|
239
|
+
downstream of the device, in the server's outbound buffer or on the wire.
|
|
240
|
+
That narrows a gap to a set of causes that includes another client
|
|
241
|
+
without singling one out. `HttpEndpointsClient::get_device_health()`
|
|
242
|
+
performs the read; `HttpSource` runs it automatically on each stream-gap
|
|
243
|
+
report and publishes the result on `StreamStats::device_health`.
|
|
244
|
+
|
|
245
|
+
For a definitive answer, look at the RTSA host itself — the HTTP Server
|
|
246
|
+
block's own panel, or the host's socket table for peers on port 54664.
|
|
247
|
+
|
|
192
248
|
### Liveness
|
|
193
249
|
|
|
194
250
|
There is no status endpoint. Aaronia's own remote control notes probe
|
|
@@ -595,7 +651,10 @@ when it is wrong.
|
|
|
595
651
|
Note: this crate's `get_health_status()` parses the response as a generic
|
|
596
652
|
configuration tree (`HealthStatus` is an alias for `ConfigItem`) rather
|
|
597
653
|
than the typed shape below, which describes the upstream block-health
|
|
598
|
-
fields.
|
|
654
|
+
fields. `get_device_health()` reduces that tree to the per-block loss
|
|
655
|
+
counters — `status/errors`, `status/usboverflows`, `status/dsboverflows`,
|
|
656
|
+
plus `status/devstate` — which is the read that says whether a stream gap
|
|
657
|
+
started at the device; see "Several clients on one server block" above.
|
|
599
658
|
|
|
600
659
|
**Subgroups** per health-aware block, per Aaronia's specification:
|
|
601
660
|
`info`, `status`, `health`, `settings`, and `components` — the last a
|
|
@@ -630,6 +689,32 @@ from packet metadata for that.
|
|
|
630
689
|
| `title` | User-facing block title |
|
|
631
690
|
| `uuid` | Global unique identifier |
|
|
632
691
|
|
|
692
|
+
#### What the config tree declares about limits
|
|
693
|
+
|
|
694
|
+
`/remoteconfig`'s numeric items carry `min`, `max` and `step`, and its
|
|
695
|
+
enums carry their full `values` list, so a client can read a device's
|
|
696
|
+
limits instead of compiling in constants for one model. Measured on a
|
|
697
|
+
SPECTRAN V6 ECO:
|
|
698
|
+
|
|
699
|
+
| Item | Declares |
|
|
700
|
+
| :--- | :--- |
|
|
701
|
+
| `centerfreq0` | `min` 5 500 000, `max` 8e9, `step` 1000, unit `Frequency` |
|
|
702
|
+
| `reflevel0` | `min` -55, `max` 23, `step` 0.5, unit `dBm` |
|
|
703
|
+
| `decimation0` | `values` `Full,1 / 2,…,1 / 512` — ten rungs |
|
|
704
|
+
|
|
705
|
+
Two of those are worth stating plainly because published clients have
|
|
706
|
+
had them wrong: the ECO tunes from **5.5 MHz**, not from ~0, and it
|
|
707
|
+
reaches **8 GHz**, not 6. Combined with `/healthstatus`'s
|
|
708
|
+
`status/iqsamples` — the native undecimated rate — the decimation list
|
|
709
|
+
gives the full set of settable sample rates.
|
|
710
|
+
|
|
711
|
+
`iqsamples` is a running measurement (61 411 246 Hz against a nominal
|
|
712
|
+
61 440 000), so snap it to the exact `receiver_clock / 1.5` rung it
|
|
713
|
+
names before treating it as a ladder top; a rate advertised to an
|
|
714
|
+
application must be one the device can be set to.
|
|
715
|
+
`DeviceCapabilities::from_trees` in this crate does all of the above,
|
|
716
|
+
and `HttpEndpointsClient::get_device_capabilities()` performs the reads.
|
|
717
|
+
|
|
633
718
|
#### User Information (`/user`)
|
|
634
719
|
**URL**: `/user`
|
|
635
720
|
**Method**: GET
|
|
@@ -862,10 +947,15 @@ connection is free. Stream Merger and Stream Splitter, the blocks that
|
|
|
862
947
|
would otherwise let several streams share one connection, are not in
|
|
863
948
|
the free licence either.
|
|
864
949
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
950
|
+
An earlier revision of this document read that limit as covering
|
|
951
|
+
*connections* and warned that running this crate alongside a second
|
|
952
|
+
client — a SoapySDR application, say — would meet it. **That is not what
|
|
953
|
+
a live system does.** On the same RTSA-Suite PRO holding one HTTP Server
|
|
954
|
+
block licence, five simultaneous `/stream` clients were all served, with
|
|
955
|
+
no error, no refusal and no licence complaint. The limit is on block
|
|
956
|
+
instances in the mission graph, not on connections to one block. What
|
|
957
|
+
several clients cost is bandwidth, not licence — see below. It has
|
|
958
|
+
nothing to do with the Remote Config licence discussed further down.
|
|
869
959
|
|
|
870
960
|
**Remote Configuration** (`/remoteconfig`):
|
|
871
961
|
- Device parameter configuration.
|
|
@@ -1029,6 +1119,7 @@ fn parse_iq_int16(data: &[u8], metadata_scale: f32) -> Vec<Complex32> {
|
|
|
1029
1119
|
|---------|------|---------|
|
|
1030
1120
|
| 1.0 | 2025-01-11 | Initial HTTP specification from original documentation |
|
|
1031
1121
|
| 2.0 | 2025-01-11 | Enhanced with comprehensive streaming protocol specification and implementation guidelines |
|
|
1122
|
+
| 2.5 | 2026-09-06 | Measured what several clients on one HTTP Server block actually do: the block serves each connection a full copy and refuses none, so *n* clients cost *n* times the egress — five concurrent clients saturated a 2.5GbE path at 293.9 MB/s and the loss landed on an arbitrary two of them, moving on a repeat run. Corrected the free-licence claim: five clients were served on a one-block licence, so the limit is on block instances, not connections. Confirmed no surface counts connections (`/info`, `/healthstatus`, `/remoteconfig`, `/stream` headers), and documented the device-block loss counters as the cross-check that at least rules the device out |
|
|
1032
1123
|
| 2.4 | 2026-08-12 | Added Aaronia support's full `/control` settings list (`deviceconnect`, `camera`, per-type fields, `receiverUUID`/`receiverName` scoping), which supersedes the specification's claim that commands cannot be addressed to a block; documented that an unrecognised `format=` silently serves the RTSA file format and that `raw16` aliases `int16`, both verified live |
|
|
1033
1124
|
| 2.3 | 2026-08-12 | Folded in Aaronia's endpoint specification (rev 11) and the block forum threads: `/control` broadcasts to every block and is PUT-only, the server drops data past an 8 MB outbound buffer, `/healthstatus` subgroups and the fields a V6 ECO reports, and the one-server/one-client free-licence limit. Measured that `status/iqsamples` is the native rate, not the delivered one. Corrected the marker-stream entry: it declares `payload: "spectra"`, so its nested samples are the spectra form and not a counter-example to flat categories |
|
|
1034
1125
|
| 2.2 | 2026-08-12 | Verified Aaronia's V6 remote control notes (rev 4) against hardware: enum writes by index, multi-group and non-`main` `simpleconfig` PUTs, the silent no-op on an unknown block name, the ignored receiver name in the config-tree form; documented mission loading and the `type` requirement on `/control`, the absence of a status endpoint, and the unresolved conflict over what "Full" means on a full V6; resolved a contradiction over what the Remote Config licence gates |
|
|
@@ -176,6 +176,75 @@ naming a narrower span from the device's own ladder that fits. The
|
|
|
176
176
|
verdict is also published as `StreamStats::link_budget` on the shared
|
|
177
177
|
stats handle, and is re-measured after a configuration restart.
|
|
178
178
|
|
|
179
|
+
### When a gap happens anyway: was it the device?
|
|
180
|
+
|
|
181
|
+
A gap that arrives despite a link with headroom has more than one cause,
|
|
182
|
+
and the crate asks the device about the one it can settle. On each
|
|
183
|
+
stream-gap report `HttpSource` reads `/healthstatus` and checks the
|
|
184
|
+
device block's own loss counters — errors, USB overflows and DSP
|
|
185
|
+
overflows, all per-second rates, so the reading describes the moment of
|
|
186
|
+
the gap. If they are nonzero the loss starts at the device and no amount
|
|
187
|
+
of network headroom will recover it. If they are zero the samples went
|
|
188
|
+
missing downstream, in the RTSA HTTP server's outbound buffer (it drops
|
|
189
|
+
past 8 MB) or on the wire. The reading is logged beside the gap warning
|
|
190
|
+
and published as `StreamStats::device_health`; the same read is available
|
|
191
|
+
directly:
|
|
192
|
+
|
|
193
|
+
```rust,no_run
|
|
194
|
+
# async fn health() -> sdr_aaronia_rs::Result<()> {
|
|
195
|
+
use sdr_aaronia_rs::http_endpoints::{AuthMethod, HttpEndpointsClient};
|
|
196
|
+
|
|
197
|
+
let client = HttpEndpointsClient::new("http://localhost:54664".into(), AuthMethod::None)?;
|
|
198
|
+
for block in client.get_device_health().await? {
|
|
199
|
+
// `None` means the block reported none of the counters — the honest
|
|
200
|
+
// answer is "cannot say", not a device that has been cleared.
|
|
201
|
+
println!("{} -> losing nothing: {:?}", block.describe(), block.losing_nothing());
|
|
202
|
+
}
|
|
203
|
+
# Ok(())
|
|
204
|
+
# }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### What the device says it can do
|
|
208
|
+
|
|
209
|
+
The same control-plane surface declares the device's own limits, and
|
|
210
|
+
`get_device_capabilities()` reduces the two trees to them: model, serial
|
|
211
|
+
and firmware version, the bounds on centre frequency and reference
|
|
212
|
+
level, and the decimation ladder's depth.
|
|
213
|
+
|
|
214
|
+
```rust,no_run
|
|
215
|
+
# async fn caps() -> sdr_aaronia_rs::Result<()> {
|
|
216
|
+
use sdr_aaronia_rs::http_endpoints::{AuthMethod, HttpEndpointsClient};
|
|
217
|
+
|
|
218
|
+
let client = HttpEndpointsClient::new("http://localhost:54664".into(), AuthMethod::None)?;
|
|
219
|
+
let caps = client.get_device_capabilities().await;
|
|
220
|
+
println!("{:?} serial {:?}", caps.model, caps.serial);
|
|
221
|
+
if let Some(range) = caps.center_frequency {
|
|
222
|
+
println!("tunes {:.3}-{:.3} MHz", range.min / 1e6, range.max / 1e6);
|
|
223
|
+
}
|
|
224
|
+
// Highest first, and every rung one the device can actually be set to:
|
|
225
|
+
// its reported native rate is a measurement, so it is snapped to an
|
|
226
|
+
// exact ladder top before being halved down.
|
|
227
|
+
println!("sample rates: {:?}", caps.sample_rates());
|
|
228
|
+
# Ok(())
|
|
229
|
+
# }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Every field is optional and independently so, because the answer feeds
|
|
233
|
+
things that must not be guessed. The SoapySDR plugin publishes exactly
|
|
234
|
+
these at probe time — a V6 ECO declares 5.5 MHz–8 GHz and −55…+23 dBm,
|
|
235
|
+
where the plugin previously advertised 10 Hz–6 GHz and −100…+10 dB for
|
|
236
|
+
every model — and falls back per field for a device that cannot answer.
|
|
237
|
+
|
|
238
|
+
**Clearing the device does not mean you are alone on the server.** One
|
|
239
|
+
of the causes it leaves standing is another client: the HTTP Server block
|
|
240
|
+
serves each connection a full copy of the stream and refuses none, so a
|
|
241
|
+
second client doubles the server's egress. Nothing in the HTTP surface
|
|
242
|
+
counts connections, and the loss under saturation lands on an arbitrary
|
|
243
|
+
subset of the clients — measured, three of five ran contiguous while two
|
|
244
|
+
lost data — so a clean stream is not evidence of being alone either. For
|
|
245
|
+
a definitive count, look at the RTSA host. See `docs/HTTPSPEC.md`,
|
|
246
|
+
"Several clients on one server block".
|
|
247
|
+
|
|
179
248
|
## Reusable configuration profiles
|
|
180
249
|
|
|
181
250
|
Build your own configuration for specific bands:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#include <stdint.h>
|
|
5
5
|
#include <stdbool.h>
|
|
6
|
+
#include <stddef.h>
|
|
6
7
|
|
|
7
8
|
#ifdef __cplusplus
|
|
8
9
|
extern "C" {
|
|
@@ -58,6 +59,43 @@ typedef struct FfiSourceInfo {
|
|
|
58
59
|
const char* device_serial;
|
|
59
60
|
} FfiSourceInfo;
|
|
60
61
|
|
|
62
|
+
// --- What the device reports about itself --- //
|
|
63
|
+
//
|
|
64
|
+
// Optionality is explicit: 0.0 is a legitimate reference level and a
|
|
65
|
+
// legitimate step, so no numeric sentinel could distinguish "the device
|
|
66
|
+
// declares 0" from "the device did not say". Strings are NULL when
|
|
67
|
+
// absent, the ranges carry a has_ flag, and sample_rate_count == 0
|
|
68
|
+
// means the ladder could not be derived. Fall back per field, not on
|
|
69
|
+
// the whole struct.
|
|
70
|
+
typedef struct FfiDeviceCapabilities {
|
|
71
|
+
const char* model; // e.g. "SPECTRAN V6 ECO"; NULL when unknown
|
|
72
|
+
const char* serial; // NULL when unknown
|
|
73
|
+
const char* version; // firmware/FPGA revisions; NULL when unknown
|
|
74
|
+
|
|
75
|
+
bool has_center_frequency;
|
|
76
|
+
double center_frequency_min_hz;
|
|
77
|
+
double center_frequency_max_hz;
|
|
78
|
+
double center_frequency_step_hz; // 0.0 = device declares no step
|
|
79
|
+
|
|
80
|
+
bool has_reference_level;
|
|
81
|
+
double reference_level_min_dbm;
|
|
82
|
+
double reference_level_max_dbm;
|
|
83
|
+
double reference_level_step_db; // 0.0 = device declares no step
|
|
84
|
+
|
|
85
|
+
// Settable IQ sample rates in Hz, highest first. Owned by this
|
|
86
|
+
// struct; NULL and 0 when the device could not be asked.
|
|
87
|
+
size_t sample_rate_count;
|
|
88
|
+
const double* sample_rates;
|
|
89
|
+
|
|
90
|
+
// Stream-clock sources in the device's own vocabulary (a V6 ECO:
|
|
91
|
+
// Consumer, Oscillator, GPS, PPS, 10MHz, and three "... Provider"
|
|
92
|
+
// variants). Owned by this struct; NULL and 0 when it did not say.
|
|
93
|
+
size_t clock_source_count;
|
|
94
|
+
const char* const* clock_sources;
|
|
95
|
+
const char* clock_source; // currently selected; NULL if unknown
|
|
96
|
+
const char* rx_antenna; // e.g. "RX1"; NULL if the mode names none
|
|
97
|
+
} FfiDeviceCapabilities;
|
|
98
|
+
|
|
61
99
|
// Opaque pointers
|
|
62
100
|
typedef struct AaroniaSourceBuilder AaroniaSourceBuilder;
|
|
63
101
|
typedef struct AaroniaSource AaroniaSource;
|
|
@@ -125,6 +163,15 @@ AaroniaFfiError aaronia_source_set_reference_level(AaroniaSource* source, double
|
|
|
125
163
|
FfiSourceInfo* aaronia_source_get_source_info(AaroniaSource* source);
|
|
126
164
|
void aaronia_source_info_free(FfiSourceInfo* info);
|
|
127
165
|
|
|
166
|
+
// Read the device's declared capabilities. BLOCKING: two control-plane
|
|
167
|
+
// GETs, so call it once and cache. Returns NULL only for a null source
|
|
168
|
+
// or when called from a current-thread tokio runtime; a device that
|
|
169
|
+
// cannot be asked yields a struct with every field absent. Answers for
|
|
170
|
+
// the HTTP backend — file and native-SDK sources report nothing, having
|
|
171
|
+
// no equivalent surface to ask.
|
|
172
|
+
FfiDeviceCapabilities* aaronia_source_get_capabilities(AaroniaSource* source);
|
|
173
|
+
void aaronia_source_capabilities_free(FfiDeviceCapabilities* caps);
|
|
174
|
+
|
|
128
175
|
// --- Sink FFI --- //
|
|
129
176
|
//
|
|
130
177
|
// WARNING: the whole TX path is hardware-unverified (driven per the
|
|
@@ -148,6 +195,14 @@ typedef struct AaroniaSink AaroniaSink; // Opaque UnifiedSink
|
|
|
148
195
|
#define AARONIA_TX_SEGMENT_END ((uint64_t)0x00000008)
|
|
149
196
|
#define AARONIA_TX_PUSH ((uint64_t)0x00008000)
|
|
150
197
|
|
|
198
|
+
// True when this build carries the native-SDK transmit path. Ask before
|
|
199
|
+
// advertising a TX capability: aaronia_sink_build succeeds everywhere,
|
|
200
|
+
// so a non-null sink proves nothing. Compile-time only — it cannot say
|
|
201
|
+
// whether the attached device has a transmitter (a V6 ECO does not), and
|
|
202
|
+
// TX also requires the native-SDK source backend, so a device opened
|
|
203
|
+
// over HTTP or from a file has no TX path regardless.
|
|
204
|
+
bool aaronia_sink_supported(void);
|
|
205
|
+
|
|
151
206
|
AaroniaSinkBuilder* aaronia_sink_builder_new(void);
|
|
152
207
|
void aaronia_sink_builder_free(AaroniaSinkBuilder* builder);
|
|
153
208
|
void aaronia_sink_builder_center_frequency(AaroniaSinkBuilder* builder, double hz);
|