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>
177 lines
6.1 KiB
Python
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 |