portrait_mode: added a loop button to rerun the queue for debugging
This commit is contained in:
+78
-30
@@ -359,7 +359,6 @@ class MainWindow(QMainWindow):
|
||||
self.compact_automation_panel.show_full_view_requested.connect(self._return_from_compact_automation_view)
|
||||
self.compact_automation_panel.annotation_selected.connect(self._handle_compact_annotation)
|
||||
|
||||
|
||||
self.tell_samples_dock = QDockWidget("Sample List", self)
|
||||
self.tell_samples_dock.setObjectName("tell_samples_dock")
|
||||
self.tell_samples_dock.setWidget(self.tell_samples)
|
||||
@@ -374,7 +373,6 @@ class MainWindow(QMainWindow):
|
||||
self.tabifyDockWidget(self.ref_tools_dock, self.tell_samples_dock)
|
||||
if self.__decoded_token.staff:
|
||||
self.ref_tools_dock.show()
|
||||
self.ref_tools_dock.raise_()
|
||||
else:
|
||||
self.ref_tools_dock.hide()
|
||||
|
||||
@@ -419,7 +417,6 @@ class MainWindow(QMainWindow):
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.fluor_panel_dock)
|
||||
self.fluor_panel_dock.hide()
|
||||
|
||||
# Create and add the dock to your main window
|
||||
self.log_dock = LogDock("Console Log", self)
|
||||
self.log_dock.setObjectName("log_dock")
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock)
|
||||
@@ -427,6 +424,8 @@ class MainWindow(QMainWindow):
|
||||
self.log_dock.attach_logger("aareGUI")
|
||||
self.log_dock.hide()
|
||||
|
||||
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
|
||||
|
||||
self.tabifyDockWidget(self.manual_sample_dock, self.automation_progress_dock)
|
||||
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
|
||||
|
||||
@@ -933,13 +932,18 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _show_samcam_feed_banner(self, message: str) -> None:
|
||||
self.__samcam_feed_banner_message = message
|
||||
self.alert_banner_secondary.show_message(message, True)
|
||||
self._show_runtime_notification(
|
||||
title="Sample camera",
|
||||
message=message,
|
||||
level="warning",
|
||||
sticky=True,
|
||||
)
|
||||
self.__samcam_feed_banner_active = True
|
||||
|
||||
def _clear_samcam_feed_banner(self) -> None:
|
||||
if not self.__samcam_feed_banner_active:
|
||||
return
|
||||
self.alert_banner_secondary.clear_message()
|
||||
self._clear_runtime_notification()
|
||||
self.__samcam_feed_banner_active = False
|
||||
|
||||
def _stop_axis_camera_threads(self) -> None:
|
||||
@@ -1183,19 +1187,31 @@ class MainWindow(QMainWindow):
|
||||
|
||||
@Slot(str, bool)
|
||||
def _portrait_alert_primary(self, msg: str, is_error: bool) -> None:
|
||||
"""Route primary alert banner — use in-panel toast in portrait mode."""
|
||||
"""Route primary alerts into the runtime dock."""
|
||||
if self.content_stack.currentWidget() is self.portrait_mode_page:
|
||||
self.portrait_mode_panel.show_portrait_alert(msg, is_error)
|
||||
else:
|
||||
self.alert_banner.show_message(msg, is_error)
|
||||
self._show_runtime_notification(
|
||||
title="Runtime alert" if is_error else "Runtime update",
|
||||
message=msg,
|
||||
level="error" if is_error else "success",
|
||||
sticky=is_error,
|
||||
auto_clear_ms=None if is_error else 4000,
|
||||
)
|
||||
|
||||
@Slot(str, bool)
|
||||
def _portrait_alert_secondary(self, msg: str, is_error: bool) -> None:
|
||||
"""Route secondary alert banner — use in-panel toast in portrait mode."""
|
||||
"""Route secondary alerts into the runtime dock."""
|
||||
if self.content_stack.currentWidget() is self.portrait_mode_page:
|
||||
self.portrait_mode_panel.show_portrait_alert(msg, is_error)
|
||||
else:
|
||||
self.alert_banner_secondary.show_message(msg, is_error)
|
||||
self._show_runtime_notification(
|
||||
title="Device alert" if is_error else "Device update",
|
||||
message=msg,
|
||||
level="warning" if is_error else "info",
|
||||
sticky=is_error,
|
||||
auto_clear_ms=None if is_error else 4000,
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def _refresh_portrait_queue_preview(self) -> None:
|
||||
@@ -1546,16 +1562,46 @@ class MainWindow(QMainWindow):
|
||||
self._dev_help_dialog.raise_()
|
||||
self._dev_help_dialog.activateWindow()
|
||||
|
||||
def _show_runtime_notification(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
message: str,
|
||||
level: str = "error",
|
||||
sticky: bool = True,
|
||||
auto_clear_ms: int | None = None,
|
||||
) -> None:
|
||||
self.log_dock.show_notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=level,
|
||||
sticky=sticky,
|
||||
auto_clear_ms=auto_clear_ms,
|
||||
)
|
||||
|
||||
def _show_runtime_waiting_notification(self, *, title: str, message: str) -> None:
|
||||
self.log_dock.show_waiting_notification(title=title, message=message)
|
||||
|
||||
def _clear_runtime_notification(self) -> None:
|
||||
self.log_dock.clear_notification()
|
||||
|
||||
def _clear_automation_critical_banner(self) -> None:
|
||||
if not self._automation_critical_banner_active:
|
||||
return
|
||||
self.alert_banner.clear_message()
|
||||
self._clear_runtime_notification()
|
||||
self._automation_critical_banner_active = False
|
||||
|
||||
@Slot(bool)
|
||||
def _on_automation_running_changed(self, running: bool) -> None:
|
||||
if running:
|
||||
self._clear_automation_critical_banner()
|
||||
self._show_runtime_notification(
|
||||
title="Automation running",
|
||||
message="Automation resumed.",
|
||||
level="success",
|
||||
sticky=False,
|
||||
auto_clear_ms=3500,
|
||||
)
|
||||
|
||||
def _is_detector_critical_failure(self, message: str) -> bool:
|
||||
text = (message or "").lower()
|
||||
@@ -1589,14 +1635,12 @@ class MainWindow(QMainWindow):
|
||||
def _on_manual_collection_critical_failure(self, message: str) -> None:
|
||||
logger.critical(f"Manual collection critical detector failure: {message}")
|
||||
|
||||
try:
|
||||
self.alert_banner_secondary.show_message(
|
||||
self._detector_error_banner_text(automation=False, message=message),
|
||||
True,
|
||||
auto_clear_ms=0,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self._show_runtime_notification(
|
||||
title="Collection paused",
|
||||
message=message,
|
||||
level="error",
|
||||
sticky=True,
|
||||
)
|
||||
|
||||
try:
|
||||
QMessageBox.critical(
|
||||
@@ -1674,14 +1718,12 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update automation progress after critical failure: {e}")
|
||||
|
||||
# 3. Banner so the operator sees it immediately
|
||||
try:
|
||||
self.alert_banner.show_message(
|
||||
banner_message, True, auto_clear_ms=0
|
||||
)
|
||||
self._automation_critical_banner_active = True
|
||||
except Exception:
|
||||
pass
|
||||
self._show_runtime_notification(
|
||||
title="Automation paused",
|
||||
message=message,
|
||||
level="error",
|
||||
sticky=True,
|
||||
)
|
||||
|
||||
# 4. Surface recovery UI
|
||||
try:
|
||||
@@ -1692,13 +1734,19 @@ class MainWindow(QMainWindow):
|
||||
self._detector_error_dialog_title(message),
|
||||
(
|
||||
"Automation has been stopped because there is an error with the detector.\n\n"
|
||||
"Please call your local contact.\n\n"
|
||||
f"Details:\n{message}"
|
||||
f"Details:\n{message}"
|
||||
),
|
||||
)
|
||||
self.show_local_contact("Detector")
|
||||
else:
|
||||
self.show_local_contact("Recovery")
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Automation halted",
|
||||
(
|
||||
"A critical error occurred during automation and the "
|
||||
"beamline could not recover automatically:\n\n"
|
||||
f"{message}\n\n"
|
||||
),
|
||||
)
|
||||
else:
|
||||
if is_detector_failure:
|
||||
QMessageBox.critical(
|
||||
|
||||
@@ -1,32 +1,239 @@
|
||||
# Python
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDockWidget, QPlainTextEdit
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QDockWidget,
|
||||
QPlainTextEdit,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QToolButton,
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger
|
||||
|
||||
|
||||
class RuntimeNotificationWidget(QFrame):
|
||||
cleared = Signal()
|
||||
show_log_requested = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("runtimeNotification")
|
||||
self.setProperty("noticeLevel", "error")
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setVisible(False)
|
||||
|
||||
self._auto_clear_timer = QTimer(self)
|
||||
self._auto_clear_timer.setSingleShot(True)
|
||||
self._auto_clear_timer.timeout.connect(self.clear_notification)
|
||||
|
||||
self._title = QLabel("", self)
|
||||
self._title.setObjectName("runtimeNotificationTitle")
|
||||
|
||||
self._message = QLabel("", self)
|
||||
self._message.setObjectName("runtimeNotificationMessage")
|
||||
self._message.setWordWrap(True)
|
||||
|
||||
self._minimise_button = QToolButton(self)
|
||||
self._minimise_button.setText("—")
|
||||
self._minimise_button.setToolTip("Minimise notification")
|
||||
self._minimise_button.clicked.connect(self._toggle_minimised)
|
||||
|
||||
self._clear_button = QPushButton("Clear", self)
|
||||
self._clear_button.clicked.connect(self.clear_notification)
|
||||
|
||||
self._show_log_button = QPushButton("Show Log", self)
|
||||
self._show_log_button.clicked.connect(self.show_log_requested.emit)
|
||||
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(self._title, 1)
|
||||
header_layout.addWidget(self._minimise_button)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self._show_log_button)
|
||||
button_layout.addWidget(self._clear_button)
|
||||
|
||||
self._body = QWidget(self)
|
||||
body_layout = QVBoxLayout(self._body)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
body_layout.setSpacing(8)
|
||||
body_layout.addWidget(self._message)
|
||||
body_layout.addLayout(button_layout)
|
||||
|
||||
root_layout = QVBoxLayout(self)
|
||||
root_layout.setContentsMargins(12, 10, 12, 10)
|
||||
root_layout.setSpacing(8)
|
||||
root_layout.addLayout(header_layout)
|
||||
root_layout.addWidget(self._body)
|
||||
|
||||
self._full_title = ""
|
||||
self._full_message = ""
|
||||
self._minimised = False
|
||||
self._sticky = True
|
||||
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QFrame#runtimeNotification {
|
||||
border: 1px solid #8a8a8a;
|
||||
border-radius: 8px;
|
||||
background-color: #fff4f4;
|
||||
}
|
||||
QFrame#runtimeNotification[noticeLevel="error"] {
|
||||
background-color: #fff1f1;
|
||||
border: 1px solid #d66;
|
||||
}
|
||||
QFrame#runtimeNotification[noticeLevel="warning"] {
|
||||
background-color: #fff8e8;
|
||||
border: 1px solid #d7aa42;
|
||||
}
|
||||
QFrame#runtimeNotification[noticeLevel="success"] {
|
||||
background-color: #eefaf0;
|
||||
border: 1px solid #6cb37a;
|
||||
}
|
||||
QFrame#runtimeNotification[noticeLevel="info"] {
|
||||
background-color: #eef5ff;
|
||||
border: 1px solid #6b9bd6;
|
||||
}
|
||||
QLabel#runtimeNotificationTitle {
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def _set_level(self, level: str) -> None:
|
||||
self.setProperty("noticeLevel", level)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
self.update()
|
||||
|
||||
@Slot()
|
||||
def _toggle_minimised(self) -> None:
|
||||
self._minimised = not self._minimised
|
||||
self._body.setVisible(not self._minimised)
|
||||
self._minimise_button.setText("+" if self._minimised else "—")
|
||||
if self._minimised:
|
||||
self._title.setText(self._full_title or "Notification")
|
||||
else:
|
||||
self._title.setText(self._full_title)
|
||||
|
||||
def show_notification(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
message: str,
|
||||
level: str = "error",
|
||||
sticky: bool = True,
|
||||
auto_clear_ms: int | None = None,
|
||||
) -> None:
|
||||
self._auto_clear_timer.stop()
|
||||
self._sticky = sticky
|
||||
self._full_title = title.strip() or "Notification"
|
||||
self._full_message = message.strip()
|
||||
|
||||
self._set_level(level)
|
||||
self._title.setText(self._full_title)
|
||||
self._message.setText(self._full_message)
|
||||
self._clear_button.setVisible(not sticky)
|
||||
self._body.setVisible(True)
|
||||
self._minimised = False
|
||||
self._minimise_button.setText("—")
|
||||
self.setVisible(True)
|
||||
|
||||
if not sticky:
|
||||
timeout = 5000 if auto_clear_ms is None else int(auto_clear_ms)
|
||||
self._auto_clear_timer.start(timeout)
|
||||
|
||||
def show_waiting(self, *, title: str, message: str, auto_clear_ms: int | None = None) -> None:
|
||||
self.show_notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level="warning",
|
||||
sticky=True,
|
||||
auto_clear_ms=auto_clear_ms,
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def clear_notification(self) -> None:
|
||||
self._auto_clear_timer.stop()
|
||||
self._full_title = ""
|
||||
self._full_message = ""
|
||||
self._title.clear()
|
||||
self._message.clear()
|
||||
self.setVisible(False)
|
||||
self.cleared.emit()
|
||||
|
||||
|
||||
class LogDock(QDockWidget):
|
||||
def __init__(self, title="Log", parent=None):
|
||||
super().__init__(title, parent)
|
||||
self.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
|
||||
self.view = QPlainTextEdit(self)
|
||||
self.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea)
|
||||
|
||||
self.container = QWidget(self)
|
||||
|
||||
self.notification = RuntimeNotificationWidget(self.container)
|
||||
self.notification.show_log_requested.connect(self._raise_and_focus_log)
|
||||
|
||||
self.view = QPlainTextEdit(self.container)
|
||||
self.view.setReadOnly(True)
|
||||
self.setWidget(self.view)
|
||||
|
||||
layout = QVBoxLayout(self.container)
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.setSpacing(6)
|
||||
layout.addWidget(self.notification)
|
||||
layout.addWidget(self.view, 1)
|
||||
|
||||
self.setWidget(self.container)
|
||||
|
||||
# connect emitter to append text safely from any thread
|
||||
self.emitter = QtLogEmitter()
|
||||
self.emitter.message.connect(self._append_line)
|
||||
|
||||
# logging handler
|
||||
self.handler = QtLogHandler(self.emitter)
|
||||
self.handler.setFormatter(find_existing_formatter())
|
||||
|
||||
def attach_logger(self, logger_name: str = "aareGUI"):
|
||||
attach_to_logger(logger_name, self.handler)
|
||||
|
||||
@Slot(str)
|
||||
def _append_line(self, text: str):
|
||||
self.view.appendPlainText(text)
|
||||
|
||||
@Slot()
|
||||
def _raise_and_focus_log(self) -> None:
|
||||
self.setVisible(True)
|
||||
self.raise_()
|
||||
self.view.setFocus()
|
||||
|
||||
def show_notification(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
message: str,
|
||||
level: str = "error",
|
||||
sticky: bool = True,
|
||||
auto_clear_ms: int | None = None,
|
||||
) -> None:
|
||||
self.setVisible(True)
|
||||
self.raise_()
|
||||
self.notification.show_notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=level,
|
||||
sticky=sticky,
|
||||
auto_clear_ms=auto_clear_ms,
|
||||
)
|
||||
|
||||
def show_waiting_notification(self, *, title: str, message: str) -> None:
|
||||
self.setVisible(True)
|
||||
self.raise_()
|
||||
self.notification.show_waiting(title=title, message=message)
|
||||
|
||||
def clear_notification(self) -> None:
|
||||
self.notification.clear_notification()
|
||||
|
||||
def clear(self):
|
||||
self.view.clear()
|
||||
@@ -9,9 +9,13 @@ from PySide6.QtGui import (
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QHBoxLayout, QLabel, QPushButton, QScrollArea,
|
||||
QSizePolicy, QStackedWidget, QVBoxLayout, QWidget,
|
||||
QSpinBox,
|
||||
)
|
||||
|
||||
from aare.common.automation_models import AutomationProgress, StepStatus, WorkflowStateKind
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger('aareGUI')
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colour palette (kept identical to gui_designer.py)
|
||||
@@ -249,6 +253,15 @@ class PortraitModePanel(QWidget):
|
||||
self._job_list_panel = None
|
||||
self._tell_samples = None
|
||||
self._is_running = False
|
||||
self._loop_enabled = False
|
||||
self._loop_remaining = 0
|
||||
self._loop_samples = []
|
||||
|
||||
self._loop_restart_timer = QTimer(self)
|
||||
self._loop_restart_timer.setInterval(1000)
|
||||
self._loop_restart_timer.timeout.connect(self._try_loop_restart)
|
||||
|
||||
self._loop_restart_deadline = None
|
||||
|
||||
# Pending alert messages received while in portrait mode
|
||||
# Each entry: (msg, is_error)
|
||||
@@ -349,13 +362,48 @@ class PortraitModePanel(QWidget):
|
||||
ctrl_frame.setStyleSheet(
|
||||
f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}"
|
||||
)
|
||||
|
||||
ctrl_layout = QHBoxLayout(ctrl_frame)
|
||||
ctrl_layout.setContentsMargins(16, 12, 16, 12)
|
||||
ctrl_layout.addStretch(1)
|
||||
ctrl_layout.setContentsMargins(12, 12, 12, 12)
|
||||
ctrl_layout.setSpacing(12)
|
||||
|
||||
ctrl_layout.addStretch()
|
||||
|
||||
self._play_pause_btn = PlayPauseButton()
|
||||
self._play_pause_btn.clicked.connect(self._on_play_pause_clicked)
|
||||
ctrl_layout.addWidget(self._play_pause_btn, alignment=Qt.AlignCenter)
|
||||
ctrl_layout.addStretch(1)
|
||||
ctrl_layout.addWidget(self._play_pause_btn)
|
||||
|
||||
# Loop arrow button
|
||||
self._loop_btn = QPushButton("↻")
|
||||
self._loop_btn.setFixedSize(52, 52)
|
||||
self._loop_btn.setCheckable(True)
|
||||
self._loop_btn.setToolTip("Loop queue")
|
||||
self._loop_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
border-radius: 26px;
|
||||
background: {BUTTON_BG};
|
||||
color: {ACCENT};
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background: {ACCENT};
|
||||
color: {BG};
|
||||
}}
|
||||
""")
|
||||
self._loop_btn.toggled.connect(self._toggle_loop)
|
||||
ctrl_layout.addWidget(self._loop_btn)
|
||||
|
||||
self._loop_count = QSpinBox()
|
||||
self._loop_count.setRange(1, 999)
|
||||
self._loop_count.setValue(2)
|
||||
self._loop_count.setPrefix("× ")
|
||||
self._loop_count.setFixedHeight(40)
|
||||
self._loop_count.setToolTip("Number of queue repeats")
|
||||
ctrl_layout.addWidget(self._loop_count)
|
||||
|
||||
ctrl_layout.addStretch()
|
||||
|
||||
layout.addWidget(ctrl_frame)
|
||||
|
||||
# "UP NEXT" header
|
||||
@@ -444,6 +492,13 @@ class PortraitModePanel(QWidget):
|
||||
self._job_list_panel = job_list_panel
|
||||
self._tell_samples = tell_samples
|
||||
|
||||
self._job_list_panel.loop_restart_requested = (
|
||||
self._restart_loop_if_needed
|
||||
)
|
||||
|
||||
self._populate_queue_from_tell_samples_if_empty()
|
||||
self.refresh_queue_preview()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public update slots
|
||||
# ------------------------------------------------------------------
|
||||
@@ -665,4 +720,126 @@ class PortraitModePanel(QWidget):
|
||||
last_msg, last_is_error = errors[-1]
|
||||
primary_banner.show_message(last_msg, last_is_error)
|
||||
self._pending_alerts.clear()
|
||||
self._dismiss_portrait_alert()
|
||||
self._dismiss_portrait_alert()
|
||||
|
||||
@Slot(bool)
|
||||
def _toggle_loop(self, enabled: bool) -> None:
|
||||
self._loop_enabled = enabled
|
||||
|
||||
if enabled and self._job_list_panel:
|
||||
self._loop_samples = list(
|
||||
self._job_list_panel.table_model.samples
|
||||
)
|
||||
self._loop_remaining = self._loop_count.value()
|
||||
else:
|
||||
self._loop_remaining = 0
|
||||
|
||||
def _try_loop_restart(self):
|
||||
"""
|
||||
Polls beamline state until safe to restart.
|
||||
"""
|
||||
|
||||
if self._job_list_panel is None:
|
||||
self._loop_restart_timer.stop()
|
||||
return
|
||||
|
||||
# Timeout protection
|
||||
if (
|
||||
self._loop_restart_deadline is not None
|
||||
and self._loop_restart_deadline.hasExpired()
|
||||
):
|
||||
self._loop_restart_timer.stop()
|
||||
|
||||
self._loop_enabled = False
|
||||
self._loop_btn.setChecked(False)
|
||||
|
||||
self.show_portrait_alert(
|
||||
"Loop stopped: beamline remained busy too long",
|
||||
True,
|
||||
)
|
||||
return
|
||||
|
||||
# Still busy, wait
|
||||
if getattr(
|
||||
self._job_list_panel,
|
||||
"_SampleQueuePanel__busy",
|
||||
False,
|
||||
):
|
||||
return
|
||||
|
||||
# Safe to restart
|
||||
self._loop_restart_timer.stop()
|
||||
|
||||
self._job_list_panel.run()
|
||||
|
||||
def _restart_loop_if_needed(self) -> bool:
|
||||
"""
|
||||
Restore queue and wait for beamline idle before restarting.
|
||||
Has timeout protection.
|
||||
"""
|
||||
|
||||
if not self._loop_enabled:
|
||||
return False
|
||||
|
||||
if not self._loop_samples:
|
||||
return False
|
||||
|
||||
if self._loop_remaining <= 0:
|
||||
return False
|
||||
|
||||
self._job_list_panel.queue_samples(
|
||||
list(self._loop_samples),
|
||||
replace=True,
|
||||
)
|
||||
|
||||
self._loop_remaining -= 1
|
||||
|
||||
# Start waiting for idle
|
||||
self._loop_restart_deadline = (
|
||||
QTimer().remainingTime()
|
||||
)
|
||||
|
||||
# 30 second safety timeout
|
||||
from PySide6.QtCore import QDeadlineTimer
|
||||
self._loop_restart_deadline = QDeadlineTimer(30000)
|
||||
|
||||
self._loop_restart_timer.start()
|
||||
|
||||
return True
|
||||
|
||||
def _populate_queue_from_tell_samples_if_empty(self) -> None:
|
||||
"""
|
||||
Fill the automation queue from TELL samples if no queue exists.
|
||||
Samples are ordered by physical sample position.
|
||||
"""
|
||||
|
||||
if self._job_list_panel is None or self._tell_samples is None:
|
||||
logger.info("Job list panel or tell_samples not ready")
|
||||
return
|
||||
|
||||
# Do not overwrite an existing queue
|
||||
if self._job_list_panel.table_model.samples:
|
||||
logger.info("Queue already exists, not populating from tell_samples")
|
||||
return
|
||||
|
||||
samples = list(
|
||||
getattr(self._tell_samples.table_model, "samples", [])
|
||||
)
|
||||
|
||||
ordered = sorted(
|
||||
[
|
||||
s for s in samples
|
||||
if getattr(s, "location", None) is not None
|
||||
],
|
||||
key=lambda s: (
|
||||
s.loc_str_sort()
|
||||
if hasattr(s, "loc_str_sort")
|
||||
else ""
|
||||
),
|
||||
)
|
||||
|
||||
if ordered:
|
||||
self._job_list_panel.queue_samples(
|
||||
ordered,
|
||||
replace=True,
|
||||
)
|
||||
@@ -37,6 +37,7 @@ class SampleQueuePanel(QFrame):
|
||||
self._step_through = False
|
||||
self.ring_current = None
|
||||
self._experiment_shutter_state = None
|
||||
self.loop_restart_requested = None
|
||||
|
||||
self.__recovery_timer = QTimer(self)
|
||||
self.__recovery_timer.setSingleShot(True)
|
||||
@@ -231,6 +232,15 @@ class SampleQueuePanel(QFrame):
|
||||
|
||||
def _finish_empty_queue(self):
|
||||
self.pause_automation()
|
||||
|
||||
if hasattr(self, "loop_restart_requested"):
|
||||
logger.info("Loop restart requested")
|
||||
try:
|
||||
if self.loop_restart_requested():
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
if self.park_and_dry_when_cleared.isChecked():
|
||||
logger.info("Automation queue empty; parking and drying TELL.")
|
||||
self.park_and_dry.emit()
|
||||
@@ -255,6 +265,7 @@ class SampleQueuePanel(QFrame):
|
||||
|
||||
if self.__pause:
|
||||
if self.__busy:
|
||||
logger.error(f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}")
|
||||
self.show_error_dialog(
|
||||
title="Beamline is busy",
|
||||
msg="Cannot run automation while beamline is busy",
|
||||
|
||||
Reference in New Issue
Block a user