python-aaronia 0.6.0__tar.gz → 0.6.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.
Files changed (80) hide show
  1. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/CHANGELOG.md +52 -3
  2. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/Cargo.lock +2 -2
  3. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/Cargo.toml +1 -1
  4. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/DESIGN.md +2 -2
  5. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/PKG-INFO +1 -1
  6. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/PLUGINS.md +9 -1
  7. python_aaronia-0.6.1/README.md +212 -0
  8. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/APPS.md +1 -1
  9. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/HTTPSPEC.md +67 -5
  10. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/QUICKSTART.md +32 -13
  11. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/USAGE.md +28 -2
  12. python_aaronia-0.6.1/docs/VERIFICATION.md +47 -0
  13. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/python-aaronia/Cargo.toml +1 -1
  14. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/soapy-aaronia/AaroniaSoapyDevice.cpp +48 -7
  15. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/http_endpoints.rs +21 -7
  16. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/lib.rs +3 -2
  17. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/native_sdk.rs +8 -2
  18. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/unified_source.rs +80 -4
  19. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/utils.rs +134 -0
  20. python_aaronia-0.6.0/README.md +0 -291
  21. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/.cargo/config.toml +0 -0
  22. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/.gitattributes +0 -0
  23. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/.gitignore +0 -0
  24. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/CONTRIBUTING.md +0 -0
  25. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/LICENSE +0 -0
  26. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/benches/decompress_block.rs +0 -0
  27. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/benches/deinterleave_dual_iq.rs +0 -0
  28. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/benches/parse_int16_packet.rs +0 -0
  29. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/benches/rtsa_open_and_read.rs +0 -0
  30. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/deny.toml +0 -0
  31. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/FILESPEC.md +0 -0
  32. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/docs/SDKSPEC.md +0 -0
  33. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/channel_hopping.rs +0 -0
  34. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/device_control.rs +0 -0
  35. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/dump_metadata.rs +0 -0
  36. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/http_iq_quickstart.rs +0 -0
  37. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/native_sdk_basic.rs +0 -0
  38. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/native_sdk_transmit.rs +0 -0
  39. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/noaa_scanner.rs +0 -0
  40. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/python_arrow_example.py +0 -0
  41. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/read_rtsa_file.rs +0 -0
  42. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/examples/soapy_python_example.py +0 -0
  43. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/include/aaronia.h +0 -0
  44. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/pyproject.toml +0 -0
  45. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/python-aaronia/README.md +0 -0
  46. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/python-aaronia/aaronia.pyi +0 -0
  47. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/python-aaronia/src/lib.rs +0 -0
  48. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/python-aaronia/test_basic.py +0 -0
  49. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/scripts/ci-local.sh +0 -0
  50. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/soapy-aaronia/AaroniaSoapyDevice.hpp +0 -0
  51. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/soapy-aaronia/CMakeLists.txt +0 -0
  52. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/soapy-aaronia/README.md +0 -0
  53. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/soapy-aaronia/Registration.cpp +0 -0
  54. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/c_api.rs +0 -0
  55. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/decompression.rs +0 -0
  56. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/detection.rs +0 -0
  57. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/error.rs +0 -0
  58. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/file_source.rs +0 -0
  59. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/http_sink.rs +0 -0
  60. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/http_source.rs +0 -0
  61. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/http_streaming.rs +0 -0
  62. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/sdk_sink.rs +0 -0
  63. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/sdk_source.rs +0 -0
  64. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/sdr_source_impl.rs +0 -0
  65. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/seify_impl.rs +0 -0
  66. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/src/unified_sink.rs +0 -0
  67. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/c_api_test.rs +0 -0
  68. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/http_mock_test.rs +0 -0
  69. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/http_resilience_test.rs +0 -0
  70. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/http_sink_test.rs +0 -0
  71. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/integration_test.rs +0 -0
  72. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/live_smoke.rs +0 -0
  73. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/native_sdk_load.rs +0 -0
  74. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/properties.proptest-regressions +0 -0
  75. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/properties.rs +0 -0
  76. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/rtsa_negative_test.rs +0 -0
  77. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/sdr_source_impl_test.rs +0 -0
  78. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/spec_coverage.rs +0 -0
  79. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/test_cw_mag.rs +0 -0
  80. {python_aaronia-0.6.0 → python_aaronia-0.6.1}/tests/test_cw_meta.rs +0 -0
