bec-widgets 0.53.2__py3-none-any.whl → 0.54.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 (67) hide show
  1. CHANGELOG.md +24 -25
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +13 -13
  4. bec_widgets/cli/client_utils.py +0 -4
  5. bec_widgets/cli/generate_cli.py +7 -5
  6. bec_widgets/cli/server.py +5 -7
  7. bec_widgets/examples/jupyter_console/jupyter_console_window.py +7 -3
  8. bec_widgets/examples/motor_movement/motor_control_compilations.py +17 -16
  9. bec_widgets/widgets/__init__.py +0 -10
  10. bec_widgets/widgets/figure/figure.py +40 -23
  11. bec_widgets/widgets/figure/plots/__init__.py +0 -0
  12. bec_widgets/widgets/figure/plots/image/__init__.py +0 -0
  13. bec_widgets/widgets/{plots → figure/plots/image}/image.py +6 -416
  14. bec_widgets/widgets/figure/plots/image/image_item.py +277 -0
  15. bec_widgets/widgets/figure/plots/image/image_processor.py +152 -0
  16. bec_widgets/widgets/figure/plots/motor_map/__init__.py +0 -0
  17. bec_widgets/widgets/{plots → figure/plots/motor_map}/motor_map.py +2 -2
  18. bec_widgets/widgets/figure/plots/waveform/__init__.py +0 -0
  19. bec_widgets/widgets/{plots → figure/plots/waveform}/waveform.py +9 -222
  20. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +227 -0
  21. bec_widgets/widgets/motor_control/__init__.py +0 -7
  22. bec_widgets/widgets/motor_control/motor_control.py +2 -948
  23. bec_widgets/widgets/motor_control/motor_table/__init__.py +0 -0
  24. bec_widgets/widgets/motor_control/motor_table/motor_table.py +483 -0
  25. bec_widgets/widgets/motor_control/movement_absolute/__init__.py +0 -0
  26. bec_widgets/widgets/motor_control/movement_absolute/movement_absolute.py +157 -0
  27. bec_widgets/widgets/motor_control/movement_relative/__init__.py +0 -0
  28. bec_widgets/widgets/motor_control/movement_relative/movement_relative.py +227 -0
  29. bec_widgets/widgets/motor_control/selection/__init__.py +0 -0
  30. bec_widgets/widgets/motor_control/selection/selection.py +110 -0
  31. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/METADATA +1 -1
  32. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/RECORD +51 -52
  33. docs/requirements.txt +1 -0
  34. pyproject.toml +1 -1
  35. tests/end-2-end/test_bec_dock_rpc_e2e.py +1 -1
  36. tests/end-2-end/test_bec_figure_rpc_e2e.py +4 -4
  37. tests/end-2-end/test_rpc_register_e2e.py +1 -1
  38. tests/unit_tests/test_bec_dock.py +1 -1
  39. tests/unit_tests/test_bec_figure.py +6 -4
  40. tests/unit_tests/test_bec_motor_map.py +2 -3
  41. tests/unit_tests/test_motor_control.py +6 -5
  42. tests/unit_tests/test_waveform1d.py +13 -1
  43. bec_widgets/validation/__init__.py +0 -2
  44. bec_widgets/validation/monitor_config_validator.py +0 -258
  45. bec_widgets/widgets/monitor/__init__.py +0 -1
  46. bec_widgets/widgets/monitor/config_dialog.py +0 -574
  47. bec_widgets/widgets/monitor/config_dialog.ui +0 -210
  48. bec_widgets/widgets/monitor/example_configs/config_device.yaml +0 -60
  49. bec_widgets/widgets/monitor/example_configs/config_scans.yaml +0 -92
  50. bec_widgets/widgets/monitor/monitor.py +0 -845
  51. bec_widgets/widgets/monitor/tab_template.ui +0 -180
  52. bec_widgets/widgets/motor_map/__init__.py +0 -1
  53. bec_widgets/widgets/motor_map/motor_map.py +0 -594
  54. bec_widgets/widgets/plots/__init__.py +0 -4
  55. tests/unit_tests/test_bec_monitor.py +0 -220
  56. tests/unit_tests/test_config_dialog.py +0 -178
  57. tests/unit_tests/test_motor_map.py +0 -171
  58. tests/unit_tests/test_validator_errors.py +0 -110
  59. /bec_widgets/{cli → assets}/bec_widgets_icon.png +0 -0
  60. /bec_widgets/{examples/jupyter_console → assets}/terminal_icon.png +0 -0
  61. /bec_widgets/widgets/{plots → figure/plots}/plot_base.py +0 -0
  62. /bec_widgets/widgets/motor_control/{motor_control_table.ui → motor_table/motor_table.ui} +0 -0
  63. /bec_widgets/widgets/motor_control/{motor_control_absolute.ui → movement_absolute/movement_absolute.ui} +0 -0
  64. /bec_widgets/widgets/motor_control/{motor_control_relative.ui → movement_relative/movement_relative.ui} +0 -0
  65. /bec_widgets/widgets/motor_control/{motor_control_selection.ui → selection/selection.ui} +0 -0
  66. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/WHEEL +0 -0
  67. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,483 @@
