From b13e80b9165fbd8024be3e1da0d4065de28c001d Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Tue, 8 Sep 2026 09:37:55 -0300 Subject: [PATCH] fix(vision): VisionTuning preview blank until camera switched, silent Apply failures Two real bugs found interactively: - The preview stayed blank after opening the widget or switching devices, and only appeared after switching camera away and back. Root cause: bec_widgets' Image.image(device=, signal=) subscribes via bec_dispatcher.connect_slot with no newest_only=True (confirmed by reading image.py directly), so it only catches publishes that happen *after* the Redis subscription is actually established -- the immediate refresh() right after subscribing could race it and be silently missed. A deferred refresh (QTimer.singleShot(200, ...)) after .image() reliably lands after the subscription is live; harmless since refresh() just republishes the same last frame. - Apply Edge Params appeared to do nothing on an invalid value (e.g. an even blur_ksize, easy to land on since the spin box steps through every integer) -- set_edge_params()'s VisionInterfaceError was only logger.exception()'d, never shown in the GUI. Added a status label so Apply/Refresh failures are visible instead of indistinguishable from a stalled widget. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015fDXnPuNvm3qUsUb4V4Gfo --- .../widgets/vision_tuning/vision_tuning.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/vision_tuning/vision_tuning.py b/csaxs_bec/bec_widgets/widgets/vision_tuning/vision_tuning.py index b6dd97a9..7d8b7e09 100644 --- a/csaxs_bec/bec_widgets/widgets/vision_tuning/vision_tuning.py +++ b/csaxs_bec/bec_widgets/widgets/vision_tuning/vision_tuning.py @@ -140,6 +140,13 @@ class VisionTuning(BECWidget, QWidget): control_row.addStretch() root.addLayout(control_row) + # Surfaces the last error from an Apply/Refresh action -- e.g. an even blur_ksize rejected + # by set_edge_params() -- instead of only logging it (a click that silently does nothing + # is indistinguishable from a bug from the operator's side). + self.status_label = QLabel("", parent=self) + self.status_label.setStyleSheet("color: #c0392b;") + root.addWidget(self.status_label) + self.image = Image(parent=self) root.addWidget(self.image, stretch=1) @@ -187,6 +194,14 @@ class VisionTuning(BECWidget, QWidget): self.camera_combo.blockSignals(False) if device_name: self.image.image(device=device_name, signal="diagnostic_image") + # The Image widget's subscription (bec_dispatcher.connect_slot, no newest_only) only + # catches publishes that happen *after* the Redis subscription is actually established + # -- a refresh() published immediately after .image() above can race it and be missed, + # leaving the preview blank until some later publish. A short deferred refresh, on top + # of the immediate one _on_camera_changed() triggers below, reliably lands after the + # subscription is live -- cheap and harmless since refresh() just republishes the same + # last frame. + QTimer.singleShot(200, self._on_refresh_clicked) if interface is not None: edge_params = interface.get_edge_params() for spin_box, key in ( @@ -267,8 +282,8 @@ class VisionTuning(BECWidget, QWidget): interface.set_edge_params( self.edge_low.value(), self.edge_high.value(), self.edge_blur.value() ) - except Exception: - logger.exception("VisionTuning: set_edge_params failed") + except Exception as exc: + self._set_status(f"set_edge_params failed: {exc}") return self._on_refresh_clicked() @@ -284,8 +299,14 @@ class VisionTuning(BECWidget, QWidget): return try: interface.refresh(self._current_camera_name) - except Exception: - logger.exception("VisionTuning: refresh failed") + except Exception as exc: + self._set_status(f"refresh failed: {exc}") + else: + self._set_status("") + + def _set_status(self, text: str): + logger.info(f"VisionTuning: {text}") if text else None + self.status_label.setText(text) @SafeSlot(bool) def _on_live_toggled(self, checked: bool):