70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
"""Формат файлов и воспроизводимые пакеты для подготовки 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,
|
|
}
|