bec-widgets 0.82.1__py3-none-any.whl → 0.83.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.
Files changed (44) hide show
  1. .gitlab-ci.yml +3 -0
  2. CHANGELOG.md +32 -34
  3. PKG-INFO +1 -1
  4. bec_widgets/cli/client_utils.py +5 -2
  5. bec_widgets/cli/server.py +24 -10
  6. bec_widgets/utils/bec_connector.py +2 -0
  7. bec_widgets/utils/bec_widget.py +6 -0
  8. bec_widgets/utils/generate_designer_plugin.py +6 -5
  9. bec_widgets/utils/reference_utils.py +92 -0
  10. bec_widgets/widgets/console/console.py +1 -1
  11. bec_widgets/widgets/device_box/__init__.py +0 -0
  12. bec_widgets/widgets/device_box/device_box.py +197 -0
  13. bec_widgets/widgets/device_box/device_box.pyproject +1 -0
  14. bec_widgets/widgets/device_box/device_box.ui +179 -0
  15. bec_widgets/widgets/device_box/device_box_plugin.py +54 -0
  16. bec_widgets/widgets/device_box/register_device_box.py +15 -0
  17. bec_widgets/widgets/figure/figure.py +3 -3
  18. bec_widgets/widgets/position_indicator/position_indicator.py +71 -0
  19. bec_widgets/widgets/position_indicator/position_indicator.pyproject +1 -0
  20. bec_widgets/widgets/position_indicator/position_indicator_plugin.py +54 -0
  21. bec_widgets/widgets/position_indicator/register_position_indicator.py +17 -0
  22. bec_widgets/widgets/spinner/__init__.py +0 -0
  23. bec_widgets/widgets/spinner/register_spinner_widget.py +15 -0
  24. bec_widgets/widgets/spinner/spinner.py +85 -0
  25. bec_widgets/widgets/spinner/spinner_widget.pyproject +1 -0
  26. bec_widgets/widgets/spinner/spinner_widget_plugin.py +54 -0
  27. bec_widgets/widgets/vscode/vscode.py +0 -14
  28. bec_widgets/widgets/website/website.py +1 -1
  29. {bec_widgets-0.82.1.dist-info → bec_widgets-0.83.0.dist-info}/METADATA +1 -1
  30. {bec_widgets-0.82.1.dist-info → bec_widgets-0.83.0.dist-info}/RECORD +44 -21
  31. pyproject.toml +1 -1
  32. tests/references/SpinnerWidget/SpinnerWidget_darwin.png +0 -0
  33. tests/references/SpinnerWidget/SpinnerWidget_linux.png +0 -0
  34. tests/references/SpinnerWidget/SpinnerWidget_started_darwin.png +0 -0
  35. tests/references/SpinnerWidget/SpinnerWidget_started_linux.png +0 -0
  36. tests/unit_tests/client_mocks.py +9 -1
  37. tests/unit_tests/test_client_utils.py +47 -0
  38. tests/unit_tests/test_device_box.py +98 -0
  39. tests/unit_tests/test_rpc_server.py +42 -0
  40. tests/unit_tests/test_spinner.py +30 -0
  41. tests/unit_tests/test_vscode_widget.py +27 -32
  42. {bec_widgets-0.82.1.dist-info → bec_widgets-0.83.0.dist-info}/WHEEL +0 -0
  43. {bec_widgets-0.82.1.dist-info → bec_widgets-0.83.0.dist-info}/entry_points.txt +0 -0
  44. {bec_widgets-0.82.1.dist-info → bec_widgets-0.83.0.dist-info}/licenses/LICENSE +0 -0
.gitlab-ci.yml CHANGED
@@ -144,6 +144,9 @@ tests:
144
144
  coverage_report:
145
145
  coverage_format: cobertura
146
146
  path: coverage.xml
147
+ paths:
148
+ - tests/reference_failures/
149
+ when: always
147
150
 
148
151
  test-matrix:
