compact_automation: making a simplified view for autoamtion.

This commit is contained in:
appleb_m
2026-06-09 11:20:29 +02:00
parent 376375673b
commit 46d76a5e0e
4 changed files with 286 additions and 20 deletions
+63 -5
View File
@@ -44,6 +44,7 @@ from aare.gui.panels.smargon_trace_panel import SmargonTracePanel
from aare.gui.panels.axis_video_panel import AxisVideoPanel
from aare.gui.panels.fluorescence_panel import FluorescencePanel
from aare.gui.panels.automation_panel import AutomationProgressWidget
from aare.gui.panels.compact_automation_panel import CompactAutomationPanel
#Scan Logic
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
@@ -271,10 +272,20 @@ class MainWindow(QMainWindow):
if cfg_get("gui.cameras.secondary_beamline_camera_url", None):
self.secondary_beamline_view = VideoGraphicsView()
self.secondary_beamline_view_panel = AxisVideoPanel("Secondary view", self.secondary_beamline_view, parent=top_widget)
self.secondary_beamline_view_panel = AxisVideoPanel("Secondary view", self.secondary_beamline_view,
parent=top_widget)
self.secondary_beamline_view_panel.refresh_requested.connect(self.refresh_axis_cameras)
self.video_tab.addTab(self.secondary_beamline_view_panel, "Secondary view")
self.compact_sample_camera = SampleCameraImageLabel(
geom=geom,
raster=self.raster,
parent=top_widget,
default_image=default_image,
)
self.compact_automation_panel = CompactAutomationPanel(self.compact_sample_camera, parent=top_widget)
self.video_tab.addTab(self.compact_automation_panel, "Automation")
top_widget_layout.addWidget(self.video_tab)
self._start_axis_camera_threads()
@@ -362,6 +373,11 @@ class MainWindow(QMainWindow):
self.tabifyDockWidget(self.manual_sample_dock, self.automation_progress_dock)
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
self.compact_automation_panel.play_pause_clicked.connect(self.job_list_panel.run)
self.compact_automation_panel.skip_clicked.connect(self.job_list_panel.skip_current_sample)
self.compact_automation_panel.step_through_toggled.connect(self.job_list_panel.set_step_through)
self.compact_automation_panel.show_full_view_requested.connect(self.restore_default_view)
self.job_list_panel.samples_in_queue_changed.connect(
self.automation_progress_panel.set_samples_in_queue
)
@@ -369,7 +385,13 @@ class MainWindow(QMainWindow):
self.automation_progress_panel.set_running
)
self.job_list_panel.automation_running_changed.connect(
self._on_automation_running_changed
self.compact_automation_panel.set_running
)
self.job_list_panel.step_through_changed.connect(
self.compact_automation_panel.set_step_through
)
self.job_list_panel.samples_in_queue_changed.connect(
self._refresh_compact_queue_preview
)
self.automation_progress_panel.set_samples_in_queue(
len(self.job_list_panel.table_model.samples)
@@ -521,19 +543,24 @@ class MainWindow(QMainWindow):
logger.debug(f"Starting prediction subscriber thread {sample_feed_addr}")
self.prediction_thread = PredictionSubscriber(pred_zmq_url=sample_feed_addr, topic=b"")
self.prediction_thread.image.connect(self.sample_camera.update_pixmap)
self.prediction_thread.image.connect(self.compact_sample_camera.update_pixmap)
self.prediction_thread.prediction.connect(self.sample_camera.update_detections)
self.prediction_thread.prediction.connect(self.compact_sample_camera.update_detections)
self.prediction_thread.prediction.connect(self.prediction_metrics_panel.update_from_prediction)
self.prediction_thread.target_point.connect(self.sample_camera.update_target_point)
self.prediction_thread.target_point.connect(self.compact_sample_camera.update_target_point)
self.prediction_thread.target_point.connect(self.target_stability_panel.update_target_point)
self.prediction_thread.focus_measure.connect(self.status_bar.update_sharpness)
self.prediction_thread.fps_measure.connect(self.status_bar.update_samcam_fps)
self.prediction_thread.camera_availability_changed.connect(self.sample_camera.set_camera_available)
self.prediction_thread.camera_availability_changed.connect(self.compact_sample_camera.set_camera_available)
self.prediction_thread.camera_availability_changed.connect(self._on_sample_camera_availability_changed)
self.prediction_thread.camera_error.connect(self._on_sample_camera_error)
self.prediction_thread.start()
else:
self.prediction_thread = None
self.sample_camera.set_camera_available(False)
self.compact_sample_camera.set_camera_available(False)
self._show_samcam_feed_banner("Sample camera feed unavailable: no stream configured")
#
@@ -627,6 +654,7 @@ class MainWindow(QMainWindow):
self.daq.update.connect(self.beamline.illumination_panel.update_daq_status)
self.daq.update.connect(self.raster.update_daq_status)
self.daq.update.connect(self.sample_camera.update_daq_status)
self.daq.update.connect(self.compact_sample_camera.update_daq_status)
self.daq.update.connect(self.tell_samples.update_daq_status)
self.daq.update.connect(self.ref_tools_panel.update_daq_status)
if self.prediction_thread is not None:
@@ -844,6 +872,30 @@ class MainWindow(QMainWindow):
self.gonio_camera_thread.frame_ready.connect(self.beamline_view_1_combined.update_frame)
self.gonio_camera_thread.start()
def _all_tell_samples_in_default_order(self) -> list:
samples = list(self.tell_samples.table_model.samples)
return sorted(
[sample for sample in samples if sample.location is not None],
key=lambda sample: sample.loc_str_sort(),
)
@Slot()
def enter_compact_automation_view(self) -> None:
self.job_list_panel.ensure_default_queue_from_samples(
self._all_tell_samples_in_default_order()
)
self._refresh_compact_queue_preview()
self.video_tab.setCurrentWidget(self.compact_automation_panel)
@Slot()
def _refresh_compact_queue_preview(self) -> None:
current_sample, next_sample, next_next_sample = self.job_list_panel.queue_preview()
self.compact_automation_panel.set_samples(
current_sample,
next_sample,
next_next_sample,
)
@Slot()
def refresh_axis_cameras(self) -> None:
logger.info("Refreshing Axis camera threads")
@@ -861,7 +913,6 @@ class MainWindow(QMainWindow):
# Main menu bar
menu_bar = self.menuBar()
# File menu
file_menu = menu_bar.addMenu("File")
quit_action = QAction("&Quit", self)
quit_action.setShortcut(QKeySequence.StandardKey.Quit)
@@ -981,6 +1032,11 @@ class MainWindow(QMainWindow):
beamline_combined_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(3))
view_menu.addAction(beamline_combined_tab_action)
compact_automation_action = QAction("Compact Automation View", self)
compact_automation_action.setShortcut(QKeySequence("Ctrl+5"))
compact_automation_action.triggered.connect(self.enter_compact_automation_view)
view_menu.addAction(compact_automation_action)
view_menu.addSeparator()
restore_default_view_action = QAction("Restore Default View", self)
@@ -1296,11 +1352,11 @@ class MainWindow(QMainWindow):
#TODO tidy up mount and sampel view fucntions
@Slot()
def mount_view(self):
self.video_tab.setCurrentIndex(3)
self.video_tab.setCurrentWidget(self.beamline_combined_panel)
@Slot()
def sample_view(self):
self.video_tab.setCurrentIndex(0)
self.video_tab.setCurrentWidget(self.sample_camera)
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
@@ -1349,6 +1405,8 @@ class MainWindow(QMainWindow):
if self._remote_close_deadline_ts is not None:
self._clear_remote_close_request()
self._refresh_compact_queue_preview()
if not self.__mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
self.__mounting = True
self.video_tab.setCurrentIndex(3)
+10 -9
View File
@@ -43,28 +43,28 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
def data(self, index, role=None):
if role == Qt.ItemDataRole.DisplayRole:
return get_entry(self.samples[index.row()], index.column())
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
elif role == Qt.ItemDataRole.TextAlignmentRole:
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
if index.row() == 0:
if self.__running:
return QBrush(QColor(255, 102, 0)) # Fluorescent orange
return QBrush(QColor(255, 102, 0))
else:
return QBrush(QColor(114, 159, 207)) # Dark blue
return QBrush(QColor(255, 255, 255)) # White
return None # For other roles, return None
return QBrush(QColor(114, 159, 207))
return QBrush(QColor(255, 255, 255))
return None
def headerData(self, section, orientation, role=None):
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal: # Column header
if orientation == Qt.Orientation.Horizontal:
return self.header[section] if self.header else f"Column {section + 1}"
if orientation == Qt.Orientation.Vertical: # Row header
return str(section + 1) # Row numbers start from 1
if orientation == Qt.Orientation.Vertical:
return str(section + 1)
return None
def updateData(self, samples: list[SampleShortInfo],):
self.beginResetModel()
self.samples = samples
self.samples = list(samples)
self.endResetModel()
def mimeTypes(self):
@@ -107,6 +107,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
def set_running(self, running: bool):
self.__running = running
self.layoutChanged.emit()
def remove_sample(self, db_id: int):
self.beginResetModel()
@@ -0,0 +1,141 @@
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtWidgets import (
QFrame,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QToolButton,
QMenu,
QWidget,
)
from aare.common.models import SampleShortInfo
class CompactAutomationPanel(QFrame):
play_pause_clicked = Signal()
skip_clicked = Signal()
step_through_toggled = Signal(bool)
annotation_selected = Signal(str)
show_full_view_requested = Signal()
ensure_default_queue_requested = Signal()
def __init__(self, camera_widget: QWidget, parent=None):
super().__init__(parent)
self._running = False
self._step_through = False
self._current_sample: SampleShortInfo | None = None
self._next_sample: SampleShortInfo | None = None
self._next_next_sample: SampleShortInfo | None = None
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setObjectName("compactAutomationPanel")
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(12, 12, 12, 12)
main_layout.setSpacing(10)
top_bar = QHBoxLayout()
top_bar.setSpacing(8)
self.menu_button = QToolButton(self)
self.menu_button.setText("")
self.menu_button.clicked.connect(self.show_full_view_requested.emit)
self.title_label = QLabel("Automation mode", self)
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
top_bar.addWidget(self.menu_button)
top_bar.addWidget(self.title_label, 1)
main_layout.addLayout(top_bar)
main_layout.addWidget(camera_widget, 1)
info_panel = QFrame(self)
info_layout = QVBoxLayout(info_panel)
info_layout.setContentsMargins(10, 10, 10, 10)
info_layout.setSpacing(6)
self.current_label = QLabel("Current: —", self)
self.next_label = QLabel("Next: —", self)
self.next_next_label = QLabel("Next next: —", self)
for label in (self.current_label, self.next_label, self.next_next_label):
label.setWordWrap(True)
info_layout.addWidget(self.current_label)
info_layout.addWidget(self.next_label)
info_layout.addWidget(self.next_next_label)
main_layout.addWidget(info_panel)
button_row = QHBoxLayout()
button_row.setSpacing(10)
self.play_pause_button = QPushButton("▶ Play", self)
self.play_pause_button.clicked.connect(self.play_pause_clicked.emit)
self.skip_button = QPushButton("⏭ Skip", self)
self.skip_button.clicked.connect(self.skip_clicked.emit)
self.step_button = QPushButton("Step Through", self)
self.step_button.setCheckable(True)
self.step_button.toggled.connect(self._on_step_toggled)
self.annotation_button = QToolButton(self)
self.annotation_button.setText("+")
self.annotation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
annotation_menu = QMenu(self.annotation_button)
for label in ["Heart", "Thumbs Up", "Thumbs Down", "Eyes", "Scan Again"]:
action = annotation_menu.addAction(label)
action.triggered.connect(
lambda checked=False, value=label: self.annotation_selected.emit(value)
)
self.annotation_button.setMenu(annotation_menu)
button_row.addWidget(self.play_pause_button, 1)
button_row.addWidget(self.skip_button, 1)
button_row.addWidget(self.step_button, 1)
button_row.addWidget(self.annotation_button)
main_layout.addLayout(button_row)
@Slot(bool)
def set_running(self, running: bool) -> None:
self._running = bool(running)
self.play_pause_button.setText("⏸ Pause" if self._running else "▶ Play")
@Slot(bool)
def set_step_through(self, enabled: bool) -> None:
self._step_through = bool(enabled)
self.step_button.blockSignals(True)
self.step_button.setChecked(self._step_through)
self.step_button.blockSignals(False)
@Slot(object, object, object)
def set_samples(
self,
current_sample: SampleShortInfo | None,
next_sample: SampleShortInfo | None,
next_next_sample: SampleShortInfo | None,
) -> None:
self._current_sample = current_sample
self._next_sample = next_sample
self._next_next_sample = next_next_sample
self.current_label.setText(f"Current: {self._format_sample(current_sample)}")
self.next_label.setText(f"Next: {self._format_sample(next_sample)}")
self.next_next_label.setText(f"Next next: {self._format_sample(next_next_sample)}")
def _on_step_toggled(self, checked: bool) -> None:
self._step_through = checked
self.step_through_toggled.emit(checked)
@staticmethod
def _format_sample(sample: SampleShortInfo | None) -> str:
if sample is None:
return ""
return f"{sample.sample_name} ({sample.loc_str()})"
+72 -6
View File
@@ -23,6 +23,7 @@ class SampleQueuePanel(QFrame):
viewer_track_online = Signal()
samples_in_queue_changed = Signal(int)
automation_running_changed = Signal(bool)
step_through_changed = Signal(bool)
def __init__(self, parent=None, samples: SampleShortInfoList | None = None):
super().__init__(parent)
@@ -32,6 +33,7 @@ class SampleQueuePanel(QFrame):
self.__pause = True
self.__set_to_pause = False
self._current_db_id: int | None = None
self._step_through = False
self.ring_current = None
self._experiment_shutter_state = None
@@ -100,6 +102,7 @@ class SampleQueuePanel(QFrame):
layout.addLayout(button_layout)
self._emit_samples_in_queue_changed()
self.automation_running_changed.emit(False)
self.step_through_changed.emit(False)
def _samples_left_in_queue(self) -> int:
total = len(self.table_model.samples)
@@ -110,6 +113,63 @@ class SampleQueuePanel(QFrame):
def _emit_samples_in_queue_changed(self) -> None:
self.samples_in_queue_changed.emit(self._samples_left_in_queue())
def set_step_through(self, enabled: bool) -> None:
self._step_through = bool(enabled)
self.step_through_changed.emit(self._step_through)
def is_step_through(self) -> bool:
return self._step_through
def queue_preview(self) -> tuple[SampleShortInfo | None, SampleShortInfo | None, SampleShortInfo | None]:
samples = self.table_model.samples
current = samples[0] if len(samples) > 0 else None
next_sample = samples[1] if len(samples) > 1 else None
next_next_sample = samples[2] if len(samples) > 2 else None
return current, next_sample, next_next_sample
def queue_samples(self, samples: list[SampleShortInfo], replace: bool = True) -> None:
if replace:
self.table_model.updateData(list(samples))
else:
merged = list(self.table_model.samples)
existing_ids = {sample.db_id for sample in merged}
for sample in samples:
if sample.db_id not in existing_ids:
merged.append(sample)
self.table_model.updateData(merged)
self._emit_samples_in_queue_changed()
def ensure_default_queue_from_samples(self, samples: list[SampleShortInfo]) -> None:
if len(self.table_model.samples) > 0:
return
ordered = sorted(
[sample for sample in samples if sample.location is not None],
key=lambda sample: sample.loc_str_sort(),
)
self.queue_samples(ordered, replace=True)
def skip_current_sample(self) -> None:
if not self.table_model.samples:
return
current = self.table_model.samples[0]
self.table_model.remove_sample(current.db_id)
self._current_db_id = None
if not self.__pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
if self._step_through:
self.pause_automation(set_id_to_None=False)
else:
self._current_db_id = next_item.db_id
self._emit_samples_in_queue_changed()
self.auto_scan.emit(next_item)
else:
self._finish_empty_queue()
self._emit_samples_in_queue_changed()
def remove_selected_samples(self):
"""Remove selected samples from the queue."""
selected_indexes = self.table_view.selectionModel().selectedRows()
@@ -328,9 +388,12 @@ class SampleQueuePanel(QFrame):
if not self.__pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
self._current_db_id = next_item.db_id
self._emit_samples_in_queue_changed()
self.auto_scan.emit(next_item)
if self._step_through:
self.pause_automation(set_id_to_None=False)
else:
self._current_db_id = next_item.db_id
self._emit_samples_in_queue_changed()
self.auto_scan.emit(next_item)
else:
self._finish_empty_queue()
@@ -365,9 +428,12 @@ class SampleQueuePanel(QFrame):
# Missing or other errors -> continue to next sample
if not self.__pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
self._current_db_id = next_item.db_id
self._emit_samples_in_queue_changed()
self.auto_scan.emit(next_item)
if self._step_through:
self.pause_automation(set_id_to_None=False)
else:
self._current_db_id = next_item.db_id
self._emit_samples_in_queue_changed()
self.auto_scan.emit(next_item)
else:
self._finish_empty_queue()