python-aaronia 0.11.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/CHANGELOG.md +23 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/Cargo.lock +2 -2
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/Cargo.toml +1 -1
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/PKG-INFO +1 -1
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/python-aaronia/Cargo.toml +1 -1
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/unified_source.rs +181 -37
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/http_resilience_test.rs +32 -7
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/.cargo/config.toml +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/.gitattributes +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/.gitignore +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/CONTRIBUTING.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/DESIGN.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/LICENSE +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/PLUGINS.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/README.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/benches/decompress_block.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/benches/deinterleave_dual_iq.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/benches/parse_int16_packet.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/benches/rtsa_open_and_read.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/deny.toml +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/APPS.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/FILESPEC.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/HTTPSPEC.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/QUICKSTART.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/SDKSPEC.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/SYNC.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/USAGE.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/docs/VERIFICATION.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/channel_hopping.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/device_control.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/dump_metadata.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/http_iq_quickstart.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/native_sdk_basic.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/native_sdk_transmit.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/noaa_scanner.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/python_arrow_example.py +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/read_rtsa_file.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/examples/soapy_python_example.py +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/include/spectran.h +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/packaging/homebrew/README.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/packaging/homebrew/soapy-aaronia.rb +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/pyproject.toml +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/python-aaronia/README.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/python-aaronia/aaronia.pyi +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/python-aaronia/src/lib.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/python-aaronia/test_basic.py +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/scripts/ci-local.sh +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/scripts/fake-rtsa-server.py +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/scripts/native-sdk-validate.ps1 +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/scripts/sdk-container-test.sh +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/scripts/validate-iq-live.py +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/CMakeLists.txt +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/README.md +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/Registration.cpp +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/SpectranSoapyDevice.cpp +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/SpectranSoapyDevice.hpp +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/packaging/install.ps1 +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/soapy-aaronia/packaging/install.sh +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/c_api.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/decompression.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/detection.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/error.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/file_source.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/http_endpoints.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/http_sink.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/http_source.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/http_streaming.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/lib.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/link_budget.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/native_sdk.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/sdk_sink.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/sdk_source.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/sdr_source_impl.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/seify_impl.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/unified_sink.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/src/utils.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/abi_drift.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/c_api_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/http_mock_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/http_sink_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/integration_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/live_smoke.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/native_sdk_live.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/native_sdk_load.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/properties.proptest-regressions +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/properties.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/rtsa_negative_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/sdr_source_impl_test.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/spec_coverage.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/test_cw_mag.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/test_cw_meta.rs +0 -0
- {python_aaronia-0.11.0 → python_aaronia-0.11.1}/tests/wire_contract.rs +0 -0
|
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.11.1] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **A stream gap now ends the block it precedes, instead of hiding inside
|
|
11
|
+
one.** Read assembly noted a dropped chunk and carried on concatenating,
|
|
12
|
+
so a returned block held samples from both sides of the hole while
|
|
13
|
+
`take_overrun` said only "somewhere in there". A consumer resetting its
|
|
14
|
+
filters once, before the block, still ran the discontinuity through
|
|
15
|
+
them — and for an FM discriminator that is a phase step, i.e. a
|
|
16
|
+
frequency spike that reads downstream as a sync edge.
|
|
17
|
+
|
|
18
|
+
A gap has a position, and the only position a consumer can act on is a
|
|
19
|
+
read boundary. The post-gap chunk is now stashed and the block ends,
|
|
20
|
+
exactly as it already did for a retune, with the flag carried to the
|
|
21
|
+
read that begins there. A gap on a read's first chunk still reports
|
|
22
|
+
against that read, since nothing precedes it to split from.
|
|
23
|
+
|
|
24
|
+
This also reaches reconnects, which are discontinuities for the same
|
|
25
|
+
reason: the stream stopped and resumed, so the samples either side are
|
|
26
|
+
not adjacent. Reads continue across a reconnect as before, they just
|
|
27
|
+
no longer splice across it.
|
|
28
|
+
|
|
29
|
+
|
|
7
30
|
## [v0.11.0] - 2026-09-09
|
|
8
31
|
|
|
9
32
|
### Changed
|
|
@@ -3092,7 +3092,7 @@ dependencies = [
|
|
|
3092
3092
|
|
|
3093
3093
|
[[package]]
|
|
3094
3094
|
name = "python-aaronia"
|
|
3095
|
-
version = "0.11.
|
|
3095
|
+
version = "0.11.1"
|
|
3096
3096
|
dependencies = [
|
|
3097
3097
|
"arrow",
|
|
3098
3098
|
"num-complex",
|
|
@@ -3621,7 +3621,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
3621
3621
|
|
|
3622
3622
|
[[package]]
|
|
3623
3623
|
name = "sdr-aaronia-rs"
|
|
3624
|
-
version = "0.11.
|
|
3624
|
+
version = "0.11.1"
|
|
3625
3625
|
dependencies = [
|
|
3626
3626
|
"anyhow",
|
|
3627
3627
|
"bitflags 2.13.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "sdr-aaronia-rs"
|
|
3
3
|
description = "Unified Rust interface for Aaronia Spectran Spectrum Analyzers / SDRs, featuring Python bindings, a SoapySDR plugin, HTTP streaming, and native SDK support."
|
|
4
4
|
license = "GPL-3.0-or-later"
|
|
5
|
-
version = "0.11.
|
|
5
|
+
version = "0.11.1"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
repository = "https://github.com/isaacbentley/sdr-aaronia-rs"
|
|
8
8
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aaronia
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -421,6 +421,17 @@ pub struct SpectranSource {
|
|
|
421
421
|
/// Capture frequency of whatever is sitting in `sample_buffer`, so a
|
|
422
422
|
/// leftover tail is never merged into a read at another frequency.
|
|
423
423
|
sample_buffer_freq_hz: f64,
|
|
424
|
+
/// Whether the stashed samples in `sample_buffer` begin immediately
|
|
425
|
+
/// after a break in the stream.
|
|
426
|
+
///
|
|
427
|
+
/// A gap has a *position*, not merely a presence. Assembly used to
|
|
428
|
+
/// note a dropped chunk and carry on concatenating, so the block
|
|
429
|
+
/// handed back straddled the hole while the overrun flag said only
|
|
430
|
+
/// "somewhere in there" — and a consumer resetting its filters once,
|
|
431
|
+
/// before the block, still ran the discontinuity through them. The
|
|
432
|
+
/// gap is now put on a read boundary instead, exactly as a retune
|
|
433
|
+
/// is, and this carries the flag to the read that starts there.
|
|
434
|
+
sample_buffer_after_gap: bool,
|
|
424
435
|
/// Capture frequency of the samples the most recent read returned —
|
|
425
436
|
/// what the stream itself reported, not what was requested. Read it
|
|
426
437
|
/// with [`SpectranSource::capture_frequency_hz`].
|
|
@@ -574,6 +585,7 @@ impl SpectranSource {
|
|
|
574
585
|
source_type: SourceType::Http, // Will be updated during detection
|
|
575
586
|
sample_buffer: VecDeque::new(),
|
|
576
587
|
sample_buffer_freq_hz: 0.0,
|
|
588
|
+
sample_buffer_after_gap: false,
|
|
577
589
|
last_read_frequency_hz: 0.0,
|
|
578
590
|
|
|
579
591
|
#[cfg(all(
|
|
@@ -1341,6 +1353,13 @@ impl SpectranSource {
|
|
|
1341
1353
|
let from_buffer = self.sample_buffer.len().min(max_samples);
|
|
1342
1354
|
if from_buffer > 0 {
|
|
1343
1355
|
buffer.extend(self.sample_buffer.drain(0..from_buffer));
|
|
1356
|
+
// These samples were stashed because they follow a
|
|
1357
|
+
// gap; this is the read that begins with them, so
|
|
1358
|
+
// this is the read that must be told.
|
|
1359
|
+
if self.sample_buffer_after_gap {
|
|
1360
|
+
self.pending_overrun = true;
|
|
1361
|
+
self.sample_buffer_after_gap = false;
|
|
1362
|
+
}
|
|
1344
1363
|
}
|
|
1345
1364
|
|
|
1346
1365
|
// Capture frequency of this read. Seeded from the
|
|
@@ -1358,6 +1377,25 @@ impl SpectranSource {
|
|
|
1358
1377
|
match recv {
|
|
1359
1378
|
Ok(Some((mut new_samples, dropped, cum_drops, ts_ns, pkt_freq_hz))) => {
|
|
1360
1379
|
if dropped {
|
|
1380
|
+
if collected > 0 {
|
|
1381
|
+
// Put the gap on a boundary rather
|
|
1382
|
+
// than inside this block: stash the
|
|
1383
|
+
// post-gap chunk and let the next
|
|
1384
|
+
// read start with it, carrying the
|
|
1385
|
+
// flag. Same shape as the retune
|
|
1386
|
+
// boundary below, for the same
|
|
1387
|
+
// reason — a consumer can only reset
|
|
1388
|
+
// its state between reads.
|
|
1389
|
+
self.sample_buffer.extend(new_samples);
|
|
1390
|
+
self.sample_buffer_freq_hz = pkt_freq_hz;
|
|
1391
|
+
self.sample_buffer_after_gap = true;
|
|
1392
|
+
self.cumulative_drops = cum_drops;
|
|
1393
|
+
self.last_timestamp_ns = ts_ns;
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
// Nothing collected yet, so this chunk
|
|
1397
|
+
// opens the block and the gap really is
|
|
1398
|
+
// immediately before it.
|
|
1361
1399
|
self.pending_overrun = true;
|
|
1362
1400
|
}
|
|
1363
1401
|
self.cumulative_drops = cum_drops;
|
|
@@ -1499,6 +1537,13 @@ impl SpectranSource {
|
|
|
1499
1537
|
let from_buffer = self.sample_buffer.len().min(max_samples);
|
|
1500
1538
|
if from_buffer > 0 {
|
|
1501
1539
|
buffer.extend(self.sample_buffer.drain(0..from_buffer));
|
|
1540
|
+
// These samples were stashed because they follow a
|
|
1541
|
+
// gap; this is the read that begins with them, so
|
|
1542
|
+
// this is the read that must be told.
|
|
1543
|
+
if self.sample_buffer_after_gap {
|
|
1544
|
+
self.pending_overrun = true;
|
|
1545
|
+
self.sample_buffer_after_gap = false;
|
|
1546
|
+
}
|
|
1502
1547
|
}
|
|
1503
1548
|
|
|
1504
1549
|
// 2. If we need more, receive from channel
|
|
@@ -1513,6 +1558,25 @@ impl SpectranSource {
|
|
|
1513
1558
|
match tokio::time::timeout(read_timeout, receiver.recv()).await {
|
|
1514
1559
|
Ok(Some((mut new_samples, dropped, cum_drops, ts_ns, pkt_freq_hz))) => {
|
|
1515
1560
|
if dropped {
|
|
1561
|
+
if collected > 0 {
|
|
1562
|
+
// Put the gap on a boundary rather
|
|
1563
|
+
// than inside this block: stash the
|
|
1564
|
+
// post-gap chunk and let the next
|
|
1565
|
+
// read start with it, carrying the
|
|
1566
|
+
// flag. Same shape as the retune
|
|
1567
|
+
// boundary below, for the same
|
|
1568
|
+
// reason — a consumer can only reset
|
|
1569
|
+
// its state between reads.
|
|
1570
|
+
self.sample_buffer.extend(new_samples);
|
|
1571
|
+
self.sample_buffer_freq_hz = pkt_freq_hz;
|
|
1572
|
+
self.sample_buffer_after_gap = true;
|
|
1573
|
+
self.cumulative_drops = cum_drops;
|
|
1574
|
+
self.last_timestamp_ns = ts_ns;
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
// Nothing collected yet, so this chunk
|
|
1578
|
+
// opens the block and the gap really is
|
|
1579
|
+
// immediately before it.
|
|
1516
1580
|
self.pending_overrun = true;
|
|
1517
1581
|
}
|
|
1518
1582
|
self.cumulative_drops = cum_drops;
|
|
@@ -2727,6 +2791,7 @@ mod tests {
|
|
|
2727
2791
|
source_type: SourceType::Http, // Will be updated
|
|
2728
2792
|
sample_buffer: VecDeque::new(),
|
|
2729
2793
|
sample_buffer_freq_hz: 0.0,
|
|
2794
|
+
sample_buffer_after_gap: false,
|
|
2730
2795
|
last_read_frequency_hz: 0.0,
|
|
2731
2796
|
#[cfg(all(
|
|
2732
2797
|
feature = "native-sdk",
|
|
@@ -2810,6 +2875,7 @@ mod tests {
|
|
|
2810
2875
|
source_type: SourceType::Http,
|
|
2811
2876
|
sample_buffer: VecDeque::new(),
|
|
2812
2877
|
sample_buffer_freq_hz: 0.0,
|
|
2878
|
+
sample_buffer_after_gap: false,
|
|
2813
2879
|
last_read_frequency_hz: 0.0,
|
|
2814
2880
|
#[cfg(all(
|
|
2815
2881
|
feature = "native-sdk",
|
|
@@ -2845,6 +2911,7 @@ mod tests {
|
|
|
2845
2911
|
source_type: SourceType::File,
|
|
2846
2912
|
sample_buffer: VecDeque::new(),
|
|
2847
2913
|
sample_buffer_freq_hz: 0.0,
|
|
2914
|
+
sample_buffer_after_gap: false,
|
|
2848
2915
|
last_read_frequency_hz: 0.0,
|
|
2849
2916
|
#[cfg(all(
|
|
2850
2917
|
feature = "native-sdk",
|
|
@@ -2882,6 +2949,7 @@ mod tests {
|
|
|
2882
2949
|
source_type: SourceType::NativeSdk,
|
|
2883
2950
|
sample_buffer: VecDeque::new(),
|
|
2884
2951
|
sample_buffer_freq_hz: 0.0,
|
|
2952
|
+
sample_buffer_after_gap: false,
|
|
2885
2953
|
last_read_frequency_hz: 0.0,
|
|
2886
2954
|
#[cfg(all(
|
|
2887
2955
|
feature = "native-sdk",
|
|
@@ -3072,18 +3140,18 @@ mod tests {
|
|
|
3072
3140
|
assert!(debug_str.contains("TEST123"));
|
|
3073
3141
|
}
|
|
3074
3142
|
|
|
3075
|
-
///
|
|
3076
|
-
///
|
|
3077
|
-
///
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
let mut source = SpectranSource {
|
|
3143
|
+
/// An HTTP-backed source fed by `chunk_rx`, for the read-assembly
|
|
3144
|
+
/// tests. Factored out because three of them build the same thing
|
|
3145
|
+
/// and a new field otherwise has to be added in every copy.
|
|
3146
|
+
fn http_test_source(
|
|
3147
|
+
chunk_rx: tokio::sync::mpsc::Receiver<(Vec<Complex32>, bool, u64, i64, f64)>,
|
|
3148
|
+
) -> SpectranSource {
|
|
3149
|
+
SpectranSource {
|
|
3083
3150
|
config: SpectranConfig::default(),
|
|
3084
3151
|
source_type: SourceType::Http,
|
|
3085
3152
|
sample_buffer: VecDeque::new(),
|
|
3086
3153
|
sample_buffer_freq_hz: 0.0,
|
|
3154
|
+
sample_buffer_after_gap: false,
|
|
3087
3155
|
last_read_frequency_hz: 0.0,
|
|
3088
3156
|
#[cfg(all(
|
|
3089
3157
|
feature = "native-sdk",
|
|
@@ -3102,7 +3170,91 @@ mod tests {
|
|
|
3102
3170
|
last_timestamp_ns: 0,
|
|
3103
3171
|
read_scratch: Vec::new(),
|
|
3104
3172
|
read_scratch_rx2: Vec::new(),
|
|
3105
|
-
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
/// A gap must land on a read boundary, not inside a block.
|
|
3177
|
+
///
|
|
3178
|
+
/// Assembly used to note a dropped chunk and keep concatenating, so
|
|
3179
|
+
/// one returned block held samples from both sides of the hole. The
|
|
3180
|
+
/// overrun flag said only "somewhere in this block", and a consumer
|
|
3181
|
+
/// that resets its filters once — before the block — still ran the
|
|
3182
|
+
/// discontinuity through them. A gap has a position, and the only
|
|
3183
|
+
/// position a consumer can act on is a boundary.
|
|
3184
|
+
#[tokio::test]
|
|
3185
|
+
async fn a_stream_gap_ends_the_block_it_precedes() {
|
|
3186
|
+
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(4);
|
|
3187
|
+
let mut source = http_test_source(chunk_rx);
|
|
3188
|
+
|
|
3189
|
+
// Three chunks: two contiguous, then one flagged as following a
|
|
3190
|
+
// dropped chunk. All the same frequency, so only the gap can
|
|
3191
|
+
// split them.
|
|
3192
|
+
chunk_tx
|
|
3193
|
+
.send((vec![Complex32::new(1.0, 0.0)], false, 0, 1, 915e6))
|
|
3194
|
+
.await
|
|
3195
|
+
.expect("channel open");
|
|
3196
|
+
chunk_tx
|
|
3197
|
+
.send((vec![Complex32::new(2.0, 0.0)], false, 0, 2, 915e6))
|
|
3198
|
+
.await
|
|
3199
|
+
.expect("channel open");
|
|
3200
|
+
chunk_tx
|
|
3201
|
+
.send((vec![Complex32::new(3.0, 0.0)], true, 1, 3, 915e6))
|
|
3202
|
+
.await
|
|
3203
|
+
.expect("channel open");
|
|
3204
|
+
drop(chunk_tx);
|
|
3205
|
+
|
|
3206
|
+
// Asking for all three must stop at the gap.
|
|
3207
|
+
let mut buffer = Vec::new();
|
|
3208
|
+
let n = source
|
|
3209
|
+
.read_samples(&mut buffer, 3)
|
|
3210
|
+
.await
|
|
3211
|
+
.expect("first read succeeds");
|
|
3212
|
+
assert_eq!(n, 2, "read should stop before the post-gap chunk");
|
|
3213
|
+
assert!(
|
|
3214
|
+
!source.take_overrun(),
|
|
3215
|
+
"this block is contiguous — the gap is after it, not in it"
|
|
3216
|
+
);
|
|
3217
|
+
|
|
3218
|
+
// The next read starts with the post-gap chunk and owns the flag.
|
|
3219
|
+
buffer.clear();
|
|
3220
|
+
let n = source
|
|
3221
|
+
.read_samples(&mut buffer, 3)
|
|
3222
|
+
.await
|
|
3223
|
+
.expect("second read succeeds");
|
|
3224
|
+
assert_eq!(n, 1);
|
|
3225
|
+
assert_eq!(buffer[0], Complex32::new(3.0, 0.0));
|
|
3226
|
+
assert!(
|
|
3227
|
+
source.take_overrun(),
|
|
3228
|
+
"the read that begins at the gap must report it"
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/// A gap on the very first chunk of a read has nothing before it to
|
|
3233
|
+
/// split from, so it is reported against that read directly.
|
|
3234
|
+
#[tokio::test]
|
|
3235
|
+
async fn a_gap_on_the_first_chunk_is_reported_immediately() {
|
|
3236
|
+
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(4);
|
|
3237
|
+
let mut source = http_test_source(chunk_rx);
|
|
3238
|
+
chunk_tx
|
|
3239
|
+
.send((vec![Complex32::new(9.0, 0.0)], true, 1, 1, 915e6))
|
|
3240
|
+
.await
|
|
3241
|
+
.expect("channel open");
|
|
3242
|
+
drop(chunk_tx);
|
|
3243
|
+
|
|
3244
|
+
let mut buffer = Vec::new();
|
|
3245
|
+
let n = source.read_samples(&mut buffer, 4).await.expect("reads");
|
|
3246
|
+
assert_eq!(n, 1);
|
|
3247
|
+
assert!(source.take_overrun(), "nothing preceded it to split from");
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
/// A read never spans a retune: samples captured before the device
|
|
3251
|
+
/// moved must not be handed back tagged with the new frequency.
|
|
3252
|
+
/// That mislabelling is what makes a sweep report a signal at a
|
|
3253
|
+
/// centre it was never received on.
|
|
3254
|
+
#[tokio::test]
|
|
3255
|
+
async fn a_read_stops_at_a_capture_frequency_boundary() {
|
|
3256
|
+
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(4);
|
|
3257
|
+
let mut source = http_test_source(chunk_rx);
|
|
3106
3258
|
|
|
3107
3259
|
// Two chunks either side of a retune, queued before any read.
|
|
3108
3260
|
chunk_tx
|
|
@@ -3139,32 +3291,14 @@ mod tests {
|
|
|
3139
3291
|
async fn take_overrun_reflects_a_dropped_http_chunk() {
|
|
3140
3292
|
// Regression test for overrun wiring: a chunk arriving from the
|
|
3141
3293
|
// HTTP reader task flagged as dropped must surface via
|
|
3142
|
-
// `take_overrun()
|
|
3294
|
+
// `take_overrun()`, and clear after being read.
|
|
3295
|
+
//
|
|
3296
|
+
// It surfaces on the read that *begins* with that chunk, not on
|
|
3297
|
+
// one that merely contains it: a gap ends the block before it,
|
|
3298
|
+
// so a consumer resetting between reads resets in the right
|
|
3299
|
+
// place. See `a_stream_gap_ends_the_block_it_precedes`.
|
|
3143
3300
|
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(4);
|
|
3144
|
-
let mut source =
|
|
3145
|
-
config: SpectranConfig::default(),
|
|
3146
|
-
source_type: SourceType::Http,
|
|
3147
|
-
sample_buffer: VecDeque::new(),
|
|
3148
|
-
sample_buffer_freq_hz: 0.0,
|
|
3149
|
-
last_read_frequency_hz: 0.0,
|
|
3150
|
-
#[cfg(all(
|
|
3151
|
-
feature = "native-sdk",
|
|
3152
|
-
any(target_os = "windows", target_os = "linux")
|
|
3153
|
-
))]
|
|
3154
|
-
native_source: None,
|
|
3155
|
-
http_client: None,
|
|
3156
|
-
http_observed: None,
|
|
3157
|
-
http_rate_bits: None,
|
|
3158
|
-
http_tuning: None,
|
|
3159
|
-
file_source: None,
|
|
3160
|
-
http_receiver: Some(chunk_rx),
|
|
3161
|
-
http_task: None,
|
|
3162
|
-
pending_overrun: false,
|
|
3163
|
-
cumulative_drops: 0,
|
|
3164
|
-
last_timestamp_ns: 0,
|
|
3165
|
-
read_scratch: Vec::new(),
|
|
3166
|
-
read_scratch_rx2: Vec::new(),
|
|
3167
|
-
};
|
|
3301
|
+
let mut source = http_test_source(chunk_rx);
|
|
3168
3302
|
|
|
3169
3303
|
assert!(!source.take_overrun(), "no chunk received yet");
|
|
3170
3304
|
|
|
@@ -3182,12 +3316,22 @@ mod tests {
|
|
|
3182
3316
|
let n = source
|
|
3183
3317
|
.read_samples(&mut buffer, 2)
|
|
3184
3318
|
.await
|
|
3185
|
-
.expect("
|
|
3186
|
-
assert_eq!(n,
|
|
3319
|
+
.expect("first read succeeds");
|
|
3320
|
+
assert_eq!(n, 1, "the gap ends this block");
|
|
3321
|
+
assert!(
|
|
3322
|
+
!source.take_overrun(),
|
|
3323
|
+
"these samples are contiguous; the gap follows them"
|
|
3324
|
+
);
|
|
3187
3325
|
|
|
3326
|
+
buffer.clear();
|
|
3327
|
+
let n = source
|
|
3328
|
+
.read_samples(&mut buffer, 2)
|
|
3329
|
+
.await
|
|
3330
|
+
.expect("second read succeeds");
|
|
3331
|
+
assert_eq!(n, 1);
|
|
3188
3332
|
assert!(
|
|
3189
3333
|
source.take_overrun(),
|
|
3190
|
-
"a dropped chunk
|
|
3334
|
+
"a dropped chunk opened this read's block"
|
|
3191
3335
|
);
|
|
3192
3336
|
assert!(
|
|
3193
3337
|
!source.take_overrun(),
|
|
@@ -271,16 +271,38 @@ async fn stream_reconnects_and_reapplies_tuning() {
|
|
|
271
271
|
|
|
272
272
|
let puts_after_start = full_tuple_control_puts(&server).await;
|
|
273
273
|
|
|
274
|
+
// A reconnect is a discontinuity: the stream stopped and resumed,
|
|
275
|
+
// so the samples either side are not adjacent. Reads therefore
|
|
276
|
+
// *stop* at it rather than splicing across — the point is that they
|
|
277
|
+
// continue afterwards, not that one read spans the hole.
|
|
274
278
|
let mut buffer = Vec::new();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
let mut total = 0usize;
|
|
280
|
+
let mut reads = 0;
|
|
281
|
+
let mut saw_overrun = false;
|
|
282
|
+
while total < 12 && reads < 6 {
|
|
283
|
+
let mut chunk = Vec::new();
|
|
284
|
+
let n = source
|
|
285
|
+
.read_samples(&mut chunk, 12 - total)
|
|
286
|
+
.await
|
|
287
|
+
.expect("reads must continue across a reconnect");
|
|
288
|
+
if n == 0 {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
buffer.extend_from_slice(&chunk);
|
|
292
|
+
total += n;
|
|
293
|
+
reads += 1;
|
|
294
|
+
saw_overrun |= source.take_overrun();
|
|
295
|
+
}
|
|
279
296
|
|
|
280
297
|
assert_eq!(
|
|
281
298
|
buffer.len(),
|
|
282
299
|
12,
|
|
283
|
-
"expected samples from several connections"
|
|
300
|
+
"expected samples from several connections, in {reads} read(s)"
|
|
301
|
+
);
|
|
302
|
+
assert!(
|
|
303
|
+
reads >= 2,
|
|
304
|
+
"the reconnect should have ended a block, so 12 samples take \
|
|
305
|
+
more than one read"
|
|
284
306
|
);
|
|
285
307
|
assert!(
|
|
286
308
|
opens.load(Ordering::SeqCst) >= 2,
|
|
@@ -291,9 +313,12 @@ async fn stream_reconnects_and_reapplies_tuning() {
|
|
|
291
313
|
"reconnect must re-apply the tuning, or a restarted server \
|
|
292
314
|
silently streams its mission's frequency instead"
|
|
293
315
|
);
|
|
316
|
+
// The flag rides with the read that *began* at the gap, so it was
|
|
317
|
+
// observed during the loop above rather than being left pending
|
|
318
|
+
// here. What matters is that some read carried it.
|
|
294
319
|
assert!(
|
|
295
|
-
|
|
296
|
-
"samples were missed across the gap; the
|
|
320
|
+
saw_overrun,
|
|
321
|
+
"samples were missed across the gap; the read beginning after a \
|
|
297
322
|
reconnect must be flagged as an overrun"
|
|
298
323
|
);
|
|
299
324
|
}
|
|
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
|
|
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
|