Files
AareDAQ/src/aare/gui/widgets/message_box.py
T
appleb_mandClaude Opus 4.8 47e05dc28c GUI/DAQ: hutch-safety gating, error pop-ups, and automation pause-and-wait
Fix the GUI pop-up path and add personnel-safety-system (PSS) gating so
door-open / beam-down / shutter-closed conditions are surfaced and acted on.

- exception pop-ups: connect the previously-orphaned http_error signal;
  failed user operations now raise a modal dialog, background/polling errors
  a non-modal banner.
- PSS device (devices/pss_state.py) reading EH1-PSYS PROHIBITED-STATE /
  ALARM-STATE; new critical DoorSafetyError + DOOR_SAFETY_ERROR code.
- mounting service blocks mount/unmount when the hutch is not prohibited or
  an alarm is active; /status now publishes pss_prohibited / pss_alarm.
- GUI blocks manual mount/unmount and the automation Run button immediately
  (pop-up) on door-open, and shows a warning banner while an alarm is active.
- centralise per-action precondition checks (ring current, safety shutter,
  hutch door) into one combined "continue?" dialog with a session-global
  "don't ask again for 1 hour" snooze, applied to all data-collection buttons.
- live automation pauses and auto-resumes on bad conditions (beam, shutter,
  door, robot) with continue-now / stop overrides, gated by a default-on
  "Pause on bad conditions" checkbox replacing the dead CHECK_ENABLED constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:30:08 +02:00

177 lines
6.1 KiB
Python

import time
from PySide6.QtCore import QTimer, QEventLoop
from PySide6.QtWidgets import QMessageBox, QCheckBox
from aare.common.logger_config import setup_logger
logger = setup_logger("aareGUI")
LOW_CURRENT_THRESHOLD = 100.0
SNOOZE_SECONDS = 3600.0 # "Don't ask again for 1 hour"
class _PreconditionSnooze:
"""Session-global 'don't ask me again for 1 hour' for precondition prompts.
In-memory only, so it auto-clears when the GUI restarts.
"""
def __init__(self):
self._until = 0.0
def active(self) -> bool:
return time.monotonic() < self._until
def snooze(self) -> None:
self._until = time.monotonic() + SNOOZE_SECONDS
logger.info("Precondition prompts snoozed for 1 hour")
def clear(self) -> None:
self._until = 0.0
# Shared across all panels for the session.
precondition_snooze = _PreconditionSnooze()
def precondition_problems(ring_current, shutter_open, door_prohibited) -> list[str]:
"""List the beamline conditions that are currently not satisfied."""
problems: list[str] = []
if ring_current is None:
problems.append("Ring current is unknown.")
elif ring_current < LOW_CURRENT_THRESHOLD:
problems.append(f"Ring current is low: {round(ring_current, 2)} mA.")
if not shutter_open:
problems.append("Experiment safety shutter is closed.")
if door_prohibited is False:
problems.append("Hutch is not in the prohibited state (doors open / not searched).")
return problems
def precondition_check(parent, *, ring_current, shutter_open, door_prohibited) -> bool:
"""Combined ring current / safety shutter / hutch door pre-action check.
Shows ONE 'Do you wish to continue?' dialog listing every failing
condition, with a 'Don't ask me again for 1 hour' checkbox. Returns True to
proceed. Honours the session-global snooze and never blocks outright.
"""
if precondition_snooze.active():
return True
problems = precondition_problems(ring_current, shutter_open, door_prohibited)
if not problems:
return True
box = QMessageBox(parent)
box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Beamline not ready")
box.setText("\n".join(problems) + "\n\nDo you wish to continue?")
box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
box.setDefaultButton(QMessageBox.StandardButton.No)
snooze_cb = QCheckBox("Don't ask me again for 1 hour")
box.setCheckBox(snooze_cb)
proceed = box.exec() == QMessageBox.StandardButton.Yes
if proceed and snooze_cb.isChecked():
precondition_snooze.snooze()
return proceed
def reply_box(parent, title: str = "Warning", msg: str = "Warning." ):
return QMessageBox.question(
parent,
title,
f"{msg} Do you wish to continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
def timer_box(parent, title: str = "Warning", msg: str = "Warning.", condition_func=None) -> QMessageBox:
box = QMessageBox(parent)
box.setWindowTitle(title)
box.setText(f"{msg} Do you wish to continue?")
box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
box.show()
timer = QTimer(box)
timer.setInterval(300)
def stop_timer():
if timer.isActive():
timer.stop()
def check():
if not box.isVisible():
stop_timer()
return
try:
if condition_func and condition_func():
# Auto-accept (as if user pressed Yes)
box.done(QMessageBox.StandardButton.Yes)
stop_timer()
except Exception as e:
# Never let errors in condition_func block UI
logger.error(f"Error in condition_func: {e}")
box.accepted.connect(stop_timer)
box.rejected.connect(stop_timer)
timer.timeout.connect(check)
timer.start()
return box
def ring_current_low_check(parent, ring_current) -> bool:
if ring_current is None:
logger.debug("Ring current: unknown")
msg = "Ring current: unknown."
too_low = True
else:
too_low = ring_current < LOW_CURRENT_THRESHOLD
if too_low:
rc = round(ring_current, 2)
logger.debug(f"Ring current low {rc} mA")
msg = f"Ring current is low: {rc} mA."
else:
logger.debug(f"Ring current: {round(ring_current, 2)} mA")
return True
# Ask user only when unknown or too low
reply = reply_box(parent, title="Ring current too low", msg=msg)
return reply == QMessageBox.StandardButton.Yes
def experiment_hutch_shutter_check(parent, shutter_state) -> bool:
if shutter_state:
logger.debug("Experiment shutter open")
return True
else:
reply = reply_box(
parent,
title="Experiment shutter open",
msg=f"Experiment shutter is Closed."
)
return reply == QMessageBox.StandardButton.Yes
def ring_current_auto_check(parent, ring_current, check_func) -> bool:
msg = "Ring current: unknown." if ring_current is None else f"Ring current is low {round(ring_current, 2)} mA."
return conditions_auto_check(parent, msg, check_func, title="Ring current too low")
def conditions_auto_check(parent, msg: str, check_func, title: str = "Beamline not ready") -> bool:
"""Pause-and-wait dialog that auto-resumes when ``check_func()`` becomes True.
Used to hold automation between samples until the beamline recovers. The
box auto-accepts (resume) as soon as the condition is satisfied; the user
can also click 'Yes' to continue now (override) or 'No' to stop automation.
Returns True to resume, False to stop.
"""
box = timer_box(parent, title=title, msg=msg, condition_func=check_func)
loop = QEventLoop()
def finish(_=None):
if loop.isRunning():
loop.quit()
box.finished.connect(finish)
box.show()
loop.exec() # returns as soon as box finishes without blocking UI processing of its signals
result = box.result()
return result == QMessageBox.StandardButton.Yes