mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat : Add bec_signal_proxy to handle signals with option to unblock them manually.
This commit is contained in:
@ -5,10 +5,10 @@ It is a preliminary version of the GUI, which will be added to the main branch a
|
|||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from bec_lib.device import Positioner, Signal
|
from bec_lib.device import Positioner as BECPositioner
|
||||||
|
from bec_lib.device import Signal as BECSignal
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from qtpy.QtCore import QSize
|
from qtpy.QtCore import QSize, Signal
|
||||||
from qtpy.QtCore import Signal as pyqtSignal
|
|
||||||
from qtpy.QtGui import QIcon
|
from qtpy.QtGui import QIcon
|
||||||
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QPushButton, QSpinBox, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QPushButton, QSpinBox, QVBoxLayout, QWidget
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class Alignment1D(BECWidget, QWidget):
|
|||||||
"""Alignment GUI to perform 1D scans"""
|
"""Alignment GUI to perform 1D scans"""
|
||||||
|
|
||||||
# Emit a signal when a motion is ongoing
|
# Emit a signal when a motion is ongoing
|
||||||
motion_is_active = pyqtSignal(bool)
|
motion_is_active = Signal(bool)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent: Optional[QWidget] = None, client=None, gui_id: Optional[str] = None
|
self, parent: Optional[QWidget] = None, client=None, gui_id: Optional[str] = None
|
||||||
@ -198,14 +198,14 @@ class Alignment1D(BECWidget, QWidget):
|
|||||||
def _setup_motor_combobox(self) -> None:
|
def _setup_motor_combobox(self) -> None:
|
||||||
"""Setup motor selection"""
|
"""Setup motor selection"""
|
||||||
# FIXME after changing the filtering in the combobox
|
# FIXME after changing the filtering in the combobox
|
||||||
motors = [name for name in self.dev if isinstance(self.dev.get(name), Positioner)]
|
motors = [name for name in self.dev if isinstance(self.dev.get(name), BECPositioner)]
|
||||||
self.ui.device_combobox.setCurrentText(motors[0])
|
self.ui.device_combobox.setCurrentText(motors[0])
|
||||||
self.ui.device_combobox.set_device_filter("Positioner")
|
self.ui.device_combobox.set_device_filter("Positioner")
|
||||||
|
|
||||||
def _setup_signal_combobox(self) -> None:
|
def _setup_signal_combobox(self) -> None:
|
||||||
"""Setup signal selection"""
|
"""Setup signal selection"""
|
||||||
# FIXME after changing the filtering in the combobox
|
# FIXME after changing the filtering in the combobox
|
||||||
signals = [name for name in self.dev if isinstance(self.dev.get(name), Signal)]
|
signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)]
|
||||||
self.ui.device_combobox_2.setCurrentText(signals[0])
|
self.ui.device_combobox_2.setCurrentText(signals[0])
|
||||||
self.ui.device_combobox_2.set_device_filter("Signal")
|
self.ui.device_combobox_2.set_device_filter("Signal")
|
||||||
|
|
||||||
|
54
bec_widgets/utils/bec_signal_proxy.py
Normal file
54
bec_widgets/utils/bec_signal_proxy.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||||
|
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
||||||
|
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||||
|
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
||||||
|
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||||
|
|
||||||
|
from pyqtgraph import SignalProxy
|
||||||
|
from qtpy.QtCore import Signal, Slot
|
||||||
|
|
||||||
|
|
||||||
|
class BECSignalProxy(SignalProxy):
|
||||||
|
"""Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args: Arguments to pass to the SignalProxy class
|
||||||
|
rateLimit (int): The rateLimit of the proxy
|
||||||
|
**kwargs: Keyword arguments to pass to the SignalProxy class
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)"""
|
||||||
|
|
||||||
|
is_blocked = Signal(bool)
|
||||||
|
|
||||||
|
def __init__(self, *args, rateLimit=25, **kwargs):
|
||||||
|
super().__init__(*args, rateLimit=rateLimit, **kwargs)
|
||||||
|
self._blocking = False
|
||||||
|
self.old_args = None
|
||||||
|
self.new_args = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blocked(self):
|
||||||
|
"""Returns if the proxy is blocked"""
|
||||||
|
return self._blocking
|
||||||
|
|
||||||
|
@blocked.setter
|
||||||
|
def blocked(self, value: bool):
|
||||||
|
self._blocking = value
|
||||||
|
self.is_blocked.emit(value)
|
||||||
|
|
||||||
|
def signalReceived(self, *args):
|
||||||
|
"""Receive signal, store the args and call signalReceived from the parent class if not blocked"""
|
||||||
|
self.new_args = args
|
||||||
|
if self.blocked is True:
|
||||||
|
return
|
||||||
|
self.blocked = True
|
||||||
|
self.old_args = args
|
||||||
|
super().signalReceived(*args)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def unblock_proxy(self):
|
||||||
|
"""Unblock the proxy, and call the signalReceived method in case there was an update of the args."""
|
||||||
|
self.blocked = False
|
||||||
|
if self.new_args != self.old_args:
|
||||||
|
self.signalReceived(*self.new_args)
|
@ -241,12 +241,6 @@ class BECArrowItem(BECIndicatorItem):
|
|||||||
self.plot_item.removeItem(self.arrow_item)
|
self.plot_item.removeItem(self.arrow_item)
|
||||||
self.item_on_plot = False
|
self.item_on_plot = False
|
||||||
|
|
||||||
def check_log(self):
|
|
||||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
|
||||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
|
||||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
|
||||||
self.set_position(self._pos)
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""Cleanup the item"""
|
"""Cleanup the item"""
|
||||||
self.remove_from_plot()
|
self.remove_from_plot()
|
||||||
|
@ -30,31 +30,6 @@ from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
|||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class BECSignalProxy(pg.SignalProxy):
|
|
||||||
"""Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.blocking = False
|
|
||||||
|
|
||||||
def signalReceived(self, *args):
|
|
||||||
"""Received signa, but store the args"""
|
|
||||||
self.args = args
|
|
||||||
if self.blocking:
|
|
||||||
return
|
|
||||||
# self.blocking = True
|
|
||||||
super().signalReceived(*args)
|
|
||||||
|
|
||||||
# def flush(self):
|
|
||||||
# """If there is a signal queued send it out"""
|
|
||||||
# super().flush()
|
|
||||||
|
|
||||||
@Slot()
|
|
||||||
def unblock_proxy(self):
|
|
||||||
"""Unblock the proxy"""
|
|
||||||
self.blocking = False
|
|
||||||
|
|
||||||
|
|
||||||
class Waveform1DConfig(SubplotConfig):
|
class Waveform1DConfig(SubplotConfig):
|
||||||
color_palette: Optional[str] = Field(
|
color_palette: Optional[str] = Field(
|
||||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||||
@ -147,7 +122,7 @@ class BECWaveform(BECPlotBase):
|
|||||||
self.proxy_update_plot = pg.SignalProxy(
|
self.proxy_update_plot = pg.SignalProxy(
|
||||||
self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves
|
self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves
|
||||||
)
|
)
|
||||||
self.proxy_update_dap = BECSignalProxy(
|
self.proxy_update_dap = pg.SignalProxy(
|
||||||
self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
|
self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
|
||||||
)
|
)
|
||||||
self.async_signal_update.connect(self.replot_async_curve)
|
self.async_signal_update.connect(self.replot_async_curve)
|
||||||
@ -1211,8 +1186,6 @@ class BECWaveform(BECPlotBase):
|
|||||||
@Slot(dict, dict)
|
@Slot(dict, dict)
|
||||||
def update_dap(self, msg, metadata):
|
def update_dap(self, msg, metadata):
|
||||||
"""Callback for DAP response message."""
|
"""Callback for DAP response message."""
|
||||||
if self.proxy_update_dap is not None:
|
|
||||||
self.proxy_update_dap.unblock_proxy()
|
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
|
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
|
||||||
|
@ -50,7 +50,7 @@ class StopButton(BECWidget, QWidget):
|
|||||||
self.queue.request_scan_halt()
|
self.queue.request_scan_halt()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__": # pragma: no cover
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
75
tests/unit_tests/test_utils_bec_signal_proxy.py
Normal file
75
tests/unit_tests/test_utils_bec_signal_proxy.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||||
|
from bec_widgets.widgets.dap_combo_box.dap_combo_box import DapComboBox
|
||||||
|
|
||||||
|
from .client_mocks import mocked_client
|
||||||
|
from .conftest import create_widget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dap_combo_box(qtbot, mocked_client):
|
||||||
|
"""Fixture for TextBox widget to test BECSignalProxy with a simple widget"""
|
||||||
|
with mock.patch(
|
||||||
|
"bec_widgets.widgets.dap_combo_box.dap_combo_box.DapComboBox._validate_dap_model",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
widget = create_widget(qtbot, DapComboBox, client=mocked_client)
|
||||||
|
yield widget
|
||||||
|
|
||||||
|
|
||||||
|
def test_bec_signal_proxy(qtbot, dap_combo_box):
|
||||||
|
"""Test BECSignalProxy"""
|
||||||
|
proxy_container = []
|
||||||
|
|
||||||
|
def proxy_callback(*args):
|
||||||
|
"""A simple callback function for the proxy"""
|
||||||
|
proxy_container.append(args)
|
||||||
|
|
||||||
|
container = []
|
||||||
|
|
||||||
|
def all_callbacks(*args):
|
||||||
|
"""A simple callback function for all signal calls"""
|
||||||
|
container.append(args)
|
||||||
|
|
||||||
|
proxy = BECSignalProxy(dap_combo_box.x_axis_updated, rateLimit=25, slot=proxy_callback)
|
||||||
|
pg_proxy = pg.SignalProxy(dap_combo_box.x_axis_updated, rateLimit=25, slot=all_callbacks)
|
||||||
|
qtbot.wait(200)
|
||||||
|
# Test that the proxy is blocked
|
||||||
|
assert container == []
|
||||||
|
assert proxy_container == []
|
||||||
|
# Set first value
|
||||||
|
dap_combo_box.x_axis = "samx"
|
||||||
|
qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000)
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert container == [(("samx",),)]
|
||||||
|
assert proxy.blocked is True
|
||||||
|
assert proxy_container == [(("samx",),)]
|
||||||
|
# Set new value samy
|
||||||
|
dap_combo_box.x_axis = "samy"
|
||||||
|
qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000)
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert container == [(("samx",),), (("samy",),)]
|
||||||
|
assert proxy.blocked is True
|
||||||
|
assert proxy_container == [(("samx",),)]
|
||||||
|
# Set new value samz
|
||||||
|
dap_combo_box.x_axis = "samz"
|
||||||
|
qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000)
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert container == [(("samx",),), (("samy",),), (("samz",),)]
|
||||||
|
assert proxy.blocked is True
|
||||||
|
proxy.unblock_proxy()
|
||||||
|
qtbot.waitSignal(proxy.is_blocked, timeout=1000)
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert proxy.blocked is True
|
||||||
|
assert proxy_container == [(("samx",),), (("samz",),)]
|
||||||
|
# Unblock the proxy again, no new argument received.
|
||||||
|
# The callback should not be called again.
|
||||||
|
proxy.unblock_proxy()
|
||||||
|
qtbot.waitSignal(proxy.is_blocked, timeout=1000)
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert proxy.blocked is False
|
||||||
|
assert proxy_container == [(("samx",),), (("samz",),)]
|
Reference in New Issue
Block a user