From 716fd0b07adb0770c7d2cc78aa4099306433a78d Mon Sep 17 00:00:00 2001 From: Ayzen Date: Mon, 22 Jun 2026 17:33:15 +0300 Subject: [PATCH] microfix --- python_app/scripts/kamil_adc_calibrate.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/python_app/scripts/kamil_adc_calibrate.py b/python_app/scripts/kamil_adc_calibrate.py index 5c3bcc4..1257054 100644 --- a/python_app/scripts/kamil_adc_calibrate.py +++ b/python_app/scripts/kamil_adc_calibrate.py @@ -31,7 +31,9 @@ from contextlib import suppress import json import logging from pathlib import Path +import shutil import statistics +import subprocess import time import numpy as np @@ -65,6 +67,60 @@ DO8_FREQ_REF_ARGS = [ ] COLLECTOR_PATH = "build/bin/kamil_adc_collector" +# Hardware the daemon / a prior collector may be holding, and how to free it. +# Mirrors what start.sh does before an interactive launch (stop the daemon, kill +# an orphaned collector) so this tool can grab the L-Card E-502 + lasers too. +RADAR_SERVICE_NAME = "radar.service" +COLLECTOR_PROCESS_PATTERN = "kamil_adc_collector" +# The E-502 is not reacquirable the instant it is released; wait before opening. +_DEVICE_SETTLE_SECONDS = 8.0 + + +def _release_radar_hardware(*, settle_seconds: float = _DEVICE_SETTLE_SECONDS) -> None: + """Free the L-Card E-502 + lasers before we open our own collector. + + Mirrors ``start.sh`` on an interactive launch: stop the headless + ``radar.service`` daemon (if active) so it releases the hardware, then kill any + orphaned ``kamil_adc_collector`` a prior (e.g. SSH-killed) run left holding the + device. Both steps are best-effort and non-fatal — the collector's own + open-retry covers the residual settle time — this just removes the usual reason + it never frees up. Passwordless ``sudo systemctl stop radar.service`` is + provisioned in ``/etc/sudoers.d/radar``. + """ + freed = False + + systemctl = shutil.which("systemctl") + if systemctl is not None: + is_active = subprocess.run( + [systemctl, "is-active", "--quiet", RADAR_SERVICE_NAME], + check=False, + ).returncode == 0 + if is_active: + logger.info("Stopping %s so it releases the radar hardware...", RADAR_SERVICE_NAME) + stopped = subprocess.run( + ["sudo", systemctl, "stop", RADAR_SERVICE_NAME], check=False + ).returncode == 0 + if stopped: + freed = True + else: + logger.warning( + "Could not stop %s (need passwordless sudo?); continuing anyway", + RADAR_SERVICE_NAME, + ) + + pkill = shutil.which("pkill") + if pkill is not None: + # pkill returns 0 when it matched & signalled at least one process. + if subprocess.run( + [pkill, "-9", "-f", COLLECTOR_PROCESS_PATTERN], check=False + ).returncode == 0: + logger.info("Killed orphaned %s process(es) holding the device", COLLECTOR_PROCESS_PATTERN) + freed = True + + if freed: + logger.info("Waiting %.0fs for the L-Card E-502 to settle...", settle_seconds) + time.sleep(settle_seconds) + def _open_with_retry(service: KamilAdcService, *, attempts: int = 4, delay_s: float = 8.0) -> None: """Open the collector, retrying the transient E-502 device-busy after a close. @@ -519,6 +575,11 @@ def main() -> int: parser.add_argument("--warmup", type=int, default=10, help="Sweeps to discard first (default 10)") parser.add_argument("--apply", action="store_true", help="Write the calibration back to --config") parser.add_argument("--no-laser", action="store_true", help="Skip laser setup (already running)") + parser.add_argument( + "--no-release", + action="store_true", + help="Do not stop radar.service / kill orphaned collectors before opening", + ) parser.add_argument( "--diagnose", action="store_true", @@ -541,6 +602,11 @@ def main() -> int: config.radar.kamil_adc.executable_path = COLLECTOR_PATH config.radar.kamil_adc.args = list(DO8_FREQ_REF_ARGS) + # Free the device the headless daemon / an orphaned collector may be holding, + # so opening our own collector does not fail with E-502 device-busy. + if not args.no_release: + _release_radar_hardware() + if not args.no_laser: logger.info("Applying laser control...") apply_kamil_adc_laser_control(config)