149
152
  parallel:
CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.83.0 (2024-07-08)
4
+
5
+ ### Feature
6
+
7
+ * feat: added reference utils to compare renderings of widgets ([`2988fd3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2988fd387e6b8076fffec1d57e3ccab89ddb2aeb))
8
+
9
+ * feat(widgets): added device box with spinner ([`1b017ed`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b017edfad8e78fa079210486123976695b8915c))
10
+
11
+ * feat(designer): added option to skip the widget validation for DesignerPluginGenerator ([`41bcb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/41bcb801674ab6c4d6069bba34ffee09c9e665db))
12
+
13
+ ### Fix
14
+
15
+ * fix(terminal): added default args to avoid designer crashes on startup ([`360d171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360d17135573e44b80ab517756da3c0b31daab0f))
16
+
17
+ * fix(widget): fixed widget cleanup routine ([`2b29e34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b29e34b52d056349647bb2fcf649b749a60d292))
18
+
19
+ * fix(bec_widget): added cleanup method to bec widget base class ([`fd8766e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fd8766ed87770661da6591aeb4df5abdaf38afc7))
20
+
21
+ * fix(website): fixed dummy input ([`903ce7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/903ce7d46b5d37d40486d0fda92d3694d3faca62))
22
+
23
+ ### Test
24
+
25
+ * test(vscode): fixed vscode tests for new cleanup routine ([`eb26e2a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb26e2a11b229a52efe2e6d4fb28d760d3740136))
26
+
27
+ * test(vscode): improved vscode test ([`5de8804`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5de8804da1e41eafad2472344904b3324438c13b))
28
+
29
+ ## v0.82.2 (2024-07-08)
30
+
31
+ ### Fix
32
+
33
+ * fix(rpc_server): pass cli config to server ([`90178e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/90178e2f61fa9dac7d82c0d0db40a9767bb133e6))
34
+
3
35
  ## v0.82.1 (2024-07-07)
4
36
 
5
37
  ### Fix
@@ -111,37 +143,3 @@
111
143
  * fix(toolbar): change default color to black to match BECFigure theme ([`b8774e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8774e0b0bc43dcd00f94f42539a778e507ca27d))
112
144
 
113
145
  * fix(motor_map): fixed bug with residual trace after changing motors ([`aaa0d10`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aaa0d1003d2e94b45bafe4f700852c2c05288aea))
114
-
115
- * fix(widget_io): widget handler adjusted for spinboxes and comboboxes ([`3dc0532`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3dc0532df05b6ec0a2522107fa0b1e210ce7d91b))
116
-
117
- ### Refactor
118
-
119
- * refactor(toolbar): cleanup and adjusted colors ([`96863ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96863adf53c15112645d20eb6200733617801c6d))
120
-
121
- ## v0.78.1 (2024-07-02)
122
-
123
- ### Fix
124
-
125
- * fix(ui_loader): ui loader is compatible with bec plugins ([`b787759`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b787759f44486dc7af2c03811efb156041e4b6cb))
126
-
127
- ## v0.78.0 (2024-07-02)
128
-
129
- ### Feature
130
-
131
- * feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog ([`c36bb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36bb80d6a4939802a4a1c8e5452c7b94bac185e))
132
-
133
- ## v0.77.0 (2024-07-02)
134
-
135
- ### Feature
136
-
137
- * feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
138
-
139
- ### Fix
140
-
141
- * fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
142
-
143
- * fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
144
-
145
- * fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
146
-
147
- * fix(image): processing of already displayed data; closes #106 ([`1173510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1173510105d2d70d7e498c2ac1e122cea3a16597))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.82.1
3
+ Version: 0.83.0
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
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import importlib
4
4
  import importlib.metadata as imd
5
+ import json
5
6
  import os
6
7
  import select
7
8
  import subprocess
@@ -87,7 +88,7 @@ def _get_output(process, logger) -> None:
87
88
  print(f"Error reading process output: {str(e)}")
