"""Manual smoke scenario for end-to-end pipeline check without GUI.""" from __future__ import annotations import argparse import ctypes import ctypes.util import os from pathlib import Path import sys import time import numpy as np PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData from python_app.models.run_config_model import RunConfigModel from python_app.orchestration.config_writer import ConfigWriter from python_app.orchestration.process_supervisor import ProcessSupervisor from python_app.orchestration.shm_reader import ShmRingReader from python_app.storage.npz_store import NpzStore, radar_key_from_config def _make_unique_ring_name(prefix: str) -> str: """Build unique POSIX SHM ring name.""" stamp = time.monotonic_ns() return f"/{prefix}_{os.getpid()}_{stamp}" def _shm_unlink(name: str) -> None: """Best-effort unlink for POSIX shared-memory object.""" libc_name = ctypes.util.find_library("c") if libc_name is None: return libc = ctypes.CDLL(libc_name, use_errno=True) libc.shm_unlink.argtypes = [ctypes.c_char_p] libc.shm_unlink.restype = ctypes.c_int result = libc.shm_unlink(name.encode("utf-8")) if result == 0: return err = ctypes.get_errno() if err != 2: # ENOENT raise OSError(err, f"shm_unlink failed for {name}") def build_synthetic_collection(config: RunConfigModel, value_scale: float) -> SweepCollection: """Build synthetic sweep collection for all configured switch combos.""" traces: list[TraceData] = [] combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions) for combo in combos: frequency_hz = np.linspace( config.radar.sweep.start_hz, config.radar.sweep.stop_hz, config.radar.sweep.points, dtype=np.float32, ) phase = np.linspace(0.0, np.pi * 2.0, config.radar.sweep.points, dtype=np.float32) s21 = value_scale * (np.cos(phase) + 1j * np.sin(phase)).astype(np.complex64) traces.append( TraceData( combo=ComboKey(input_pos=combo.input, output_pos=combo.output), frequency_hz=frequency_hz, s21=s21, ) ) return SweepCollection(collection_id=1, monotonic_ns=time.monotonic_ns(), traces=traces) def main() -> int: """Run manual smoke-test pipeline scenario.""" parser = argparse.ArgumentParser() parser.add_argument("--duration", type=float, default=3.0) args = parser.parse_args() project_root = PROJECT_ROOT store = NpzStore(project_root / "python_app/data") config_writer = ConfigWriter(project_root / "python_app/runtime") supervisor = ProcessSupervisor(project_root) config = RunConfigModel.load_from_path(project_root / "run_config.json") config.radar.driver_mode = "mock" config.input_switch.driver_mode = "mock" config.output_switch.driver_mode = "mock" config.combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions) config.rings.raw.name = _make_unique_ring_name("radar_raw_smoke") config.rings.raw_tap.name = _make_unique_ring_name("radar_raw_tap_smoke") config.rings.preprocessed.name = _make_unique_ring_name("radar_preprocessed_smoke") config.rings.preprocessed_tap.name = _make_unique_ring_name("radar_preprocessed_tap_smoke") config.rings.results.name = _make_unique_ring_name("radar_results_smoke") radar_key = radar_key_from_config( model_name=config.radar.model, serial=config.radar.serial, sweep_start_hz=config.radar.sweep.start_hz, sweep_stop_hz=config.radar.sweep.stop_hz, sweep_points=config.radar.sweep.points, ifbw_hz=config.radar.sweep.if_bandwidth_hz, power_dbm=config.radar.sweep.power_dbm, ) calibration_set = build_synthetic_collection(config, value_scale=1.0) reference_set = build_synthetic_collection(config, value_scale=0.3) store.save_set("calibration", radar_key, "smoke_cal", calibration_set) store.save_set("reference", radar_key, "smoke_ref", reference_set) calibration_bundle, reference_bundle = config_writer.prepare_bundles(store, radar_key, "smoke_cal", "smoke_ref") config.preprocess.calibration_set = "smoke_cal" config.preprocess.reference_set = "smoke_ref" config.preprocess.calibration_bundle_path = str(calibration_bundle) config.preprocess.reference_bundle_path = str(reference_bundle) config_path = config_writer.write(config, project_root / "python_app/runtime/run_config_smoke.json") result_reader: ShmRingReader | None = None try: supervisor.start(config_path) result_reader = ShmRingReader(config.rings.results.name) deadline = time.monotonic() + args.duration received = 0 while time.monotonic() < deadline: result = result_reader.pop_result_collection() if result_reader is not None else None if result is not None: received += 1 time.sleep(0.02) print(f"Received result collections: {received}") finally: supervisor.stop_all() if result_reader is not None: result_reader.close() _shm_unlink(config.rings.raw.name) _shm_unlink(config.rings.raw_tap.name) _shm_unlink(config.rings.preprocessed.name) _shm_unlink(config.rings.preprocessed_tap.name) _shm_unlink(config.rings.results.name) return 0 if __name__ == "__main__": raise SystemExit(main())