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

107
tools/qpsk_rf_tx.py Normal file
View File

@@ -0,0 +1,107 @@
"""RF-001: построение 100 QPSK-пакетов и IQ только в режиме dry-run."""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sys
import numpy as np
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from protocol.video003_modem import SAMPLE_RATE_HZ, build_frame
from protocol.video005_iq_waveform import GUARD_SAMPLES, shape_packet
from tools.qpsk_rf_common import (
GAP_MS,
GAP_SAMPLES,
IQ_FORMAT,
PACKET_COUNT,
PAYLOAD_BYTES,
SEED,
TEST_ID,
payload_for_sequence,
radio_mode,
sha256_file,
)
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / "data/raw/rf001/offline_qpsk_100_packets"
def build_dry_run() -> tuple[np.ndarray, dict]:
"""Собирает ровно утверждённый режим; границы остаются только в TX-ведомости."""
separator = GAP_SAMPLES - 2 * GUARD_SAMPLES
if separator < 0:
raise AssertionError("пауза короче двух защитных интервалов RRC")
waveforms = []
expected = []
starts = []
position = 0
for sequence_number in range(PACKET_COUNT):
payload = payload_for_sequence(SEED, sequence_number)
frame = build_frame(payload, "QPSK", "2/3", sequence_number=sequence_number)
waveform = shape_packet(frame)
starts.append(position)
waveforms.append(waveform)
expected.append({
"sequence_number": sequence_number,
"payload_sha256": hashlib.sha256(payload).hexdigest(),
"packet_sha256": hashlib.sha256(frame.packet).hexdigest(),
})
position += waveform.size + (separator if sequence_number < PACKET_COUNT - 1 else 0)
iq = np.zeros(position, dtype="<c8")
for index, waveform in enumerate(waveforms):
iq[starts[index]:starts[index] + waveform.size] = waveform
metadata = {
"schema_version": 1,
"test_id": TEST_ID,
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
**radio_mode(),
"application_payload_bytes": PAYLOAD_BYTES,
"packet_count": PACKET_COUNT,
"inter_packet_gap_ms": GAP_MS,
"inter_packet_gap_samples": GAP_SAMPLES,
"inter_array_zero_samples": separator,
"seed": SEED,
"iq_format": IQ_FORMAT,
"dtype": "<c8",
"waveform_sample_count": int(iq.size),
"waveform_duration_s": iq.size / SAMPLE_RATE_HZ,
"packet_waveform_start_samples": starts,
"expected_packet_hashes": expected,
"rf_tx": 0,
"sdr_hardware": "NOT_ACCESSED",
"status": "PREPARED_NOT_RUN",
}
return iq, metadata
def write_dry_run(output_directory: Path = DEFAULT_OUTPUT) -> tuple[Path, Path]:
"""Создаёт новый каталог; существующие IQ и метаданные не перезаписывает."""
output_directory.mkdir(parents=True, exist_ok=False)
iq, metadata = build_dry_run()
iq_path = output_directory / "tx_waveform.c64le"
metadata_path = output_directory / "tx_metadata.json"
iq.tofile(iq_path)
metadata["iq_sha256"] = sha256_file(iq_path)
metadata_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
return iq_path, metadata_path
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
arguments = parser.parse_args()
iq_path, metadata_path = write_dry_run(arguments.output_dir)
print(f"RF-001 DRY-RUN: {iq_path}; metadata={metadata_path}; RF TX=0")
if __name__ == "__main__":
main()