diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index 0f349a31..fe5f6a74 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -280,6 +280,12 @@ class OMNY_XRayEye(BECWidget, QWidget): super().__init__(parent=parent, **kwargs) self._live_view_signal = CAMERA[1] self._last_smear_composite = None + # pixel_clock_slider is index-based over this list (populated once, + # see _init_pixel_clock_options()) rather than a raw MHz range -- + # many uEye sensors only accept a short discrete list of pixel + # clocks, not every value in [min, max] (confirmed on hardware). + self._pixel_clock_options: list[int] = [] + self._last_pixel_clock_mhz: int | None = None self._connected_motor = None self._dap_params_forwarding_connected = False self._queue_busy = False @@ -322,6 +328,7 @@ class OMNY_XRayEye(BECWidget, QWidget): self.resize(800, 600) QTimer.singleShot(0, self._init_queue_status) QTimer.singleShot(0, self._init_gui_trigger) + QTimer.singleShot(0, self._init_pixel_clock_options) def _init_ui(self): self.root_layout = QVBoxLayout(self) @@ -467,9 +474,18 @@ class OMNY_XRayEye(BECWidget, QWidget): # Pixel clock (MHz): lowering it raises the max achievable exposure # time (at the cost of frame rate) -- exposed here since operators # hitting the exposure slider's ceiling need this knob to go further. + # Many uEye sensors only accept a short discrete list of pixel + # clocks (confirmed on hardware -- most values in the driver's own + # reported min/max range were rejected), so this slider's *position* + # is an index into self._pixel_clock_options, not a raw MHz value -- + # every position it can land on is therefore one the hardware + # actually accepts. The real list is fetched once, over RPC, in + # _init_pixel_clock_options() (it's static per camera, not something + # that needs a live subscription); disabled until then. self.pixel_clock_label = QLabel("Pixel clock", parent=self) self.pixel_clock_slider = QSlider(Qt.Orientation.Horizontal, parent=self) - self.pixel_clock_slider.setRange(1, 100) # MHz; placeholder, reseeded on connect + self.pixel_clock_slider.setRange(0, 0) # placeholder; see _set_pixel_clock_options() + self.pixel_clock_slider.setEnabled(False) self.pixel_clock_value_label = QLabel("-- MHz", parent=self) self.pixel_clock_value_label.setMinimumWidth(60) self.pixel_clock_slider.valueChanged.connect(self._update_pixel_clock_value_label) @@ -987,23 +1003,16 @@ class OMNY_XRayEye(BECWidget, QWidget): # No auto_gain_enabled handling here -- see the comment above the # (removed) toggle in _init_ui. - # Reseed the pixel-clock slider's bounds/value before the exposure - # ones below -- changing the pixel clock changes the exposure range, - # and both arrive together in this same message. - pixel_clock_min = signals.get(f"{CAMERA[0]}_pixel_clock_min") - pixel_clock_max = signals.get(f"{CAMERA[0]}_pixel_clock_max") - if pixel_clock_min is not None and pixel_clock_max is not None: - lo = int(pixel_clock_min.get("value")) - hi = int(pixel_clock_max.get("value")) - if (lo, hi) != (self.pixel_clock_slider.minimum(), self.pixel_clock_slider.maximum()): - self.pixel_clock_slider.setRange(lo, hi) - + # Update the pixel-clock slider's position before the exposure ones + # below -- changing the pixel clock changes the exposure range, and + # both arrive together in this same message. (No range/bounds to + # reseed here -- pixel_clock_slider's range is fixed once + # self._pixel_clock_options is populated; see _init_pixel_clock_ + # options()/_set_pixel_clock_options(). pixel_clock_min/max still + # exist on the device as informational metadata, just unused here.) pixel_clock = signals.get(f"{CAMERA[0]}_pixel_clock") if pixel_clock is not None: - self.pixel_clock_slider.blockSignals(True) - self.pixel_clock_slider.setValue(int(pixel_clock.get("value"))) - self.pixel_clock_slider.blockSignals(False) - self._update_pixel_clock_value_label(self.pixel_clock_slider.value()) + self._set_pixel_clock_display(int(pixel_clock.get("value"))) # Reseed the exposure slider's bounds next (rare -- only changes if # the pixel clock changed), so a value arriving in the same message @@ -1049,11 +1058,69 @@ class OMNY_XRayEye(BECWidget, QWidget): value_ms = self.exposure_time_slider.value() / self._EXPOSURE_SLIDER_SCALE self.dev.get(CAMERA[0]).exposure_time.put(value_ms) - def _update_pixel_clock_value_label(self, value: int): - self.pixel_clock_value_label.setText(f"{value} MHz") + def _init_pixel_clock_options(self): + """One-time fetch of the camera's actually-supported pixel clocks + (see get_pixel_clock_list()'s docstring for why this can't just be + [min, max] from a Kind.config signal) -- not a polling loop, this + list is static per camera, so a single RPC call at widget startup is + the right cost/benefit trade-off versus adding a whole signal path + for something that never changes at runtime.""" + try: + options = self.dev.get(CAMERA[0]).get_pixel_clock_list() + except Exception as exc: + logger.warning(f"Failed to fetch pixel clock options for OMNY_XRayEye: {exc}") + return + self._set_pixel_clock_options(sorted(int(v) for v in options)) + + def _set_pixel_clock_options(self, options: list[int]): + self._pixel_clock_options = options + if not options: + self.pixel_clock_slider.setEnabled(False) + return + self.pixel_clock_slider.blockSignals(True) + self.pixel_clock_slider.setRange(0, len(options) - 1) + self.pixel_clock_slider.blockSignals(False) + self.pixel_clock_slider.setEnabled(True) + if self._last_pixel_clock_mhz is not None: + # A status message already arrived before this RPC call + # returned -- apply it now that we can actually resolve it to a + # slider position. + self._set_pixel_clock_display(self._last_pixel_clock_mhz) + else: + # No status message yet either -- keep the label in sync with + # the slider's default position (index 0, the lowest option). + self._update_pixel_clock_value_label(self.pixel_clock_slider.value()) + + def _set_pixel_clock_display(self, mhz: int): + """Move the slider to the option nearest `mhz` (from hardware, e.g. + via getting_camera_status()) and update the value label -- never a + raw MHz value on the slider itself, only a valid index.""" + self._last_pixel_clock_mhz = mhz + if not self._pixel_clock_options: + # Options not fetched yet -- _set_pixel_clock_options() will + # call back into this once they arrive. + self.pixel_clock_value_label.setText(f"{mhz} MHz") + return + index = min( + range(len(self._pixel_clock_options)), + key=lambda i: abs(self._pixel_clock_options[i] - mhz), + ) + self.pixel_clock_slider.blockSignals(True) + self.pixel_clock_slider.setValue(index) + self.pixel_clock_slider.blockSignals(False) + self._update_pixel_clock_value_label(index) + + def _update_pixel_clock_value_label(self, index: int): + if 0 <= index < len(self._pixel_clock_options): + self.pixel_clock_value_label.setText(f"{self._pixel_clock_options[index]} MHz") + else: + self.pixel_clock_value_label.setText("-- MHz") def pixel_clock_submitted(self): - self.dev.get(CAMERA[0]).pixel_clock.put(self.pixel_clock_slider.value()) + index = self.pixel_clock_slider.value() + if not 0 <= index < len(self._pixel_clock_options): + return + self.dev.get(CAMERA[0]).pixel_clock.put(self._pixel_clock_options[index]) @SafeSlot(bool) def opening_shutter(self, enabled: bool):