Harden RF handoff and stream OTA decode
This commit is contained in:
109
tests/test_rf001a_streaming.py
Normal file
109
tests/test_rf001a_streaming.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Проверки оконного cs8-декодера RF-001A без SDR и RF TX."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from protocol.video003_modem import build_frame
|
||||
from protocol.video005_iq_waveform import shape_packet
|
||||
from tools.qpsk_rf_common import SEED, payload_for_sequence
|
||||
from tools.qpsk_rf_rx import decode_file
|
||||
from tools.rf001a_streaming import (
|
||||
MemoryPeakSampler,
|
||||
SampleWindow,
|
||||
count_cs8_clipping,
|
||||
cs8_sample_count,
|
||||
decode_cs8_streaming,
|
||||
read_cs8_window,
|
||||
receive_cs8_windows,
|
||||
required_overlap_samples,
|
||||
rf001_packet_waveform_samples,
|
||||
timestamp_sample_window,
|
||||
)
|
||||
|
||||
|
||||
def _write_cs8(path: Path, iq: np.ndarray) -> None:
|
||||
peak = float(np.max(np.abs(iq))) if iq.size else 1.0
|
||||
normalized = iq / max(peak, 1e-12) * 0.75
|
||||
interleaved = np.empty(iq.size * 2, dtype=np.int8)
|
||||
interleaved[0::2] = np.rint(normalized.real * 127).astype(np.int8)
|
||||
interleaved[1::2] = np.rint(normalized.imag * 127).astype(np.int8)
|
||||
interleaved.tofile(path)
|
||||
|
||||
|
||||
def test_timestamp_to_sample_window_uses_safe_margins() -> None:
|
||||
capture = datetime(2026, 9, 22, 12, 0, tzinfo=timezone.utc)
|
||||
window = timestamp_sample_window(
|
||||
capture_started_at=capture.isoformat(),
|
||||
tx_started_at=(capture + timedelta(seconds=10)).isoformat(),
|
||||
tx_finished_at=(capture + timedelta(seconds=11.1042285)).isoformat(),
|
||||
total_samples=160_000_000,
|
||||
margin_seconds=2.0,
|
||||
)
|
||||
assert window.start == 64_000_000
|
||||
assert window.stop == 104_833_824
|
||||
|
||||
|
||||
def test_chunked_cs8_reading_from_large_sparse_capture(tmp_path: Path) -> None:
|
||||
capture = tmp_path / "large_sparse.cs8"
|
||||
with capture.open("wb") as stream:
|
||||
stream.seek(1_920_000_000 - 1)
|
||||
stream.write(b"\0")
|
||||
raw = np.memmap(capture, dtype=np.int8, mode="r")
|
||||
window = read_cs8_window(raw, 900_000_000, 900_001_000)
|
||||
assert capture.stat().st_size == 1_920_000_000
|
||||
assert cs8_sample_count(capture) == 960_000_000
|
||||
assert window.dtype == np.complex64
|
||||
assert window.size == 1_000
|
||||
assert window.nbytes == 8_000
|
||||
with MemoryPeakSampler() as memory:
|
||||
assert count_cs8_clipping(capture) == 0
|
||||
assert memory.additional_peak_bytes < 512_000_000
|
||||
|
||||
|
||||
def test_packet_across_chunk_boundary_is_decoded_once(tmp_path: Path) -> None:
|
||||
payload = payload_for_sequence(SEED, 7)
|
||||
frame = build_frame(payload, "QPSK", "2/3", sequence_number=7)
|
||||
waveform = shape_packet(frame)
|
||||
chunk_samples = 120_000
|
||||
packet_start = chunk_samples - waveform.size // 2
|
||||
iq = np.zeros(chunk_samples * 2, dtype=np.complex64)
|
||||
iq[packet_start:packet_start + waveform.size] = waveform
|
||||
capture = tmp_path / "boundary.cs8"
|
||||
_write_cs8(capture, iq)
|
||||
|
||||
result = receive_cs8_windows(
|
||||
capture,
|
||||
(SampleWindow(0, iq.size),),
|
||||
decoder_backend="legacy",
|
||||
chunk_samples=chunk_samples,
|
||||
)
|
||||
|
||||
assert required_overlap_samples() == rf001_packet_waveform_samples() == 48_865
|
||||
assert [packet.packet for packet in result.reception.packets] == [frame.packet]
|
||||
assert result.maximum_loaded_samples < iq.size
|
||||
|
||||
|
||||
def test_capture_without_rf001a_signal_has_zero_packets(tmp_path: Path) -> None:
|
||||
capture = tmp_path / "noise_only.cs8"
|
||||
np.zeros(400_000, dtype=np.int8).tofile(capture)
|
||||
result = decode_cs8_streaming(capture, decoder_backend="legacy")
|
||||
assert result.window_source == "STREAMING_POWER_LOCATOR"
|
||||
assert result.windows == ()
|
||||
assert result.reception.packets == ()
|
||||
assert result.maximum_loaded_samples == 0
|
||||
|
||||
truth = Path(__file__).resolve().parents[1] / "configs/rf/rf001a_truth_metadata.json"
|
||||
report = decode_file(
|
||||
capture,
|
||||
iq_format="cs8",
|
||||
truth_metadata_path=truth,
|
||||
decoder_backend="legacy",
|
||||
)
|
||||
assert report["exact_packets"] == 0
|
||||
assert report["crc_valid_packets"] == 0
|
||||
assert report["streaming_decoder"] is True
|
||||
assert report["peak_additional_ram_bytes"] < 1_000_000_000
|
||||
Reference in New Issue
Block a user