88
89
 
89
90
 
90
- def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
91
+ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None:
91
92
  """
92
93
  Start the plot in a new process.
93
94
 
@@ -98,6 +99,8 @@ def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
98
99
  # pylint: disable=subprocess-run-check
99
100
  command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__]
100
101
  if config:
102
+ if isinstance(config, dict):
103
+ config = json.dumps(config)
101
104
  command.extend(["--config", config])
102
105
 
103
106
  env_dict = os.environ.copy()
@@ -190,7 +193,7 @@ class BECGuiClientMixin:
190
193
  if self._process is None or self._process.poll() is not None:
191
194
  self._start_update_script()
192
195
  self._process, self._process_output_processing_thread = _start_plot_process(
193
- self._gui_id, self.__class__, self._client._service_config.config_path
196
+ self._gui_id, self.__class__, self._client._service_config.config
194
197
  )
195
198
  while not self.gui_is_alive():
196
199
  print("Waiting for GUI to start...")
bec_widgets/cli/server.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import json
4
5
  import signal
5
6
  import sys
6
7
  from contextlib import redirect_stderr, redirect_stdout
@@ -141,10 +142,30 @@ class SimpleFileLikeFromLogOutputFunc:
141
142
  return
142
143
 
143
144
 
145
+ def _start_server(gui_id: str, gui_class: Union[BECFigure, BECDockArea], config: str | None = None):
146
+ if config:
147
+ try:
148
+ config = json.loads(config)
149
+ service_config = ServiceConfig(config=config)
150
+ except (json.JSONDecodeError, TypeError):
151
+ service_config = ServiceConfig(config_path=config)
152
+ else:
153
+ # if no config is provided, use the default config
154
+ service_config = ServiceConfig()
155
+
156
+ bec_logger.configure(
157
+ service_config.redis,
158
+ QtRedisConnector,
159
+ service_name="BECWidgetsCLIServer",
160
+ service_config=service_config.service_config,
161
+ )
162
+ server = BECWidgetsCLIServer(gui_id=gui_id, config=service_config, gui_class=gui_class)
163
+ return server
164
+
165
+
144
166
  def main():
145
167
  import argparse
146
168
  import os
147
- import sys
148
169
 
149
170
  from qtpy.QtCore import QSize
150
171
  from qtpy.QtGui import QIcon
@@ -159,7 +180,7 @@ def main():
159
180
  type=str,
160
181
  help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
161
182
  )
162
- parser.add_argument("--config", type=str, help="Config file")
183
+ parser.add_argument("--config", type=str, help="Config file or config string.")
163
184
 
164
185
  args = parser.parse_args()
165
186
 
@@ -188,14 +209,7 @@ def main():
188
209
  win = QMainWindow()
189
210
  win.setWindowTitle("BEC Widgets")
190
211
 
191
- service_config = ServiceConfig(args.config)
192
- bec_logger.configure(
193
- service_config.redis,
194
- QtRedisConnector,
195
- service_name="BECWidgetsCLIServer",
196
- service_config=service_config.service_config,
197
- )
198
- server = BECWidgetsCLIServer(gui_id=args.id, config=service_config, gui_class=gui_class)
212
+ server = _start_server(args.id, gui_class, args.config)
199
213
 
200
214
  gui = server.gui
201
215
  win.setCentralWidget(gui)
@@ -289,6 +289,8 @@ class BECConnector(BECWidget):
289
289
  print("No more connections. Shutting down GUI BEC client.")
290
290
  self.bec_dispatcher.disconnect_all()
291
291
  self.client.shutdown()
292
+ if hasattr(super(), "cleanup"):
293
+ super().cleanup()
292
294
 
293
295
  # def closeEvent(self, event):
294
296
  # self.cleanup()
@@ -1,2 +1,8 @@
1
1
  class BECWidget:
2
2
  """Base class for all BEC widgets."""
3
+
4
+ def closeEvent(self, event):
5
+ if hasattr(self, "cleanup"):
6
+ self.cleanup()
7
+ if hasattr(super(), "closeEvent"):
8
+ super().closeEvent(event)
@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
58
58
  os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
59
59
  )
60
60
 
61
- def run(self):
61
+ def run(self, validate=True):
62
62
  if self._excluded:
63
63
  print(f"Plugin {self.widget.__name__} is excluded from generation.")
64
64
  return
65
- self._check_class_validity()
65
+ if validate:
66
+ self._check_class_validity()
66
67
  self._load_templates()
67
68
  self._write_templates()
68
69
 
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
142
143
 
143
144
  if __name__ == "__main__": # pragma: no cover
144
145
  # from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
145
- from bec_widgets.widgets.dock import BECDockArea
146
+ from bec_widgets.widgets.spinner.spinner import SpinnerWidget
146
147
 
147
- generator = DesignerPluginGenerator(BECDockArea)
148
- generator.run()
148
+ generator = DesignerPluginGenerator(SpinnerWidget)
149
+ generator.run(validate=False)
@@ -0,0 +1,92 @@
1
+ import os
2
+ import sys
3
+
4
+ from PIL import Image, ImageChops
5
+ from qtpy.QtGui import QPixmap
6
+
7
+ import bec_widgets
8
+
9
+ REFERENCE_DIR = os.path.join(
10
+ os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
11
+ )
12
+ REFERENCE_DIR_FAILURES = os.path.join(
13
+ os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
14
+ )
15
+
16
+
17
+ def compare_images(image1_path: str, reference_image_path: str):
18
+ """
19
+ Load two images and compare them pixel by pixel
20
+
21
+ Args:
22
+ image1_path(str): The path to the first image
23
+ reference_image_path(str): The path to the reference image
24
+
25
+ Raises:
26
+ ValueError: If the images are different
27
+ """
28
+ image1 = Image.open(image1_path)
29
+ image2 = Image.open(reference_image_path)
30
+ if image1.size != image2.size:
31
+ raise ValueError("Image size has changed")
32
+ diff = ImageChops.difference(image1, image2)
33
+ if diff.getbbox():
34
+ # copy image1 to the reference directory to upload as artifact
35
+ os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
36
+ image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
37
+ image1.save(image_name)
38
+ print(f"Image saved to {image_name}")
39
+
40
+ raise ValueError("Images are different")
41
+
42
+
43
+ def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
44
+ """
45
+ Save a rendering of a widget and compare it to a reference image
46
+
47
+ Args:
48
+ widget(any): The widget to render
49
+ output_directory(str): The directory to save the image to
50
+ suffix(str): A suffix to append to the image name
51
+
52
+ Raises:
53
+ ValueError: If the images are different
54
+
55
+ Examples:
56
+ snap_and_compare(widget, tmpdir, suffix="started")
57
+
58
+ """
59
+
60
+ if not isinstance(output_directory, str):
61
+ output_directory = str(output_directory)
62
+
63
+ os_suffix = sys.platform
64
+
65
+ name = (
66
+ f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
67
+ if suffix
68
+ else f"{widget.__class__.__name__}_{os_suffix}.png"
69
+ )
70
+
71
+ # Save the widget to a pixmap
72
+ test_image_path = os.path.join(output_directory, name)
73
+ pixmap = QPixmap(widget.size())
74
+ widget.render(pixmap)
75
+ pixmap.save(test_image_path)
76
+
77
+ try:
78
+ reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
79
+ reference_image_path = os.path.join(reference_path, name)
80
+
81
+ if not os.path.exists(reference_image_path):
82
+ raise ValueError(f"Reference image not found: {reference_image_path}")
83
+
84
+ compare_images(test_image_path, reference_image_path)
85
+
86
+ except ValueError:
87
+ image = Image.open(test_image_path)
88
+ os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
89
+ image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
90
+ image.save(image_name)
91
+ print(f"Image saved to {image_name}")
92
+ raise
@@ -222,7 +222,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
222
222
  Start ``Backend`` process and render Pyte output as text.
223
223
  """