@@ -4,6 +4,55 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.6.1] - 2026-08-12
8
+
9
+ ### Fixed
10
+ - **The SoapySDR plugin advertised sample rates the hardware cannot
11
+ produce.** Seven of the ten rates it listed, 1, 2, 5, 10 and 20 MHz
12
+ among them, do not exist on the device, which runs at 61.44 MHz
13
+ divided by a power of two. Applications build their rate dropdowns
14
+ from that list, so choosing 10 MHz ran the device at a different rate
15
+ while the application went on displaying 10. The list is now the real
16
+ ladder, 61.44 MHz down to 120 kHz, and `setSampleRate` snaps to the
17
+ nearest one and logs when it has to.
18
+ - **The crate reported the requested sample rate rather than the one in
19
+ use.** The device adjusts a rate it cannot produce, so
20
+ `get_source_info()` described a capture that was not happening. HTTP
21
+ sources now report the rate, centre frequency and usable bandwidth
22
+ from the stream's own metadata once packets arrive.
23
+
24
+ ### Added
25
+ - `iq_sample_rates`, `usable_bandwidth_hz`, `nearest_iq_sample_rate` and
26
+ `iq_sample_rate_for_bandwidth` in `utils`, with the constants
27
+ `IQ_CLOCK_HZ` and `USABLE_BANDWIDTH_RATIO`. The device samples at
28
+ 61.44 MHz divided by a power of two and delivers 0.8 of that as
29
+ alias-free bandwidth. Callers were deriving their own rates from
30
+ guesses, so the relationships now live in one tested place. Wanting
31
+ 8 MHz of spectrum needs 10 MHz of sampling, and
32
+ `iq_sample_rate_for_bandwidth` returns the 15.36 MHz that provides it.
33
+
34
+ ### Fixed (native SDK)
35
+ - **Sweep mode set the wrong resolution-bandwidth key.** It sent
36
+ `main/rbw`, which no Aaronia sample uses; the key is `main/rbwfreq`.
37
+ Checked against Aaronia's published `SweepSpectrumEco` sample, which
38
+ also confirms `main/startfreq`, `main/stopfreq`, `main/reflevel` and
39
+ the `spectranv6eco/sweepsa` open string that this crate already used.
40
+ The sweep path remains hardware-unverified.
41
+
42
+ ### Documentation
43
+ - **GPS time needs GPS switched on, and the crate does not do it.**
44
+ Devices ship with `device/gpsmode` disabled, so `get_gps_time` would
45
+ return `None` indefinitely and appear broken. Aaronia's `GPSTime`
46
+ sample sets `device/gpsmode` to "Location and Time" and
47
+ `device/sclksource` to "GPS Provider" before starting the device;
48
+ `get_gps_time` now says so.
49
+ - Documented what `/control`'s `frequencySpan` actually means. It is a
50
+ request for usable bandwidth, not a sample rate: the device picks the
51
+ rate whose alias-free span is nearest, so 2.5 MHz yields 3.84 MHz and
52
+ 10 MHz yields 15.36 MHz. Values on the rate ladder round-trip exactly,
53
+ which is why the field looks like a sample rate in ordinary use.
54
+ Verified across nine requests on a V6 ECO.
55
+
7
56
  ## [v0.6.0] - 2026-08-11
8
57
 
9
58
  Reliability and documentation release. The HTTP backend now handles a
@@ -80,9 +129,9 @@ hardware and which have not.
80
129
  ### Release
81
130
  - Release archives now carry the module, install instructions and the
82
131
  licence, and their names state the version, OS and architecture.
83
- - The Linux module ships stripped of debug symbols, down from about
84
- 19 MB. The release job checks the stripped module still loads before
85
- publishing it.
132
+ - The Linux module ships stripped of debug symbols, 19.1 MB down to
133
+ 15.4 MB. The rest is statically linked Rust, not symbols. The release
134
+ job checks the stripped module still loads before publishing it.
86
135
  - GitHub releases now use this file's entry for the tag as their
87
136
  description, with the generated commit list below it.
88
137
 
@@ -3027,7 +3027,7 @@ dependencies = [
3027
3027
 
3028
3028
  [[package]]
3029
3029
  name = "python-aaronia"
3030
- version = "0.6.0"
3030
+ version = "0.6.1"
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.0"
3559
+ version = "0.6.1"
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.0"
5
+ version = "0.6.1"
6
6
  edition = "2024"
7
7
  repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
8
8
  readme = "README.md"
@@ -51,7 +51,7 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
51
51
  - **IQ mode constraint**: enforces `span * 1.5 <= receiverclock` by reading the live device clock before streaming.
52
52
  - **Data path (RX)**: polls `GetPacket` on the C++ side every 5 ms. Where the packet stride allows, the raw sample buffer is reinterpreted in place as `Complex32` (no per-sample conversion) before being copied into the caller's buffer.
53
53
  - **Data path (TX)**: provides an experimental `TxStream` API built on `SendPacket`. This path is hardware-unverified pending full V6 testing, as the ECO model lacks TX.
54
- - **Sweep-spectra mode**: configures non-IQ sweeping via `SweepsaConfig` by modifying `"main/startfreq"`, `"main/stopfreq"`, `"main/rbw"`, and related keys (these config paths are inferred from the SDK naming convention and are hardware-unverified). Supports the `"spectranv6/sweepsa"` and `"spectranv6eco/sweepsa"` open strings.
54
+ - **Sweep-spectra mode**: configures non-IQ sweeping via `SweepsaConfig` by modifying `"main/startfreq"`, `"main/stopfreq"`, `"main/rbwfreq"` and `"main/reflevel"`. These key names match Aaronia's own `SweepSpectrumEco` sample; the code previously sent `"main/rbw"`, which no sample uses. Supports the `"spectranv6/sweepsa"` and `"spectranv6eco/sweepsa"` open strings, both of which the samples confirm. The path itself remains hardware-unverified.
55
55
  - **Health and GPS telemetry**: exposes `HealthState` and `GpsState` structs populated by a recursive configuration tree-walker (`walk_health_tree`) that fetches device diagnostics dynamically. Not yet exercised against live hardware.
56
56
 
57
57
  ### 4.2 HTTP Streaming
@@ -72,7 +72,7 @@ Available on Windows and Linux only, behind the non-default `native-sdk` feature
72
72
  Under the `sdr-source` feature, `AaroniaSdrSource` (in `sdr_source_impl.rs`) implements the `SdrSource` trait from the external `orecchiette-sdr-source-rs` crate, which the crate re-exports as `sdr_source`. Automatic mid-stream retuning via `SourceConfig.channels_hz` is supported per backend:
73
73
 
74
74
  - **Native SDK**: re-issues `configure_iq_receiver` with the new center frequency, applying the change to the open device handle via the `main/centerfreq` configuration key. No stream restart is required.
75
- - **HTTP**: `AaroniaSource::set_center_frequency` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). The RTSA-Suite "Remote Config" license gates the separate `/remoteconfig` write path, which the hopping code deliberately avoids hopping is not gated on the license probe.
75
+ - **HTTP**: `AaroniaSource::set_center_frequency` wraps `HttpEndpointsClient::configure_capture` as a `PUT` to the license-free `/control` endpoint, always sending the complete capture tuple (center, span, reference level) with unchanged values filled from the cached config. The full tuple is required: RTSA servers silently ignore a capture `PUT` carrying only one of the two frequency fields (it returns `{"success":true}` but the device stays put — live-verified). Hopping deliberately avoids `/remoteconfig` and is not gated on the license probe. (Whether that path needs the "Remote Config" license at all is unconfirmed: a system without the license accepted `/remoteconfig` writes in testing. See docs/HTTPSPEC.md.)
76
76
  - **File**: not supported.
