CLOCK-001: validate independent-clock QPSK receiver

This commit is contained in:
LittleSam129
2026-09-17 15:05:44 +03:00
parent 6df5f8aa39
commit 3c31bac0da
12 changed files with 4458 additions and 1 deletions

69
tools/qpsk_rf_common.py Normal file
View File

@@ -0,0 +1,69 @@
"""Формат файлов и воспроизводимые пакеты для подготовки RF-001 без SDR."""
from __future__ import annotations
import hashlib
from pathlib import Path
import numpy as np
from protocol.video003_modem import SAMPLE_RATE_HZ
from protocol.video005_iq_waveform import NOMINAL_CARRIER_HZ, SYMBOL_RATE
TEST_ID = "RF001_QPSK_500K_R23_256B_100P_GAP5MS"
SEED = 20260917
PAYLOAD_BYTES = 256
PACKET_COUNT = 100
GAP_MS = 5
GAP_SAMPLES = SAMPLE_RATE_HZ * GAP_MS // 1000
IQ_FORMAT = "c64le"
def payload_for_sequence(seed: int, sequence_number: int) -> bytes:
"""Создаёт 256 байт через SHA-256, не завися от версии генератора NumPy."""
if not 0 <= seed < 2**64 or not 0 <= sequence_number <= 0xFFFF:
raise ValueError("seed или номер пакета вне допустимого диапазона")
prefix = seed.to_bytes(8, "big") + sequence_number.to_bytes(2, "big")
return b"".join(
hashlib.sha256(prefix + block.to_bytes(2, "big")).digest()
for block in range(PAYLOAD_BYTES // hashlib.sha256().digest_size)
)
def sha256_file(path: Path) -> str:
"""Хеширует файл потоково, не копируя весь IQ в память."""
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1 << 20):
digest.update(chunk)
return digest.hexdigest()
def read_iq_file(path: Path, iq_format: str = IQ_FORMAT) -> tuple[np.ndarray, int | None]:
"""Читает файловый IQ; `cs8` сохраняет исходный файл без изменений."""
size = path.stat().st_size
if iq_format == "c64le":
if size == 0 or size % 8:
raise ValueError("c64le должен содержать целое ненулевое число отсчётов")
return np.memmap(path, dtype="<c8", mode="r"), None
if iq_format == "cs8":
if size == 0 or size % 2:
raise ValueError("cs8 должен содержать целое ненулевое число пар I/Q")
raw = np.memmap(path, dtype=np.int8, mode="r")
clipping = int(np.count_nonzero((raw == -128) | (raw == 127)))
iq = ((raw[0::2].astype(np.float32) + 1j * raw[1::2].astype(np.float32))
/ 128).astype(np.complex64)
return iq, clipping
raise ValueError("поддерживаются только c64le и cs8")
def radio_mode() -> dict[str, int | float | str]:
"""Параметры номинального режима, которые разрешено знать приёмнику."""
return {
"modulation": "QPSK",
"symbol_rate": SYMBOL_RATE,
"sample_rate": SAMPLE_RATE_HZ,
"fec_rate": "2/3",
"digital_carrier_offset_hz": NOMINAL_CARRIER_HZ,
}