GUI: added protection against smargon and raster moves while camera is off
This commit is contained in:
@@ -82,6 +82,8 @@ class MainWindow(QMainWindow):
|
||||
self.__base_url = base_url
|
||||
self.__token = token
|
||||
self.__mounting = False
|
||||
self.__samcam_feed_banner_active = False
|
||||
self.__samcam_feed_banner_message = "Sample camera feed unavailable"
|
||||
self._dev_help_dialog = None
|
||||
self._beamline_recovery_dialog = None
|
||||
self._controls_help_dialog = None
|
||||
@@ -388,8 +390,13 @@ class MainWindow(QMainWindow):
|
||||
self.camera_thread.start()
|
||||
self.camera_thread.focus_measure.connect(self.status_bar.update_sharpness)
|
||||
self.camera_thread.fps_measure.connect(self.status_bar.update_samcam_fps)
|
||||
self.camera_thread.camera_availability_changed.connect(self.sample_camera.set_camera_available)
|
||||
self.camera_thread.camera_availability_changed.connect(self._on_sample_camera_availability_changed)
|
||||
self.camera_thread.camera_error.connect(self._on_sample_camera_error)
|
||||
else:
|
||||
self.camera_thread = None
|
||||
self.sample_camera.set_camera_available(False)
|
||||
self._show_samcam_feed_banner("Sample camera feed unavailable: no stream configured")
|
||||
|
||||
# Prediction subscriber thread
|
||||
if pred_zmq_addr is not None:
|
||||
@@ -499,7 +506,8 @@ class MainWindow(QMainWindow):
|
||||
self.daq.update.connect(self.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)
|
||||
self.daq.update.connect(self.camera_thread.update_daq_status)
|
||||
if self.camera_thread is not None:
|
||||
self.daq.update.connect(self.camera_thread.update_daq_status)
|
||||
|
||||
if self.__decoded_token.staff:
|
||||
self.daq.update.connect(self.beamline.beam_size.update_daq_status)
|
||||
@@ -612,6 +620,30 @@ class MainWindow(QMainWindow):
|
||||
age_s = time.monotonic() - self._last_pred_image_ts
|
||||
self._pred_is_preferred = age_s <= self._pred_preferred_timeout_s
|
||||
|
||||
@Slot(bool)
|
||||
def _on_sample_camera_availability_changed(self, available: bool) -> None:
|
||||
if available:
|
||||
self._clear_samcam_feed_banner()
|
||||
return
|
||||
|
||||
self._show_samcam_feed_banner("Sample camera feed unavailable")
|
||||
|
||||
@Slot(str)
|
||||
def _on_sample_camera_error(self, message: str) -> None:
|
||||
logger.warning(message)
|
||||
self._show_samcam_feed_banner(message or "Sample camera feed unavailable")
|
||||
|
||||
def _show_samcam_feed_banner(self, message: str) -> None:
|
||||
self.__samcam_feed_banner_message = message
|
||||
self.alert_banner_secondary.show_message(message, 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.__samcam_feed_banner_active = False
|
||||
|
||||
def start_text_tutorial(self) -> None:
|
||||
self.tutorial_manager.start("manual_workflow_demo")
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ class SampleCameraThread(QThread):
|
||||
camera_image = Signal(QPixmap)
|
||||
focus_measure = Signal(float) # Emits Laplacian variance (higher = sharper)
|
||||
fps_measure = Signal(float)
|
||||
camera_availability_changed = Signal(bool)
|
||||
camera_error = Signal(str)
|
||||
|
||||
def __init__(self, zmq_url: str, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -38,6 +40,21 @@ class SampleCameraThread(QThread):
|
||||
self.__last_frame_time = None
|
||||
self.__no_frame_timeout_s = 5.0
|
||||
|
||||
self.__camera_available = False
|
||||
self.__last_camera_error: str | None = None
|
||||
|
||||
def __set_camera_available(self, available: bool, error: str | None = None) -> None:
|
||||
if available != self.__camera_available:
|
||||
self.__camera_available = available
|
||||
self.camera_availability_changed.emit(available)
|
||||
|
||||
if error is not None and error != self.__last_camera_error:
|
||||
self.__last_camera_error = error
|
||||
self.camera_error.emit(error)
|
||||
|
||||
if available:
|
||||
self.__last_camera_error = None
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.__beam_x = s.geom.beam_location_pxl.x
|
||||
@@ -89,7 +106,7 @@ class SampleCameraThread(QThread):
|
||||
encoded = np.frombuffer(data, dtype=np.uint8)
|
||||
bgr = cv2.imdecode(encoded, cv2.IMREAD_COLOR)
|
||||
if bgr is None:
|
||||
print("Failed to decode JPEG image from zmq stream")
|
||||
self.__set_camera_available(False, "Sample camera feed unavailable: failed to decode JPEG frame")
|
||||
continue
|
||||
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
||||
elif "shape" in header:
|
||||
@@ -97,9 +114,11 @@ class SampleCameraThread(QThread):
|
||||
raw = np.frombuffer(data, np.uint8).reshape((h, w))
|
||||
rgb = cv2.cvtColor(raw, cv2.COLOR_BAYER_GB2RGB)
|
||||
else:
|
||||
print(f"Unsupported header in zmq stream: {header}")
|
||||
self.__set_camera_available(False, f"Sample camera feed unavailable: unsupported frame header {header}")
|
||||
continue
|
||||
|
||||
self.__set_camera_available(True)
|
||||
|
||||
if self.__measure_focus:
|
||||
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
||||
if self.__focus_mask is None or self.__focus_mask.shape != gray.shape:
|
||||
@@ -114,19 +133,24 @@ class SampleCameraThread(QThread):
|
||||
qimage = QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888).copy()
|
||||
self.camera_image.emit(QPixmap.fromImage(qimage))
|
||||
else:
|
||||
print("NO HEADER in zmq stream")
|
||||
self.__set_camera_available(False, "Sample camera feed unavailable: no frame header in zmq stream")
|
||||
except zmq.Again: # Timeout occurred
|
||||
now = time.perf_counter()
|
||||
elapsed = now - self.__fps_window_start
|
||||
if elapsed >= self.__fps_emit_period_s:
|
||||
last = self.__fps_frame_count
|
||||
no_frames_long = (last is None) or ((now - last) >= self.__no_frame_timeout_s)
|
||||
no_frames_long = (
|
||||
self.__last_frame_time is None
|
||||
or (now - self.__last_frame_time) >= self.__no_frame_timeout_s
|
||||
)
|
||||
self.fps_measure.emit(float("nan") if no_frames_long else 0.0)
|
||||
self.__fps_window_start = now
|
||||
self.__fps_frame_count = 0
|
||||
|
||||
if no_frames_long:
|
||||
self.__set_camera_available(False, "Sample camera feed unavailable")
|
||||
continue # Check self.running again
|
||||
except Exception as e:
|
||||
print(f"Error in sample camera thread {e}")
|
||||
self.__set_camera_available(False, f"Sample camera feed unavailable: {e}")
|
||||
|
||||
def stop(self):
|
||||
# Signal the thread to stop
|
||||
|
||||
@@ -72,6 +72,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__session_state: SessionsStateEnum | None = None
|
||||
self.__sam_cam = SampleCameraSettings(exposure=0.1, gain=100.0)
|
||||
self.__is_daq_busy = False
|
||||
self.__camera_available = True
|
||||
|
||||
self.__geom = geom
|
||||
self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList()
|
||||
@@ -91,6 +92,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.setScene(self.scene)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
self.__update_camera_interaction_feedback()
|
||||
|
||||
if default_image is not None:
|
||||
pixmap = QPixmap(default_image)
|
||||
@@ -125,6 +127,30 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__detections = [] # list of dicts from publisher
|
||||
self.__det_shape = None # shape from payload [h,w] so we can scale
|
||||
|
||||
def __update_camera_interaction_feedback(self) -> None:
|
||||
if self.__camera_available:
|
||||
self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.setToolTip("")
|
||||
else:
|
||||
self.viewport().setCursor(Qt.CursorShape.ForbiddenCursor)
|
||||
self.setToolTip("Sample camera feed unavailable")
|
||||
|
||||
def __show_camera_unavailable_tooltip(self, event, action: str = "Sample camera interaction") -> None:
|
||||
QToolTip.showText(
|
||||
self.mapToGlobal(event.pos()),
|
||||
f"{action} disabled: sample camera feed unavailable",
|
||||
self,
|
||||
)
|
||||
|
||||
def __camera_interaction_enabled(self) -> bool:
|
||||
return self.__camera_available
|
||||
|
||||
@Slot(bool)
|
||||
def set_camera_available(self, available: bool):
|
||||
self.__camera_available = available
|
||||
self.__update_camera_interaction_feedback()
|
||||
self.update()
|
||||
|
||||
@Slot(dict)
|
||||
def update_detections(self, payload: dict):
|
||||
# payload: { 'time', 'frame_id', 'shape':[h,w], 'boxes':[{'x1',...,'label','conf'}] }
|
||||
@@ -227,6 +253,42 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
|
||||
painter.restore()
|
||||
|
||||
def __draw_camera_unavailable_overlay(self, painter: QPainter):
|
||||
if self.__camera_available:
|
||||
return
|
||||
|
||||
painter.save()
|
||||
painter.resetTransform()
|
||||
|
||||
font = QFont()
|
||||
font.setPointSize(22)
|
||||
font.setBold(True)
|
||||
painter.setFont(font)
|
||||
|
||||
text = "Sample camera feed unavailable"
|
||||
fm = QFontMetrics(font)
|
||||
text_rect = fm.boundingRect(text)
|
||||
padding = 16
|
||||
|
||||
position_x = 50
|
||||
position_y = 120
|
||||
|
||||
bg_rect = QRect(
|
||||
position_x - padding,
|
||||
position_y - padding,
|
||||
text_rect.width() + 2 * padding,
|
||||
text_rect.height() + 2 * padding,
|
||||
)
|
||||
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 220), 2))
|
||||
painter.setBrush(QColor(180, 60, 0, 180))
|
||||
painter.drawRoundedRect(bg_rect, 10, 10)
|
||||
|
||||
painter.setPen(QPen(QColor(255, 255, 255)))
|
||||
painter.drawText(QPoint(position_x, position_y + fm.ascent()), text)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def drawForeground(self, painter, rect):
|
||||
self.__draw_ml_bounding_box(painter)
|
||||
self.__draw_beam_center(painter)
|
||||
@@ -234,6 +296,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__draw_helical(painter)
|
||||
self.__draw_busy_overlay(painter)
|
||||
self.__draw_session_overlay(painter)
|
||||
self.__draw_camera_unavailable_overlay(painter)
|
||||
self.__draw_detections(painter, rect)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
@@ -241,6 +304,12 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__scaling()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if not self.__camera_interaction_enabled():
|
||||
if event.button() in (Qt.MouseButton.LeftButton, Qt.MouseButton.RightButton):
|
||||
self.__show_camera_unavailable_tooltip(event)
|
||||
event.accept()
|
||||
return
|
||||
|
||||
self.start_point = self.mapToScene(event.pos())
|
||||
|
||||
match self.__state:
|
||||
@@ -272,6 +341,9 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__raster_mgr.resize_active_grid(self.end_point)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if not self.__camera_interaction_enabled():
|
||||
return
|
||||
|
||||
mouse_pos = self.mapToScene(event.pos())
|
||||
|
||||
if self.__state == SampleCameraImageState.IDLE:
|
||||
@@ -299,12 +371,24 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.update()
|
||||
|
||||
def __on_raster_timer_timeout(self):
|
||||
if self.__pending_load_pos is not None:
|
||||
self.load_image.emit(self.__pending_load_pos)
|
||||
self.__pending_load_pos = None
|
||||
return
|
||||
|
||||
if self.__pending_load_pos is not None:
|
||||
self.load_image.emit(self.__pending_load_pos)
|
||||
self.__pending_load_pos = None
|
||||
self.raster_timer.start(self.raster_timer_interval)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if not self.__camera_interaction_enabled():
|
||||
self.click_timer.stop()
|
||||
if self.__state != SampleCameraImageState.BEAM_MARKING:
|
||||
self.__state = SampleCameraImageState.IDLE
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
if self.click_timer.isActive():
|
||||
self.click_timer.stop()
|
||||
@@ -323,6 +407,9 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self.__state = SampleCameraImageState.IDLE
|
||||
|
||||
def __right_click_menu(self, event):
|
||||
if not self.__camera_interaction_enabled():
|
||||
self.__show_camera_unavailable_tooltip(event)
|
||||
return
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
@@ -373,7 +460,6 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
elif action == grab_with_overlay_action:
|
||||
self.__screenshot_with_dialog(overlay=True)
|
||||
elif action == autofocus_action:
|
||||
c = self.mapToScene(event.pos())
|
||||
self.autofocus.emit(AutofocusSettings(center_x_pxl=None, center_y_pxl=None,
|
||||
radius_pxl=30,
|
||||
z_range_um=2000,
|
||||
@@ -471,6 +557,11 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
|
||||
|
||||
def __left_single_click(self, event):
|
||||
if not self.__camera_interaction_enabled():
|
||||
logger.info("Ignoring click because sample camera feed is unavailable")
|
||||
self.__show_camera_unavailable_tooltip(event, "Point-on-click")
|
||||
return
|
||||
|
||||
match self.__state:
|
||||
case SampleCameraImageState.BEAM_MARKING:
|
||||
if event.button() == Qt.MouseButton.LeftButton and event.modifiers() & Qt.KeyboardModifier.ShiftModifier:
|
||||
@@ -521,6 +612,8 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
'loop_all': QColor('green'),
|
||||
'loop_face': QColor('yellow'),
|
||||
'crystal': QColor('blue'),
|
||||
'needle': QColor('magenta'),
|
||||
'ice': QColor('cyan'),
|
||||
}
|
||||
|
||||
for det in self.__detections:
|
||||
@@ -666,7 +759,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
mouse_scene_pos = self.mapToScene(mouse_view_pos)
|
||||
|
||||
if event.key() == Qt.Key.Key_Shift:
|
||||
if self.__state == SampleCameraImageState.IDLE:
|
||||
if self.__state == SampleCameraImageState.IDLE and self.__camera_interaction_enabled():
|
||||
if not self.raster_timer.isActive():
|
||||
self.load_image.emit(mouse_scene_pos)
|
||||
self.raster_timer.start(self.raster_timer_interval)
|
||||
|
||||
Reference in New Issue
Block a user