77
77
 
78
78
  Hop dwell deadlines come from `sdr_source::DwellController`; per-hop pacing additionally inserts a ~75 ms settle after every retune (`RETUNE_SETTLE` in `sdr_source_impl.rs`) to ride out the RTSA's apply-config latency and flush stale samples from the pipeline.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aaronia
3
- Version: 0.6.0
3
+ Version: 0.6.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+)
@@ -122,7 +122,15 @@ Two different knobs, easy to confuse:
122
122
  sdr = SoapySDR.Device("driver=aaronia,url=http://atc.local:54664,format=I16")
123
123
  ```
124
124
 
125
- ## 3. Metrics and Error Handling
125
+ ## 3. Transmit
126
+
127
+ TX is available through the SoapySDR plugin only when it is built
128
+ against the native SDK on Windows or Linux. Elsewhere `setupStream` for
129
+ TX fails with a descriptive error. Bursts are sent for immediate
130
+ transmission; timed TX is not supported. The whole TX path is
131
+ hardware-unverified. See [docs/VERIFICATION.md](docs/VERIFICATION.md).
132
+
133
+ ## 4. Metrics and Error Handling
126
134
 
127
135
  Both the native Rust API and the Python/C++ bindings expose critical metrics to monitor the health of your RTSA stream:
128
136
 
@@ -0,0 +1,212 @@
1
+ # sdr-aaronia-rs
2
+
3
+ [![Crates.io](https://img.shields.io/crates/v/sdr-aaronia-rs.svg)](https://crates.io/crates/sdr-aaronia-rs)
4
+ [![Docs.rs](https://docs.rs/sdr-aaronia-rs/badge.svg)](https://docs.rs/sdr-aaronia-rs)
5
+ [![CI](https://github.com/isaacbentley/sdr-aaronia-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/isaacbentley/sdr-aaronia-rs/actions/workflows/ci.yml)
6
+ [![License: GPL-3.0-or-later](https://img.shields.io/github/license/isaacbentley/sdr-aaronia-rs.svg)](https://choosealicense.com/licenses/gpl-3.0/)
7
+
8
+ One API for Aaronia SPECTRAN analyzers and SDRs, whether the samples
9
+ come from the native SDK, an RTSA-Suite HTTP server, or a recorded file.
10
+ Python bindings and a SoapySDR plugin come from the same engine.
11
+
12
+ *Disclaimer: This project is not affiliated with Aaronia AG. Aaronia, SPECTRAN, and RTSA-Suite PRO are trademarks of Aaronia AG.*
13
+
14
+ Working with a SPECTRAN usually means choosing a transport first and
15
+ then writing against whatever API that transport exposes. `AaroniaSource`
16
+ removes the choice: point it at a file, a URL, or nothing at all, and it
17
+ selects a backend and presents the same interface either way.
18
+
19
+ | What you configure | What it uses |
20
+ | --- | --- |
21
+ | `file_path` | Buffered playback of an RTSA capture file |
22
+ | `http_base_url` | HTTP streaming from an RTSA-Suite server block |
23
+ | Neither | The native SDK, falling back to `localhost:54664` |
24
+ | `force_source_type` | Exactly the backend you name |
25
+
26
+ ## What it does
27
+
28
+ - **Streams IQ and spectra over HTTP** in JSON, Int16, Float16 or
29
+ Float32, with Basic and token authentication. Retuning mid-stream
30
+ needs no Aaronia licence, and dropped streams reconnect on their own.
31
+ - **Reads `.rtsa` capture files** through buffered I/O, with metadata
32
+ extraction and multi-stream support.
33
+ - **Talks to the hardware directly** through the Aaronia SDK on Windows
34
+ and Linux, including transmit. The SDK is not available for macOS.
35
+ - **Controls and monitors the device**: streaming parameters, health,
36
+ input enumeration and the configuration tree.
37
+ - **Plugs into FutureSDR** with `HttpSource` and `HttpSink` flowgraph
38
+ blocks.
39
+
40
+ ## Installation
41
+
42
+ Add the following to your `Cargo.toml`:
43
+
44
+ ```toml
45
+ [dependencies]
46
+ # By default, includes HTTP, File, native sdr-source trait, and C FFI backend support
47
+ sdr-aaronia-rs = "0.6"
48
+ tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
49
+
50
+ # To enable additional backends, opt into their features (e.g. native-sdk, futuresdr)
51
+ # sdr-aaronia-rs = { version = "0.6", features = ["native-sdk", "futuresdr"] }
52
+ ```
53
+
54
+ ## Quickstart
55
+
56
+ Set the RF parameters and read:
57
+
58
+ ```rust,no_run
59
+ use sdr_aaronia_rs::{AaroniaSource, AaroniaConfig};
60
+ use anyhow::Result;
61
+
62
+ #[tokio::main]
63
+ async fn main() -> Result<()> {
64
+ let config = AaroniaConfig::default()
65
+ .center_frequency(446.0e6) // 446 MHz
66
+ .span_frequency(10.0e6) // 10 MHz span
67
+ .reference_level(-30.0); // -30 dBm
68
+
69
+ let mut source = AaroniaSource::new(config).await?;
70
+
71
+ let mut buffer = Vec::with_capacity(1024);
72
+ let n = source.read_samples(&mut buffer, 1024).await?;
73
+ println!("Received {} IQ samples", n);
74
+
75
+ Ok(())
76
+ }
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ [docs/USAGE.md](docs/USAGE.md) has worked examples for everything the
82
+ quickstart leaves out: the builder pattern, explicit backend selection,
83
+ wire formats and network bandwidth, configuration profiles, device
84
+ control, FutureSDR integration, authentication and low-level stream
85
+ access. Its Rust snippets are compiled as doctests, and it indexes the
86
+ runnable programs in [`examples/`](examples/).
87
+
88
+ [docs/QUICKSTART.md](docs/QUICKSTART.md) covers configuring the
89
+ RTSA-Suite HTTP Server block, which all of the above depends on.
90
+
91
+ ## Using it from other tools
92
+
93
+ The Rust crate is the engine. The same code drives three other
94
+ surfaces, so an Aaronia device works in the tools people already use.
95
+
96
+ ### Python
97
+
98
+ ```bash
99
+ pip install python-aaronia
100
+ ```
101
+
102
+ ```python
103
+ import aaronia
104
+
105
+ cfg = aaronia.AaroniaConfig()
106
+ cfg.http_base_url = "http://localhost:54664"
107
+ cfg.center_freq = 2.44e9
108
+ cfg.sample_rate = 15.36e6
109
+
110
+ src = aaronia.AaroniaSource()
111
+ src.start_streaming(cfg)
112
+ samples = src.read_samples_numpy(65536) # numpy complex64
113
+ src.stop_streaming()
114
+ ```
115
+
116
+ Reads land in NumPy or PyArrow with one copy out of the receive buffer.
117
+ Blocking calls release the GIL, errors arrive as typed exceptions, and
118
+ the package ships type stubs. Wheels are abi3 for CPython 3.9 and
119
+ later. Full reference: [python-aaronia/README.md](python-aaronia/README.md).
120
+
121
+ ### SoapySDR: GQRX, SDR++, GNU Radio and others
122
+
123
+ ```bash
124
+ SoapySDRUtil --probe="driver=aaronia,url=http://localhost:54664"
125
+ ```
126
+
127
+ Every release attaches a prebuilt plugin for Linux, macOS and Windows,
128
+ so no toolchain is needed. The plugin streams CF32 and CS16, honours
129
+ `timeoutUs` with partial reads, stays safe to retune while streaming,
130
+ and reports timestamps and dropped-block counts.
131
+
132
+ Per-application setup is in [docs/APPS.md](docs/APPS.md). Installation,
133
+ building from source and the wire-format trade-offs are in
134
+ [PLUGINS.md](PLUGINS.md); pass `format=I16` to halve network bandwidth,
135
+ which is a real wire-format change rather than a client-side conversion.
136
+
137
+ ### seify (Rust-native)
138
+
139
+ Enable the `seify` feature and construct the device with
140
+ `AaroniaSeifyDevice::from_args`. It is not part of seify's built-in
141
+ enumeration, so it will not appear in `seify::enumerate()`. See
142
+ [PLUGINS.md](PLUGINS.md).
143
+
144
+ ### What has been tested
145
+
146
+ Not every path has run against hardware. Transmit, dual-channel capture
147
+ and the native-SDK backend have not.
148
+ [docs/VERIFICATION.md](docs/VERIFICATION.md) gives the status of each
149
+ feature and how it was checked.
150
+
151
+ ## Connection Resilience
152
+
153
+ Connecting (the `/info` probe and initial tuning PUT) retries transient
154
+ failures up to 4 times with exponential backoff, bounded by a 10 second
155
+ total budget. Refused connections, unresolved DNS and 5xx/408/429
156
+ responses are retried; 4xx and configuration errors fail on the first
157
+ attempt. This matters for `*.local` hostnames, which refuse the first
158
+ connection from a cold process while mDNS resolves.
159
+
160
+ `AaroniaConfig::read_timeout` (default 30 s) bounds `read_samples`.
161
+ `read_samples_deadline`, and therefore the SoapySDR and seify paths, uses
162
+ its caller's per-call deadline instead.
163
+
164
+ A dropped HTTP stream, from an RTSA restart or a network interruption,
165
+ reconnects automatically. This is `AaroniaConfig::auto_reconnect`,
166
+ enabled by default. The reader reopens the stream, re-applies the
167
+ current tuning (a restarted server returns to its mission's frequency),
168
+ and flags the first packet after the gap as an overrun so callers know
169
+ samples were missed. After 5 attempts, roughly 8 seconds of backoff, it
170
+ stops and reads report a closed stream, matching the behaviour of
171
+ `auto_reconnect(false)`.
172
+
173
+ ## Feature Flags
174
+
175
+ Functionality is grouped behind Cargo features so unused dependencies stay out of your build:
176
+
177
+ | Feature | Description | Default |
178
+ |---|---|---|
179
+ | `http` | HTTP streaming via `reqwest` and `tokio`. | **Yes** |
180
+ | `file` | Buffered RTSA file parsing. | **Yes** |
181
+ | `native-sdk` | Links the proprietary Aaronia C++ SDK. Windows/Linux only. | No |
182
+ | `futuresdr` | Enables the FutureSDR block API: `HttpSource`, `HttpSink`, and their builders. Implies `http`. | No |
183
+ | `sdr-source` | Integrates `AaroniaSdrSource` implementing the native `SdrSource` traits. | **Yes** |
184
+ | `ffi` | Builds the C-API export layer. | **Yes** |
185
+
186
+ ## Testing & Contributing
187
+
188
+ The test suite consists of unit tests, integration tests against LFS captures, and property tests enforcing specification invariants.
189
+
190
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions on running the test suite, generating coverage reports, and formatting your code before submitting a Pull Request.
191
+
192
+ ## Documentation
193
+
194
+ Start here:
195
+
196
+ - [Quickstart](docs/QUICKSTART.md) — configuring an RTSA-Suite mission, first samples in Rust, Python and SoapySDR, and troubleshooting for common setup failures.
197
+ - [Usage](docs/USAGE.md) — worked examples for each part of the API, plus the `AARONIA_SDK_PATH` and `AARONIA_USER_AGENT` environment variables.
198
+ - [Using existing SDR apps](docs/APPS.md) — SDR++, GQRX, GNU Radio, SoapySDR from Python.
199
+
200
+ Reference:
201
+
202
+ - [Verification status](docs/VERIFICATION.md) — which features have been tested against real hardware.
203
+ - [SDR plugins](PLUGINS.md) — SoapySDR and seify setup, wire-format trade-offs, metrics.
204
+ - [Architecture & Design](DESIGN.md) — Internal architecture and execution flow.
205
+ - [RTSA File Format Specification](docs/FILESPEC.md) — On-disk `.rtsa` capture-file format and how this crate parses it.
206
+ - [HTTP API Specification](docs/HTTPSPEC.md) — The RTSA HTTP streaming and control API, including Remote Config licence detection.
207
+ - [Native SDK Specification](docs/SDKSPEC.md) — The Aaronia RTSA-Suite PRO SDK surface and the Rust binding notes.
208
+ - [Changelog](CHANGELOG.md) — Release history.
209
+
210
+ ## License
211
+
212
+ This project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later) - see the LICENSE file for details.
@@ -73,7 +73,7 @@ from SoapySDR import SOAPY_SDR_RX, SOAPY_SDR_CF32
73
73
  import numpy as np
