bec-widgets 1.16.5__py3-none-any.whl → 1.17.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. CHANGELOG.md +37 -0
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +46 -0
  4. bec_widgets/utils/bec_signal_proxy.py +39 -11
  5. bec_widgets/widgets/containers/dock/dock_area.py +1 -1
  6. bec_widgets/widgets/control/device_control/positioner_box/__init__.py +11 -0
  7. bec_widgets/widgets/control/device_control/positioner_box/_base/__init__.py +3 -0
  8. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +243 -0
  9. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/__init__.py +0 -0
  10. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +242 -0
  11. bec_widgets/widgets/control/device_control/positioner_box/{positioner_box_plugin.py → positioner_box/positioner_box_plugin.py} +1 -1
  12. bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_box.py → positioner_box/register_positioner_box.py} +1 -1
  13. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/__init__.py +0 -0
  14. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box2_d.pyproject +1 -0
  15. bec_widgets/widgets/control/{device_input/signal_combobox/signal_combo_box_plugin.py → device_control/positioner_box/positioner_box_2d/positioner_box2_d_plugin.py} +11 -9
  16. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +482 -0
  17. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +562 -0
  18. bec_widgets/widgets/control/{device_input/signal_combobox/register_signal_combo_box.py → device_control/positioner_box/positioner_box_2d/register_positioner_box2_d.py} +3 -3
  19. bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.py → positioner_control_line/positioner_control_line.py} +5 -2
  20. bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line_plugin.py → positioner_control_line/positioner_control_line_plugin.py} +1 -3
  21. bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_control_line.py → positioner_control_line/register_positioner_control_line.py} +1 -1
  22. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +7 -6
  23. {bec_widgets-1.16.5.dist-info → bec_widgets-1.17.1.dist-info}/METADATA +1 -1
  24. {bec_widgets-1.16.5.dist-info → bec_widgets-1.17.1.dist-info}/RECORD +32 -26
  25. pyproject.toml +1 -1
  26. bec_widgets/widgets/control/device_control/positioner_box/positioner_box.py +0 -352
  27. bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject +0 -1
  28. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.pyproject → positioner_box/positioner_box.pyproject} +0 -0
  29. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.ui → positioner_box/positioner_box.ui} +0 -0
  30. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.pyproject → positioner_control_line/positioner_control_line.pyproject} +0 -0
  31. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.ui → positioner_control_line/positioner_control_line.ui} +0 -0
  32. {bec_widgets-1.16.5.dist-info → bec_widgets-1.17.1.dist-info}/WHEEL +0 -0
  33. {bec_widgets-1.16.5.dist-info → bec_widgets-1.17.1.dist-info}/entry_points.txt +0 -0
  34. {bec_widgets-1.16.5.dist-info → bec_widgets-1.17.1.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,6 +1,43 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.17.1 (2025-01-26)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **bec_signal_proxy**: Timeout for blocking implemented
9
+ ([`6f2f2aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6f2f2aa06ae9b50f0451029caa1d8d83890a5b30))
10
+
11
+
12
+ ## v1.17.0 (2025-01-23)
13
+
14
+ ### Bug Fixes
15
+
16
+ - Focus policy and tab order for positioner_box_2d
17
+ ([`6df5710`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6df57103bb57c97bedda570b07a31a3cc6e57d5d))
18
+
19
+ ### Documentation
20
+
21
+ - Add documentation for 2D positioner box
22
+ ([`9a8cc31`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a8cc31f6c1fa5595f73c2a60372ef10d4c8eabb))
23
+
24
+ ### Features
25
+
26
+ - **widget**: Add 2d positioner box widget
27
+ ([`d2ffddb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2ffddb6d8d2473d8718f5aa650559902067ff12))
28
+
29
+ ### Refactoring
30
+
31
+ - Move positioner_box and line into submodule
32
+ ([`2419521`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2419521f5f05d8ff8ce975219629f77efb7fe6be))
33
+
34
+ PositionerBox and PositionerControlLine are now exported from from
35
+ bec_widgets.widgets.control.device_control.positioner_box, removing one level of hierarchy
36
+
37
+ - Move positioner_box logic to base class
38
+ ([`3770db5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3770db51be68a5f3fa65e0a67a4ed3efd1c7d6fe))
39
+
40
+
4
41
  ## v1.16.5 (2025-01-22)
5
42
 
6
43
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.16.5
3
+ Version: 1.17.1
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
bec_widgets/cli/client.py CHANGED
@@ -34,6 +34,7 @@ class Widgets(str, enum.Enum):
34
34
  Minesweeper = "Minesweeper"
35
35
  PositionIndicator = "PositionIndicator"
36
36
  PositionerBox = "PositionerBox"
37
+ PositionerBox2D = "PositionerBox2D"
37
38
  PositionerControlLine = "PositionerControlLine"
38
39
  ResetButton = "ResetButton"
39
40
  ResumeButton = "ResumeButton"
@@ -3235,6 +3236,51 @@ class PositionerBox(RPCBase):
3235
3236
  """
3236
3237
 
3237
3238
 
3239
+ class PositionerBox2D(RPCBase):
3240
+ @rpc_call
3241
+ def set_positioner_hor(self, positioner: "str | Positioner"):
3242
+ """
3243
+ Set the device
3244
+
3245
+ Args:
3246
+ positioner (Positioner | str) : Positioner to set, accepts str or the device
3247
+ """
3248
+
3249
+ @rpc_call
3250
+ def set_positioner_ver(self, positioner: "str | Positioner"):
3251
+ """
3252
+ Set the device
3253
+
3254
+ Args:
3255
+ positioner (Positioner | str) : Positioner to set, accepts str or the device
3256
+ """
3257
+
3258
+
3259
+ class PositionerBoxBase(RPCBase):
3260
+ @property
3261
+ @rpc_call
3262
+ def _config_dict(self) -> "dict":
3263
+ """
3264
+ Get the configuration of the widget.
3265
+
3266
+ Returns:
3267
+ dict: The configuration of the widget.
3268
+ """
3269
+
3270
+ @rpc_call
3271
+ def _get_all_rpc(self) -> "dict":
3272
+ """
3273
+ Get all registered RPC objects.
3274
+ """
3275
+
3276
+ @property
3277
+ @rpc_call
3278
+ def _rpc_id(self) -> "str":
3279
+ """
3280
+ Get the RPC ID of the widget.
3281
+ """
3282
+
3283
+
3238
3284
  class PositionerControlLine(RPCBase):
3239
3285
  @rpc_call
3240
3286
  def set_positioner(self, positioner: "str | Positioner"):
@@ -5,28 +5,43 @@ analyse data. Requesting a new fit may lead to request piling up and an overall
5
5
  will allow you to decide by yourself when to unblock and execute the callback again."""
6
6
 
7
7
  from pyqtgraph import SignalProxy
8
- from qtpy.QtCore import Signal, Slot
8
+ from qtpy.QtCore import QTimer, Signal
9
+
10
+ from bec_widgets.qt_utils.error_popups import SafeSlot
9
11
 
10
12
 
11
13
  class BECSignalProxy(SignalProxy):
12
- """Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored
14
+ """
15
+ Thin wrapper around the SignalProxy class to allow signal calls to be blocked,
16
+ but arguments still being stored.
13
17
 
14
18
  Args:
15
- *args: Arguments to pass to the SignalProxy class
16
- rateLimit (int): The rateLimit of the proxy
17
- **kwargs: Keyword arguments to pass to the SignalProxy class
19
+ *args: Arguments to pass to the SignalProxy class.
20
+ rateLimit (int): The rateLimit of the proxy.
21
+ timeout (float): The number of seconds after which the proxy automatically
22
+ unblocks if still blocked. Default is 10.0 seconds.
23
+ **kwargs: Keyword arguments to pass to the SignalProxy class.
18
24
 
19
25
  Example:
20
- >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)"""
26
+ >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)
27
+ """
21
28
 
22
29
  is_blocked = Signal(bool)
23
30
 
24
- def __init__(self, *args, rateLimit=25, **kwargs):
31
+ def __init__(self, *args, rateLimit=25, timeout=10.0, **kwargs):
25
32
  super().__init__(*args, rateLimit=rateLimit, **kwargs)
26
33
  self._blocking = False
27
34
  self.old_args = None
28
35
  self.new_args = None
29
36
 
37
+ # Store timeout value (in seconds)
38
+ self._timeout = timeout
39
+
40
+ # Create a single-shot timer for auto-unblocking
41
+ self._timer = QTimer()
42
+ self._timer.setSingleShot(True)
43
+ self._timer.timeout.connect(self._timeout_unblock)
44
+
30
45
  @property
31
46
  def blocked(self):
32
47
  """Returns if the proxy is blocked"""
@@ -46,9 +61,22 @@ class BECSignalProxy(SignalProxy):
46
61
  self.old_args = args
47
62
  super().signalReceived(*args)
48
63
 
49
- @Slot()
64
+ self._timer.start(int(self._timeout * 1000))
65
+
66
+ @SafeSlot()
50
67
  def unblock_proxy(self):
51
68
  """Unblock the proxy, and call the signalReceived method in case there was an update of the args."""
52
- self.blocked = False
53
- if self.new_args != self.old_args:
54
- self.signalReceived(*self.new_args)
69
+ if self.blocked:
70
+ self._timer.stop()
71
+ self.blocked = False
72
+ if self.new_args != self.old_args:
73
+ self.signalReceived(*self.new_args)
74
+
75
+ @SafeSlot()
76
+ def _timeout_unblock(self):
77
+ """
78
+ Internal method called by the QTimer upon timeout. Unblocks the proxy
79
+ automatically if it is still blocked.
80
+ """
81
+ if self.blocked:
82
+ self.unblock_proxy()
@@ -20,7 +20,7 @@ from bec_widgets.qt_utils.toolbar import (
20
20
  from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
21
21
  from bec_widgets.utils.bec_widget import BECWidget
22
22
  from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
23
- from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
23
+ from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
24
24
  from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
25
25
  from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
26
26
  from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
@@ -0,0 +1,11 @@
1
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
2
+ PositionerBox,
3
+ )
4
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
5
+ PositionerBox2D,
6
+ )
7
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
8
+ PositionerControlLine,
9
+ )
10
+
11
+ __ALL__ = ["PositionerBox", "PositionerControlLine", "PositionerBox2D"]
@@ -0,0 +1,3 @@
1
+ from .positioner_box_base import PositionerBoxBase
2
+
3
+ __ALL__ = ["PositionerBoxBase"]
@@ -0,0 +1,243 @@
1
+ import uuid
2
+ from abc import abstractmethod
3
+ from ast import Tuple
4
+ from typing import Callable, TypedDict
5
+
6
+ from bec_lib.device import Positioner
7
+ from bec_lib.endpoints import MessageEndpoints
8
+ from bec_lib.logger import bec_logger
9
+ from bec_lib.messages import ScanQueueMessage
10
+ from qtpy.QtWidgets import (
11
+ QDialog,
12
+ QDoubleSpinBox,
13
+ QGroupBox,
14
+ QLabel,
15
+ QLineEdit,
16
+ QPushButton,
17
+ QVBoxLayout,
18
+ )
19
+
20
+ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
21
+ from bec_widgets.utils.bec_widget import BECWidget
22
+ from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
23
+ PositionIndicator,
24
+ )
25
+ from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
26
+ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
27
+ DeviceLineEdit,
28
+ )
29
+ from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
30
+
31
+ logger = bec_logger.logger
32
+
33
+
34
+ class DeviceUpdateUIComponents(TypedDict):
35
+ spinner: SpinnerWidget
36
+ setpoint: QLineEdit
37
+ readback: QLabel
38
+ position_indicator: PositionIndicator
39
+ step_size: QDoubleSpinBox
40
+ device_box: QGroupBox
41
+ stop: QPushButton
42
+ tweak_increase: QPushButton
43
+ tweak_decrease: QPushButton
44
+
45
+
46
+ class PositionerBoxBase(BECWidget, CompactPopupWidget):
47
+ """Contains some core logic for positioner box widgets"""
48
+
49
+ current_path = ""
50
+ ICON_NAME = "switch_right"
51
+
52
+ def __init__(self, parent=None, **kwargs):
53
+ """Initialize the PositionerBox widget.
54
+
55
+ Args:
56
+ parent: The parent widget.
57
+ device (Positioner): The device to control.
58
+ """
59
+ super().__init__(**kwargs)
60
+ CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
61
+ self._dialog = None
62
+ self.get_bec_shortcuts()
63
+
64
+ def _check_device_is_valid(self, device: str):
65
+ """Check if the device is a positioner
66
+
67
+ Args:
68
+ device (str): The device name
69
+ """
70
+ if device not in self.dev:
71
+ logger.info(f"Device {device} not found in the device list")
72
+ return False
73
+ if not isinstance(self.dev[device], Positioner):
74
+ logger.info(f"Device {device} is not a positioner")
75
+ return False
76
+ return True
77
+
78
+ @abstractmethod
79
+ def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents: ...
80
+
81
+ def _init_device(
82
+ self,
83
+ device: str,
84
+ position_emit: Callable[[float], None],
85
+ limit_update: Callable[[tuple[float, float]], None],
86
+ ):
87
+ """Init the device view and readback"""
88
+ if self._check_device_is_valid(device):
89
+ data = self.dev[device].read()
90
+ self._on_device_readback(
91
+ device,
92
+ self._device_ui_components(device),
93
+ {"signals": data},
94
+ {},
95
+ position_emit,
96
+ limit_update,
97
+ )
98
+
99
+ def _stop_device(self, device: str):
100
+ """Stop call"""
101
+ request_id = str(uuid.uuid4())
102
+ params = {"device": device, "rpc_id": request_id, "func": "stop", "args": [], "kwargs": {}}
103
+ msg = ScanQueueMessage(
104
+ scan_type="device_rpc",
105
+ parameter=params,
106
+ queue="emergency",
107
+ metadata={"RID": request_id, "response": False},
108
+ )
109
+ self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
110
+
111
+ # pylint: disable=unused-argument
112
+ def _on_device_readback(
113
+ self,
114
+ device: str,
115
+ ui_components: DeviceUpdateUIComponents,
116
+ msg_content: dict,
117
+ metadata: dict,
118
+ position_emit: Callable[[float], None],
119
+ limit_update: Callable[[tuple[float, float]], None],
120
+ ):
121
+ signals = msg_content.get("signals", {})
122
+ # pylint: disable=protected-access
123
+ hinted_signals = self.dev[device]._hints
124
+ precision = self.dev[device].precision
125
+
126
+ spinner = ui_components["spinner"]
127
+ position_indicator = ui_components["position_indicator"]
128
+ readback = ui_components["readback"]
129
+ setpoint = ui_components["setpoint"]
130
+
131
+ readback_val = None
132
+ setpoint_val = None
133
+
134
+ if len(hinted_signals) == 1:
135
+ signal = hinted_signals[0]
136
+ readback_val = signals.get(signal, {}).get("value")
137
+
138
+ for setpoint_signal in ["setpoint", "user_setpoint"]:
139
+ setpoint_val = signals.get(f"{device}_{setpoint_signal}", {}).get("value")
140
+ if setpoint_val is not None:
141
+ break
142
+
143
+ for moving_signal in ["motor_done_move", "motor_is_moving"]:
144
+ is_moving = signals.get(f"{device}_{moving_signal}", {}).get("value")
145
+ if is_moving is not None:
146
+ break
147
+
148
+ if is_moving is not None:
149
+ spinner.setVisible(True)
150
+ if is_moving:
151
+ spinner.start()
152
+ spinner.setToolTip("Device is moving")
153
+ self.set_global_state("warning")
154
+ else:
155
+ spinner.stop()
156
+ spinner.setToolTip("Device is idle")
157
+ self.set_global_state("success")
158
+ else:
159
+ spinner.setVisible(False)
160
+
161
+ if readback_val is not None:
162
+ readback.setText(f"{readback_val:.{precision}f}")
163
+ position_emit(readback_val)
164
+
165
+ if setpoint_val is not None:
166
+ setpoint.setText(f"{setpoint_val:.{precision}f}")
167
+
168
+ limits = self.dev[device].limits
169
+ limit_update(limits)
170
+ if limits is not None and readback_val is not None and limits[0] != limits[1]:
171
+ pos = (readback_val - limits[0]) / (limits[1] - limits[0])
172
+ position_indicator.set_value(pos)
173
+
174
+ def _update_limits_ui(
175
+ self, limits: tuple[float, float], position_indicator, setpoint_validator
176
+ ):
177
+ if limits is not None and limits[0] != limits[1]:
178
+ position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
179
+ setpoint_validator.setRange(limits[0], limits[1])
180
+ else:
181
+ position_indicator.setToolTip("No limits set")
182
+ setpoint_validator.setRange(float("-inf"), float("inf"))
183
+
184
+ def _update_device_ui(self, device: str, ui: DeviceUpdateUIComponents):
185
+ ui["device_box"].setTitle(device)
186
+ ui["readback"].setToolTip(f"{device} readback")
187
+ ui["setpoint"].setToolTip(f"{device} setpoint")
188
+ ui["step_size"].setToolTip(f"Step size for {device}")
189
+ precision = self.dev[device].precision
190
+ if precision is not None:
191
+ ui["step_size"].setDecimals(precision)
192
+ ui["step_size"].setValue(10**-precision * 10)
193
+
194
+ def _swap_readback_signal_connection(self, slot, old_device, new_device):
195
+ self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
196
+ self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device))
197
+
198
+ def _toggle_enable_buttons(self, ui: DeviceUpdateUIComponents, enable: bool) -> None:
199
+ """Toogle enable/disable on available buttons
200
+
201
+ Args:
202
+ enable (bool): Enable buttons
203
+ """
204
+ ui["tweak_increase"].setEnabled(enable)
205
+ ui["tweak_decrease"].setEnabled(enable)
206
+ ui["stop"].setEnabled(enable)
207
+ ui["setpoint"].setEnabled(enable)
208
+ ui["step_size"].setEnabled(enable)
209
+
210
+ def _on_device_change(
211
+ self,
212
+ old_device: str,
213
+ new_device: str,
214
+ position_emit: Callable[[float], None],
215
+ limit_update: Callable[[tuple[float, float]], None],
216
+ on_device_readback: Callable,
217
+ ui: DeviceUpdateUIComponents,
218
+ ):
219
+ logger.info(f"Device changed from {old_device} to {new_device}")
220
+ self._toggle_enable_buttons(ui, True)
221
+ self._init_device(new_device, position_emit, limit_update)
222
+ self._swap_readback_signal_connection(on_device_readback, old_device, new_device)
223
+ self._update_device_ui(new_device, ui)
224
+
225
+ def _open_dialog_selection(self, set_positioner: Callable):
226
+ def _ods():
227
+ """Open dialog window for positioner selection"""
228
+ self._dialog = QDialog(self)
229
+ self._dialog.setWindowTitle("Positioner Selection")
230
+ layout = QVBoxLayout()
231
+ line_edit = DeviceLineEdit(
232
+ self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
233
+ )
234
+ line_edit.textChanged.connect(set_positioner)
235
+ layout.addWidget(line_edit)
236
+ close_button = QPushButton("Close")
237
+ close_button.clicked.connect(self._dialog.accept)
238
+ layout.addWidget(close_button)
239
+ self._dialog.setLayout(layout)
240
+ self._dialog.exec()
241
+ self._dialog = None
242
+
243
+ return _ods