108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""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()
|