74
74
 
75
75
  sdr = SoapySDR.Device("driver=aaronia,url=http://localhost:54664")
76
- sdr.setSampleRate(SOAPY_SDR_RX, 0, 12.288e6)
76
+ sdr.setSampleRate(SOAPY_SDR_RX, 0, 15.36e6)
77
77
  sdr.setFrequency(SOAPY_SDR_RX, 0, 2.44e9)
78
78
 
79
79
  rx = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32)
@@ -283,6 +283,16 @@ packet-metadata object, so `payload`, `minPower`, `maxPower`, and
283
283
  }
284
284
  ```
285
285
 
286
+ > **`frequencySpan` is a usable-bandwidth request, not a sample rate.**
287
+ > The device runs at 61.44 MHz divided by a power of two. Given a span
288
+ > it cannot produce, it selects the rate whose alias-free bandwidth
289
+ > (0.8 x Fs) is nearest the request: 2.5 MHz yields Fs = 3.84 MHz,
290
+ > 1.3 MHz yields 1.92 MHz, and 10 MHz yields 15.36 MHz. A value that is
291
+ > itself on the rate ladder round-trips exactly, which is why the field
292
+ > behaves like a sample rate in ordinary use. `sampleFrequency` in the
293
+ > packet metadata reports the rate in force; `startFrequency` and
294
+ > `endFrequency` bound the usable span. Verified on a V6 ECO.
295
+
286
296
  > **Both frequency fields are required for a retune to apply.** Live
287
297
  > testing against RTSA-Suite PRO (HTTP server block fed by a SPECTRAN
288
298
  > V6 ECO) shows the server returns `{"success":true}` for a capture
@@ -388,6 +398,44 @@ packet-metadata object, so `payload`, `minPower`, `maxPower`, and
388
398
  }
389
399
  ```
