diff --git a/src/aare/gui/panels/illumination_panel.py b/src/aare/gui/panels/illumination_panel.py index 160d00df..1c8d2dd7 100644 --- a/src/aare/gui/panels/illumination_panel.py +++ b/src/aare/gui/panels/illumination_panel.py @@ -1,9 +1,15 @@ +import time + from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtWidgets import QGridLayout, QLabel, QSlider, QWidget from aare.gui.widgets.title_label import TitleLabel +# Skip readback updates this long after a user change: a status response already +# in flight when the PUT was queued would otherwise snap the slider back. +READBACK_GRACE_S = 1.5 + class IlluminationPanel(QWidget): front_light = Signal(int) @@ -21,11 +27,18 @@ class IlluminationPanel(QWidget): front_label.setAlignment(Qt.AlignmentFlag.AlignCenter) grid_layout.addWidget(front_label, 1, 0, 1, 2) self.is_sliding = False + self._last_user_change = 0.0 self.front_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) self.front_light_slider.setRange(0, 100) + # tracking off: valueChanged fires once per deliberate change (wheel notch, + # key step, groove page-step, drag release). sliderReleased alone missed + # every path except a handle drag, so wheel adjustments were never sent + # and the 500 ms status poll reverted them. + self.front_light_slider.setTracking(False) self.front_light_slider.sliderPressed.connect(self.on_slider_pressed) - self.front_light_slider.sliderReleased.connect(self.on_front_slider_released) + self.front_light_slider.sliderReleased.connect(self.on_slider_released) + self.front_light_slider.valueChanged.connect(self.on_front_value_changed) grid_layout.addWidget(self.front_light_slider, 2, 0, 1, 2) back_label = QLabel("Back light", parent=self) @@ -34,8 +47,10 @@ class IlluminationPanel(QWidget): self.back_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) self.back_light_slider.setRange(0, 100) + self.back_light_slider.setTracking(False) self.back_light_slider.sliderPressed.connect(self.on_slider_pressed) - self.back_light_slider.sliderReleased.connect(self.on_back_slider_released) + self.back_light_slider.sliderReleased.connect(self.on_slider_released) + self.back_light_slider.valueChanged.connect(self.on_back_value_changed) grid_layout.addWidget(self.back_light_slider, 4, 0, 1, 2) @Slot() @@ -43,17 +58,76 @@ class IlluminationPanel(QWidget): self.is_sliding = True @Slot() - def on_front_slider_released(self): + def on_slider_released(self): self.is_sliding = False - self.front_light.emit(self.front_light_slider.value()) - @Slot() - def on_back_slider_released(self): - self.is_sliding = False - self.back_light.emit(self.back_light_slider.value()) + @Slot(int) + def on_front_value_changed(self, v: int): + self._last_user_change = time.monotonic() + self.front_light.emit(v) + + @Slot(int) + def on_back_value_changed(self, v: int): + self._last_user_change = time.monotonic() + self.back_light.emit(v) @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): - if not self.is_sliding: # Update only if not sliding - self.front_light_slider.setValue(round(s.bl.front_light)) - self.back_light_slider.setValue(round(s.bl.back_light)) + if self.is_sliding or time.monotonic() - self._last_user_change < READBACK_GRACE_S: + return + # blockSignals: readback must not loop back into valueChanged and echo + # a PUT to the server every poll. + for slider, val in ( + (self.front_light_slider, s.bl.front_light), + (self.back_light_slider, s.bl.back_light), + ): + slider.blockSignals(True) + slider.setValue(round(val)) + slider.blockSignals(False) + + +if __name__ == "__main__": + # ponytail: smallest check that fails if the send/readback wiring breaks + import os + from types import SimpleNamespace + + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + from PySide6.QtCore import QPoint, QPointF + from PySide6.QtGui import QWheelEvent + from PySide6.QtWidgets import QApplication + + app = QApplication([]) + panel = IlluminationPanel() + sent = [] + panel.front_light.connect(sent.append) + + # wheel notch must emit (was silently discarded before) + panel.front_light_slider.setFocus() + QApplication.sendEvent( + panel.front_light_slider, + QWheelEvent( + QPointF(5, 5), + QPointF(5, 5), + QPoint(0, 0), + QPoint(0, 120), + Qt.MouseButton.RightButton, + Qt.KeyboardModifier.NoModifier, + Qt.ScrollPhase.NoScrollPhase, + False, + ), + ) + assert sent, "wheel adjustment must emit front_light" + + # readback right after a user change must not clobber the slider + held = panel.front_light_slider.value() + status = SimpleNamespace(bl=SimpleNamespace(front_light=90.0, back_light=10.0)) + panel.update_daq_status(status) + assert panel.front_light_slider.value() == held, "grace window must hold user value" + + # after the grace window, readback applies without emitting a PUT + panel._last_user_change = time.monotonic() - READBACK_GRACE_S - 1 + n_sent = len(sent) + panel.update_daq_status(status) + assert panel.front_light_slider.value() == 90, "readback must apply after grace" + assert len(sent) == n_sent, "readback must not echo a PUT" + print("gude")