bec-widgets 0.55.0__py3-none-any.whl → 0.56.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 (44) hide show
  1. .gitlab-ci.yml +113 -8
  2. CHANGELOG.md +34 -28
  3. PKG-INFO +3 -1
  4. bec_widgets/examples/jupyter_console/jupyter_console_window.py +28 -38
  5. bec_widgets/examples/motor_movement/motor_control_compilations.py +1 -7
  6. bec_widgets/utils/__init__.py +1 -0
  7. bec_widgets/utils/crosshair.py +13 -9
  8. bec_widgets/utils/ui_loader.py +58 -0
  9. bec_widgets/widgets/motor_control/motor_table/motor_table.py +44 -43
  10. bec_widgets/widgets/motor_control/movement_absolute/movement_absolute.py +25 -23
  11. bec_widgets/widgets/motor_control/movement_relative/movement_relative.py +51 -48
  12. bec_widgets/widgets/spiral_progress_bar/ring.py +5 -5
  13. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/METADATA +3 -1
  14. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/RECORD +22 -43
  15. docs/user/apps.md +1 -26
  16. pyproject.toml +2 -1
  17. tests/end-2-end/test_bec_dock_rpc_e2e.py +1 -1
  18. tests/unit_tests/test_client_utils.py +2 -2
  19. tests/unit_tests/test_crosshair.py +5 -5
  20. tests/unit_tests/test_motor_control.py +49 -45
  21. bec_widgets/examples/eiger_plot/__init__.py +0 -0
  22. bec_widgets/examples/eiger_plot/eiger_plot.py +0 -307
  23. bec_widgets/examples/eiger_plot/eiger_plot.ui +0 -207
  24. bec_widgets/examples/mca_readout/__init__.py +0 -0
  25. bec_widgets/examples/mca_readout/mca_plot.py +0 -159
  26. bec_widgets/examples/mca_readout/mca_sim.py +0 -28
  27. bec_widgets/examples/modular_app/___init__.py +0 -0
  28. bec_widgets/examples/modular_app/modular.ui +0 -92
  29. bec_widgets/examples/modular_app/modular_app.py +0 -197
  30. bec_widgets/examples/motor_movement/config_example.yaml +0 -17
  31. bec_widgets/examples/motor_movement/csax_bec_config.yaml +0 -10
  32. bec_widgets/examples/motor_movement/csaxs_config.yaml +0 -17
  33. bec_widgets/examples/motor_movement/motor_example.py +0 -1344
  34. bec_widgets/examples/stream_plot/__init__.py +0 -0
  35. bec_widgets/examples/stream_plot/line_plot.ui +0 -155
  36. bec_widgets/examples/stream_plot/stream_plot.py +0 -337
  37. docs/user/apps/modular_app.md +0 -6
  38. docs/user/apps/motor_app.md +0 -34
  39. docs/user/apps/motor_app_10fps.gif +0 -0
  40. docs/user/apps/plot_app.md +0 -6
  41. tests/unit_tests/test_eiger_plot.py +0 -115
  42. tests/unit_tests/test_stream_plot.py +0 -158
  43. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/WHEEL +0 -0
  44. {bec_widgets-0.55.0.dist-info → bec_widgets-0.56.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,1344 +0,0 @@
1
- import csv
2
- import os
3
- from enum import Enum
4
- from functools import partial
5
-
6
- import numpy as np
7
- import pyqtgraph as pg
8
- from bec_lib import messages
9
- from bec_lib.endpoints import MessageEndpoints
10
- from pyqtgraph.Qt import QtCore, QtWidgets, uic
11
- from qtpy import QtGui
12
- from qtpy.QtCore import Qt, QThread
13
- from qtpy.QtCore import Signal as pyqtSignal
14
- from qtpy.QtCore import Slot as pyqtSlot
15
- from qtpy.QtGui import QDoubleValidator, QKeySequence
16
- from qtpy.QtWidgets import (
17
- QApplication,
18
- QDialog,
19
- QFileDialog,
20
- QFrame,
21
- QLabel,
22
- QMessageBox,
23
- QPushButton,
24
- QShortcut,
25
- QVBoxLayout,
26
- QWidget,
27
- )
28
-
29
- from bec_widgets.utils import DoubleValidationDelegate
30
-
31
- # TODO - General features
32
- # - put motor status (moving, stopped, etc)
33
- # - add mouse interactions with the plot -> click to select coordinates, double click to move?
34
- # - adjust right click actions
35
-
36
-
37
- class MotorApp(QWidget):
38
- """
39
- Main class for MotorApp, designed to control motor positions based on a flexible YAML configuration.
40
-
41
- Attributes:
42
- coordinates_updated (pyqtSignal): Signal to trigger coordinate updates.
43
- selected_motors (dict): Dictionary containing pre-selected motors from the configuration file.
44
- plot_motors (dict): Dictionary containing settings for plotting motor positions.
45
-
46
- Args:
47
- selected_motors (dict): Dictionary specifying the selected motors.
48
- plot_motors (dict): Dictionary specifying settings for plotting motor positions.
49
- parent (QWidget, optional): Parent widget.
50
- """
51
-
52
- coordinates_updated = pyqtSignal(float, float)
53
-
54
- def __init__(self, selected_motors: dict = {}, plot_motors: dict = {}, parent=None):
55
- super(MotorApp, self).__init__(parent)
56
- current_path = os.path.dirname(__file__)
57
- uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self)
58
-
59
- # Motor Control Thread
60
- self.motor_thread = MotorControl()
61
-
62
- self.motor_x, self.motor_y = None, None
63
- self.limit_x, self.limit_y = None, None
64
-
65
- # Coordinates tracking
66
- self.motor_positions = np.array([])
67
-
68
- # Config file settings
69
- self.max_points = plot_motors.get("max_points", 5000)
70
- self.num_dim_points = plot_motors.get("num_dim_points", 100)
71
- self.scatter_size = plot_motors.get("scatter_size", 5)
72
- self.precision = plot_motors.get("precision", 2)
73
- self.extra_columns = plot_motors.get("extra_columns", None)
74
- self.mode_lock = plot_motors.get("mode_lock", False)
75
-
76
- # Saved motors from config file
77
- self.selected_motors = selected_motors
78
-
79
- # QThread for motor movement + signals
80
- self.motor_thread.motors_loaded.connect(self.get_available_motors)
81
- self.motor_thread.motors_selected.connect(self.get_selected_motors)
82
- self.motor_thread.limits_retrieved.connect(self.update_limits)
83
-
84
- # UI
85
- self.init_ui()
86
- self.tag_N = 1 # position label for saved coordinates
87
-
88
- # State tracking for entries
89
- self.last_selected_index = -1
90
- self.is_next_entry_end = False
91
-
92
- # Get all motors available
93
- self.motor_thread.retrieve_all_motors() # TODO link to combobox that it always refresh
94
-
95
- def connect_motor(self, motor_x_name: str, motor_y_name: str):
96
- """
97
- Connects to the specified motors and initializes the UI for motor control.
98
-
99
- Args:
100
- motor_x_name (str): Name of the motor controlling the x-axis.
101
- motor_y_name (str): Name of the motor controlling the y-axis.
102
- """
103
- self.motor_thread.connect_motors(motor_x_name, motor_y_name)
104
- self.motor_thread.retrieve_motor_limits(self.motor_x, self.motor_y)
105
-
106
- # self.init_motor_map()
107
-
108
- self.motorControl.setEnabled(True)
109
- self.motorControl_absolute.setEnabled(True)
110
- self.tabWidget_tables.setTabEnabled(1, True)
111
-
112
- self.generate_table_coordinate(
113
- self.tableWidget_coordinates,
114
- self.motor_thread.retrieve_coordinates(),
115
- tag=f"{motor_x_name},{motor_y_name}",
116
- precision=self.precision,
117
- )
118
-
119
- @pyqtSlot(object, object)
120
- def get_selected_motors(self, motor_x, motor_y):
121
- """
122
- Slot to receive and set the selected motors.
123
-
124
- Args:
125
- motor_x (object): The selected motor for the x-axis.
126
- motor_y (object): The selected motor for the y-axis.
127
- """
128
- self.motor_x, self.motor_y = motor_x, motor_y
129
-
130
- @pyqtSlot(list, list)
131
- def get_available_motors(self, motors_x, motors_y):
132
- """
133
- Slot to populate the available motors in the combo boxes and set the index based on the configuration.
134
-
135
- Args:
136
- motors_x (list): List of available motors for the x-axis.
137
- motors_y (list): List of available motors for the y-axis.
138
- """
139
- self.comboBox_motor_x.addItems(motors_x)
140
- self.comboBox_motor_y.addItems(motors_y)
141
-
142
- # Set index based on the motor names in the configuration, if available
143
- selected_motor_x = ""
144
- selected_motor_y = ""
145
-
146
- if self.selected_motors:
147
- selected_motor_x = self.selected_motors.get("motor_x", "")
148
- selected_motor_y = self.selected_motors.get("motor_y", "")
149
-
150
- index_x = self.comboBox_motor_x.findText(selected_motor_x)
151
- index_y = self.comboBox_motor_y.findText(selected_motor_y)
152
-
153
- if index_x != -1:
154
- self.comboBox_motor_x.setCurrentIndex(index_x)
155
- else:
156
- print(
157
- f"Warning: Motor '{selected_motor_x}' specified in the config file is not available."
158
- )
159
- self.comboBox_motor_x.setCurrentIndex(0) # Optionally set to first item or any default
160
-
161
- if index_y != -1:
162
- self.comboBox_motor_y.setCurrentIndex(index_y)
163
- else:
164
- print(
165
- f"Warning: Motor '{selected_motor_y}' specified in the config file is not available."
166
- )
167
- self.comboBox_motor_y.setCurrentIndex(0) # Optionally set to first item or any default
168
-
169
- @pyqtSlot(list, list)
170
- def update_limits(self, x_limits: list, y_limits: list) -> None:
171
- """
172
- Slot to update the limits for x and y motors.
173
-
174
- Args:
175
- x_limits (list): List containing the lower and upper limits for the x-axis motor.
176
- y_limits (list): List containing the lower and upper limits for the y-axis motor.
177
- """
178
- self.limit_x = x_limits
179
- self.limit_y = y_limits
180
- self.spinBox_x_min.setValue(self.limit_x[0])
181
- self.spinBox_x_max.setValue(self.limit_x[1])
182
- self.spinBox_y_min.setValue(self.limit_y[0])
183
- self.spinBox_y_max.setValue(self.limit_y[1])
184
-
185
- for spinBox in (
186
- self.spinBox_x_min,
187
- self.spinBox_x_max,
188
- self.spinBox_y_min,
189
- self.spinBox_y_max,
190
- ):
191
- spinBox.setStyleSheet("")
192
-
193
- # TODO - names can be get from MotorController
194
- self.label_Y_max.setText(f"+ ({self.motor_y.name})")
195
- self.label_Y_min.setText(f"- ({self.motor_y.name})")
196
- self.label_X_max.setText(f"+ ({self.motor_x.name})")
197
- self.label_X_min.setText(f"- ({self.motor_x.name})")
198
-
199
- self.init_motor_map() # reinitialize the map with the new limits
200
-
201
- @pyqtSlot()
202
- def enable_motor_control(self):
203
- self.motorControl.setEnabled(True)
204
-
205
- def enable_motor_controls(self, disable: bool) -> None:
206
- self.motorControl.setEnabled(disable)
207
- self.motorSelection.setEnabled(disable)
208
-
209
- # Disable or enable all controls within the motorControl_absolute group box
210
- for widget in self.motorControl_absolute.findChildren(QtWidgets.QWidget):
211
- widget.setEnabled(disable)
212
-
213
- # Enable the pushButton_stop if the motor is moving
214
- self.pushButton_stop.setEnabled(True)
215
-
216
- def move_motor_absolute(self, x: float, y: float) -> None:
217
- self.enable_motor_controls(False)
218
- target_coordinates = (x, y)
219
- self.motor_thread.move_to_coordinates(target_coordinates)
220
- if self.checkBox_save_with_go.isChecked():
221
- self.save_absolute_coordinates()
222
-
223
- def move_motor_relative(self, motor, axis: str, direction: int) -> None:
224
- self.enable_motor_controls(False)
225
- if axis == "x":
226
- step = direction * self.spinBox_step_x.value()
227
- elif axis == "y":
228
- step = direction * self.spinBox_step_y.value()
229
- self.motor_thread.move_relative(motor, step)
230
-
231
- def update_plot_setting(self, max_points, num_dim_points, scatter_size):
232
- self.max_points = max_points
233
- self.num_dim_points = num_dim_points
234
- self.scatter_size = scatter_size
235
-
236
- for spinBox in (
237
- self.spinBox_max_points,
238
- self.spinBox_num_dim_points,
239
- self.spinBox_scatter_size,
240
- ):
241
- spinBox.setStyleSheet("")
242
-
243
- def set_from_config(self) -> None:
244
- """Set the values from the config file to the UI elements"""
245
-
246
- self.spinBox_max_points.setValue(self.max_points)
247
- self.spinBox_num_dim_points.setValue(self.num_dim_points)
248
- self.spinBox_scatter_size.setValue(self.scatter_size)
249
- self.spinBox_precision.setValue(self.precision)
250
- self.update_precision(self.precision)
251
-
252
- def init_ui_plot_elements(self) -> None:
253
- """Initialize the plot elements"""
254
- self.label_coorditanes = self.glw.addLabel(f"Motor position: (X, Y)", row=0, col=0)
255
- self.plot_map = self.glw.addPlot(row=1, col=0)
256
- self.limit_map = pg.ImageItem()
257
- self.plot_map.addItem(self.limit_map)
258
- self.motor_map = pg.ScatterPlotItem(
259
- size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255)
260
- )
261
- self.motor_map.setZValue(0)
262
-
263
- self.saved_motor_map_start = pg.ScatterPlotItem(
264
- size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 255)
265
- )
266
- self.saved_motor_map_end = pg.ScatterPlotItem(
267
- size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(0, 0, 255, 255)
268
- )
269
-
270
- self.saved_motor_map_individual = pg.ScatterPlotItem(
271
- size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(0, 255, 0, 255)
272
- )
273
-
274
- self.saved_motor_map_start.setZValue(1) # for saved motor positions
275
- self.saved_motor_map_end.setZValue(1) # for saved motor positions
276
- self.saved_motor_map_individual.setZValue(1) # for saved motor positions
277
-
278
- self.plot_map.addItem(self.motor_map)
279
- self.plot_map.addItem(self.saved_motor_map_start)
280
- self.plot_map.addItem(self.saved_motor_map_end)
281
- self.plot_map.addItem(self.saved_motor_map_individual)
282
- self.plot_map.showGrid(x=True, y=True)
283
-
284
- def init_ui_motor_control(self) -> None:
285
- """Initialize the motor control elements"""
286
-
287
- # Connect checkbox and spinBoxes
288
- self.checkBox_same_xy.stateChanged.connect(self.sync_step_sizes)
289
- self.spinBox_step_x.valueChanged.connect(self.update_step_size_x)
290
- self.spinBox_step_y.valueChanged.connect(self.update_step_size_y)
291
-
292
- self.toolButton_right.clicked.connect(
293
- lambda: self.move_motor_relative(self.motor_x, "x", 1)
294
- )
295
- self.toolButton_left.clicked.connect(
296
- lambda: self.move_motor_relative(self.motor_x, "x", -1)
297
- )
298
- self.toolButton_up.clicked.connect(lambda: self.move_motor_relative(self.motor_y, "y", 1))
299
- self.toolButton_down.clicked.connect(
300
- lambda: self.move_motor_relative(self.motor_y, "y", -1)
301
- )
302
-
303
- # Switch between key shortcuts active
304
- self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts)
305
- self.update_arrow_key_shortcuts()
306
-
307
- # Move to absolute coordinates
308
- self.pushButton_go_absolute.clicked.connect(
309
- lambda: self.move_motor_absolute(
310
- self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
311
- )
312
- )
313
-
314
- self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
315
- self.pushButton_save.clicked.connect(self.save_current_coordinates)
316
- self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
317
-
318
- # Enable/Disable GUI
319
- self.motor_thread.move_finished.connect(lambda: self.enable_motor_controls(True))
320
-
321
- # Precision update
322
- self.spinBox_precision.valueChanged.connect(lambda x: self.update_precision(x))
323
-
324
- def init_ui_motor_configs(self) -> None:
325
- """Limit and plot spinBoxes"""
326
-
327
- # SpinBoxes change color to yellow before updated, limits are updated with update button
328
- self.spinBox_x_min.valueChanged.connect(lambda: self.param_changed(self.spinBox_x_min))
329
- self.spinBox_x_max.valueChanged.connect(lambda: self.param_changed(self.spinBox_x_max))
330
- self.spinBox_y_min.valueChanged.connect(lambda: self.param_changed(self.spinBox_y_min))
331
- self.spinBox_y_max.valueChanged.connect(lambda: self.param_changed(self.spinBox_y_max))
332
-
333
- # SpinBoxes - Max Points and N Dim Points
334
- self.spinBox_max_points.valueChanged.connect(
335
- lambda: self.param_changed(self.spinBox_max_points)
336
- )
337
- self.spinBox_num_dim_points.valueChanged.connect(
338
- lambda: self.param_changed(self.spinBox_num_dim_points)
339
- )
340
- self.spinBox_scatter_size.valueChanged.connect(
341
- lambda: self.param_changed(self.spinBox_scatter_size)
342
- )
343
-
344
- # Limit Update
345
- self.pushButton_updateLimits.clicked.connect(
346
- lambda: self.update_all_motor_limits(
347
- x_limit=[self.spinBox_x_min.value(), self.spinBox_x_max.value()],
348
- y_limit=[self.spinBox_y_min.value(), self.spinBox_y_max.value()],
349
- )
350
- )
351
-
352
- # Plot Update
353
- self.pushButton_update_config.clicked.connect(
354
- lambda: self.update_plot_setting(
355
- max_points=self.spinBox_max_points.value(),
356
- num_dim_points=self.spinBox_num_dim_points.value(),
357
- scatter_size=self.spinBox_scatter_size.value(),
358
- )
359
- )
360
-
361
- self.pushButton_enableGUI.clicked.connect(lambda: self.enable_motor_controls(True))
362
-
363
- def init_ui_motor_connections(self) -> None:
364
- # Signal from motor thread to update coordinates
365
- self.motor_thread.coordinates_updated.connect(
366
- lambda x, y: self.update_image_map(round(x, self.precision), round(y, self.precision))
367
- )
368
-
369
- # Motor connections button
370
- self.pushButton_connecMotors.clicked.connect(
371
- lambda: self.connect_motor(
372
- self.comboBox_motor_x.currentText(), self.comboBox_motor_y.currentText()
373
- )
374
- )
375
-
376
- # Check if there are any motors connected
377
- if self.motor_x or self.motor_y is None:
378
- self.motorControl.setEnabled(False)
379
- self.motorControl_absolute.setEnabled(False)
380
- self.tabWidget_tables.setTabEnabled(1, False)
381
-
382
- def init_keyboard_shortcuts(self) -> None:
383
- """Initialize the keyboard shortcuts"""
384
-
385
- # Delete table entry
386
- delete_shortcut = QShortcut(QKeySequence("Delete"), self)
387
- backspace_shortcut = QShortcut(QKeySequence("Backspace"), self)
388
- delete_shortcut.activated.connect(self.delete_selected_row)
389
- backspace_shortcut.activated.connect(self.delete_selected_row)
390
-
391
- # Increase/decrease step size for X motor
392
- increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
393
- decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
394
- increase_x_shortcut.activated.connect(lambda: self.change_step_size(self.spinBox_step_x, 2))
395
- decrease_x_shortcut.activated.connect(
396
- lambda: self.change_step_size(self.spinBox_step_x, 0.5)
397
- )
398
-
399
- # Increase/decrease step size for Y motor
400
- increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
401
- decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
402
- increase_y_shortcut.activated.connect(lambda: self.change_step_size(self.spinBox_step_y, 2))
403
- decrease_y_shortcut.activated.connect(
404
- lambda: self.change_step_size(self.spinBox_step_y, 0.5)
405
- )
406
-
407
- # Go absolute button
408
- self.pushButton_go_absolute.setShortcut("Ctrl+G")
409
- self.pushButton_go_absolute.setToolTip("Ctrl+G")
410
-
411
- # Set absolute coordinates
412
- self.pushButton_set.setShortcut("Ctrl+D")
413
- self.pushButton_set.setToolTip("Ctrl+D")
414
-
415
- # Save Current coordinates
416
- self.pushButton_save.setShortcut("Ctrl+S")
417
- self.pushButton_save.setToolTip("Ctrl+S")
418
-
419
- # Stop Button
420
- self.pushButton_stop.setShortcut("Ctrl+X")
421
- self.pushButton_stop.setToolTip("Ctrl+X")
422
-
423
- def init_ui_table(self) -> None:
424
- """Initialize the table validators for x and y coordinates and table signals"""
425
-
426
- # Validators
427
- self.double_delegate = DoubleValidationDelegate(self.tableWidget_coordinates)
428
-
429
- # Init Default mode
430
- self.mode_switch()
431
-
432
- # Buttons
433
- self.pushButton_exportCSV.clicked.connect(
434
- lambda: self.export_table_to_csv(self.tableWidget_coordinates)
435
- )
436
- self.pushButton_importCSV.clicked.connect(
437
- lambda: self.load_table_from_csv(self.tableWidget_coordinates, precision=self.precision)
438
- )
439
- self.pushButton_resize_table.clicked.connect(
440
- lambda: self.resizeTable(self.tableWidget_coordinates)
441
- )
442
- self.pushButton_duplicate.clicked.connect(
443
- lambda: self.duplicate_last_row(self.tableWidget_coordinates)
444
- )
445
- self.pushButton_help.clicked.connect(self.show_help_dialog)
446
-
447
- # Mode switch
448
- self.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
449
-
450
- # Manual Edit
451
- self.tableWidget_coordinates.itemChanged.connect(self.handle_manual_edit)
452
-
453
- def init_mode_lock(self) -> None:
454
- if self.mode_lock is False:
455
- return
456
- elif self.mode_lock == "Individual":
457
- self.comboBox_mode.setCurrentIndex(0)
458
- self.comboBox_mode.setEnabled(False)
459
- elif self.mode_lock == "Start/Stop":
460
- self.comboBox_mode.setCurrentIndex(1)
461
- self.comboBox_mode.setEnabled(False)
462
- else:
463
- self.mode_lock = False
464
- print(f"Warning: Mode lock '{self.mode_lock}' not recognized.")
465
- print(f"Unlocking mode lock.")
466
-
467
- def init_ui(self) -> None:
468
- """Setup all ui elements"""
469
-
470
- self.set_from_config() # Set default parameters
471
- self.init_ui_plot_elements() # 2D Plot
472
- self.init_ui_motor_control() # Motor Controls
473
- self.init_ui_motor_configs() # Motor Configs
474
- self.init_ui_motor_connections() # Motor Connections
475
- self.init_keyboard_shortcuts() # Keyboard Shortcuts
476
- self.init_ui_table() # Table validators for x and y coordinates
477
- self.init_mode_lock() # Mode lock
478
-
479
- def init_motor_map(self):
480
- # Get motor limits
481
- limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(self.motor_x)
482
- limit_y_min, limit_y_max = self.motor_thread.get_motor_limits(self.motor_y)
483
-
484
- self.offset_x = limit_x_min
485
- self.offset_y = limit_y_min
486
-
487
- # Define the size of the image map based on the motor's limits
488
- map_width = int(limit_x_max - limit_x_min + 1)
489
- map_height = int(limit_y_max - limit_y_min + 1)
490
-
491
- # Create an empty image map
492
- self.background_value = 25
493
- self.limit_map_data = np.full(
494
- (map_width, map_height), self.background_value, dtype=np.float32
495
- )
496
- self.limit_map.setImage(self.limit_map_data)
497
-
498
- # Set the initial position on the map
499
- init_pos = self.motor_thread.retrieve_coordinates()
500
- self.motor_positions = np.array([init_pos])
501
- self.brushes = [pg.mkBrush(255, 255, 255, 255)]
502
-
503
- self.motor_map.setData(pos=self.motor_positions, brush=self.brushes)
504
-
505
- # Translate and scale the image item to match the motor coordinates
506
- self.tr = QtGui.QTransform()
507
- self.tr.translate(limit_x_min, limit_y_min)
508
- self.limit_map.setTransform(self.tr)
509
-
510
- if hasattr(self, "highlight_V") and hasattr(self, "highlight_H"):
511
- self.plot_map.removeItem(self.highlight_V)
512
- self.plot_map.removeItem(self.highlight_H)
513
-
514
- # Crosshair to highlight the current position
515
- self.highlight_V = pg.InfiniteLine(
516
- angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
517
- )
518
- self.highlight_H = pg.InfiniteLine(
519
- angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
520
- )
521
-
522
- self.plot_map.addItem(self.highlight_V)
523
- self.plot_map.addItem(self.highlight_H)
524
-
525
- self.highlight_V.setPos(init_pos[0])
526
- self.highlight_H.setPos(init_pos[1])
527
-
528
- def update_image_map(self, x, y):
529
- # Update label
530
- self.label_coorditanes.setText(f"Motor position: ({x}, {y})")
531
-
532
- # Add new point with full brightness
533
- new_pos = np.array([x, y])
534
- self.motor_positions = np.vstack((self.motor_positions, new_pos))
535
-
536
- # If the number of points exceeds max_points, delete the oldest points
537
- if len(self.motor_positions) > self.max_points:
538
- self.motor_positions = self.motor_positions[-self.max_points :]
539
-
540
- # Determine brushes based on position in the array
541
- self.brushes = [pg.mkBrush(50, 50, 50, 255)] * len(self.motor_positions)
542
-
543
- # Calculate the decrement step based on self.num_dim_points
544
- decrement_step = (255 - 50) / self.num_dim_points
545
-
546
- for i in range(1, min(self.num_dim_points + 1, len(self.motor_positions) + 1)):
547
- brightness = max(60, 255 - decrement_step * (i - 1))
548
- self.brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
549
-
550
- self.brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
551
-
552
- self.motor_map.setData(pos=self.motor_positions, brush=self.brushes, size=self.scatter_size)
553
-
554
- # Set Highlight
555
- self.highlight_V.setPos(x)
556
- self.highlight_H.setPos(y)
557
-
558
- def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None:
559
- self.motor_thread.update_all_motor_limits(x_limit=x_limit, y_limit=y_limit)
560
-
561
- def update_arrow_key_shortcuts(self):
562
- if self.checkBox_enableArrows.isChecked():
563
- # Set the arrow key shortcuts for motor movement
564
- self.toolButton_right.setShortcut(Qt.Key_Right)
565
- self.toolButton_left.setShortcut(Qt.Key_Left)
566
- self.toolButton_up.setShortcut(Qt.Key_Up)
567
- self.toolButton_down.setShortcut(Qt.Key_Down)
568
- else:
569
- # Clear the shortcuts
570
- self.toolButton_right.setShortcut("")
571
- self.toolButton_left.setShortcut("")
572
- self.toolButton_up.setShortcut("")
573
- self.toolButton_down.setShortcut("")
574
-
575
- def mode_switch(self):
576
- current_index = self.comboBox_mode.currentIndex()
577
-
578
- if self.tableWidget_coordinates.rowCount() > 0:
579
- msgBox = QMessageBox()
580
- msgBox.setIcon(QMessageBox.Warning)
581
- msgBox.setText(
582
- "Switching modes will delete all table entries. Do you want to continue?"
583
- )
584
- msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
585
- returnValue = msgBox.exec()
586
-
587
- if returnValue == QMessageBox.Cancel:
588
- self.comboBox_mode.blockSignals(True) # Block signals
589
- self.comboBox_mode.setCurrentIndex(self.last_selected_index)
590
- self.comboBox_mode.blockSignals(False) # Unblock signals
591
- return
592
-
593
- self.tableWidget_coordinates.setRowCount(0) # Wipe table
594
-
595
- # Clear saved points from map
596
- self.saved_motor_map_start.clear()
597
- self.saved_motor_map_end.clear()
598
- self.saved_motor_map_individual.clear()
599
-
600
- if current_index == 0: # 'individual' is selected
601
- header = ["Show", "Move", "Tag", "X", "Y"]
602
-
603
- self.tableWidget_coordinates.setColumnCount(len(header))
604
- self.tableWidget_coordinates.setHorizontalHeaderLabels(header)
605
- self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
606
- self.tableWidget_coordinates.setItemDelegateForColumn(4, self.double_delegate)
607
-
608
- elif current_index == 1: # 'start/stop' is selected
609
- header = [
610
- "Show",
611
- "Move [start]",
612
- "Move [end]",
613
- "Tag",
614
- "X [start]",
615
- "Y [start]",
616
- "X [end]",
617
- "Y [end]",
618
- ]
619
- self.tableWidget_coordinates.setColumnCount(len(header))
620
- self.tableWidget_coordinates.setHorizontalHeaderLabels(header)
621
- self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
622
- self.tableWidget_coordinates.setItemDelegateForColumn(4, self.double_delegate)
623
- self.tableWidget_coordinates.setItemDelegateForColumn(5, self.double_delegate)
624
- self.tableWidget_coordinates.setItemDelegateForColumn(6, self.double_delegate)
625
-
626
- self.last_selected_index = current_index # Save the last selected index
627
-
628
- def generate_table_coordinate(
629
- self, table: QtWidgets.QTableWidget, coordinates: tuple, tag: str = None, precision: int = 0
630
- ) -> None:
631
- # To not call replot points during table generation
632
- self.replot_lock = True
633
-
634
- current_index = self.comboBox_mode.currentIndex()
635
-
636
- if current_index == 1 and self.is_next_entry_end:
637
- target_row = table.rowCount() - 1 # Last row
638
- else:
639
- new_row_count = table.rowCount() + 1
640
- table.setRowCount(new_row_count)
641
- target_row = new_row_count - 1 # New row
642
-
643
- # Create QDoubleValidator
644
- validator = QDoubleValidator()
645
- validator.setDecimals(precision)
646
-
647
- # Checkbox for visibility switch -> always first column
648
- checkBox = QtWidgets.QCheckBox()
649
- checkBox.setChecked(True)
650
- checkBox.stateChanged.connect(lambda: self.replot_based_on_table(table))
651
- table.setCellWidget(target_row, 0, checkBox)
652
-
653
- # Apply validator to x and y coordinate QTableWidgetItem
654
- item_x = QtWidgets.QTableWidgetItem(str(f"{coordinates[0]:.{precision}f}"))
655
- item_y = QtWidgets.QTableWidgetItem(str(f"{coordinates[1]:.{precision}f}"))
656
- item_x.setFlags(item_x.flags() | Qt.ItemIsEditable)
657
- item_y.setFlags(item_y.flags() | Qt.ItemIsEditable)
658
-
659
- # Mode switch
660
- if current_index == 1: # start/stop mode
661
- # Create buttons for start and end coordinates
662
- button_start = QPushButton("Go [start]")
663
- button_end = QPushButton("Go [end]")
664
-
665
- # Add buttons to table
666
- table.setCellWidget(target_row, 1, button_start)
667
- table.setCellWidget(target_row, 2, button_end)
668
-
669
- button_end.setEnabled(
670
- self.is_next_entry_end
671
- ) # Enable only if end coordinate is present
672
-
673
- # Connect buttons to the slot
674
- button_start.clicked.connect(self.move_to_row_coordinates)
675
- button_end.clicked.connect(self.move_to_row_coordinates)
676
-
677
- # Set Tag
678
- table.setItem(target_row, 3, QtWidgets.QTableWidgetItem(str(tag)))
679
-
680
- # Add coordinates to table
681
- col_index = 8
682
- if self.is_next_entry_end:
683
- table.setItem(target_row, 6, item_x)
684
- table.setItem(target_row, 7, item_y)
685
- else:
686
- table.setItem(target_row, 4, item_x)
687
- table.setItem(target_row, 5, item_y)
688
- self.is_next_entry_end = not self.is_next_entry_end
689
- else: # Individual mode
690
- button_start = QPushButton("Go")
691
- table.setCellWidget(target_row, 1, button_start)
692
- button_start.clicked.connect(self.move_to_row_coordinates)
693
-
694
- # Set Tag
695
- table.setItem(target_row, 2, QtWidgets.QTableWidgetItem(str(tag)))
696
-
697
- col_index = 5
698
- table.setItem(target_row, 3, item_x)
699
- table.setItem(target_row, 4, item_y)
700
-
701
- # Adding extra columns
702
- # TODO simplify nesting
703
- if current_index != 1 or self.is_next_entry_end:
704
- if self.extra_columns:
705
- table.setColumnCount(col_index + len(self.extra_columns))
706
- for col_dict in self.extra_columns:
707
- for col_name, default_value in col_dict.items():
708
- if target_row == 0:
709
- item = QtWidgets.QTableWidgetItem(str(default_value))
710
-
711
- else:
712
- prev_item = table.item(target_row - 1, col_index)
713
- item_text = prev_item.text() if prev_item else ""
714
- item = QtWidgets.QTableWidgetItem(item_text)
715
-
716
- item.setFlags(item.flags() | Qt.ItemIsEditable)
717
- table.setItem(target_row, col_index, item)
718
-
719
- if target_row == 0 or (current_index == 1 and not self.is_next_entry_end):
720
- table.setHorizontalHeaderItem(
721
- col_index, QtWidgets.QTableWidgetItem(col_name)
722
- )
723
-
724
- col_index += 1
725
-
726
- self.align_table_center(table)
727
-
728
- if self.checkBox_resize_auto.isChecked():
729
- table.resizeColumnsToContents()
730
-
731
- # Unlock Replot
732
- self.replot_lock = False
733
-
734
- # Replot the saved motor map
735
- self.replot_based_on_table(table)
736
-
737
- def duplicate_last_row(self, table: QtWidgets.QTableWidget) -> None:
738
- if self.is_next_entry_end is True:
739
- msgBox = QMessageBox()
740
- msgBox.setIcon(QMessageBox.Warning)
741
- msgBox.setText("The end coordinates were not set for previous entry!")
742
- msgBox.setStandardButtons(QMessageBox.Ok)
743
- returnValue = msgBox.exec()
744
-
745
- if returnValue == QMessageBox.Ok:
746
- return
747
-
748
- last_row = table.rowCount() - 1
749
- if last_row == -1:
750
- return
751
-
752
- # Get the tag and coordinates from the last row
753
- tag = table.item(last_row, 2).text() if table.item(last_row, 2) else None
754
- mode_index = self.comboBox_mode.currentIndex()
755
-
756
- if mode_index == 1: # start/stop mode
757
- x_start = float(table.item(last_row, 4).text()) if table.item(last_row, 4) else None
758
- y_start = float(table.item(last_row, 5).text()) if table.item(last_row, 5) else None
759
- x_end = float(table.item(last_row, 6).text()) if table.item(last_row, 6) else None
760
- y_end = float(table.item(last_row, 7).text()) if table.item(last_row, 7) else None
761
-
762
- # Duplicate the 'start' coordinates
763
- self.generate_table_coordinate(table, (x_start, y_start), tag, precision=self.precision)
764
-
765
- # Duplicate the 'end' coordinates
766
- self.generate_table_coordinate(table, (x_end, y_end), tag, precision=self.precision)
767
-
768
- else: # individual mode
769
- x = float(table.item(last_row, 3).text()) if table.item(last_row, 3) else None
770
- y = float(table.item(last_row, 4).text()) if table.item(last_row, 4) else None
771
-
772
- # Duplicate the coordinates
773
- self.generate_table_coordinate(table, (x, y), tag, precision=self.precision)
774
-
775
- self.align_table_center(table)
776
-
777
- if self.checkBox_resize_auto.isChecked():
778
- table.resizeColumnsToContents()
779
-
780
- def handle_manual_edit(self, item):
781
- table = item.tableWidget()
782
- row, col = item.row(), item.column()
783
- mode_index = self.comboBox_mode.currentIndex()
784
-
785
- # Determine the columns where the x and y coordinates are stored based on the mode.
786
- coord_cols = [3, 4] if mode_index == 0 else [4, 5, 6, 7]
787
-
788
- if col not in coord_cols:
789
- return # Only proceed if the edited columns are coordinate columns
790
-
791
- # Replot based on the table
792
- self.replot_based_on_table(table)
793
-
794
- @staticmethod
795
- def align_table_center(table: QtWidgets.QTableWidget) -> None:
796
- for row in range(table.rowCount()):
797
- for col in range(table.columnCount()):
798
- item = table.item(row, col)
799
- if item:
800
- item.setTextAlignment(Qt.AlignCenter)
801
-
802
- def move_to_row_coordinates(self):
803
- # Find out the mode and decide columns accordingly
804
- mode = self.comboBox_mode.currentIndex()
805
-
806
- # Get the button that emitted the signal# Get the button that emitted the signal
807
- button = self.sender()
808
-
809
- # Find the row and column where the button is located
810
- row = self.tableWidget_coordinates.indexAt(button.pos()).row()
811
- col = self.tableWidget_coordinates.indexAt(button.pos()).column()
812
-
813
- # Decide which coordinates to move to based on the column
814
- if mode == 1:
815
- if col == 1: # Go to 'start' coordinates
816
- x_col, y_col = 4, 5
817
- elif col == 2: # Go to 'end' coordinates
818
- x_col, y_col = 6, 7
819
- else: # Default case
820
- x_col, y_col = 3, 4 # For "individual" mode
821
-
822
- # Fetch and move coordinates
823
- x = float(self.tableWidget_coordinates.item(row, x_col).text())
824
- y = float(self.tableWidget_coordinates.item(row, y_col).text())
825
- self.move_motor_absolute(x, y)
826
-
827
- def replot_based_on_table(self, table):
828
- if self.replot_lock is True:
829
- return
830
-
831
- print("Replot Triggered")
832
- start_points = []
833
- end_points = []
834
- individual_points = []
835
- # self.rectangles = [] #TODO introduce later
836
-
837
- for row in range(table.rowCount()):
838
- visibility = table.cellWidget(row, 0).isChecked()
839
- if not visibility:
840
- continue
841
-
842
- if self.comboBox_mode.currentIndex() == 1: # start/stop mode
843
- x_start = float(table.item(row, 4).text()) if table.item(row, 4) else None
844
- y_start = float(table.item(row, 5).text()) if table.item(row, 5) else None
845
- x_end = float(table.item(row, 6).text()) if table.item(row, 6) else None
846
- y_end = float(table.item(row, 7).text()) if table.item(row, 7) else None
847
-
848
- if x_start is not None and y_start is not None:
849
- start_points.append([x_start, y_start])
850
- print(f"added start points:{start_points}")
851
- if x_end is not None and y_end is not None:
852
- end_points.append([x_end, y_end])
853
- print(f"added end points:{end_points}")
854
-
855
- else: # individual mode
856
- x_ind = float(table.item(row, 3).text()) if table.item(row, 3) else None
857
- y_ind = float(table.item(row, 4).text()) if table.item(row, 4) else None
858
- if x_ind is not None and y_ind is not None:
859
- individual_points.append([x_ind, y_ind])
860
- print(f"added individual points:{individual_points}")
861
-
862
- if start_points:
863
- self.saved_motor_map_start.setData(pos=np.array(start_points))
864
- print("plotted start")
865
- if end_points:
866
- self.saved_motor_map_end.setData(pos=np.array(end_points))
867
- print("plotted end")
868
- if individual_points:
869
- self.saved_motor_map_individual.setData(pos=np.array(individual_points))
870
- print("plotted individual")
871
-
872
- # TODO will be adapted with logic to handle start/end points
873
- def draw_rectangles(self, start_points, end_points):
874
- for start, end in zip(start_points, end_points):
875
- self.draw_rectangle(start, end)
876
-
877
- def draw_rectangle(self, start, end):
878
- pass
879
-
880
- def delete_selected_row(self):
881
- selected_rows = self.tableWidget_coordinates.selectionModel().selectedRows()
882
- rows_to_delete = [row.row() for row in selected_rows]
883
- rows_to_delete.sort(reverse=True) # Sort in descending order
884
-
885
- # Remove the row from the table
886
- for row_index in rows_to_delete:
887
- self.tableWidget_coordinates.removeRow(row_index)
888
-
889
- # Replot the saved motor map
890
- self.replot_based_on_table(self.tableWidget_coordinates)
891
-
892
- def resizeTable(self, table):
893
- table.resizeColumnsToContents()
894
-
895
- def export_table_to_csv(self, table: QtWidgets.QTableWidget):
896
- options = QFileDialog.Options()
897
- filePath, _ = QFileDialog.getSaveFileName(
898
- self, "Save File", "", "CSV Files (*.csv);;All Files (*)", options=options
899
- )
900
-
901
- if filePath:
902
- if not filePath.endswith(".csv"):
903
- filePath += ".csv"
904
-
905
- with open(filePath, mode="w", newline="") as file:
906
- writer = csv.writer(file)
907
-
908
- col_offset = 2 if self.comboBox_mode.currentIndex() == 0 else 3
909
-
910
- # Write the header
911
- header = []
912
- for col in range(col_offset, table.columnCount()):
913
- header_item = table.horizontalHeaderItem(col)
914
- header.append(header_item.text() if header_item else "")
915
- writer.writerow(header)
916
-
917
- # Write the content
918
- for row in range(table.rowCount()):
919
- row_data = []
920
- for col in range(col_offset, table.columnCount()):
921
- item = table.item(row, col)
922
- row_data.append(item.text() if item else "")
923
- writer.writerow(row_data)
924
-
925
- def load_table_from_csv(self, table: QtWidgets.QTableWidget, precision: int = 0):
926
- options = QFileDialog.Options()
927
- filePath, _ = QFileDialog.getOpenFileName(
928
- self, "Open File", "", "CSV Files (*.csv);;All Files (*)", options=options
929
- )
930
-
931
- if filePath:
932
- with open(filePath, mode="r") as file:
933
- reader = csv.reader(file)
934
- header = next(reader)
935
-
936
- # Wipe the current table
937
- table.setRowCount(0)
938
-
939
- # Populate data
940
- for row_data in reader:
941
- tag = row_data[0]
942
-
943
- if self.comboBox_mode.currentIndex() == 0: # Individual mode
944
- x = float(row_data[1])
945
- y = float(row_data[2])
946
- self.generate_table_coordinate(table, (x, y), tag, precision)
947
-
948
- elif self.comboBox_mode.currentIndex() == 1: # Start/Stop mode
949
- x_start = float(row_data[1])
950
- y_start = float(row_data[2])
951
- x_end = float(row_data[3])
952
- y_end = float(row_data[4])
953
-
954
- self.generate_table_coordinate(table, (x_start, y_start), tag, precision)
955
- self.generate_table_coordinate(table, (x_end, y_end), tag, precision)
956
-
957
- if self.checkBox_resize_auto.isChecked():
958
- table.resizeColumnsToContents()
959
-
960
- def save_absolute_coordinates(self):
961
- self.generate_table_coordinate(
962
- self.tableWidget_coordinates,
963
- (self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()),
964
- tag=f"Pos {self.tag_N}",
965
- precision=self.precision,
966
- )
967
-
968
- self.tag_N += 1
969
-
970
- def save_current_coordinates(self):
971
- self.generate_table_coordinate(
972
- self.tableWidget_coordinates,
973
- self.motor_thread.retrieve_coordinates(),
974
- tag=f"Cur {self.tag_N}",
975
- precision=self.precision,
976
- )
977
-
978
- self.tag_N += 1
979
-
980
- def update_precision(self, precision: int):
981
- self.precision = precision
982
- self.spinBox_step_x.setDecimals(self.precision)
983
- self.spinBox_step_y.setDecimals(self.precision)
984
- self.spinBox_absolute_x.setDecimals(self.precision)
985
- self.spinBox_absolute_y.setDecimals(self.precision)
986
-
987
- def change_step_size(self, spinBox: QtWidgets.QDoubleSpinBox, factor: float) -> None:
988
- old_step = spinBox.value()
989
- new_step = old_step * factor
990
- spinBox.setValue(new_step)
991
-
992
- # TODO generalize these functions
993
-
994
- def sync_step_sizes(self):
995
- """Sync step sizes based on checkbox state."""
996
- if self.checkBox_same_xy.isChecked():
997
- value = self.spinBox_step_x.value()
998
- self.spinBox_step_y.setValue(value)
999
-
1000
- def update_step_size_x(self):
1001
- """Update step size for x if checkbox is checked."""
1002
- if self.checkBox_same_xy.isChecked():
1003
- value = self.spinBox_step_x.value()
1004
- self.spinBox_step_y.setValue(value)
1005
-
1006
- def update_step_size_y(self):
1007
- """Update step size for y if checkbox is checked."""
1008
- if self.checkBox_same_xy.isChecked():
1009
- value = self.spinBox_step_y.value()
1010
- self.spinBox_step_x.setValue(value)
1011
-
1012
- # def sync_step_sizes(self, spinBox1, spinBox2): #TODO move to more general solution like this
1013
- # if self.checkBox_same_xy.isChecked():
1014
- # value = spinBox1.value()
1015
- # spinBox2.setValue(value)
1016
-
1017
- def show_help_dialog(self):
1018
- dialog = QDialog(self)
1019
- dialog.setWindowTitle("Help")
1020
-
1021
- layout = QVBoxLayout()
1022
-
1023
- # Key bindings section
1024
- layout.addWidget(QLabel("Keyboard Shortcuts:"))
1025
-
1026
- key_bindings = [
1027
- ("Delete/Backspace", "Delete selected row"),
1028
- ("Ctrl+A", "Increase step size for X motor by factor of 2"),
1029
- ("Ctrl+Z", "Decrease step size for X motor by factor of 2"),
1030
- ("Alt+A", "Increase step size for Y motor by factor of 2"),
1031
- ("Alt+Z", "Decrease step size for Y motor by factor of 2"),
1032
- ("Ctrl+G", "Go absolute"),
1033
- ("Ctrl+D", "Set absolute coordinates"),
1034
- ("Ctrl+S", "Save Current coordinates"),
1035
- ("Ctrl+X", "Stop"),
1036
- ]
1037
-
1038
- for keys, action in key_bindings:
1039
- layout.addWidget(QLabel(f"{keys} - {action}"))
1040
-
1041
- # Separator
1042
- separator = QFrame()
1043
- separator.setFrameShape(QFrame.HLine)
1044
- separator.setFrameShadow(QFrame.Sunken)
1045
- layout.addWidget(separator)
1046
-
1047
- # Import/Export section
1048
- layout.addWidget(QLabel("Import/Export of Table:"))
1049
- layout.addWidget(
1050
- QLabel(
1051
- "Create additional table columns in config yaml file.\n"
1052
- "Be sure to load the correct config file with console argument -c.\n"
1053
- "When importing a table, the first three columns must be [Tag, X, Y] in the case of Individual mode \n"
1054
- "and [Tag, X [start], Y [start], X [end], Y [end] in the case of Start/Stop mode.\n"
1055
- "Failing to do so will break the table!"
1056
- )
1057
- )
1058
- layout.addWidget(
1059
- QLabel(
1060
- "Note: Importing a table will overwrite the current table. Import in correct mode."
1061
- )
1062
- )
1063
-
1064
- # Another Separator
1065
- another_separator = QFrame()
1066
- another_separator.setFrameShape(QFrame.HLine)
1067
- another_separator.setFrameShadow(QFrame.Sunken)
1068
- layout.addWidget(another_separator)
1069
-
1070
- # PyQtGraph Controls
1071
- layout.addWidget(QLabel("Graph Window Controls:"))
1072
- graph_controls = [("Left Drag", "Pan the view"), ("Right Drag or Scroll", "Zoom in/out")]
1073
- for action, description in graph_controls:
1074
- layout.addWidget(QLabel(f"{action} - {description}"))
1075
-
1076
- ok_button = QPushButton("OK")
1077
- ok_button.clicked.connect(dialog.close)
1078
- layout.addWidget(ok_button)
1079
-
1080
- dialog.setLayout(layout)
1081
- dialog.exec()
1082
-
1083
- @staticmethod
1084
- def param_changed(ui_element):
1085
- ui_element.setStyleSheet("background-color: #FFA700;")
1086
-
1087
-
1088
- class MotorActions(Enum):
1089
- MOVE_TO_COORDINATES = "move_to_coordinates"
1090
- MOVE_RELATIVE = "move_relative"
1091
-
1092
-
1093
- class MotorControl(QThread):
1094
- """
1095
- QThread subclass for controlling motor actions asynchronously.
1096
-
1097
- Attributes:
1098
- coordinates_updated (pyqtSignal): Signal to emit current coordinates.
1099
- limits_retrieved (pyqtSignal): Signal to emit current limits.
1100
- move_finished (pyqtSignal): Signal to emit when the move is finished.
1101
- motors_loaded (pyqtSignal): Signal to emit when the motors are loaded.
1102
- motors_selected (pyqtSignal): Signal to emit when the motors are selected.
1103
- """
1104
-
1105
- coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
1106
- limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits
1107
- move_finished = pyqtSignal() # Signal to emit when the move is finished
1108
- motors_loaded = pyqtSignal(list, list) # Signal to emit when the motors are loaded
1109
- motors_selected = pyqtSignal(object, object) # Signal to emit when the motors are selected
1110
- # progress_updated = pyqtSignal(int) #TODO Signal to emit progress percentage
1111
-
1112
- def __init__(self, parent=None):
1113
- super().__init__(parent)
1114
-
1115
- self.action = None
1116
- self._initialize_motor()
1117
-
1118
- def connect_motors(self, motor_x_name: str, motor_y_name: str) -> None:
1119
- """
1120
- Connect to the specified motors by their names.
1121
-
1122
- Args:
1123
- motor_x_name (str): The name of the motor for the x-axis.
1124
- motor_y_name (str): The name of the motor for the y-axis.
1125
- """
1126
- self.motor_x_name = motor_x_name
1127
- self.motor_y_name = motor_y_name
1128
-
1129
- self.motor_x, self.motor_y = (dev[self.motor_x_name], dev[self.motor_y_name])
1130
-
1131
- (self.current_x, self.current_y) = self.get_coordinates()
1132
-
1133
- if self.motors_consumer is not None:
1134
- self.motors_consumer.shutdown()
1135
-
1136
- self.motors_consumer = client.connector.consumer(
1137
- topics=[
1138
- MessageEndpoints.device_readback(self.motor_x.name),
1139
- MessageEndpoints.device_readback(self.motor_y.name),
1140
- ],
1141
- cb=self._device_status_callback_motors,
1142
- parent=self,
1143
- )
1144
-
1145
- self.motors_consumer.start()
1146
-
1147
- self.motors_selected.emit(self.motor_x, self.motor_y)
1148
-
1149
- def get_all_motors(self) -> list:
1150
- """
1151
- Retrieve a list of all available motors.
1152
-
1153
- Returns:
1154
- list: List of all available motors.
1155
- """
1156
- all_motors = (
1157
- client.device_manager.devices.enabled_devices
1158
- ) # .acquisition_group("motor") #TODO remove motor group?
1159
- return all_motors
1160
-
1161
- def get_all_motors_names(self) -> list:
1162
- all_motors = client.device_manager.devices.enabled_devices # .acquisition_group("motor")
1163
- all_motors_names = [motor.name for motor in all_motors]
1164
- return all_motors_names
1165
-
1166
- def retrieve_all_motors(self):
1167
- self.all_motors = self.get_all_motors()
1168
- self.all_motors_names = self.get_all_motors_names()
1169
- self.motors_loaded.emit(self.all_motors_names, self.all_motors_names)
1170
-
1171
- return self.all_motors, self.all_motors_names
1172
-
1173
- def get_coordinates(self) -> tuple:
1174
- """Get current motor position"""
1175
- x = self.motor_x.readback.get()
1176
- y = self.motor_y.readback.get()
1177
- return x, y
1178
-
1179
- def retrieve_coordinates(self) -> tuple:
1180
- """Get current motor position for export to main app"""
1181
- return self.current_x, self.current_y
1182
-
1183
- def get_motor_limits(self, motor) -> list:
1184
- """
1185
- Retrieve the limits for a specific motor.
1186
-
1187
- Args:
1188
- motor (object): Motor object.
1189
-
1190
- Returns:
1191
- tuple: Lower and upper limit for the motor.
1192
- """
1193
- try:
1194
- return motor.limits
1195
- except AttributeError:
1196
- # If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
1197
- print(f"The device {motor} does not have defined limits.")
1198
- return None
1199
-
1200
- def retrieve_motor_limits(self, motor_x, motor_y):
1201
- limit_x = self.get_motor_limits(motor_x)
1202
- limit_y = self.get_motor_limits(motor_y)
1203
- self.limits_retrieved.emit(limit_x, limit_y)
1204
-
1205
- def update_motor_limits(self, motor, low_limit=None, high_limit=None) -> None:
1206
- current_low_limit, current_high_limit = self.get_motor_limits(motor)
1207
-
1208
- # Check if the low limit has changed and is not None
1209
- if low_limit is not None and low_limit != current_low_limit:
1210
- motor.low_limit = low_limit
1211
-
1212
- # Check if the high limit has changed and is not None
1213
- if high_limit is not None and high_limit != current_high_limit:
1214
- motor.high_limit = high_limit
1215
-
1216
- def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None:
1217
- current_position = self.get_coordinates()
1218
-
1219
- if x_limit is not None:
1220
- if current_position[0] < x_limit[0] or current_position[0] > x_limit[1]:
1221
- raise ValueError("Current motor position is outside the new limits (X)")
1222
- else:
1223
- self.update_motor_limits(self.motor_x, low_limit=x_limit[0], high_limit=x_limit[1])
1224
-
1225
- if y_limit is not None:
1226
- if current_position[1] < y_limit[0] or current_position[1] > y_limit[1]:
1227
- raise ValueError("Current motor position is outside the new limits (Y)")
1228
- else:
1229
- self.update_motor_limits(self.motor_y, low_limit=y_limit[0], high_limit=y_limit[1])
1230
-
1231
- self.retrieve_motor_limits(self.motor_x, self.motor_y)
1232
-
1233
- def move_to_coordinates(self, target_coordinates: tuple):
1234
- self.action = MotorActions.MOVE_TO_COORDINATES
1235
- self.target_coordinates = target_coordinates
1236
- self.start()
1237
-
1238
- def move_relative(self, motor, value: float):
1239
- self.action = MotorActions.MOVE_RELATIVE
1240
- self.motor = motor
1241
- self.value = value
1242
- self.start()
1243
-
1244
- def run(self):
1245
- if self.action == MotorActions.MOVE_TO_COORDINATES:
1246
- self._move_motor_coordinate()
1247
- elif self.action == MotorActions.MOVE_RELATIVE:
1248
- self._move_motor_relative(self.motor, self.value)
1249
-
1250
- def set_target_coordinates(self, target_coordinates: tuple) -> None:
1251
- self.target_coordinates = target_coordinates
1252
-
1253
- def _initialize_motor(self) -> None:
1254
- self.motor_x, self.motor_y = None, None
1255
- self.current_x, self.current_y = None, None
1256
-
1257
- self.motors_consumer = None
1258
-
1259
- # Get all available motors in the client
1260
- self.all_motors = self.get_all_motors()
1261
- self.all_motors_names = self.get_all_motors_names()
1262
- self.retrieve_all_motors() # send motor list to GUI
1263
-
1264
- self.target_coordinates = None
1265
-
1266
- def _move_motor_coordinate(self) -> None:
1267
- """Move the motor to the specified coordinates"""
1268
- status = scans.mv(
1269
- self.motor_x,
1270
- self.target_coordinates[0],
1271
- self.motor_y,
1272
- self.target_coordinates[1],
1273
- relative=False,
1274
- )
1275
-
1276
- status.wait()
1277
- self.move_finished.emit()
1278
-
1279
- def _move_motor_relative(self, motor, value: float) -> None:
1280
- status = scans.mv(motor, value, relative=True)
1281
-
1282
- status.wait()
1283
- self.move_finished.emit()
1284
-
1285
- def stop_movement(self):
1286
- queue.request_scan_abortion()
1287
- queue.request_queue_reset()
1288
-
1289
- @staticmethod
1290
- def _device_status_callback_motors(msg, *, parent, **_kwargs) -> None:
1291
- deviceMSG = msg.value
1292
- if parent.motor_x.name in deviceMSG.content["signals"]:
1293
- parent.current_x = deviceMSG.content["signals"][parent.motor_x.name]["value"]
1294
- elif parent.motor_y.name in deviceMSG.content["signals"]:
1295
- parent.current_y = deviceMSG.content["signals"][parent.motor_y.name]["value"]
1296
- parent.coordinates_updated.emit(parent.current_x, parent.current_y)
1297
-
1298
-
1299
- if __name__ == "__main__":
1300
- import argparse
1301
-
1302
- import yaml
1303
- from bec_lib import BECClient, ServiceConfig
1304
-
1305
- parser = argparse.ArgumentParser(description="Motor App")
1306
-
1307
- parser.add_argument(
1308
- "--config", "-c", help="Path to the .yaml configuration file", default="config_example.yaml"
1309
- )
1310
- parser.add_argument(
1311
- "--bec-config", "-b", help="Path to the BEC .yaml configuration file", default=None
1312
- )
1313
-
1314
- args = parser.parse_args()
1315
-
1316
- try:
1317
- with open(args.config, "r") as file:
1318
- config = yaml.safe_load(file)
1319
-
1320
- selected_motors = config.get("selected_motors", {})
1321
- plot_motors = config.get("plot_motors", {})
1322
-
1323
- except FileNotFoundError:
1324
- print(f"The file {args.config} was not found.")
1325
- exit(1)
1326
- except Exception as e:
1327
- print(f"An error occurred while loading the config file: {e}")
1328
- exit(1)
1329
-
1330
- client = BECClient()
1331
-
1332
- if args.bec_config:
1333
- client.initialize(config=ServiceConfig(config_path=args.bec_config))
1334
-
1335
- client.start()
1336
- dev = client.device_manager.devices
1337
- scans = client.scans
1338
- queue = client.queue
1339
-
1340
- app = QApplication([])
1341
- MotorApp = MotorApp(selected_motors=selected_motors, plot_motors=plot_motors)
1342
- window = MotorApp
1343
- window.show()
1344
- app.exec()