390
400
 
401
+ **Full PUT form** (config tree). Aaronia's own automation example uses this
402
+ shape for every setting it changes:
403
+
404
+ ```json
405
+ {
406
+ "request": 11,
407
+ "receiverName": "Block_Spectran_V6B_0",
408
+ "config": {
409
+ "type": "group",
410
+ "items": [
411
+ {
412
+ "type": "group",
413
+ "name": "main",
414
+ "items": [
415
+ { "type": "float", "name": "centerfreq", "value": 2440000000 }
416
+ ]
417
+ }
418
+ ]
419
+ }
420
+ }
421
+ ```
422
+
423
+ Notes verified against a live V6 ECO:
424
+
425
+ - **Field names are model-specific.** The V6B example writes `centerfreq`;
426
+ a V6 ECO exposes `centerfreq0` and `centerfreq1` for its two channels.
427
+ Read `GET /remoteconfig` and use the names that device reports.
428
+ - **A frequency change needs only the frequency field.** This differs from
429
+ `/control`, where a capture request is ignored unless `frequencyCenter`
430
+ and `frequencySpan` are both present.
431
+ - **The `receiverName` key is case-tolerant.** `receivername` was accepted
432
+ identically.
433
+ - Enum-valued settings are written as their label string, for example
434
+ `decimation` as `"1 / 128"`, not as an index.
435
+ - Other keys the example writes: `run` (bool), `preamp` (`"Auto"`),
436
+ `reflevel` (float), and `filerecord` (bool) plus a filename template on
437
+ a FileWriter block, which is how it starts and stops recording.
438
+
391
439
  **Simplified PUT form**: in addition to the full `{request, config}` shape, the server accepts a shorter per-block write (used by the SDR++ and SDRangel Aaronia plugins), exposed via `HttpEndpointsClient::simple_remote_config`:
