CLOCK-001: validate independent-clock QPSK receiver
This commit is contained in:
69
tools/qpsk_rf_common.py
Normal file
69
tools/qpsk_rf_common.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user