compact_automation: making a simplified view for autoamtion.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()})"
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user