bec-widgets 1.16.4__py3-none-any.whl → 1.17.0__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.
- CHANGELOG.md +41 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +46 -0
- bec_widgets/cli/server.py +3 -7
- bec_widgets/qt_utils/error_popups.py +3 -8
- bec_widgets/widgets/containers/dock/dock_area.py +1 -1
- bec_widgets/widgets/control/device_control/positioner_box/__init__.py +11 -0
- bec_widgets/widgets/control/device_control/positioner_box/_base/__init__.py +3 -0
- bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +243 -0
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/__init__.py +0 -0
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +242 -0
- bec_widgets/widgets/control/device_control/positioner_box/{positioner_box_plugin.py → positioner_box/positioner_box_plugin.py} +1 -1
- bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_box.py → positioner_box/register_positioner_box.py} +1 -1
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/__init__.py +0 -0
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box2_d.pyproject +1 -0
- 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
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +482 -0
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +562 -0
- 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
- bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.py → positioner_control_line/positioner_control_line.py} +5 -2
- bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line_plugin.py → positioner_control_line/positioner_control_line_plugin.py} +1 -3
- bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_control_line.py → positioner_control_line/register_positioner_control_line.py} +1 -1
- bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +7 -6
- {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/METADATA +1 -1
- {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/RECORD +33 -27
- pyproject.toml +1 -1
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box.py +0 -352
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject +0 -1
- /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.pyproject → positioner_box/positioner_box.pyproject} +0 -0
- /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.ui → positioner_box/positioner_box.ui} +0 -0
- /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.pyproject → positioner_control_line/positioner_control_line.pyproject} +0 -0
- /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.ui → positioner_control_line/positioner_control_line.ui} +0 -0
- {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,47 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v1.17.0 (2025-01-23)
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
- Focus policy and tab order for positioner_box_2d
|
9
|
+
([`6df5710`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6df57103bb57c97bedda570b07a31a3cc6e57d5d))
|
10
|
+
|
11
|
+
### Documentation
|
12
|
+
|
13
|
+
- Add documentation for 2D positioner box
|
14
|
+
([`9a8cc31`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a8cc31f6c1fa5595f73c2a60372ef10d4c8eabb))
|
15
|
+
|
16
|
+
### Features
|
17
|
+
|
18
|
+
- **widget**: Add 2d positioner box widget
|
19
|
+
([`d2ffddb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2ffddb6d8d2473d8718f5aa650559902067ff12))
|
20
|
+
|
21
|
+
### Refactoring
|
22
|
+
|
23
|
+
- Move positioner_box and line into submodule
|
24
|
+
([`2419521`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2419521f5f05d8ff8ce975219629f77efb7fe6be))
|
25
|
+
|
26
|
+
PositionerBox and PositionerControlLine are now exported from from
|
27
|
+
bec_widgets.widgets.control.device_control.positioner_box, removing one level of hierarchy
|
28
|
+
|
29
|
+
- Move positioner_box logic to base class
|
30
|
+
([`3770db5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3770db51be68a5f3fa65e0a67a4ed3efd1c7d6fe))
|
31
|
+
|
32
|
+
|
33
|
+
## v1.16.5 (2025-01-22)
|
34
|
+
|
35
|
+
### Bug Fixes
|
36
|
+
|
37
|
+
- **cli**: Server log level info and error
|
38
|
+
([`df961a9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df961a9b885fa996e0ef44a36c937690670637c8))
|
39
|
+
|
40
|
+
- **error_popups**: Errors in SafeProperty and in SafeSlot are always logged, even with error
|
41
|
+
message popup enabled
|
42
|
+
([`219d43d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/219d43d325260569e17a8eb7d56f63267d6e9649))
|
43
|
+
|
44
|
+
|
4
45
|
## v1.16.4 (2025-01-21)
|
5
46
|
|
6
47
|
### Bug Fixes
|
PKG-INFO
CHANGED
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"):
|
bec_widgets/cli/server.py
CHANGED
@@ -218,15 +218,11 @@ def main():
|
|
218
218
|
|
219
219
|
args = parser.parse_args()
|
220
220
|
|
221
|
+
bec_logger.level = bec_logger.LOGLEVEL.INFO
|
221
222
|
if args.hide:
|
222
|
-
# if we start hidden, it means we are under control of the client
|
223
|
-
# -> set the log level to critical to not see all the messages
|
224
223
|
# pylint: disable=protected-access
|
225
|
-
|
226
|
-
bec_logger.
|
227
|
-
else:
|
228
|
-
# verbose log
|
229
|
-
bec_logger.level = bec_logger.LOGLEVEL.DEBUG
|
224
|
+
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
225
|
+
bec_logger._update_sinks()
|
230
226
|
|
231
227
|
if args.gui_class == "BECDockArea":
|
232
228
|
gui_class = BECDockArea
|
@@ -46,8 +46,7 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None,
|
|
46
46
|
|
47
47
|
if popup_error:
|
48
48
|
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
|
49
|
-
|
50
|
-
logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}")
|
49
|
+
logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}")
|
51
50
|
return default
|
52
51
|
|
53
52
|
class PropertyWrapper:
|
@@ -74,10 +73,7 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None,
|
|
74
73
|
ErrorPopupUtility().custom_exception_hook(
|
75
74
|
*sys.exc_info(), popup_error=True
|
76
75
|
)
|
77
|
-
|
78
|
-
logger.error(
|
79
|
-
f"SafeProperty error in SETTER of '{prop_name}':\n{error_msg}"
|
80
|
-
)
|
76
|
+
logger.error(f"SafeProperty error in SETTER of '{prop_name}':\n{error_msg}")
|
81
77
|
return
|
82
78
|
|
83
79
|
# Return the full read/write Property
|
@@ -116,8 +112,7 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
|
116
112
|
ErrorPopupUtility().custom_exception_hook(
|
117
113
|
*sys.exc_info(), popup_error=popup_error
|
118
114
|
)
|
119
|
-
|
120
|
-
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
115
|
+
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
121
116
|
|
122
117
|
return wrapper
|
123
118
|
|
@@ -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
|
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,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
|
File without changes
|
@@ -0,0 +1,242 @@
|
|
1
|
+
""" Module for a PositionerBox widget to control a positioner device."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import os
|
6
|
+
|
7
|
+
from bec_lib.device import Positioner
|
8
|
+
from bec_lib.logger import bec_logger
|
9
|
+
from bec_qthemes import material_icon
|
10
|
+
from qtpy.QtCore import Signal
|
11
|
+
from qtpy.QtGui import QDoubleValidator
|
12
|
+
from qtpy.QtWidgets import QDoubleSpinBox
|
13
|
+
|
14
|
+
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
15
|
+
from bec_widgets.utils import UILoader
|
16
|
+
from bec_widgets.utils.colors import get_accent_colors, set_theme
|
17
|
+
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
|
18
|
+
from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
|
19
|
+
DeviceUpdateUIComponents,
|
20
|
+
)
|
21
|
+
|
22
|
+
logger = bec_logger.logger
|
23
|
+
|
24
|
+
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
25
|
+
|
26
|
+
|
27
|
+
class PositionerBox(PositionerBoxBase):
|
28
|
+
"""Simple Widget to control a positioner in box form"""
|
29
|
+
|
30
|
+
ui_file = "positioner_box.ui"
|
31
|
+
dimensions = (234, 224)
|
32
|
+
|
33
|
+
PLUGIN = True
|
34
|
+
|
35
|
+
USER_ACCESS = ["set_positioner"]
|
36
|
+
device_changed = Signal(str, str)
|
37
|
+
# Signal emitted to inform listeners about a position update
|
38
|
+
position_update = Signal(float)
|
39
|
+
|
40
|
+
def __init__(self, parent=None, device: Positioner | str | None = None, **kwargs):
|
41
|
+
"""Initialize the PositionerBox widget.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
parent: The parent widget.
|
45
|
+
device (Positioner): The device to control.
|
46
|
+
"""
|
47
|
+
super().__init__(parent=parent, **kwargs)
|
48
|
+
|
49
|
+
self._device = ""
|
50
|
+
self._limits = None
|
51
|
+
if self.current_path == "":
|
52
|
+
self.current_path = os.path.dirname(__file__)
|
53
|
+
|
54
|
+
self.init_ui()
|
55
|
+
self.device = device
|
56
|
+
self._init_device(self.device, self.position_update.emit, self.update_limits)
|
57
|
+
|
58
|
+
def init_ui(self):
|
59
|
+
"""Init the ui"""
|
60
|
+
self.device_changed.connect(self.on_device_change)
|
61
|
+
|
62
|
+
self.ui = UILoader(self).loader(os.path.join(self.current_path, self.ui_file))
|
63
|
+
|
64
|
+
self.addWidget(self.ui)
|
65
|
+
self.layout.setSpacing(0)
|
66
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
67
|
+
|
68
|
+
# fix the size of the device box
|
69
|
+
db = self.ui.device_box
|
70
|
+
db.setFixedHeight(self.dimensions[0])
|
71
|
+
db.setFixedWidth(self.dimensions[1])
|
72
|
+
|
73
|
+
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
74
|
+
self.ui.stop.clicked.connect(self.on_stop)
|
75
|
+
self.ui.stop.setToolTip("Stop")
|
76
|
+
self.ui.stop.setStyleSheet(
|
77
|
+
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
78
|
+
)
|
79
|
+
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
|
80
|
+
self.ui.tweak_right.setToolTip("Tweak right")
|
81
|
+
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
|
82
|
+
self.ui.tweak_left.setToolTip("Tweak left")
|
83
|
+
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
|
84
|
+
|
85
|
+
self.setpoint_validator = QDoubleValidator()
|
86
|
+
self.ui.setpoint.setValidator(self.setpoint_validator)
|
87
|
+
self.ui.spinner_widget.start()
|
88
|
+
self.ui.tool_button.clicked.connect(self._open_dialog_selection(self.set_positioner))
|
89
|
+
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
|
90
|
+
self.ui.tool_button.setIcon(icon)
|
91
|
+
|
92
|
+
def force_update_readback(self):
|
93
|
+
self._init_device(self.device, self.position_update.emit, self.update_limits)
|
94
|
+
|
95
|
+
@SafeProperty(str)
|
96
|
+
def device(self):
|
97
|
+
"""Property to set the device"""
|
98
|
+
return self._device
|
99
|
+
|
100
|
+
@device.setter
|
101
|
+
def device(self, value: str):
|
102
|
+
"""Setter, checks if device is a string"""
|
103
|
+
if not value or not isinstance(value, str):
|
104
|
+
return
|
105
|
+
if not self._check_device_is_valid(value):
|
106
|
+
return
|
107
|
+
old_device = self._device
|
108
|
+
self._device = value
|
109
|
+
if not self.label:
|
110
|
+
self.label = value
|
111
|
+
self.device_changed.emit(old_device, value)
|
112
|
+
|
113
|
+
@SafeProperty(bool)
|
114
|
+
def hide_device_selection(self):
|
115
|
+
"""Hide the device selection"""
|
116
|
+
return not self.ui.tool_button.isVisible()
|
117
|
+
|
118
|
+
@hide_device_selection.setter
|
119
|
+
def hide_device_selection(self, value: bool):
|
120
|
+
"""Set the device selection visibility"""
|
121
|
+
self.ui.tool_button.setVisible(not value)
|
122
|
+
|
123
|
+
@SafeSlot(bool)
|
124
|
+
def show_device_selection(self, value: bool):
|
125
|
+
"""Show the device selection
|
126
|
+
|
127
|
+
Args:
|
128
|
+
value (bool): Show the device selection
|
129
|
+
"""
|
130
|
+
self.hide_device_selection = not value
|
131
|
+
|
132
|
+
@SafeSlot(str)
|
133
|
+
def set_positioner(self, positioner: str | Positioner):
|
134
|
+
"""Set the device
|
135
|
+
|
136
|
+
Args:
|
137
|
+
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
138
|
+
"""
|
139
|
+
if isinstance(positioner, Positioner):
|
140
|
+
positioner = positioner.name
|
141
|
+
self.device = positioner
|
142
|
+
|
143
|
+
@SafeSlot(str, str)
|
144
|
+
def on_device_change(self, old_device: str, new_device: str):
|
145
|
+
"""Upon changing the device, a check will be performed if the device is a Positioner.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
old_device (str): The old device name.
|
149
|
+
new_device (str): The new device name.
|
150
|
+
"""
|
151
|
+
if not self._check_device_is_valid(new_device):
|
152
|
+
return
|
153
|
+
self._on_device_change(
|
154
|
+
old_device,
|
155
|
+
new_device,
|
156
|
+
self.position_update.emit,
|
157
|
+
self.update_limits,
|
158
|
+
self.on_device_readback,
|
159
|
+
self._device_ui_components(new_device),
|
160
|
+
)
|
161
|
+
|
162
|
+
def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents:
|
163
|
+
return {
|
164
|
+
"spinner": self.ui.spinner_widget,
|
165
|
+
"position_indicator": self.ui.position_indicator,
|
166
|
+
"readback": self.ui.readback,
|
167
|
+
"setpoint": self.ui.setpoint,
|
168
|
+
"step_size": self.ui.step_size,
|
169
|
+
"device_box": self.ui.device_box,
|
170
|
+
"stop": self.ui.stop,
|
171
|
+
"tweak_increase": self.ui.tweak_right,
|
172
|
+
"tweak_decrease": self.ui.tweak_left,
|
173
|
+
}
|
174
|
+
|
175
|
+
@SafeSlot(dict, dict)
|
176
|
+
def on_device_readback(self, msg_content: dict, metadata: dict):
|
177
|
+
"""Callback for device readback.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
msg_content (dict): The message content.
|
181
|
+
metadata (dict): The message metadata.
|
182
|
+
"""
|
183
|
+
self._on_device_readback(
|
184
|
+
self.device,
|
185
|
+
self._device_ui_components(self.device),
|
186
|
+
msg_content,
|
187
|
+
metadata,
|
188
|
+
self.position_update.emit,
|
189
|
+
self.update_limits,
|
190
|
+
)
|
191
|
+
|
192
|
+
def update_limits(self, limits: tuple):
|
193
|
+
"""Update limits
|
194
|
+
|
195
|
+
Args:
|
196
|
+
limits (tuple): Limits of the positioner
|
197
|
+
"""
|
198
|
+
if limits == self._limits:
|
199
|
+
return
|
200
|
+
self._limits = limits
|
201
|
+
self._update_limits_ui(limits, self.ui.position_indicator, self.setpoint_validator)
|
202
|
+
|
203
|
+
@SafeSlot()
|
204
|
+
def on_stop(self):
|
205
|
+
self._stop_device(self.device)
|
206
|
+
|
207
|
+
@property
|
208
|
+
def step_size(self):
|
209
|
+
"""Step size for tweak"""
|
210
|
+
return self.ui.step_size.value()
|
211
|
+
|
212
|
+
@SafeSlot()
|
213
|
+
def on_tweak_right(self):
|
214
|
+
"""Tweak motor right"""
|
215
|
+
self.dev[self.device].move(self.step_size, relative=True)
|
216
|
+
|
217
|
+
@SafeSlot()
|
218
|
+
def on_tweak_left(self):
|
219
|
+
"""Tweak motor left"""
|
220
|
+
self.dev[self.device].move(-self.step_size, relative=True)
|
221
|
+
|
222
|
+
@SafeSlot()
|
223
|
+
def on_setpoint_change(self):
|
224
|
+
"""Change the setpoint for the motor"""
|
225
|
+
self.ui.setpoint.clearFocus()
|
226
|
+
setpoint = self.ui.setpoint.text()
|
227
|
+
self.dev[self.device].move(float(setpoint), relative=False)
|
228
|
+
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
|
229
|
+
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
|
230
|
+
|
231
|
+
|
232
|
+
if __name__ == "__main__": # pragma: no cover
|
233
|
+
import sys
|
234
|
+
|
235
|
+
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
236
|
+
|
237
|
+
app = QApplication(sys.argv)
|
238
|
+
set_theme("dark")
|
239
|
+
widget = PositionerBox(device="bpm4i")
|
240
|
+
|
241
|
+
widget.show()
|
242
|
+
sys.exit(app.exec_())
|