Harden RF handoff and stream OTA decode
This commit is contained in:
@@ -31,6 +31,12 @@ from tools.qpsk_rf_common import (
|
||||
GAP_MS, PACKET_COUNT, PAYLOAD_BYTES, SEED, TEST_ID,
|
||||
payload_for_sequence, read_iq_file, sha256_file,
|
||||
)
|
||||
from tools.rf001a_streaming import (
|
||||
DEFAULT_CHUNK_SAMPLES,
|
||||
MemoryPeakSampler,
|
||||
cs8_sample_count,
|
||||
decode_cs8_streaming,
|
||||
)
|
||||
|
||||
|
||||
def _bit_errors(observed: bytes, expected: bytes) -> tuple[int, int]:
|
||||
@@ -124,7 +130,7 @@ def summarize_result(
|
||||
unique_sequences = set(sequences)
|
||||
cfo = [row["cfo_hz"] for row in rows]
|
||||
report: dict[str, Any] = {
|
||||
"rf001_status": "PREPARED_NOT_RUN",
|
||||
"rf001_status": "OFFLINE_ANALYZED",
|
||||
"test_id": truth.get("test_id") if truth is not None else None,
|
||||
"modulation": "QPSK",
|
||||
"symbol_rate": 500_000,
|
||||
@@ -242,10 +248,32 @@ def decode_file(
|
||||
iq_format: str = "c64le",
|
||||
truth_metadata_path: Path | None = None,
|
||||
decoder_backend: str = "numba",
|
||||
capture_started_at: str | None = None,
|
||||
tx_started_at: str | None = None,
|
||||
tx_finished_at: str | None = None,
|
||||
chunk_samples: int = DEFAULT_CHUNK_SAMPLES,
|
||||
) -> dict[str, Any]:
|
||||
"""Открывает только файл; truth читается после вызова рабочего приёмника."""
|
||||
iq, clipping_codes = read_iq_file(iq_path, iq_format)
|
||||
reception = receive_iq_stream(iq, decoder_backend=decoder_backend)
|
||||
"""Декодирует c64le целиком, а большие cs8 — ограниченными окнами."""
|
||||
|
||||
streaming = None
|
||||
with MemoryPeakSampler() as memory:
|
||||
if iq_format == "cs8":
|
||||
streaming = decode_cs8_streaming(
|
||||
iq_path,
|
||||
capture_started_at=capture_started_at,
|
||||
tx_started_at=tx_started_at,
|
||||
tx_finished_at=tx_finished_at,
|
||||
decoder_backend=decoder_backend,
|
||||
chunk_samples=chunk_samples,
|
||||
)
|
||||
reception = streaming.reception
|
||||
iq_sample_count = cs8_sample_count(iq_path)
|
||||
clipping_codes = streaming.clipping_codes
|
||||
iq = None
|
||||
else:
|
||||
iq, clipping_codes = read_iq_file(iq_path, iq_format)
|
||||
reception = receive_iq_stream(iq, decoder_backend=decoder_backend)
|
||||
iq_sample_count = int(iq.size)
|
||||
truth = (json.loads(truth_metadata_path.read_text(encoding="utf-8"))
|
||||
if truth_metadata_path is not None else None)
|
||||
if truth is not None and (
|
||||
@@ -262,17 +290,38 @@ def decode_file(
|
||||
or truth.get("seed") != SEED
|
||||
):
|
||||
raise ValueError("TX-ведомость не соответствует режиму RF-001")
|
||||
raw_bits = (_raw_ber_after_decode(iq, reception, int(truth["seed"]), decoder_backend)
|
||||
if truth is not None else None)
|
||||
return summarize_result(
|
||||
raw_bits = (
|
||||
_raw_ber_after_decode(iq, reception, int(truth["seed"]), decoder_backend)
|
||||
if truth is not None and iq is not None
|
||||
else None
|
||||
)
|
||||
report = summarize_result(
|
||||
reception,
|
||||
iq_sample_count=int(iq.size),
|
||||
iq_sample_count=iq_sample_count,
|
||||
iq_sha256=sha256_file(iq_path),
|
||||
iq_format=iq_format,
|
||||
clipping_codes=clipping_codes,
|
||||
truth=truth,
|
||||
raw_bits=raw_bits,
|
||||
)
|
||||
report["peak_analysis_memory_bytes"] = memory.peak_bytes
|
||||
report["peak_additional_ram_bytes"] = memory.additional_peak_bytes
|
||||
if streaming is not None:
|
||||
report.update({
|
||||
"streaming_decoder": True,
|
||||
"window_source": streaming.window_source,
|
||||
"analysis_windows": [
|
||||
{"start_sample": window.start, "stop_sample": window.stop}
|
||||
for window in streaming.windows
|
||||
],
|
||||
"overlap_samples": streaming.overlap_samples,
|
||||
"maximum_loaded_complex_samples": streaming.maximum_loaded_samples,
|
||||
"maximum_loaded_iq_bytes": streaming.maximum_loaded_samples * 8,
|
||||
"raw_ber_scope": "NOT_COMPUTED_FOR_STREAMING_WINDOWS",
|
||||
})
|
||||
else:
|
||||
report["streaming_decoder"] = False
|
||||
return report
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -282,6 +331,10 @@ def main() -> None:
|
||||
parser.add_argument("--truth-metadata", type=Path)
|
||||
parser.add_argument("--report", required=True, type=Path)
|
||||
parser.add_argument("--numba-packages", type=Path)
|
||||
parser.add_argument("--capture-started-at")
|
||||
parser.add_argument("--tx-started-at")
|
||||
parser.add_argument("--tx-finished-at")
|
||||
parser.add_argument("--chunk-samples", type=int, default=DEFAULT_CHUNK_SAMPLES)
|
||||
arguments = parser.parse_args()
|
||||
if arguments.report.exists():
|
||||
parser.error("отчёт уже существует; перезапись запрещена")
|
||||
@@ -290,7 +343,11 @@ def main() -> None:
|
||||
parser.error("каталог пакетов Numba отсутствует")
|
||||
sys.path.insert(0, str(arguments.numba_packages.resolve()))
|
||||
result = decode_file(arguments.iq_file, iq_format=arguments.iq_format,
|
||||
truth_metadata_path=arguments.truth_metadata)
|
||||
truth_metadata_path=arguments.truth_metadata,
|
||||
capture_started_at=arguments.capture_started_at,
|
||||
tx_started_at=arguments.tx_started_at,
|
||||
tx_finished_at=arguments.tx_finished_at,
|
||||
chunk_samples=arguments.chunk_samples)
|
||||
arguments.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
arguments.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user