392
440
  ```json
393
441
  {
@@ -552,10 +600,23 @@ Data captured using antennas with location or directional information.
552
600
  - `/info` - Device information
553
601
  - `/healthstatus` - Device health monitoring
554
602
 
555
- **Remote Configuration** (Requires separate license):
556
- - `/remoteconfig` - Device parameter configuration
557
- - **License Required**: "Remote Config" license from Aaronia
558
- - **Alternative**: Use Native SDK for configuration without HTTP licensing restrictions
603
+ **Remote Configuration** (`/remoteconfig`):
604
+ - Device parameter configuration.
605
+ - Aaronia sells a "Remote Config" license, and this document previously
606
+ stated that `/remoteconfig` writes fail without it. **That is not what
607
+ a live system does.** On RTSA-Suite PRO driving a SPECTRAN V6 ECO whose
608
+ license list contains no Remote Config entry, `PUT /remoteconfig`
609
+ retuned the device repeatedly, in both the config-tree and simplified
610
+ forms, returning HTTP 200 with the change applied.
611
+ - What that license actually gates is therefore unconfirmed. It may
612
+ cover a different feature, or a different edition or version, or
613
+ parameters other than the ones tested. Treat any claim that
614
+ `/remoteconfig` writes require it as unverified.
615
+ - The system tested did hold "Block: HTTP Server" and "Block: HTTP
616
+ Client" licenses, so the HTTP surface itself is licensed separately
617
+ and may be what actually matters.
618
+ - This crate does not depend on the answer: it tunes through `/control`,
619
+ which needs no license.
559
620
 
560
621
  **License Detection Methods**:
561
622
 
@@ -564,7 +625,7 @@ Data captured using antennas with location or directional information.
564
625
  The client therefore exposes two methods:
565
626
 
566
627
  - `detect_remote_config_license()` — **read-only**. Never touches device state. Classifies 401/403 responses; on read success it returns `Unknown` (write capability unproven).
567
- - `probe_remote_config_write_license()` — **active probe**. Performs a read-modify-restore cycle on `reflevel` (+1 dB, restored best-effort) to positively verify write capability. Use only when you genuinely need proof of `/remoteconfig` write access. Frequency hopping does **not** need it — retuning goes through the license-free `/control` endpoint (see the capture-control note above; the silent-ignore behavior once attributed to licensing was traced to partial capture payloads).
628
+ - `probe_remote_config_write_license()` — **active probe**. Performs a read-modify-restore cycle on `reflevel` (+1 dB, restored best-effort) to positively verify write capability. Use only when you genuinely need proof of `/remoteconfig` write access. `AaroniaSource::probe_remote_config_license()` delegates to it for HTTP sources, and reports `Active` for the file and native-SDK backends, which do not need the licence. Frequency hopping does **not** need it — retuning goes through the license-free `/control` endpoint (see the capture-control note above; the silent-ignore behavior once attributed to licensing was traced to partial capture payloads).
568
629
 
569
630
  **Practical Detection (Active Probe)**:
570
631
  ```rust
@@ -707,6 +768,7 @@ consult:
707
768
 
