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