Harden RF handoff and stream OTA decode

This commit is contained in:
LittleSam129
2026-09-22 17:43:58 +03:00
parent 168e03d9e7
commit a94f2db4ee
17 changed files with 1548 additions and 497 deletions

View 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