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

fix: rename DeviceBox to PositionerBox, fix test for validation

This commit is contained in:
2024-08-07 15:42:12 +02:00
parent eb54e9f788
commit 37aa371e7c
12 changed files with 215 additions and 154 deletions

View File

@ -21,9 +21,9 @@ class Widgets(str, enum.Enum):
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECWaveformWidget = "BECWaveformWidget"
DeviceBox = "DeviceBox"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
PositionerBox = "PositionerBox"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
@ -2287,24 +2287,6 @@ class BECWaveformWidget(RPCBase):
"""
class DeviceBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class DeviceComboBox(RPCBase):
@property
@rpc_call
@ -2359,6 +2341,17 @@ class DeviceLineEdit(RPCBase):
"""
class PositionerBox(RPCBase):
@rpc_call
def set_positioner(self, positioner: str):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class Ring(RPCBase):
@rpc_call
def _get_all_rpc(self) -> "dict":

View File

@ -1 +0,0 @@
{'files': ['device_box.py']}

View File

@ -83,8 +83,8 @@ class BECDockArea(BECWidget, QWidget):
"scan_control": IconAction(
icon_path="scan_control.svg", tooltip="Add Scan Control"
),
"device_box": IconAction(
icon_path="device_box.svg", tooltip="Add Device Box"
"positioner_box": IconAction(
icon_path="positioner_box.svg", tooltip="Add Device Box"
),
},
),
@ -132,8 +132,8 @@ class BECDockArea(BECWidget, QWidget):
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
lambda: self.add_dock(widget="ScanControl", prefix="scan_control")
)
self.toolbar.widgets["menu_devices"].widgets["device_box"].triggered.connect(
lambda: self.add_dock(widget="DeviceBox", prefix="device_box")
self.toolbar.widgets["menu_devices"].widgets["positioner_box"].triggered.connect(
lambda: self.add_dock(widget="PositionerBox", prefix="positioner_box")
)
# Menu Utils

View File

@ -1,8 +1,11 @@
""" Module for a PositionerBox widget to control a positioner device."""
import os
import uuid
from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.messages import ScanQueueMessage
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtGui import QDoubleValidator
@ -12,12 +15,23 @@ from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
logger = bec_logger.logger
class DeviceBox(BECWidget, QWidget):
class PositionerBox(BECWidget, QWidget):
"""Simple Widget to control a positioner in box form"""
USER_ACCESS = ["set_positioner"]
device_changed = Signal(str, str)
def __init__(self, parent=None, device: Positioner = None, *args, **kwargs):
super().__init__(*args, **kwargs)
"""Initialize the PositionerBox widget.
Args:
parent: The parent widget.
device (Positioner): The device to control.
"""
super().__init__(**kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts()
self._device = ""
@ -30,10 +44,11 @@ class DeviceBox(BECWidget, QWidget):
self.init_device()
def init_ui(self):
"""Init the ui"""
self.device_changed.connect(self.on_device_change)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
self.ui = UILoader(self).loader(os.path.join(current_path, "positioner_box.ui"))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
@ -58,11 +73,17 @@ class DeviceBox(BECWidget, QWidget):
self.ui.spinner_widget.start()
def init_device(self):
if self.device in self.dev and isinstance(self.dev[self.device], Positioner):
"""Init the device view and readback"""
if self._check_device_is_valid(self.device):
data = self.dev[self.device].read()
self.on_device_readback({"signals": data}, {})
def _toogle_enable_buttons(self, enable: bool) -> None:
"""Toogle enable/disable on available buttons
Args:
enable (bool): Enable buttons
"""
self.ui.tweak_left.setEnabled(enable)
self.ui.tweak_right.setEnabled(enable)
self.ui.stop.setEnabled(enable)
@ -71,25 +92,53 @@ class DeviceBox(BECWidget, QWidget):
@Property(str)
def device(self):
"""Property to set the device"""
return self._device
@device.setter
def device(self, value: str):
"""Setter, checks if device is a string"""
if not value or not isinstance(value, str):
return
old_device = self._device
self._device = value
self.device_changed.emit(old_device, value)
def set_positioner(self, positioner: str):
"""Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
if isinstance(positioner, Positioner):
positioner = positioner.name
self.device = positioner
def _check_device_is_valid(self, device: str):
"""Check if the device is a positioner
Args:
device (str): The device name
"""
if device not in self.dev:
logger.info(f"Device {device} not found in the device list")
return False
if not isinstance(self.dev[device], Positioner):
logger.info(f"Device {device} is not a positioner")
return False
return True
@Slot(str, str)
def on_device_change(self, old_device: str, new_device: str):
if new_device not in self.dev:
print(f"Device {new_device} not found in the device list")
"""Upon changing the device, a check will be performed if the device is a Positioner.
Args:
old_device (str): The old device name.
new_device (str): The new device name.
"""
if not self._check_device_is_valid(new_device):
return
if not isinstance(self.dev[new_device], Positioner):
print(f"Device {new_device} is not a positioner")
return
print(f"Device changed from {old_device} to {new_device}")
logger.info(f"Device changed from {old_device} to {new_device}")
self._toogle_enable_buttons(True)
self.init_device()
self.bec_dispatcher.disconnect_slot(
@ -110,6 +159,12 @@ class DeviceBox(BECWidget, QWidget):
@Slot(dict, dict)
def on_device_readback(self, msg_content: dict, metadata: dict):
"""Callback for device readback.
Args:
msg_content (dict): The message content.
metadata (dict): The message metadata.
"""
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[self.device]._hints
@ -146,7 +201,12 @@ class DeviceBox(BECWidget, QWidget):
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
self.ui.position_indicator.on_position_update(pos)
def update_limits(self, limits):
def update_limits(self, limits: tuple):
"""Update limits
Args:
limits (tuple): Limits of the positioner
"""
if limits == self._limits:
return
self._limits = limits
@ -159,6 +219,7 @@ class DeviceBox(BECWidget, QWidget):
@Slot()
def on_stop(self):
"""Stop call"""
request_id = str(uuid.uuid4())
params = {
"device": self.device,
@ -177,18 +238,22 @@ class DeviceBox(BECWidget, QWidget):
@property
def step_size(self):
"""Step size for tweak"""
return self.ui.step_size.value()
@Slot()
def on_tweak_right(self):
"""Tweak motor right"""
self.dev[self.device].move(self.step_size, relative=True)
@Slot()
def on_tweak_left(self):
"""Tweak motor left"""
self.dev[self.device].move(-self.step_size, relative=True)
@Slot()
def on_setpoint_change(self):
"""Change the setpoint for the motor"""
self.ui.setpoint.clearFocus()
setpoint = self.ui.setpoint.text()
self.dev[self.device].move(float(setpoint), relative=False)
@ -203,7 +268,7 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
apply_theme("light")
widget = DeviceBox(device="bpm4i")
widget = PositionerBox(device="bpm4i")
widget.show()
sys.exit(app.exec_())

View File

@ -0,0 +1 @@
{'files': ['positioner_box.py']}

View File

@ -1,44 +1,39 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
import bec_widgets
from bec_widgets.widgets.device_box.device_box import DeviceBox
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceBox' name='device_box'>
<widget class='PositionerBox' name='positioner_box'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = DeviceBox(parent)
t = PositionerBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
return ""
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "device_box.png")
return QIcon(icon_path)
return QIcon()
def includeFile(self):
return "device_box"
return "positioner_box"
def initialize(self, form_editor):
self._form_editor = form_editor
@ -50,10 +45,10 @@ class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return self._form_editor is not None
def name(self):
return "DeviceBox"
return "PositionerBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
return "Simple Widget to control a positioner in box form"
def whatsThis(self):
return self.toolTip()

View File

@ -6,9 +6,9 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_box.device_box_plugin import DeviceBoxPlugin
from bec_widgets.widgets.positioner_box.positioner_box_plugin import PositionerBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerBoxPlugin())
if __name__ == "__main__": # pragma: no cover

View File

@ -127,10 +127,12 @@ def test_toolbar_add_plot_motor_map(bec_dock_area):
assert bec_dock_area.panels["motor_map_1"].widgets[0].config.widget_class == "BECMotorMapWidget"
def test_toolbar_add_device_device_box(bec_dock_area):
bec_dock_area.toolbar.widgets["menu_devices"].widgets["device_box"].trigger()
assert "device_box_1" in bec_dock_area.panels
assert bec_dock_area.panels["device_box_1"].widgets[0].config.widget_class == "DeviceBox"
def test_toolbar_add_device_positioner_box(bec_dock_area):
bec_dock_area.toolbar.widgets["menu_devices"].widgets["positioner_box"].trigger()
assert "positioner_box_1" in bec_dock_area.panels
assert (
bec_dock_area.panels["positioner_box_1"].widgets[0].config.widget_class == "PositionerBox"
)
def test_toolbar_add_utils_queue(bec_dock_area):

View File

@ -1,98 +0,0 @@
from unittest import mock
import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtGui import QValidator
from bec_widgets.widgets.device_box.device_box import DeviceBox
from .client_mocks import mocked_client
@pytest.fixture
def device_box(qtbot, mocked_client):
with mock.patch("bec_widgets.widgets.device_box.device_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
db = DeviceBox(device="samx", client=mocked_client)
qtbot.addWidget(db)
yield db
def test_device_box(device_box):
assert device_box.device == "samx"
data = device_box.dev["samx"].read()
setpoint_text = device_box.ui.setpoint.text()
# check that the setpoint is taken correctly after init
assert float(setpoint_text) == data["samx_setpoint"]["value"]
# check that the precision is taken correctly after init
precision = device_box.dev["samx"].precision
assert setpoint_text == f"{data['samx_setpoint']['value']:.{precision}f}"
# check that the step size is set according to the device precision
assert device_box.ui.step_size.value() == 10**-precision * 10
def test_device_box_update_limits(device_box):
device_box._limits = None
device_box.update_limits([0, 10])
assert device_box._limits == [0, 10]
assert device_box.setpoint_validator.bottom() == 0
assert device_box.setpoint_validator.top() == 10
assert device_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Intermediate,
"100",
0,
)
device_box.update_limits(None)
assert device_box._limits is None
assert device_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Acceptable,
"100",
0,
)
def test_device_box_on_stop(device_box):
with mock.patch.object(device_box.client.connector, "send") as mock_send:
device_box.on_stop()
params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}}
msg = ScanQueueMessage(
scan_type="device_rpc",
parameter=params,
queue="emergency",
metadata={"RID": "fake_uuid", "response": False},
)
mock_send.assert_called_once_with(MessageEndpoints.scan_queue_request(), msg)
def test_device_box_setpoint_change(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.setpoint.setText("100")
device_box.on_setpoint_change()
mock_move.assert_called_once_with(100, relative=False)
def test_device_box_on_tweak_right(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.step_size.setValue(0.1)
device_box.on_tweak_right()
mock_move.assert_called_once_with(0.1, relative=True)
def test_device_box_on_tweak_left(device_box):
with mock.patch.object(device_box.dev["samx"], "move") as mock_move:
device_box.ui.step_size.setValue(0.1)
device_box.on_tweak_left()
mock_move.assert_called_once_with(-0.1, relative=True)
def test_device_box_setpoint_out_of_range(device_box):
device_box.update_limits([0, 10])
device_box.ui.setpoint.setText("100")
device_box.on_setpoint_change()
assert device_box.ui.setpoint.text() == "100"
assert device_box.ui.setpoint.hasAcceptableInput() == False

View File

@ -0,0 +1,104 @@
from unittest import mock
import pytest
from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtGui import QValidator
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
from .client_mocks import mocked_client
@pytest.fixture
def positioner_box(qtbot, mocked_client):
with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
with mock.patch(
"bec_widgets.widgets.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
return_value=True,
):
db = PositionerBox(device="samx", client=mocked_client)
qtbot.addWidget(db)
yield db
def test_positioner_box(positioner_box):
assert positioner_box.device == "samx"
data = positioner_box.dev["samx"].read()
# Avoid check for Positioner class from BEC in _init_device
setpoint_text = positioner_box.ui.setpoint.text()
# check that the setpoint is taken correctly after init
assert float(setpoint_text) == data["samx_setpoint"]["value"]
# check that the precision is taken correctly after isnit
precision = positioner_box.dev["samx"].precision
assert setpoint_text == f"{data['samx_setpoint']['value']:.{precision}f}"
# check that the step size is set according to the device precision
assert positioner_box.ui.step_size.value() == 10**-precision * 10
def test_positioner_box_update_limits(positioner_box):
positioner_box._limits = None
positioner_box.update_limits([0, 10])
assert positioner_box._limits == [0, 10]
assert positioner_box.setpoint_validator.bottom() == 0
assert positioner_box.setpoint_validator.top() == 10
assert positioner_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Intermediate,
"100",
0,
)
positioner_box.update_limits(None)
assert positioner_box._limits is None
assert positioner_box.setpoint_validator.validate("100", 0) == (
QValidator.State.Acceptable,
"100",
0,
)
def test_positioner_box_on_stop(positioner_box):
with mock.patch.object(positioner_box.client.connector, "send") as mock_send:
positioner_box.on_stop()
params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}}
msg = ScanQueueMessage(
scan_type="device_rpc",
parameter=params,
queue="emergency",
metadata={"RID": "fake_uuid", "response": False},
)
mock_send.assert_called_once_with(MessageEndpoints.scan_queue_request(), msg)
def test_positioner_box_setpoint_change(positioner_box):
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change()
mock_move.assert_called_once_with(100, relative=False)
def test_positioner_box_on_tweak_right(positioner_box):
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_right()
mock_move.assert_called_once_with(0.1, relative=True)
def test_positioner_box_on_tweak_left(positioner_box):
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_left()
mock_move.assert_called_once_with(-0.1, relative=True)
def test_positioner_box_setpoint_out_of_range(positioner_box):
positioner_box.update_limits([0, 10])
positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change()
assert positioner_box.ui.setpoint.text() == "100"
assert positioner_box.ui.setpoint.hasAcceptableInput() == False