Independent monitor + checker for CHANGE_CURRENT_LD1 variation

This commit is contained in:
awe
2026-07-27 16:24:58 +03:00
parent 3efe968dd1
commit 52c1218bf7
12 changed files with 1248 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
"""Standalone laser temperature checker process.
Reads the target setpoints frozen at variation start and tails the JSONL readings
channel produced by the monitor. For every reading it validates both lasers and
prints a console warning whenever a measured temperature drifts from its target by
more than the tolerance (default 0.03 °C). Never touches the serial port, so it is
fully independent of the monitor and can be started/stopped at any time.
Example::
python -m python_app.scripts.laser_temp_checker
python -m python_app.scripts.laser_temp_checker --session s.json --readings r.jsonl
"""
from __future__ import annotations
import argparse
import logging
import signal
import threading
from pathlib import Path
from python_app.hardware_full.laser_control.monitoring import (
DEFAULT_READINGS_PATH,
DEFAULT_SESSION_PATH,
LaserTemperatureChecker,
LaserVariationSession,
ReadingReader,
)
logger = logging.getLogger("laser_temp_checker")
_POLL_INTERVAL_S = 0.2
def main() -> int:
parser = argparse.ArgumentParser(description="Validate laser temperature against setpoints")
parser.add_argument("--session", type=Path, default=DEFAULT_SESSION_PATH,
help="Session snapshot with target setpoints + tolerance")
parser.add_argument("--readings", type=Path, default=DEFAULT_READINGS_PATH,
help="JSONL readings channel to tail")
parser.add_argument("--tolerance", type=float, default=None,
help="Override tolerance in °C (default: from session)")
parser.add_argument("--reminder-every", type=int, default=0,
help="Repeat a warning every N readings while off target (0=off)")
parser.add_argument("--from-start", action="store_true",
help="Validate the whole readings file, not just new lines")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
session = LaserVariationSession.load(args.session)
if args.tolerance is not None:
session.tolerance_c = args.tolerance
checker = LaserTemperatureChecker.from_session(session, reminder_every=args.reminder_every)
logger.info(
"Checking against T1=%.3f T2=%.3f °C, tolerance ±%.3f °C (%s)",
session.target_temp1, session.target_temp2, session.tolerance_c, session.variation_type,
)
reader = ReadingReader(args.readings, from_start=args.from_start)
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)
while not stop_event.is_set():
for reading in reader.poll():
checker.process(reading)
stop_event.wait(_POLL_INTERVAL_S)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+142
View File
@@ -0,0 +1,142 @@
"""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.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,
)
controller.connect()
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())