1
+ # pylint: disable = no-name-in-module,missing-module-docstring
2
+ import os
3
+
4
+ from qtpy import uic
5
+ from qtpy.QtCore import Qt
6
+ from qtpy.QtCore import Signal as pyqtSignal
7
+ from qtpy.QtCore import Slot as pyqtSlot
8
+ from qtpy.QtGui import QDoubleValidator, QKeySequence
9
+ from qtpy.QtWidgets import (
10
+ QCheckBox,
11
+ QLineEdit,
12
+ QMessageBox,
13
+ QPushButton,
14
+ QShortcut,
15
+ QTableWidget,
16
+ QTableWidgetItem,
17
+ )
18
+
19
+ from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
20
+
21
+
22
+ class MotorCoordinateTable(MotorControlWidget):
23
+ """
24
+ Widget to save coordinates from motor, display them in the table and move back to them.
25
+ There are two modes of operation:
26
+ - Individual: Each row is a single coordinate.
27
+ - Start/Stop: Each pair of rows is a start and end coordinate.
28
+ Signals:
29
+ plot_coordinates_signal (pyqtSignal(list, str, str)): Signal to plot the coordinates in the MotorMap.
30
+ Slots:
31
+ add_coordinate (pyqtSlot(tuple)): Slot to add a coordinate to the table.
32
+ mode_switch (pyqtSlot): Slot to switch between individual and start/stop mode.
33
+ """
34
+
35
+ plot_coordinates_signal = pyqtSignal(list, str, str)
36
+
37
+ def _load_ui(self):
38
+ """Load the UI for the coordinate table."""
39
+ current_path = os.path.dirname(__file__)
40
+ uic.loadUi(os.path.join(current_path, "motor_table.ui"), self)
41
+
42
+ def _init_ui(self):
43
+ """Initialize the UI"""
44
+ # Setup table behaviour
45
+ self._setup_table()
46
+ self.table.setSelectionBehavior(QTableWidget.SelectRows)
47
+
48
+ # for tag columns default tag
49
+ self.tag_counter = 1
50
+
51
+ # Connect signals and slots
52
+ self.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
53
+ self.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
54
+
55
+ # Keyboard shortcuts for deleting a row
56
+ self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.table)
57
+ self.delete_shortcut.activated.connect(self.delete_selected_row)
58
+ self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table)
59
+ self.backspace_shortcut.activated.connect(self.delete_selected_row)
60
+
61
+ # Warning message for mode switch enable/disable
62
+ self.warning_message = True
63
+
64
+ @pyqtSlot(dict)
65
+ def on_config_update(self, config: dict) -> None:
66
+ """
67
+ Update config dict
68
+ Args:
69
+ config(dict): New config dict
70
+ """
71
+ self.config = config
72
+
73
+ # Get motor names
74
+ self.motor_x, self.motor_y = (
75
+ self.config["motor_control"]["motor_x"],
76
+ self.config["motor_control"]["motor_y"],
77
+ )
78
+
79
+ # Decimal precision of the table coordinates
80
+ self.precision = self.config["motor_control"].get("precision", 3)
81
+
82
+ # Mode switch default option
83
+ self.mode = self.config["motor_control"].get("mode", "Individual")
84
+
85
+ # Set combobox to default mode
86
+ self.comboBox_mode.setCurrentText(self.mode)
87
+
88
+ self._init_ui()
89
+
90
+ def _setup_table(self):
91
+ """Setup the table with appropriate headers and configurations."""
92
+ mode = self.comboBox_mode.currentText()
93
+
94
+ if mode == "Individual":
95
+ self._setup_individual_mode()
96
+ elif mode == "Start/Stop":
97
+ self._setup_start_stop_mode()
98
+ self.start_stop_counter = 0 # TODO: remove this??
99
+
100
+ self.wipe_motor_map_coordinates()
101
+
102
+ def _setup_individual_mode(self):
103
+ """Setup the table for individual mode."""
104
+ self.table.setColumnCount(5)
105
+ self.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
106
+ self.table.verticalHeader().setVisible(False)
107
+
108
+ def _setup_start_stop_mode(self):
109
+ """Setup the table for start/stop mode."""
110
+ self.table.setColumnCount(8)
111
+ self.table.setHorizontalHeaderLabels(
112
+ [
113
+ "Show",
114
+ "Move [start]",
115
+ "Move [end]",
116
+ "Tag",
117
+ "X [start]",
118
+ "Y [start]",
119
+ "X [end]",
120
+ "Y [end]",
121
+ ]
122
+ )
123
+ self.table.verticalHeader().setVisible(False)
124
+ # Set flag to track if the coordinate is stat or the end of the entry
125
+ self.is_next_entry_end = False
126
+
127
+ def mode_switch(self):
128
+ """Switch between individual and start/stop mode."""
129
+ last_selected_index = self.comboBox_mode.currentIndex()
130
+
131
+ if self.table.rowCount() > 0 and self.warning_message is True:
132
+ msgBox = QMessageBox()
133
+ msgBox.setIcon(QMessageBox.Critical)
134
+ msgBox.setText(
135
+ "Switching modes will delete all table entries. Do you want to continue?"
136
+ )
137
+ msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
138
+ returnValue = msgBox.exec()
139
+
140
+ if returnValue is QMessageBox.Cancel:
141
+ self.comboBox_mode.blockSignals(True) # Block signals
142
+ self.comboBox_mode.setCurrentIndex(last_selected_index)
143
+ self.comboBox_mode.blockSignals(False) # Unblock signals
144
+ return
145
+
146
+ # Wipe table
147
+ self.wipe_motor_map_coordinates()
148
+
149
+ # Initiate new table with new mode
150
+ self._setup_table()
151
+
152
+ @pyqtSlot(tuple)
153
+ def add_coordinate(self, coordinates: tuple):
154
+ """
155
+ Add a coordinate to the table.
156
+ Args:
157
+ coordinates(tuple): Coordinates (x,y) to add to the table.
158
+ """
159
+ tag = f"Pos {self.tag_counter}"
160
+ self.tag_counter += 1
161
+ x, y = coordinates
162
+ self._add_row(tag, x, y)
163
+
164
+ def _add_row(self, tag: str, x: float, y: float) -> None:
165
+ """
166
+ Add a row to the table.
167
+ Args:
168
+ tag(str): Tag of the coordinate.
169
+ x(float): X coordinate.
170
+ y(float): Y coordinate.
171
+ """
172
+
173
+ mode = self.comboBox_mode.currentText()
174
+ if mode == "Individual":
175
+ checkbox_pos = 0
176
+ button_pos = 1
177
+ tag_pos = 2
178
+ x_pos = 3
179
+ y_pos = 4
180
+ coordinate_reference = "Individual"
181
+ color = "green"
182
+
183
+ # Add new row -> new entry
184
+ row_count = self.table.rowCount()
185
+ self.table.insertRow(row_count)
186
+
187
+ # Add Widgets
188
+ self._add_widgets(
189
+ tag,
190
+ x,
191
+ y,
192
+ row_count,
193
+ checkbox_pos,
194
+ tag_pos,
195
+ button_pos,
196
+ x_pos,
197
+ y_pos,
198
+ coordinate_reference,
199
+ color,
200
+ )
201
+
202
+ if mode == "Start/Stop":
203
+ # These positions are always fixed
204
+ checkbox_pos = 0
205
+ tag_pos = 3
206
+
207
+ if self.is_next_entry_end is False: # It is the start position of the entry
208
+ print("Start position")
209
+ button_pos = 1
210
+ x_pos = 4
211
+ y_pos = 5
212
+ coordinate_reference = "Start"
213
+ color = "blue"
214
+
215
+ # Add new row -> new entry
216
+ row_count = self.table.rowCount()
217
+ self.table.insertRow(row_count)
218
+
219
+ # Add Widgets
220
+ self._add_widgets(
221
+ tag,
222
+ x,
223
+ y,
224
+ row_count,
225
+ checkbox_pos,
226
+ tag_pos,
227
+ button_pos,
228
+ x_pos,
229
+ y_pos,
230
+ coordinate_reference,
231
+ color,
232
+ )
233
+
234
+ # Next entry will be the end of the current entry
235
+ self.is_next_entry_end = True
236
+
237
+ elif self.is_next_entry_end is True: # It is the end position of the entry
238
+ print("End position")
239
+ row_count = self.table.rowCount() - 1 # Current row
240
+ button_pos = 2
241
+ x_pos = 6
242
+ y_pos = 7
243
+ coordinate_reference = "Stop"
244
+ color = "red"
245
+
246
+ # Add Widgets
247
+ self._add_widgets(
248
+ tag,
249
+ x,
250
+ y,
251
+ row_count,
252
+ checkbox_pos,
253
+ tag_pos,
254
+ button_pos,
255
+ x_pos,
256
+ y_pos,
257
+ coordinate_reference,
258
+ color,
259
+ )
260
+ self.is_next_entry_end = False # Next entry will be the start of the new entry
261
+
262
+ # Auto table resize
263
+ self.resize_table_auto()
264
+
265
+ def _add_widgets(
266
+ self,
267
+ tag: str,
268
+ x: float,
269
+ y: float,
270
+ row: int,
271
+ checkBox_pos: int,
272
+ tag_pos: int,
273
+ button_pos: int,
274
+ x_pos: int,
275
+ y_pos: int,
276
+ coordinate_reference: str,
277
+ color: str,
278
+ ) -> None:
279
+ """
280
+ Add widgets to the table.
281
+ Args:
282
+ tag(str): Tag of the coordinate.
283
+ x(float): X coordinate.
284
+ y(float): Y coordinate.
285
+ row(int): Row of the QTableWidget where to add the widgets.
286
+ checkBox_pos(int): Column where to put CheckBox.
287
+ tag_pos(int): Column where to put Tag.
288
+ button_pos(int): Column where to put Move button.
289
+ x_pos(int): Column where to link x coordinate.
290
+ y_pos(int): Column where to link y coordinate.
291
+ coordinate_reference(str): Reference to the coordinate for MotorMap.
292
+ color(str): Color of the coordinate for MotorMap.
293
+ """
294
+ # Add widgets
295
+ self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
296
+ self._add_move_button(row, button_pos, x_pos, y_pos)
297
+ self.table.setItem(row, tag_pos, QTableWidgetItem(tag))
298
+ self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
299
+ self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
300
+
301
+ # # Emit the coordinates to be plotted
302
+ self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
303
+
304
+ # Connect item edit to emit coordinates
305
+ self.table.itemChanged.connect(
306
+ lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
307
+ )
308
+ self.table.itemChanged.connect(
309
+ lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
310
+ )
311
+
312
+ def _add_checkbox(self, row: int, checkBox_pos: int, x_pos: int, y_pos: int):
313
+ """
314
+ Add a checkbox to the table.
315
+ Args:
316
+ row(int): Row of QTableWidget where to add the checkbox.
317
+ checkBox_pos(int): Column where to put CheckBox.
318
+ x_pos(int): Column where to link x coordinate.
319
+ y_pos(int): Column where to link y coordinate.
320
+ """
321
+ show_checkbox = QCheckBox()
322
+ show_checkbox.setChecked(True)
323
+ show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
324
+ self.table.setCellWidget(row, checkBox_pos, show_checkbox)
325
+
326
+ def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
327
+ """
328
+ Add a move button to the table.
329
+ Args:
330
+ row(int): Row of QTableWidget where to add the move button.
331
+ button_pos(int): Column where to put move button.
332
+ x_pos(int): Column where to link x coordinate.
333
+ y_pos(int): Column where to link y coordinate.
334
+ """
335
+ move_button = QPushButton("Move")
336
+ move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
337
+ self.table.setCellWidget(row, button_pos, move_button)
338
+
339
+ def _add_line_edit(
340
+ self,
341
+ value: float,
342
+ row: int,
343
+ line_pos: int,
344
+ x_pos: int,
345
+ y_pos: int,
346
+ coordinate_reference: str,
347
+ color: str,
348
+ ) -> None:
349
+ """
350
+ Add a QLineEdit to the table.
351
+ Args:
352
+ value(float): Initial value of the QLineEdit.
353
+ row(int): Row of QTableWidget where to add the QLineEdit.
354
+ line_pos(int): Column where to put QLineEdit.
355
+ x_pos(int): Column where to link x coordinate.
356
+ y_pos(int): Column where to link y coordinate.
357
+ coordinate_reference(str): Reference to the coordinate for MotorMap.
358
+ color(str): Color of the coordinate for MotorMap.
359
+ """
360
+ # Adding validator
361
+ validator = QDoubleValidator()
362
+ validator.setDecimals(self.precision)
363
+
364
+ # Create line edit
365
+ edit = QLineEdit(str(f"{value:.{self.precision}f}"))
366
+ edit.setValidator(validator)
367
+ edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
368
+
369
+ # Add line edit to the table
370
+ self.table.setCellWidget(row, line_pos, edit)
371
+ edit.textChanged.connect(
372
+ lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
373
+ )
374
+
375
+ def wipe_motor_map_coordinates(self):
376
+ """Wipe the motor map coordinates."""
377
+ try:
378
+ self.table.itemChanged.disconnect() # Disconnect all previous connections
379
+ except TypeError:
380
+ print("No previous connections to disconnect")
381
+ self.table.setRowCount(0)
382
+ reference_tags = ["Individual", "Start", "Stop"]
383
+ for reference_tag in reference_tags:
384
+ self.plot_coordinates_signal.emit([], reference_tag, "green")
385
+
386
+ def handle_move_button_click(self, x_pos: int, y_pos: int) -> None:
387
+ """
388
+ Handle the move button click.
389
+ Args:
390
+ x_pos(int): X position of the coordinate.
391
+ y_pos(int): Y position of the coordinate.
392
+ """
393
+ button = self.sender()
394
+ row = self.table.indexAt(button.pos()).row()
395
+
396
+ x = self.get_coordinate(row, x_pos)
397
+ y = self.get_coordinate(row, y_pos)
398
+ self.move_motor(x, y)
399
+
400
+ def emit_plot_coordinates(self, x_pos: float, y_pos: float, reference_tag: str, color: str):
401
+ """
402
+ Emit the coordinates to be plotted.
403
+ Args:
404
+ x_pos(float): X position of the coordinate.
405
+ y_pos(float): Y position of the coordinate.
406
+ reference_tag(str): Reference tag of the coordinate.
407
+ color(str): Color of the coordinate.
408
+ """
409
+ print(
410
+ f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
411
+ )
412
+ coordinates = []
413
+ for row in range(self.table.rowCount()):
414
+ show = self.table.cellWidget(row, 0).isChecked()
415
+ x = self.get_coordinate(row, x_pos)
416
+ y = self.get_coordinate(row, y_pos)
417
+
418
+ coordinates.append((x, y, show)) # (x, y, show_flag)
419
+ self.plot_coordinates_signal.emit(coordinates, reference_tag, color)
420
+
421
+ def get_coordinate(self, row: int, column: int) -> float:
422
+ """
423
+ Helper function to get the coordinate from the table QLineEdit cells.
424
+ Args:
425
+ row(int): Row of the table.
426
+ column(int): Column of the table.
427
+ Returns:
428
+ float: Value of the coordinate.
429
+ """
430
+ edit = self.table.cellWidget(row, column)
431
+ value = float(edit.text()) if edit and edit.text() != "" else None
432
+ if value:
433
+ return value
434
+
435
+ def delete_selected_row(self):
436
+ """Delete the selected row from the table."""
437
+ selected_rows = self.table.selectionModel().selectedRows()
438
+ for row in selected_rows:
439
+ self.table.removeRow(row.row())
440
+ if self.comboBox_mode.currentText() == "Start/Stop":
441
+ self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
442
+ self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
443
+ self.is_next_entry_end = False
444
+ elif self.comboBox_mode.currentText() == "Individual":
445
+ self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
446
+
447
+ def resize_table_auto(self):
448
+ """Resize the table to fit the contents."""
449
+ if self.checkBox_resize_auto.isChecked():
450
+ self.table.resizeColumnsToContents()
451
+
452
+ def move_motor(self, x: float, y: float) -> None:
453
+ """
454
+ Move the motor to the target coordinates.
455
+ Args:
456
+ x(float): Target x coordinate.
457
+ y(float): Target y coordinate.
458
+ """
459
+ self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
460
+
461
+ @pyqtSlot(str, str)
462
+ def change_motors(self, motor_x: str, motor_y: str) -> None:
463
+ """
464
+ Change the active motors and update config.
465
+ Can be connected to the selected_motors_signal from MotorControlSelection.
466
+ Args:
467
+ motor_x(str): New motor X to be controlled.
468
+ motor_y(str): New motor Y to be controlled.
469
+ """
470
+ self.motor_x = motor_x
471
+ self.motor_y = motor_y
472
+ self.config["motor_control"]["motor_x"] = motor_x
473
+ self.config["motor_control"]["motor_y"] = motor_y
474
+
475
+ @pyqtSlot(int)
476
+ def set_precision(self, precision: int) -> None:
477
+ """
478
+ Set the precision of the coordinates.
479
+ Args:
480
+ precision(int): Precision of the coordinates.
481
+ """
482
+ self.precision = precision
483
+ self.config["motor_control"]["precision"] = precision
@@ -0,0 +1,157 @@
1
+ import os
2
+
3
+ from qtpy import uic
4
+ from qtpy.QtCore import Signal as pyqtSignal
5
+ from qtpy.QtCore import Slot as pyqtSlot
6
+
7
+ from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
8
+
9
+
10
+ class MotorControlAbsolute(MotorControlWidget):
11
+ """
12
+ Widget for controlling the motors to absolute coordinates.
13
+
14
+ Signals:
15
+ coordinates_signal (pyqtSignal(tuple)): Signal to emit the coordinates.
16
+ Slots:
17
+ change_motors (pyqtSlot): Slot to change the active motors.
18
+ enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls.
19
+ """
20
+
21
+ coordinates_signal = pyqtSignal(tuple)
22
+
23
+ def _load_ui(self):
24
+ """Load the UI from the .ui file."""
25
+ current_path = os.path.dirname(__file__)
26
+ uic.loadUi(os.path.join(current_path, "movement_absolute.ui"), self)
27
+
28
+ def _init_ui(self):
29
+ """Initialize the UI."""
30
+
31
+ # Check if there are any motors connected
32
+ if self.motor_x is None or self.motor_y is None:
33
+ self.motorControl_absolute.setEnabled(False)
34
+ return
35
+
36
+ # Move to absolute coordinates
37
+ self.pushButton_go_absolute.clicked.connect(
38
+ lambda: self.move_motor_absolute(
39
+ self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
40
+ )
41
+ )
42
+
43
+ self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
44
+ self.pushButton_save.clicked.connect(self.save_current_coordinates)
45
+ self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
46
+
47
+ # Enable/Disable GUI
48
+ self.motor_thread.lock_gui.connect(self.enable_motor_controls)
49
+
50
+ # Error messages
51
+ self.motor_thread.motor_error.connect(
52
+ lambda error: MotorControlErrors.display_error_message(error)
53
+ )
54
+
55
+ # Keyboard shortcuts
56
+ self._init_keyboard_shortcuts()
57
+
58
+ @pyqtSlot(dict)
59
+ def on_config_update(self, config: dict) -> None:
60
+ """Update config dict"""
61
+ self.config = config
62
+
63
+ # Get motor names
64
+ self.motor_x, self.motor_y = (
65
+ self.config["motor_control"]["motor_x"],
66
+ self.config["motor_control"]["motor_y"],
67
+ )
68
+
69
+ # Update step precision
70
+ self.precision = self.config["motor_control"]["precision"]
71
+
72
+ self._init_ui()
73
+
74
+ @pyqtSlot(bool)
75
+ def enable_motor_controls(self, enable: bool) -> None:
76
+ """
77
+ Enable or disable the motor controls.
78
+ Args:
79
+ enable(bool): True to enable, False to disable.
80
+ """
81
+
82
+ # Disable or enable all controls within the motorControl_absolute group box
83
+ for widget in self.motorControl_absolute.findChildren(QWidget):
84
+ widget.setEnabled(enable)
85
+
86
+ # Enable the pushButton_stop if the motor is moving
87
+ self.pushButton_stop.setEnabled(True)
88
+
89
+ @pyqtSlot(str, str)
90
+ def change_motors(self, motor_x: str, motor_y: str):
91
+ """
92
+ Change the active motors and update config.
93
+ Can be connected to the selected_motors_signal from MotorControlSelection.
94
+ Args:
95
+ motor_x(str): New motor X to be controlled.
96
+ motor_y(str): New motor Y to be controlled.
97
+ """
98
+ self.motor_x = motor_x
99
+ self.motor_y = motor_y
100
+ self.config["motor_control"]["motor_x"] = motor_x
101
+ self.config["motor_control"]["motor_y"] = motor_y
102
+
103
+ @pyqtSlot(int)
104
+ def set_precision(self, precision: int) -> None:
105
+ """
106
+ Set the precision of the coordinates.
107
+ Args:
108
+ precision(int): Precision of the coordinates.
109
+ """
110
+ self.precision = precision
111
+ self.config["motor_control"]["precision"] = precision
112
+ self.spinBox_absolute_x.setDecimals(precision)
113
+ self.spinBox_absolute_y.setDecimals(precision)
114
+
115
+ def move_motor_absolute(self, x: float, y: float) -> None:
116
+ """
117
+ Move the motor to the target coordinates.
118
+ Args:
119
+ x(float): Target x coordinate.
120
+ y(float): Target y coordinate.
121
+ """
122
+ # self._enable_motor_controls(False)
123
+ target_coordinates = (x, y)
124
+ self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
125
+ if self.checkBox_save_with_go.isChecked():
126
+ self.save_absolute_coordinates()
127
+
128
+ def _init_keyboard_shortcuts(self):
129
+ """Initialize the keyboard shortcuts."""
130
+ # Go absolute button
131
+ self.pushButton_go_absolute.setShortcut("Ctrl+G")
132
+ self.pushButton_go_absolute.setToolTip("Ctrl+G")
133
+
134
+ # Set absolute coordinates
135
+ self.pushButton_set.setShortcut("Ctrl+D")
136
+ self.pushButton_set.setToolTip("Ctrl+D")
137
+
138
+ # Save Current coordinates
139
+ self.pushButton_save.setShortcut("Ctrl+S")
140
+ self.pushButton_save.setToolTip("Ctrl+S")
141
+
142
+ # Stop Button
143
+ self.pushButton_stop.setShortcut("Ctrl+X")
144
+ self.pushButton_stop.setToolTip("Ctrl+X")
145
+
146
+ def save_absolute_coordinates(self):
147
+ """Emit the setup coordinates from the spinboxes"""
148
+
149
+ x, y = round(self.spinBox_absolute_x.value(), self.precision), round(
150
+ self.spinBox_absolute_y.value(), self.precision
151
+ )
152
+ self.coordinates_signal.emit((x, y))
153
+
154
+ def save_current_coordinates(self):
155
+ """Emit the current coordinates from the motor thread"""
156
+ x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
157
+ self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))