0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat : Add bec_signal_proxy to handle signals with option to unblock them manually.

This commit is contained in:
2024-10-01 17:00:10 +02:00
parent f554f3c167
commit 1dcfeb6cfc
6 changed files with 137 additions and 41 deletions

View File

@ -5,10 +5,10 @@ It is a preliminary version of the GUI, which will be added to the main branch a
import os
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 qtpy.QtCore import QSize
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import QSize, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QPushButton, QSpinBox, QVBoxLayout, QWidget
@ -33,7 +33,7 @@ class Alignment1D(BECWidget, QWidget):
"""Alignment GUI to perform 1D scans"""
# Emit a signal when a motion is ongoing
motion_is_active = pyqtSignal(bool)
motion_is_active = Signal(bool)
def __init__(
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:
"""Setup motor selection"""
# 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.set_device_filter("Positioner")
def _setup_signal_combobox(self) -> None:
"""Setup signal selection"""
# 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.set_device_filter("Signal")

View 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)

View File

@ -241,12 +241,6 @@ class BECArrowItem(BECIndicatorItem):
self.plot_item.removeItem(self.arrow_item)
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:
"""Cleanup the item"""
self.remove_from_plot()

View File

@ -30,31 +30,6 @@ from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
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):
color_palette: Optional[str] = Field(
"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.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.async_signal_update.connect(self.replot_async_curve)
@ -1211,8 +1186,6 @@ class BECWaveform(BECPlotBase):
@Slot(dict, dict)
def update_dap(self, msg, metadata):
"""Callback for DAP response message."""
if self.proxy_update_dap is not None:
self.proxy_update_dap.unblock_proxy()
# pylint: disable=unused-variable
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]

View File

@ -50,7 +50,7 @@ class StopButton(BECWidget, QWidget):
self.queue.request_scan_halt()
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication

View 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",),)]