fix: send light slider changes from every input path, guard readback
CI / lint (push) Skipped
CI / test (3.11) (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped

Sliders only emitted on sliderReleased, so right-click+wheel, arrow keys
and groove clicks changed the value locally without a PUT and the 500 ms
status poll snapped it back. setTracking(False) + valueChanged now sends
one PUT per deliberate change; readback setValue is wrapped in
blockSignals to avoid echoing PUTs, and a 1.5 s grace window stops
in-flight stale status responses from bouncing the slider.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 13:42:07 +02:00
co-authored by Claude Fable 5
parent d94fb88148
commit 869a66ff4b
+85 -11
View File
@@ -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")