fix(xrayeye): make the pixel clock slider index-based over supported values
The server-side snap-to-nearest fix (previous commit) corrected what got written to hardware, but the slider itself still let an operator drag to -- and briefly display -- any integer in [min, max] before self-correcting on the next status message, which reads as "the slider allows any setting" even though bad ones don't stick. pixel_clock_slider's range is now [0, len(options)-1], an index into the real discrete list of supported pixel clocks (fetched once over RPC at widget startup via get_pixel_clock_list(), disabled until it arrives -- not a polling loop, this list never changes at runtime). Every position the slider can physically be dragged to is therefore one the hardware has already confirmed it accepts. getting_camera_status() now maps the hardware-reported MHz value to the nearest option's index instead of setting the slider to a raw MHz value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
This commit is contained in:
1 parent
a550fd4f03
commit
afca16bfe3
1 file changed
+86
-19
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user