708
769
  - **RTSA-Suite PRO HTTP streaming** (Aaronia V6 forum) — [v6-forum.aaronia.de/forum/topic/rtsa-suite-pro-http-streaming](https://v6-forum.aaronia.de/forum/topic/rtsa-suite-pro-http-streaming/)
709
770
  - Aaronia RTSA-Suite PRO product documentation — <https://rtsa-manual.aaronia.com/>
771
+ - **Aaronia's own automation example** — [Aaronia-Open-source/python_RTSA_HTTP_API_Sequence_Example](https://github.com/Aaronia-Open-source/python_RTSA_HTTP_API_Sequence_Example). A Python script that runs a measurement sequence and records to disk, driving the device entirely through `PUT /remoteconfig`. It is the source of the config-tree write example below.
710
772
 
711
773
  The content here is derived from the above plus empirical analysis of live
712
774
  RTSA-Suite PRO streams. "Aaronia", "RTSA", and "Spectran" are the property of
@@ -60,7 +60,7 @@ use sdr_aaronia_rs::{AaroniaConfig, AaroniaSource};
60
60
  async fn main() -> anyhow::Result<()> {
61
61
  let config = AaroniaConfig::from_http("http://localhost:54664")
62
62
  .center_frequency(2.44e9)
63
- .span_frequency(12.288e6) // sample rate, not RF bandwidth
63
+ .span_frequency(15.36e6) // sample rate (Fs), not RF bandwidth
64
64
  .reference_level(-20.0);
65
65
 
66
66
  let mut source = AaroniaSource::new(config).await?;
@@ -77,7 +77,7 @@ async fn main() -> anyhow::Result<()> {
77
77
 
78
78
  ```bash
79
79
  # args: <center-hz> <sample-rate-hz> <url>
80
- cargo run --example http_iq_quickstart --features http -- 2440e6 12.288e6 http://localhost:54664
80
+ cargo run --example http_iq_quickstart --features http -- 2440e6 15.36e6 http://localhost:54664
81
81
  ```
82
82
 
83
83
  ### Python
@@ -92,7 +92,7 @@ import aaronia
92
92
  cfg = aaronia.AaroniaConfig()
93
93
  cfg.http_base_url = "http://localhost:54664"
94
94
  cfg.center_freq = 2.44e9
95
- cfg.sample_rate = 12.288e6
95
+ cfg.sample_rate = 15.36e6
96
96
 
97
97
  src = aaronia.AaroniaSource()
98
98
  src.start_streaming(cfg)
@@ -124,22 +124,41 @@ Usually the missing connection from step 4: the device block's output is
124
124
  not wired to the HTTP Server block. `curl /sample` shows whether data is
125
125
  present.
126
126
 
127
- **Span and sample rate.**
127
+ **Span, sample rate and the "1 / 4" in the GUI.**
128
128
  `span_frequency` is the IQ sample rate (Fs). The name comes from the
129
- Aaronia API. It is not the usable RF bandwidth, which is smaller because
130
- of the anti-alias filter (roughly 49 MHz usable within a 61.44 MHz Fs
131
- capture). The device derives its decimation steps from a 61.44 MHz
132
- clock. Over HTTP the RTSA-Suite accepts rates across its supported
133
- range, and the crate rejects a rate that would violate the IQ-mode
134
- constraint before streaming starts.
129
+ Aaronia API. Three numbers describe the same capture and they are all
130
+ different:
131
+
132
+ | Where you see it | Example | Meaning |
133
+ | --- | --- | --- |
134
+ | `span_frequency`, and `sampleFrequency` in packet metadata | 15.36 MHz | The sample rate, Fs |
135
+ | `startFrequency`..`endFrequency` in packet metadata | 12.288 MHz | Usable alias-free bandwidth, which measures 0.8 x Fs |
136
+ | The Span control in the RTSA GUI | `1 / 4` | Decimation of the 61.44 MHz clock, so Fs = 61.44 / 4 |
137
+
138
+ The device runs at 61.44 MHz divided by a power of two, ten rates from
139
+ 61.44 MHz down to 120 kHz, shown in the GUI as Full through `1 / 512`.
140
+
141
+ **Pass one of those rates and you get it exactly.** Anything else is
142
+ silently adjusted, and not to the nearest rate: the server reads the
143
+ requested span as the *usable bandwidth* you want and picks the rate
144
+ whose usable span is closest. Asking for 2.5 MHz gives Fs = 3.84 MHz,
145
+ whose usable span is 3.07 MHz, rather than the numerically closer
146
+ 1.92 MHz. Verified across nine requests on a V6 ECO.
147
+
148
+ `AaroniaSource::get_source_info()` reports the rate the server is
149
+ actually sending once packets are flowing, so read it back rather than
150
+ assuming.
151
+
152
+ The crate rejects a rate that would violate the IQ-mode constraint
153
+ before streaming starts, but it does not second-guess the ladder.
135
154
 
136
155
  **Retuning has no effect.**
137
156
  The `/control` endpoint applies a frequency change only when
138
157
  `frequencyCenter` and `frequencySpan` are both present. A request
139
158
  carrying one of them returns `{"success":true}` and is ignored. This
140
159
  crate always sends the complete tuple, so use `set_center_frequency`
141
- instead of issuing PUTs directly. No Aaronia licence is involved; the
142
- licence gates `/remoteconfig`, which the crate does not use for tuning.
160
+ instead of issuing PUTs directly. No Aaronia licence is involved: the
161
+ crate tunes through `/control`, which needs none.
143
162
 
144
163
  **Network saturation at high sample rates.**
145
164
  Float32 requires roughly 740 MB/s at 92 MSPS. On anything other than
@@ -165,7 +184,7 @@ use sdr_aaronia_rs::{AaroniaConfig, AaroniaSource};
165
184
  let config = AaroniaConfig::default()
166
185
  .force_native_sdk()
167
186
  .center_frequency(2.44e9)
168
- .span_frequency(12.288e6);
187
+ .span_frequency(15.36e6);
169
188
  let mut source = AaroniaSource::new(config).await?;
170
189
  # Ok(())
171
190
  # }
@@ -5,7 +5,7 @@ compiled as a doctest, so it stays valid as the API changes.
5
5
 
6
6
  For first-time setup of the RTSA-Suite HTTP Server block, see
7
7
  [QUICKSTART.md](QUICKSTART.md). For runnable programs, see
8
- [`examples/`](../examples/).
8
+ [Runnable examples](#runnable-examples) below.
9
9
 
10
10
  ## Unified API with auto-detection
11
11
 
@@ -303,9 +303,35 @@ async fn main() -> Result<()> {
303
303
  }
304
304
  ```
305
305
 
306
+ ## Environment variables
307
+
308
+ | Variable | Effect |
309
+ |---|---|
310
+ | `AARONIA_SDK_PATH` | Overrides the RTSA-Suite installation directory used for SDK / `RTSAFileTool` detection. Works on every platform. On macOS — which has no default install path and no native SDK build — it only affects `RTSAFileTool` and XML-config detection. |
311
+ | `AARONIA_USER_AGENT` | Overrides the HTTP `User-Agent` string sent by every outbound request (default: `sdr-aaronia-rs/<version>`). |
312
+
313
+ ## Runnable examples
314
+
315
+ The programs in `examples/` cover the same ground as this document, as
316
+ code you can run. CI builds them, so they stay current:
317
+
318
+ | Task | Example |
319
+ | --- | --- |
320
+ | HTTP IQ streaming, first samples | [`http_iq_quickstart.rs`](../examples/http_iq_quickstart.rs) |
321
+ | Health checks, input enumeration, recording control, license probing | [`device_control.rs`](../examples/device_control.rs) |
322
+ | Frequency hopping via the `sdr-source` traits | [`channel_hopping.rs`](../examples/channel_hopping.rs) |
323
+ | FutureSDR flowgraph with FM demodulation | [`noaa_scanner.rs`](../examples/noaa_scanner.rs) |
324
+ | Native SDK capture and transmit | [`native_sdk_basic.rs`](../examples/native_sdk_basic.rs), [`native_sdk_transmit.rs`](../examples/native_sdk_transmit.rs) |
325
+ | RTSA file playback and metadata inspection | [`read_rtsa_file.rs`](../examples/read_rtsa_file.rs), [`dump_metadata.rs`](../examples/dump_metadata.rs) |
326
+ | Python (NumPy and Arrow), SoapySDR from Python | [`python_arrow_example.py`](../examples/python_arrow_example.py), [`soapy_python_example.py`](../examples/soapy_python_example.py) |
327
+
328
+ ```bash
329
+ # args: <center-hz> <sample-rate-hz> <url>
330
+ cargo run --example http_iq_quickstart --features http -- 2440e6 15.36e6 http://localhost:54664
331
+ ```
332
+
306
333
  ## Related
307
334
 
308
335
  - [QUICKSTART.md](QUICKSTART.md) — RTSA-Suite mission setup and troubleshooting.
309
336
  - [APPS.md](APPS.md) — SDR++, GQRX, GNU Radio and SoapySDR from Python.
310
337
  - [HTTPSPEC.md](HTTPSPEC.md) — the RTSA HTTP API this crate speaks.
311
- - [`examples/`](../examples/) — the same material as runnable programs.
@@ -0,0 +1,47 @@
1
+ # Hardware verification status
2
+
3
+ Not every code path has been exercised against hardware. The
4
+ development device is a SPECTRAN V6 ECO with a single RX channel and no
5
+ TX licence, driven through RTSA-Suite PRO over HTTP from macOS. Paths
6
+ requiring a second RX input, a transmitter, or the Windows/Linux native
7
+ SDK are marked unverified.
8
+
9
+ | Capability | Backend | Status |
10
+ | --- | --- | --- |
11
+ | IQ streaming, all four wire formats (F32 / F16 / I16 / JSON) | HTTP | **Live-verified** |
12
+ | Spectra streaming | HTTP | **Live-verified** |
13
+ | Mid-stream retuning (centre, span) | HTTP | **Live-verified** |
14
+ | Mid-stream reference-level change | HTTP | Confirmed manually against the device; no automated live assertion |
15
+ | Auto-reconnect after a dropped stream | HTTP | Streaming live-verified; the drop-and-recover path is mock-tested |
16
+ | Drop/overrun detection, rate reduction, `scale=N` | HTTP | **Live-verified** |
17
+ | Long-run stability (>120 s continuous) | HTTP | **Live-verified** |
18
+ | Connect retry | HTTP | Mock-tested; the mDNS race it addresses did not reproduce on demand |
19
+ | `.rtsa` playback and metadata | File | **Verified against real captures**, byte-compared with the official format specification |
20
+ | seify backend | HTTP | **Live-verified** |
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
+ | Python bindings RX | HTTP | Verified manually (NumPy and Arrow); no automated live test |
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 |
25
+ | GPS hardware time | Native SDK | **Hardware-unverified** |
26
+ | Native SDK capture generally | Native SDK | **Hardware-unverified**; compiled and unit-tested in a Linux VM each release |
27
+ | HTTP TX push (`/sample`) | HTTP | Endpoint exercised live; RF output not measured |
28
+
29
+ "Live-verified" means an `#[ignore]`d test in
30
+ [`tests/live_smoke.rs`](../tests/live_smoke.rs) asserts the behaviour
31
+ against hardware, and is reproducible by anyone with a device. Entries
32
+ marked "verified manually" were observed working but have no automated
33
+ assertion and can regress without detection. Run the automated set
34
+ with:
35
+
36
+ ```bash
37
+ cargo test --all-features --test live_smoke -- --ignored --nocapture
38
+ ```
39
+
40
+ Contributions that convert an unverified row, particularly from users
41
+ with a full V6 or a TX licence, are welcome.
42
+
43
+ ## Related
44
+
45
+ - [QUICKSTART.md](QUICKSTART.md) — setting up an RTSA-Suite mission.
46
+ - [USAGE.md](USAGE.md) — worked examples for each part of the API.
47
+ - [../CHANGELOG.md](../CHANGELOG.md) — what changed in each release.