224
224
 
225
- def __init__(self, parent, numColumns, numLines, **kwargs):
225
+ def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
226
226
  super().__init__(parent)
227
227
 
228
228
  # file descriptor to communicate with the subprocess
File without changes
@@ -0,0 +1,197 @@
1
+ import os
2
+ import uuid
3
+
4
+ from bec_lib.endpoints import MessageEndpoints
5
+ from bec_lib.messages import ScanQueueMessage
6
+ from qtpy.QtCore import Property, Signal, Slot
7
+ from qtpy.QtGui import QDoubleValidator
8
+ from qtpy.QtWidgets import QDoubleSpinBox, QVBoxLayout, QWidget
9
+
10
+ from bec_widgets.utils import UILoader
11
+ from bec_widgets.utils.bec_connector import BECConnector
12
+
13
+
14
+ class DeviceBox(BECConnector, QWidget):
15
+ device_changed = Signal(str, str)
16
+
17
+ def __init__(self, parent=None, device=None, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+ QWidget.__init__(self, parent=parent)
20
+ self.get_bec_shortcuts()
21
+ self._device = ""
22
+ self._limits = None
23
+
24
+ self.init_ui()
25
+
26
+ if device is not None:
27
+ self.device = device
28
+ self.init_device()
29
+
30
+ def init_ui(self):
31
+ self.device_changed.connect(self.on_device_change)
32
+
33
+ current_path = os.path.dirname(__file__)
34
+ self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
35
+
36
+ self.layout = QVBoxLayout(self)
37
+ self.layout.addWidget(self.ui)
38
+ self.layout.setSpacing(0)
39
+ self.layout.setContentsMargins(0, 0, 0, 0)
40
+
41
+ # fix the size of the device box
42
+ db = self.ui.device_box
43
+ db.setFixedHeight(234)
44
+ db.setFixedWidth(224)
45
+
46
+ self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
47
+ self.ui.stop.clicked.connect(self.on_stop)
48
+ self.ui.tweak_right.clicked.connect(self.on_tweak_right)
49
+ self.ui.tweak_right.setToolTip("Tweak right")
50
+ self.ui.tweak_left.clicked.connect(self.on_tweak_left)
51
+ self.ui.tweak_left.setToolTip("Tweak left")
52
+ self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
53
+
54
+ self.setpoint_validator = QDoubleValidator()
55
+ self.ui.setpoint.setValidator(self.setpoint_validator)
56
+ self.ui.spinner_widget.start()
57
+
58
+ def init_device(self):
59
+ if self.device in self.dev:
60
+ data = self.dev[self.device].read()
61
+ self.on_device_readback({"signals": data}, {})
62
+
63
+ @Property(str)
64
+ def device(self):
65
+ return self._device
66
+
67
+ @device.setter
68
+ def device(self, value):
69
+ if not value or not isinstance(value, str):
70
+ return
71
+ old_device = self._device
72
+ self._device = value
73
+ self.device_changed.emit(old_device, value)
74
+
75
+ @Slot(str, str)
76
+ def on_device_change(self, old_device: str, new_device: str):
77
+ if new_device not in self.dev:
78
+ print(f"Device {new_device} not found in the device list")
79
+ return
80
+ print(f"Device changed from {old_device} to {new_device}")
81
+ self.init_device()
82
+ self.bec_dispatcher.disconnect_slot(
83
+ self.on_device_readback, MessageEndpoints.device_readback(old_device)
84
+ )
85
+ self.bec_dispatcher.connect_slot(
86
+ self.on_device_readback, MessageEndpoints.device_readback(new_device)
87
+ )
88
+ self.ui.device_box.setTitle(new_device)
89
+ self.ui.readback.setToolTip(f"{self.device} readback")
90
+ self.ui.setpoint.setToolTip(f"{self.device} setpoint")
91
+ self.ui.step_size.setToolTip(f"Step size for {new_device}")
92
+
93
+ precision = self.dev[new_device].precision
94
+ if precision is not None:
95
+ self.ui.step_size.setDecimals(precision)
96
+ self.ui.step_size.setValue(10**-precision * 10)
97
+
98
+ @Slot(dict, dict)
99
+ def on_device_readback(self, msg_content: dict, metadata: dict):
100
+ signals = msg_content.get("signals", {})
101
+ # pylint: disable=protected-access
102
+ hinted_signals = self.dev[self.device]._hints
103
+ precision = self.dev[self.device].precision
104
+
105
+ readback_val = None
106
+ setpoint_val = None
107
+
108
+ if len(hinted_signals) == 1:
109
+ signal = hinted_signals[0]
110
+ readback_val = signals.get(signal, {}).get("value")
111
+
112
+ if f"{self.device}_setpoint" in signals:
113
+ setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
114
+
115
+ if f"{self.device}_motor_is_moving" in signals:
116
+ is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
117
+ if is_moving:
118
+ self.ui.spinner_widget.start()
119
+ self.ui.spinner_widget.setToolTip("Device is moving")
120
+ else:
121
+ self.ui.spinner_widget.stop()
122
+ self.ui.spinner_widget.setToolTip("Device is idle")
123
+
124
+ if readback_val is not None:
125
+ self.ui.readback.setText(f"{readback_val:.{precision}f}")
126
+
127
+ if setpoint_val is not None:
128
+ self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
129
+
130
+ limits = self.dev[self.device].limits
131
+ self.update_limits(limits)
132
+ if limits is not None and readback_val is not None and limits[0] != limits[1]:
133
+ pos = (readback_val - limits[0]) / (limits[1] - limits[0])
134
+ self.ui.position_indicator.on_position_update(pos)
135
+
136
+ def update_limits(self, limits):
137
+ if limits == self._limits:
138
+ return
139
+ self._limits = limits
140
+ if limits is not None and limits[0] != limits[1]:
141
+ self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
142
+ self.setpoint_validator.setRange(limits[0], limits[1])
143
+ else:
144
+ self.ui.position_indicator.setToolTip("No limits set")
145
+ self.setpoint_validator.setRange(float("-inf"), float("inf"))
146
+
147
+ @Slot()
148
+ def on_stop(self):
149
+ request_id = str(uuid.uuid4())
150
+ params = {
151
+ "device": self.device,
152
+ "rpc_id": request_id,
153
+ "func": "stop",
154
+ "args": [],
155
+ "kwargs": {},
156
+ }
157
+ msg = ScanQueueMessage(
158
+ scan_type="device_rpc",
159
+ parameter=params,
160
+ queue="emergency",
161
+ metadata={"RID": request_id, "response": False},
162
+ )
163
+ self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
164
+
165
+ @property
166
+ def step_size(self):
167
+ return self.ui.step_size.value()
168
+
169
+ @Slot()
170
+ def on_tweak_right(self):
171
+ self.dev[self.device].move(self.step_size, relative=True)
172
+
173
+ @Slot()
174
+ def on_tweak_left(self):
175
+ self.dev[self.device].move(-self.step_size, relative=True)
176
+
177
+ @Slot()
178
+ def on_setpoint_change(self):
179
+ self.ui.setpoint.clearFocus()
180
+ setpoint = self.ui.setpoint.text()
181
+ self.dev[self.device].move(float(setpoint), relative=False)
182
+ self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
183
+ self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
184
+
185
+
186
+ if __name__ == "__main__": # pragma: no cover
187
+ import sys
188
+
189
+ import qdarktheme
190
+ from qtpy.QtWidgets import QApplication
191
+
192
+ app = QApplication(sys.argv)
193
+ qdarktheme.setup_theme("light")
194
+ widget = DeviceBox(device="samx")
195
+
196
+ widget.show()
197
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['device_box.py']}