mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-11 07:38:54 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b119c5ad76 | |||
| 9a58dba414 |
@@ -1,6 +1,14 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v3.13.4 (2026-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **positioner_box**: Fix STOP button
|
||||
([`9a58dba`](https://github.com/bec-project/bec_widgets/commit/9a58dba414d9eec32fd7de7fc64c97c38f020b84))
|
||||
|
||||
|
||||
## v3.13.3 (2026-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@ class PositionerBox2D(PositionerBoxBase):
|
||||
|
||||
@SafeSlot()
|
||||
def on_stop(self):
|
||||
self._stop_device(f"{self.device_hor} or {self.device_ver}")
|
||||
self._stop_device([self.device_hor, self.device_ver])
|
||||
|
||||
@SafeProperty(float)
|
||||
def step_size_hor(self):
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from typing import Callable, TypedDict
|
||||
from typing import Callable, Sequence, TypedDict
|
||||
|
||||
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 bec_lib.messages import VariableMessage
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDoubleSpinBox,
|
||||
@@ -116,17 +115,16 @@ class PositionerBoxBase(BECWidget, QWidget):
|
||||
else:
|
||||
ui["units"].setVisible(False)
|
||||
|
||||
def _stop_device(self, device: str):
|
||||
def _stop_device(self, device: str | Sequence[str]):
|
||||
"""Stop call"""
|
||||
request_id = str(uuid.uuid4())
|
||||
params = {"device": device, "rpc_id": request_id, "func": "stop", "args": [], "kwargs": {}}
|
||||
msg = ScanQueueMessage(
|
||||
scan_type="device_rpc",
|
||||
parameter=params,
|
||||
queue="emergency",
|
||||
metadata={"RID": request_id, "response": False},
|
||||
)
|
||||
self.client.connector.send(MessageEndpoints.scan_queue_request(self.client.username), msg)
|
||||
devices = [device] if isinstance(device, str) else list(device)
|
||||
devices = [dev for dev in devices if dev]
|
||||
if not devices:
|
||||
logger.warning("Stop requested without a valid device.")
|
||||
return
|
||||
|
||||
msg = VariableMessage(value=devices)
|
||||
self.client.connector.send(MessageEndpoints.stop_devices(), msg)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _on_device_readback(
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
from bec_lib.builtin_actor_hli import ScanInterlockHli
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import BlStateStatus
|
||||
from qtpy import QtGui, QtWidgets
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget, ConnectionConfig
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
|
||||
|
||||
class ScanInterlockToggle(ToggleSwitch):
|
||||
def __init__(self, interlock: ScanInterlockHli, parent):
|
||||
super().__init__(parent=parent)
|
||||
self._interlock = interlock
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self.isEnabled() and event.button() == Qt.MouseButton.LeftButton:
|
||||
self._interlock.enabled = not self.checked
|
||||
|
||||
|
||||
class BlStateList(QtWidgets.QListWidget):
|
||||
def __init__(self, interlock: ScanInterlockHli, parent):
|
||||
super().__init__(parent)
|
||||
self._interlock = interlock
|
||||
|
||||
def dropEvent(self, event: QtGui.QDropEvent, /) -> None:
|
||||
data = event.mimeData()
|
||||
if not data.hasText():
|
||||
return
|
||||
self._interlock.add_state_to_interlock(data.text(), "valid")
|
||||
|
||||
|
||||
class ScanInterlockControl(BECWidget, QtWidgets.QWidget):
|
||||
"""
|
||||
ScanInterlockControl can be used to enable/disable the scan interlock actor,
|
||||
and add/remove beamline states for it to watch.
|
||||
"""
|
||||
|
||||
RPC = False
|
||||
PLUGIN = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QtWidgets.QWidget | None = None,
|
||||
client=None,
|
||||
config: ConnectionConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
theme_update: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
parent (QtWidgets.QWidget, optional): The parent widget.
|
||||
client: The BEC client.
|
||||
config (ConnectionConfig, optional): The connection configuration.
|
||||
gui_id (str, optional): The GUI ID.
|
||||
theme_update (bool, optional): Whether to subscribe to theme updates. Defaults to False.
|
||||
"""
|
||||
super().__init__(
|
||||
parent=parent,
|
||||
client=client,
|
||||
config=config,
|
||||
gui_id=gui_id,
|
||||
theme_update=theme_update,
|
||||
**kwargs,
|
||||
)
|
||||
self._interlock = self.client.builtin_actors.scan_interlock
|
||||
self._layout = QtWidgets.QVBoxLayout(self)
|
||||
self._setup_control_layout()
|
||||
self._setup_list_layout()
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self._update_all_content,
|
||||
MessageEndpoints.builtin_actor_update_notif("ScanInterlockActor"),
|
||||
)
|
||||
self._update_all_content()
|
||||
|
||||
def _setup_control_layout(self):
|
||||
self._controls_layout = QtWidgets.QHBoxLayout()
|
||||
self._layout.addLayout(self._controls_layout)
|
||||
self._enabled_text = QtWidgets.QLabel()
|
||||
self._enabled_text.setText("Widget Uninitialised")
|
||||
self._enabled_toggle = ScanInterlockToggle(self._interlock, parent=self)
|
||||
self._controls_layout.addWidget(self._enabled_text)
|
||||
self._controls_layout.addWidget(self._enabled_toggle)
|
||||
self._enabled_toggle
|
||||
|
||||
def _set_enabled_text(self, enabled: bool):
|
||||
self._enabled_text.setText(
|
||||
"Scan Interlock Enabled" if enabled else "Scan Interlock Disabled"
|
||||
)
|
||||
|
||||
def _setup_list_layout(self):
|
||||
self._list_layout = QtWidgets.QVBoxLayout()
|
||||
self._layout.addLayout(self._list_layout)
|
||||
self._list_layout.addWidget(QtWidgets.QLabel("Beamline states watched:"))
|
||||
self._bl_states_list = BlStateList(self._interlock, self)
|
||||
self._bl_states_list.setDragDropMode(QtWidgets.QListWidget.DragDropMode.DropOnly)
|
||||
self._list_layout.addWidget(self._bl_states_list)
|
||||
|
||||
self._delete_button_layout = QtWidgets.QHBoxLayout()
|
||||
self._list_layout.addLayout(self._delete_button_layout)
|
||||
|
||||
self._delete_button = QtWidgets.QPushButton("Remove selected states from interlock")
|
||||
self._delete_button_layout.addWidget(self._delete_button)
|
||||
self._delete_button.clicked.connect(self._delete_selected)
|
||||
|
||||
self._delete_all_button = QtWidgets.QPushButton("Remove all states")
|
||||
self._delete_button_layout.addWidget(self._delete_all_button)
|
||||
self._delete_all_button.clicked.connect(self._delete_all)
|
||||
|
||||
@SafeSlot()
|
||||
def _delete_selected(self):
|
||||
to_delete = [i.text() for i in self._bl_states_list.selectedItems()]
|
||||
for state in to_delete:
|
||||
self._interlock.remove_state_from_interlock(state)
|
||||
|
||||
@SafeSlot()
|
||||
def _delete_all(self):
|
||||
self._interlock.clear_all()
|
||||
|
||||
def _update_list(self, list_content: dict[str, BlStateStatus]):
|
||||
self._bl_states_list.clear()
|
||||
for item in list_content:
|
||||
self._bl_states_list.addItem(item)
|
||||
|
||||
@SafeSlot()
|
||||
def _update_all_content(self, *_, **__):
|
||||
self._set_enabled_text(self._interlock.enabled)
|
||||
self._enabled_toggle.setChecked(self._interlock.enabled)
|
||||
self._update_list(self._interlock.states_watched)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
app = QApplication([])
|
||||
main_window = QtWidgets.QMainWindow()
|
||||
|
||||
central_widget = QtWidgets.QWidget()
|
||||
button = DarkModeButton()
|
||||
layout = QtWidgets.QVBoxLayout(central_widget)
|
||||
main_window.setCentralWidget(central_widget)
|
||||
scan_interlock_control = ScanInterlockControl() # type: ignore
|
||||
layout.addWidget(button)
|
||||
layout.addWidget(scan_interlock_control)
|
||||
|
||||
class TestList(QtWidgets.QListWidget):
|
||||
def mimeData(self, items, /):
|
||||
mimedata = super().mimeData(items)
|
||||
text = ",".join([i.text() for i in items])
|
||||
mimedata.setText(text)
|
||||
return mimedata
|
||||
|
||||
test_list = TestList()
|
||||
test_list.addItems(["samx_in_limits", "samy_in_limits"])
|
||||
test_list.setDragEnabled(True)
|
||||
|
||||
layout.addWidget(test_list)
|
||||
lineedit = QtWidgets.QLineEdit()
|
||||
lineedit.setDragEnabled(True)
|
||||
layout.addWidget(lineedit)
|
||||
main_window.setWindowTitle("Scan Interlock Control")
|
||||
main_window.resize(800, 400)
|
||||
main_window.show()
|
||||
app.exec_()
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "3.13.3"
|
||||
version = "3.13.4"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.11"
|
||||
classifiers = [
|
||||
@@ -71,6 +71,7 @@ qtermwidget = ["pyside6_qtermwidget"]
|
||||
|
||||
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -2,7 +2,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import ScanQueueMessage
|
||||
from bec_lib.messages import VariableMessage
|
||||
from qtpy.QtCore import Qt, QTimer
|
||||
from qtpy.QtGui import QValidator
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
@@ -34,15 +34,11 @@ class PositionerWithoutPrecision(Positioner):
|
||||
def positioner_box(qtbot, mocked_client):
|
||||
"""Fixture for PositionerBox widget"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(qtbot, PositionerBox, device="samx", client=mocked_client)
|
||||
yield db
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(qtbot, PositionerBox, device="samx", client=mocked_client)
|
||||
yield db
|
||||
|
||||
|
||||
def test_positioner_box(positioner_box):
|
||||
@@ -89,16 +85,8 @@ def test_positioner_box_on_stop(positioner_box):
|
||||
"""Test on stop button"""
|
||||
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(positioner_box.client.username), msg
|
||||
)
|
||||
msg = VariableMessage(value=["samx"])
|
||||
mock_send.assert_called_once_with(MessageEndpoints.stop_devices(), msg)
|
||||
|
||||
|
||||
def test_positioner_box_setpoint_change(positioner_box):
|
||||
@@ -139,19 +127,15 @@ def test_positioner_control_line(qtbot, mocked_client):
|
||||
Inherits from PositionerBox, but the layout is changed. Check dimensions only
|
||||
"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = PositionerControlLine(device="samx", client=mocked_client)
|
||||
qtbot.addWidget(db)
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = PositionerControlLine(device="samx", client=mocked_client)
|
||||
qtbot.addWidget(db)
|
||||
|
||||
assert db.ui.device_box.height() == db.height()
|
||||
assert db.ui.device_box.height() >= db.dimensions[0]
|
||||
assert db.ui.device_box.width() == 600
|
||||
assert db.ui.device_box.height() == db.height()
|
||||
assert db.ui.device_box.height() >= db.dimensions[0]
|
||||
assert db.ui.device_box.width() == 600
|
||||
|
||||
|
||||
def test_positioner_box_open_dialog_selection(qtbot, positioner_box):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import VariableMessage
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox2D
|
||||
|
||||
@@ -12,17 +14,13 @@ from .conftest import create_widget
|
||||
def positioner_box_2d(qtbot, mocked_client):
|
||||
"""Fixture for PositionerBox widget"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(
|
||||
qtbot, PositionerBox2D, device_hor="samx", device_ver="samy", client=mocked_client
|
||||
)
|
||||
yield db
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(
|
||||
qtbot, PositionerBox2D, device_hor="samx", device_ver="samy", client=mocked_client
|
||||
)
|
||||
yield db
|
||||
|
||||
|
||||
def test_positioner_box_2d(positioner_box_2d):
|
||||
@@ -82,6 +80,14 @@ def test_positioner_box_setpoint_changes(positioner_box_2d: PositionerBox2D):
|
||||
mock_move.assert_called_once_with(100, relative=False)
|
||||
|
||||
|
||||
def test_positioner_box_2d_on_stop(positioner_box_2d: PositionerBox2D):
|
||||
"""Stop button sends both positioners to the immediate stop endpoint."""
|
||||
with mock.patch.object(positioner_box_2d.client.connector, "send") as mock_send:
|
||||
positioner_box_2d.on_stop()
|
||||
msg = VariableMessage(value=["samx", "samy"])
|
||||
mock_send.assert_called_once_with(MessageEndpoints.stop_devices(), msg)
|
||||
|
||||
|
||||
def _hor_buttons(widget: PositionerBox2D):
|
||||
return [
|
||||
widget.ui.tweak_increase_hor,
|
||||
|
||||
Reference in New Issue
Block a user