Independent monitor + checker for CHANGE_CURRENT_LD1 variation, where the board sweeps LD1 current autonomously while both laser temperatures must stay on their static setpoints. Nothing previously verified that the setpoints were actually reached, so a stale temperature silently corrupted measurements. New package python_app/hardware_full/laser_control/monitoring/: - session.py: LaserVariationSession snapshot (targets + tolerance frozen at start) - readings_channel.py: JSONL append/tail channel for per-sweep readings - monitor.py: LaserTemperatureMonitor polls the board once per sweep, publishes - checker.py: LaserTemperatureChecker validates temp1/temp2 vs targets (0.03 C), warns to console per laser with anti-spam state CLI entry points scripts/laser_temp_monitor.py and scripts/laser_temp_checker.py run as independent processes communicating via IPC files. Also: add temp_tolerance_c to LaserVariationModeModel (schema + codec round-trip) and write the session snapshot from apply_kamil_adc_laser_control's variation path so the checker works with pipeline-started runs too. Tests in tests/test_laser_temp_monitoring.py (17 cases); existing laser-control protocol and config-codec suites remain green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
5.3 KiB
Python
150 lines
5.3 KiB
Python
"""Standalone laser temperature monitor process.
|
|
|
|
Owns the laser serial port, (optionally) starts a current-variation task, then
|
|
polls the board once per sweep and appends each reading to a JSONL channel that
|
|
the temperature checker tails. Runs until SIGINT/SIGTERM.
|
|
|
|
Examples::
|
|
|
|
# Start LD1 current variation from a run config, then monitor:
|
|
python -m python_app.scripts.laser_temp_monitor --config run_config.json --start
|
|
|
|
# Monitor a variation that is already running:
|
|
python -m python_app.scripts.laser_temp_monitor --config run_config.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import signal
|
|
import threading
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from python_app.hardware_full.laser_control.controller import (
|
|
DEVICE_MAIN_MESSAGE_ID,
|
|
LaserController,
|
|
)
|
|
from python_app.hardware_full.laser_control.exceptions import PortBusyError
|
|
from python_app.hardware_full.laser_control.models import VariationType
|
|
from python_app.hardware_full.laser_control.monitoring import (
|
|
DEFAULT_READINGS_PATH,
|
|
DEFAULT_SESSION_PATH,
|
|
LaserTemperatureMonitor,
|
|
LaserVariationSession,
|
|
ReadingWriter,
|
|
resolve_period_s,
|
|
)
|
|
from python_app.models.run_config_model import RunConfigModel
|
|
|
|
logger = logging.getLogger("laser_temp_monitor")
|
|
|
|
|
|
def _start_variation(controller: LaserController, variation) -> None:
|
|
"""Send the CHANGE_CURRENT_LD1 task and freeze the session snapshot."""
|
|
controller.reset()
|
|
controller.set_manual_mode(
|
|
temp1=variation.static_temp1,
|
|
temp2=variation.static_temp2,
|
|
current1=variation.static_current1,
|
|
current2=variation.static_current2,
|
|
message_id=DEVICE_MAIN_MESSAGE_ID,
|
|
)
|
|
controller.start_variation(
|
|
variation_type=VariationType[variation.variation_type],
|
|
params={
|
|
"static_temp1": variation.static_temp1,
|
|
"static_temp2": variation.static_temp2,
|
|
"static_current1": variation.static_current1,
|
|
"static_current2": variation.static_current2,
|
|
"min_value": variation.min_value,
|
|
"max_value": variation.max_value,
|
|
"step": variation.step,
|
|
"time_step": variation.time_step,
|
|
"delay_time": variation.delay_time,
|
|
},
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Poll laser temperature once per sweep")
|
|
parser.add_argument("--config", required=True, type=Path, help="Path to run_config.json")
|
|
parser.add_argument("--readings", type=Path, default=DEFAULT_READINGS_PATH,
|
|
help="JSONL readings channel to append to")
|
|
parser.add_argument("--session", type=Path, default=DEFAULT_SESSION_PATH,
|
|
help="Session snapshot path (written with --start)")
|
|
parser.add_argument("--strategy", default="computed",
|
|
help="'computed' (per sweep) or 'interval:<ms>'")
|
|
parser.add_argument("--start", action="store_true",
|
|
help="Send CHANGE_CURRENT_LD1 before monitoring")
|
|
args = parser.parse_args()
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
|
|
config = RunConfigModel.load_from_path(args.config)
|
|
laser = config.radar.laser_control
|
|
variation = laser.variation
|
|
if variation.variation_type != "CHANGE_CURRENT_LD1":
|
|
logger.warning(
|
|
"Only CHANGE_CURRENT_LD1 is supported by firmware; got %s",
|
|
variation.variation_type,
|
|
)
|
|
|
|
period_s = resolve_period_s(
|
|
args.strategy,
|
|
min_value=variation.min_value,
|
|
max_value=variation.max_value,
|
|
step=variation.step,
|
|
time_step_us=variation.time_step,
|
|
delay_time_ms=variation.delay_time,
|
|
)
|
|
|
|
stop_event = threading.Event()
|
|
|
|
def request_stop(_signum: int, _frame: object) -> None:
|
|
stop_event.set()
|
|
|
|
signal.signal(signal.SIGINT, request_stop)
|
|
signal.signal(signal.SIGTERM, request_stop)
|
|
|
|
controller = LaserController(
|
|
port=laser.port or None,
|
|
pi_coeff1_p=laser.pi_coeff1_p,
|
|
pi_coeff1_i=laser.pi_coeff1_i,
|
|
pi_coeff2_p=laser.pi_coeff2_p,
|
|
pi_coeff2_i=laser.pi_coeff2_i,
|
|
)
|
|
try:
|
|
controller.connect()
|
|
except PortBusyError as exc:
|
|
# Expected, benign conflict: the manual-control UI (or another monitor)
|
|
# already owns the port. Exit cleanly with guidance, not a traceback.
|
|
logger.error("%s", exc)
|
|
return 2
|
|
try:
|
|
if args.start:
|
|
_start_variation(controller, variation)
|
|
LaserVariationSession(
|
|
variation_type=variation.variation_type,
|
|
target_temp1=variation.static_temp1,
|
|
target_temp2=variation.static_temp2,
|
|
tolerance_c=variation.temp_tolerance_c,
|
|
started_at_iso=datetime.now().isoformat(timespec="seconds"),
|
|
).save(args.session)
|
|
logger.info("Started CHANGE_CURRENT_LD1 and wrote session %s", args.session)
|
|
|
|
with ReadingWriter(args.readings) as writer:
|
|
monitor = LaserTemperatureMonitor(
|
|
controller=controller, writer=writer, period_s=period_s
|
|
)
|
|
logger.info("Monitoring to %s (period=%.3fs)", args.readings, period_s)
|
|
monitor.run(stop_event)
|
|
finally:
|
|
controller.disconnect()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|