symetrie-hexapod 0.17.3__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.
@@ -0,0 +1,1484 @@
1
+ import logging
2
+ from typing import Dict
3
+ from typing import List
4
+ from typing import Optional
5
+ from typing import Union
6
+
7
+ from PyQt5.QtCore import QEvent
8
+ from PyQt5.QtCore import QSize
9
+ from PyQt5.QtCore import QTimer
10
+ from PyQt5.QtCore import Qt
11
+ from PyQt5.QtGui import QIcon
12
+ from PyQt5.QtGui import QPixmap
13
+ from PyQt5.QtWidgets import QAction
14
+ from PyQt5.QtWidgets import QComboBox
15
+ from PyQt5.QtWidgets import QFrame
16
+ from PyQt5.QtWidgets import QGroupBox
17
+ from PyQt5.QtWidgets import QHBoxLayout
18
+ from PyQt5.QtWidgets import QLabel
19
+ from PyQt5.QtWidgets import QLineEdit
20
+ from PyQt5.QtWidgets import QMainWindow
21
+ from PyQt5.QtWidgets import QPushButton
22
+ from PyQt5.QtWidgets import QSizePolicy
23
+ from PyQt5.QtWidgets import QTabWidget
24
+ from PyQt5.QtWidgets import QVBoxLayout
25
+ from PyQt5.QtWidgets import QWidget
26
+
27
+ from egse.decorators import deprecate
28
+ from egse.gui import show_warning_message
29
+ from egse.gui.buttons import ToggleButton
30
+ from egse.gui.buttons import TouchButton
31
+ from egse.gui.led import LED
32
+ from egse.gui.led import ShapeEnum
33
+ from egse.observer import Observable
34
+ from egse.observer import Observer
35
+ from egse.resource import get_resource
36
+ from egse.state import UnknownStateError
37
+
38
+ MODULE_LOGGER = logging.getLogger(__name__)
39
+
40
+
41
+ class VLine(QFrame):
42
+ """Presents a simple Vertical Bar that can be used in e.g. the status bar."""
43
+
44
+ def __init__(self):
45
+ super().__init__()
46
+ self.setFrameShape(self.VLine | self.Sunken)
47
+
48
+
49
+ class Container(QWidget):
50
+ """
51
+ An empty container that is used currently as a place-holder for a QWidget that is to be
52
+ implemented.
53
+ """
54
+
55
+ def __init__(self, text):
56
+ super().__init__()
57
+
58
+ self.hbox = QHBoxLayout()
59
+ self.hbox.setSpacing(0)
60
+ self.hbox.setContentsMargins(0, 0, 0, 0)
61
+ self.setLayout(self.hbox)
62
+
63
+ self.button = QPushButton(text)
64
+ self.hbox.addWidget(self.button)
65
+
66
+
67
+ class ValidationIcon(QWidget):
68
+ """
69
+ This Icon is used
70
+ """
71
+
72
+ def __init__(self):
73
+ super().__init__()
74
+
75
+ self._valid = QPixmap(str(get_resource(":/icons/valid.png")))
76
+ self._invalid = QPixmap(str(get_resource(":/icons/invalid.png")))
77
+ self._disabled = QPixmap(str(get_resource(":/icons/unvalid.png")))
78
+
79
+ self._label = QLabel()
80
+
81
+ self.disable()
82
+
83
+ hbox = QHBoxLayout()
84
+ hbox.setSpacing(0)
85
+ hbox.setContentsMargins(0, 0, 0, 0)
86
+ hbox.addWidget(self._label)
87
+
88
+ self.setLayout(hbox)
89
+
90
+ def eventFilter(self, source, event):
91
+ if event.type() == QEvent.FocusOut:
92
+ self.disable()
93
+ return True
94
+
95
+ def validate(self):
96
+ self._label.setPixmap(self._valid)
97
+
98
+ def invalidate(self):
99
+ self._label.setPixmap(self._invalid)
100
+
101
+ def disable(self):
102
+ self._label.setPixmap(self._disabled)
103
+ self._label.setToolTip("No validation was performed.")
104
+
105
+ def setToolTip(self, message: str) -> None:
106
+ self._label.setToolTip(message)
107
+
108
+
109
+ class Positioning(QWidget):
110
+ """
111
+ The Positioning widget which allows to manually command the Hexapod to a certain position,
112
+ absolute or relative.
113
+ """
114
+
115
+ def __init__(self, view, observable):
116
+ super().__init__()
117
+ self.observable = observable
118
+ self.view = view
119
+
120
+ # initialize instance variables used by this class
121
+
122
+ self.manual_mode: QGroupBox = None
123
+ self.manual_mode_positions_widget: QFrame = None
124
+ self.manual_mode_positions: List = None
125
+
126
+ self.combo_absolute_relative: QComboBox = None
127
+
128
+ self.validate_label = None
129
+
130
+ self.specific_positions: QGroupBox = None
131
+ self.specific_positions_widget: QFrame = None
132
+
133
+ self.init_gui()
134
+
135
+ def init_gui(self):
136
+ """Initialize the main user interface for this component."""
137
+
138
+ # Setup the Manual Mode GroupBox widget
139
+
140
+ hbox = QHBoxLayout()
141
+
142
+ self.manual_mode = QGroupBox("Manual Mode")
143
+ self.manual_mode.setLayout(hbox)
144
+ self.manual_mode.setObjectName("ManualModeGroupBox")
145
+
146
+ self.manual_mode_positions_widget = self.create_manual_mode_position_widget()
147
+
148
+ hbox.addWidget(self.manual_mode_positions_widget)
149
+
150
+ # Setup the Specific Positions GroupBox widget
151
+
152
+ hbox = QHBoxLayout()
153
+
154
+ self.specific_positions = QGroupBox("Specific Positions")
155
+ self.specific_positions.setLayout(hbox)
156
+ self.manual_mode.setObjectName("SpecificPositionsGroupBox")
157
+
158
+ self.specific_positions_widget = self.create_specific_positions_widget()
159
+
160
+ hbox.addWidget(self.specific_positions_widget)
161
+
162
+ # Finally, within the Positions widget, a VBoxLayout holds the manual mode and the
163
+ # specific positions widgets.
164
+
165
+ vbox = QVBoxLayout()
166
+
167
+ vbox.addWidget(self.manual_mode)
168
+ vbox.addWidget(self.specific_positions)
169
+
170
+ self.setLayout(vbox)
171
+
172
+ def create_specific_positions_widget(self):
173
+ hbox = QHBoxLayout()
174
+
175
+ self.combo_specific_position = QComboBox()
176
+ self.combo_specific_position.addItems(["Position ZERO", "Position RETRACTED"])
177
+ self.combo_specific_position.setMinimumContentsLength(18)
178
+ self.combo_specific_position.adjustSize()
179
+
180
+ self.move_to_button = QPushButton("Move To")
181
+ self.move_to_button.setToolTip(
182
+ "Move to the specific position that is selected in the combobox."
183
+ "<ul>"
184
+ "<li><strong>ZERO</strong> moves Tx, Ty, Tz, Rx, Ry, Rz to position 0.0"
185
+ "<li><strong>RETRACTED</strong> moves the hexapod into its smallest height (useful for "
186
+ "loading or storage)"
187
+ "</ul>"
188
+ )
189
+ self.move_to_button.clicked.connect(self.handle_move_to_specific_position)
190
+
191
+ hbox.addWidget(self.combo_specific_position)
192
+ hbox.addWidget(self.move_to_button)
193
+
194
+ frame = QFrame()
195
+ frame.setObjectName("SpecificPositions")
196
+ frame.setLayout(hbox)
197
+
198
+ return frame
199
+
200
+ def create_manual_mode_position_widget(self) -> QFrame:
201
+ """Creates the internal frame for the manual mode box."""
202
+
203
+ vbox = QVBoxLayout()
204
+
205
+ self.manual_mode_positions = [
206
+ [QLabel("X"), QLineEdit(), QLabel("mm")],
207
+ [QLabel("Y"), QLineEdit(), QLabel("mm")],
208
+ [QLabel("Z"), QLineEdit(), QLabel("mm")],
209
+ [QLabel("Rx"), QLineEdit(), QLabel("deg")],
210
+ [QLabel("Ry"), QLineEdit(), QLabel("deg")],
211
+ [QLabel("Rz"), QLineEdit(), QLabel("deg")],
212
+ ]
213
+
214
+ for mm_pos in self.manual_mode_positions:
215
+ hbox = QHBoxLayout()
216
+ hbox.addWidget(mm_pos[0])
217
+ hbox.addWidget(mm_pos[1])
218
+ hbox.addWidget(mm_pos[2])
219
+ mm_pos[0].setMinimumWidth(20)
220
+ mm_pos[1].setText("0.0000")
221
+ mm_pos[1].setStyleSheet("QLabel { background-color : LightGray; }")
222
+ mm_pos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
223
+ mm_pos[1].setMinimumWidth(50)
224
+ vbox.addLayout(hbox)
225
+
226
+ # Add the two buttons, (1) Copy, and (2) Clear
227
+
228
+ hbox = QHBoxLayout()
229
+
230
+ copy_button = QPushButton("Copy")
231
+ copy_button.setToolTip("Copy the positions from Object [in User].")
232
+ copy_button.clicked.connect(self.handle_copy_positions)
233
+
234
+ clear_button = QPushButton("Clear")
235
+ clear_button.setToolTip("Clear the input fields.")
236
+ clear_button.clicked.connect(self.handle_clear_inputs)
237
+
238
+ hbox.addWidget(copy_button)
239
+ hbox.addStretch()
240
+ hbox.addWidget(clear_button)
241
+
242
+ vbox.addLayout(hbox)
243
+
244
+ # Make sure the hboxes defined above stay nicely together when vertically resizing the
245
+ # Frame.
246
+
247
+ vbox.addStretch()
248
+
249
+ # Add the QComboBox to select either absolute or relative movement
250
+
251
+ hbox = QHBoxLayout()
252
+
253
+ self.combo_absolute_relative = QComboBox()
254
+ self.combo_absolute_relative.addItems(["Absolute", "Relative object", "Relative user"])
255
+ self.combo_absolute_relative.setMinimumContentsLength(15)
256
+ self.combo_absolute_relative.adjustSize()
257
+
258
+ self.move_button = QPushButton("Move")
259
+ self.move_button.setToolTip(
260
+ "When you press this button, the Hexapod will start moving \n"
261
+ "to the position you have given in manual mode above.\n"
262
+ "Depending on the control setting, the movement will be absolute or relative."
263
+ )
264
+ self.move_button.clicked.connect(self.handle_movement)
265
+
266
+ hbox.addWidget(self.combo_absolute_relative)
267
+ hbox.addWidget(self.move_button)
268
+
269
+ vbox.addLayout(hbox)
270
+
271
+ # Add the two buttons, (1) Move, and (2) Validate Movement.
272
+
273
+ hbox = QHBoxLayout()
274
+
275
+ self.validate_button = QPushButton("Validate Movement...")
276
+ self.validate_button.setToolTip(
277
+ "When you press this button, the Hexapod controller will validate the input position "
278
+ "in manual mode.\n"
279
+ "Depending on the control setting, the movement will be absolute or relative."
280
+ )
281
+ self.validate_button.clicked.connect(self.handle_validate)
282
+
283
+ self.validate_label = ValidationIcon()
284
+ self.validate_label.installEventFilter(self.validate_label)
285
+
286
+ hbox.addWidget(self.validate_label)
287
+ hbox.addWidget(self.validate_button)
288
+
289
+ vbox.addLayout(hbox)
290
+
291
+ frame = QFrame()
292
+ frame.setObjectName("ManualPositions")
293
+ frame.setLayout(vbox)
294
+
295
+ return frame
296
+
297
+ def disable_movement(self):
298
+ self.move_button.setDisabled(True)
299
+ self.move_to_button.setDisabled(True)
300
+ self.validate_button.setDisabled(True)
301
+
302
+ def enable_movement(self):
303
+ self.move_button.setEnabled(True)
304
+ self.move_to_button.setEnabled(True)
305
+ self.validate_button.setEnabled(True)
306
+
307
+ def set_position_validation_icon(self, error_codes, tooltip: str = None):
308
+ if error_codes:
309
+ self.validate_label.setFocus()
310
+ self.validate_label.invalidate()
311
+ self.validate_label.setToolTip(format_tooltip(tooltip or error_codes))
312
+ else:
313
+ self.validate_label.setFocus()
314
+ self.validate_label.validate()
315
+ self.validate_label.setToolTip("Movement command is valid.")
316
+
317
+ def get_manual_inputs(self):
318
+ """Returns the input positions as a list of floats."""
319
+ try:
320
+ pos = [float(mm_pos[1].text().replace(",", ".")) for mm_pos in self.manual_mode_positions]
321
+ except ValueError as exc:
322
+ MODULE_LOGGER.error(f"Incorrect manual position input given: {exc}")
323
+
324
+ description = "Input errors in manual positions"
325
+ info_text = (
326
+ "Some of the values that you have filled into the manual position fields are "
327
+ "invalid. The fields can only contain floating point numbers, both '.' and ',"
328
+ "' are allowed."
329
+ )
330
+ show_warning_message(description, info_text)
331
+
332
+ return None
333
+
334
+ return pos
335
+
336
+ def handle_move_to_specific_position(self):
337
+ # Check which movement was requested
338
+
339
+ selected_text = self.combo_specific_position.currentText()
340
+ if "ZERO" in selected_text:
341
+ action = "goto_zero_position"
342
+ value = 1
343
+ elif "RETRACTED" in selected_text:
344
+ action = "goto_retracted_position"
345
+ value = 2
346
+ else:
347
+ MODULE_LOGGER.error(f"Unknown action requested: {selected_text}, no observer action performed.")
348
+ return
349
+
350
+ self.observable.action_observers({action: value})
351
+
352
+ def handle_movement(self):
353
+ # Read out the values in manual mode into an array of floats
354
+ # Users may use comma or point as a decimal delimiter
355
+
356
+ pos = self.get_manual_inputs()
357
+ if pos is None:
358
+ return
359
+
360
+ # Check which movement was requested
361
+
362
+ selected_text = self.combo_absolute_relative.currentText()
363
+ if selected_text == "Absolute":
364
+ movement = "move_absolute"
365
+ elif selected_text == "Relative object":
366
+ movement = "move_relative_object"
367
+ elif selected_text == "Relative user":
368
+ movement = "move_relative_user"
369
+ else:
370
+ MODULE_LOGGER.error(f"Unknown action requested: {selected_text}, no observer action performed.")
371
+ return
372
+
373
+ self.observable.action_observers({movement: pos})
374
+
375
+ def handle_validate(self):
376
+ pos = self.get_manual_inputs()
377
+ if pos is None:
378
+ return
379
+
380
+ # Check which movement was requested
381
+
382
+ selected_text = self.combo_absolute_relative.currentText()
383
+ if selected_text == "Absolute":
384
+ validation = "check_absolute_movement"
385
+ elif selected_text == "Relative object":
386
+ validation = "check_relative_object_movement"
387
+ elif selected_text == "Relative user":
388
+ validation = "check_relative_user_movement"
389
+ else:
390
+ MODULE_LOGGER.error(f"Unknown action requested: {selected_text}, no observer action performed.")
391
+ return
392
+
393
+ self.observable.action_observers({validation: pos})
394
+
395
+ def handle_copy_positions(self):
396
+ for pos, mm_pos in zip(self.view.user_positions, self.manual_mode_positions):
397
+ mm_pos[1].setText(pos[1].text())
398
+
399
+ self.validate_label.disable()
400
+ self.manual_mode_positions_widget.repaint()
401
+
402
+ def handle_clear_inputs(self):
403
+ for mm_pos in self.manual_mode_positions:
404
+ mm_pos[1].setText("0.0000")
405
+
406
+ self.validate_label.disable()
407
+ self.manual_mode_positions_widget.repaint()
408
+
409
+
410
+ class SpeedSettings(QWidget):
411
+ def __init__(self, view, observable):
412
+ super().__init__()
413
+ self.observable = observable
414
+ self.view = view
415
+
416
+ self.set_speed_widget: Optional[QGroupBox] = None
417
+ self.set_speed_widget = self.create_set_speed_widget()
418
+
419
+ vbox = QVBoxLayout()
420
+
421
+ hbox = QHBoxLayout()
422
+ hbox.addWidget(self.set_speed_widget)
423
+ hbox.addStretch()
424
+
425
+ # Add the Fetch and Apply for the Speed to the HBOX here
426
+
427
+ vbox_speed = QVBoxLayout()
428
+
429
+ fetch_button = QPushButton("Fetch")
430
+ fetch_button.setToolTip("Fetch speed settings from the Controller.")
431
+ fetch_button.clicked.connect(self.handle_fetch_speed_settings)
432
+
433
+ apply_button = QPushButton("Apply")
434
+ apply_button.setToolTip("Apply BOTH speed settings to the Controller.")
435
+ apply_button.clicked.connect(self.handle_apply_speed_settings)
436
+
437
+ vbox_speed.addWidget(fetch_button)
438
+ vbox_speed.addWidget(apply_button)
439
+
440
+ hbox.addLayout(vbox_speed)
441
+
442
+ vbox.addLayout(hbox)
443
+
444
+ self.setLayout(vbox)
445
+
446
+ def create_set_speed_widget(self):
447
+ """Creates the widget for the user set/get speed box."""
448
+
449
+ vbox = QVBoxLayout()
450
+
451
+ self.user_set_speed = [
452
+ [QLabel("Translation Speed (vt): "), QLineEdit(), QLabel("mm/s")],
453
+ [QLabel("Rotation Speed (vr): "), QLineEdit(), QLabel("°/s")],
454
+ ]
455
+
456
+ for speed in self.user_set_speed:
457
+ hbox = QHBoxLayout()
458
+ hbox.addWidget(speed[0])
459
+ hbox.addWidget(speed[1])
460
+ hbox.setSpacing(0)
461
+ hbox.addWidget(speed[2])
462
+ speed[0].setMinimumWidth(150)
463
+ speed[1].setText("0.0000")
464
+ speed[1].setStyleSheet("QLabel { background-color : LightGray; }")
465
+ speed[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
466
+ speed[1].setFixedWidth(100)
467
+ vbox.addLayout(hbox)
468
+
469
+ gbox_set_speed = QGroupBox("Set Speed parameters", self)
470
+ gbox_set_speed.setLayout(vbox)
471
+
472
+ return gbox_set_speed
473
+
474
+ def get_speed_settings_input(self):
475
+ tr_speed = float(self.user_set_speed[0][1].text())
476
+ rot_speed = float(self.user_set_speed[1][1].text())
477
+
478
+ return tr_speed, rot_speed
479
+
480
+ def set_speed(self, vt, vr):
481
+ self.user_set_speed[0][1].setText(str(vt))
482
+ self.user_set_speed[1][1].setText(str(vr))
483
+ self.set_speed_widget.repaint()
484
+
485
+ def handle_apply_speed_settings(self):
486
+ # Read out the values in the speed settings group
487
+
488
+ translation_speed, rotation_speed = self.get_speed_settings_input()
489
+
490
+ self.observable.action_observers({"set_speed": (translation_speed, rotation_speed)})
491
+
492
+ def handle_fetch_speed_settings(self):
493
+ self.observable.action_observers({"fetch_speed": True})
494
+
495
+
496
+ class CoordinateSystems(QWidget):
497
+ """This Widget allow to set the User and Object coordinate systems."""
498
+
499
+ def __init__(self, view, observable):
500
+ super().__init__()
501
+ self.observable = observable
502
+ self.view = view
503
+
504
+ # initialize instance variables used by this class
505
+
506
+ self.user_coordinates_system_widget: QGroupBox = None
507
+ self.user_coordinates_system: List = None
508
+
509
+ self.object_coordinates_system_widget: QGroupBox = None
510
+ self.object_coordinates_system: List = None
511
+
512
+ self.init_gui()
513
+
514
+ def init_gui(self):
515
+ """Initialize the main interface for this component."""
516
+
517
+ vbox = QVBoxLayout()
518
+
519
+ self.user_coordinates_system_widget = self.create_user_coordinates_system_widget()
520
+ self.object_coordinates_system_widget = self.create_object_coordinates_system_widget()
521
+
522
+ # The double arrow buttons are used to copy parameters from user to object coordinate
523
+ # system or vice versa.
524
+
525
+ copy_right_icon = QIcon(str(get_resource(":/icons/double-right-arrow.svg")))
526
+ copy_right_button = QPushButton()
527
+ copy_right_button.setIcon(copy_right_icon)
528
+ copy_right_button.setToolTip("Copy User coordinates to Object coordinates")
529
+ copy_right_button.clicked.connect(self.on_copy_right)
530
+
531
+ copy_left_icon = QIcon(str(get_resource(":/icons/double-left-arrow.svg")))
532
+ copy_left_button = QPushButton()
533
+ copy_left_button.setIcon(copy_left_icon)
534
+ copy_left_button.setToolTip("Copy Object coordinates to User coordinates")
535
+ copy_left_button.clicked.connect(self.on_copy_left)
536
+
537
+ vbox_icons = QVBoxLayout()
538
+ vbox_icons.addStretch()
539
+ vbox_icons.addWidget(copy_left_button)
540
+ vbox_icons.addWidget(copy_right_button)
541
+ vbox_icons.addStretch()
542
+
543
+ hbox = QHBoxLayout()
544
+
545
+ hbox.addWidget(self.user_coordinates_system_widget)
546
+ hbox.addLayout(vbox_icons)
547
+ hbox.addWidget(self.object_coordinates_system_widget)
548
+
549
+ vbox.addLayout(hbox)
550
+
551
+ apply_button = QPushButton("Apply")
552
+ apply_button.setToolTip("Apply the Coordinates Systems to the Controller.")
553
+ apply_button.clicked.connect(self.handle_apply_coordinates_systems)
554
+
555
+ fetch_button = QPushButton("Fetch")
556
+ fetch_button.setToolTip("Fetch the Coordinates Systems from the Controller.")
557
+ fetch_button.clicked.connect(self.handle_fetch_coordinates_systems)
558
+
559
+ hbox = QHBoxLayout()
560
+ hbox.addWidget(fetch_button)
561
+ hbox.addStretch()
562
+ hbox.addWidget(apply_button)
563
+ vbox.addLayout(hbox)
564
+
565
+ vbox.addStretch(1)
566
+
567
+ self.setLayout(vbox)
568
+
569
+ def set_coordinates_systems(self, user_cs, object_cs):
570
+ for usr, value in zip(self.user_coordinates_system, user_cs):
571
+ usr[1].setText(str(value))
572
+ for obj, value in zip(self.object_coordinates_system, object_cs):
573
+ obj[1].setText(str(value))
574
+ self.object_coordinates_system_widget.repaint()
575
+ self.user_coordinates_system_widget.repaint()
576
+
577
+ def create_user_coordinates_system_widget(self) -> QWidget:
578
+ """Creates the widget for the user coordinates system box."""
579
+
580
+ vbox = QVBoxLayout()
581
+
582
+ self.user_coordinates_system = [
583
+ [QLabel("X"), QLineEdit(), QLabel("mm")],
584
+ [QLabel("Y"), QLineEdit(), QLabel("mm")],
585
+ [QLabel("Z"), QLineEdit(), QLabel("mm")],
586
+ [QLabel("Rx"), QLineEdit(), QLabel("deg")],
587
+ [QLabel("Ry"), QLineEdit(), QLabel("deg")],
588
+ [QLabel("Rz"), QLineEdit(), QLabel("deg")],
589
+ ]
590
+
591
+ for mm_pos in self.user_coordinates_system:
592
+ hbox = QHBoxLayout()
593
+ hbox.addWidget(mm_pos[0])
594
+ hbox.addWidget(mm_pos[1])
595
+ hbox.addWidget(mm_pos[2])
596
+ mm_pos[0].setMinimumWidth(20)
597
+ mm_pos[1].setText("0.0000")
598
+ mm_pos[1].setStyleSheet("QLabel { background-color : LightGray; }")
599
+ mm_pos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
600
+ mm_pos[1].setMinimumWidth(50)
601
+ vbox.addLayout(hbox)
602
+
603
+ # Make sure the hboxes defined above stay nicely together when vertically resizing the
604
+ # Frame.
605
+
606
+ # vbox.addStretch()
607
+
608
+ gbox_user_coordinates_system = QGroupBox("User Coordinate System", self)
609
+ gbox_user_coordinates_system.setLayout(vbox)
610
+ gbox_user_coordinates_system.setToolTip("The User Coordinate System.")
611
+
612
+ return gbox_user_coordinates_system
613
+
614
+ def create_object_coordinates_system_widget(self) -> QWidget:
615
+ """Creates the widget for the object coordinates system box."""
616
+
617
+ vbox = QVBoxLayout()
618
+
619
+ self.object_coordinates_system = [
620
+ [QLabel("X"), QLineEdit(), QLabel("mm")],
621
+ [QLabel("Y"), QLineEdit(), QLabel("mm")],
622
+ [QLabel("Z"), QLineEdit(), QLabel("mm")],
623
+ [QLabel("Rx"), QLineEdit(), QLabel("deg")],
624
+ [QLabel("Ry"), QLineEdit(), QLabel("deg")],
625
+ [QLabel("Rz"), QLineEdit(), QLabel("deg")],
626
+ ]
627
+
628
+ for mm_pos in self.object_coordinates_system:
629
+ hbox = QHBoxLayout()
630
+ hbox.addWidget(mm_pos[0])
631
+ hbox.addWidget(mm_pos[1])
632
+ hbox.addWidget(mm_pos[2])
633
+ mm_pos[0].setMinimumWidth(20)
634
+ mm_pos[1].setText("0.0000")
635
+ mm_pos[1].setStyleSheet("QLabel { background-color : LightGray; }")
636
+ mm_pos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
637
+ mm_pos[1].setMinimumWidth(50)
638
+ vbox.addLayout(hbox)
639
+
640
+ # Make sure the hboxes defined above stay nicely together when vertically resizing the
641
+ # Frame.
642
+
643
+ # vbox.addStretch()
644
+
645
+ gbox_object_coordinates_system = QGroupBox("Object Coordinate System", self)
646
+ gbox_object_coordinates_system.setLayout(vbox)
647
+ gbox_object_coordinates_system.setToolTip("The Object Coordinate System.")
648
+
649
+ return gbox_object_coordinates_system
650
+
651
+ def on_copy_right(self, icon):
652
+ for usr, obj in zip(self.user_coordinates_system, self.object_coordinates_system):
653
+ obj[1].setText(usr[1].text())
654
+ self.object_coordinates_system_widget.repaint()
655
+
656
+ def on_copy_left(self, icon):
657
+ for usr, obj in zip(self.user_coordinates_system, self.object_coordinates_system):
658
+ usr[1].setText(obj[1].text())
659
+ self.user_coordinates_system_widget.repaint()
660
+
661
+ def get_coordinates_systems_inputs(self):
662
+ """Returns the values from the user and object coordinates systems as a tuple of two
663
+ lists of floats.
664
+
665
+ Returns:
666
+ A tuple containing two lists of floats, the first list for the user coordinates
667
+ system, the second list for the object coordinates system.
668
+ """
669
+ try:
670
+ user_cs = [float(pos[1].text().replace(",", ".")) for pos in self.user_coordinates_system]
671
+ object_cs = [float(pos[1].text().replace(",", ".")) for pos in self.object_coordinates_system]
672
+ except ValueError as exc:
673
+ MODULE_LOGGER.error(f"Incorrect manual input given for user or object coordinates system: {exc}")
674
+
675
+ description = "Input errors for coordinates systems"
676
+ info_text = (
677
+ "Some of the values that you have filled into the fields for the coordinates "
678
+ "systems are invalid. The fields can only contain floating point numbers, "
679
+ "both '.' and ',' are allowed."
680
+ )
681
+ show_warning_message(description, info_text)
682
+
683
+ return None, None
684
+
685
+ return user_cs, object_cs
686
+
687
+ def handle_apply_coordinates_systems(self):
688
+ # Read out the values in manual mode into an array of floats
689
+ # Users may use comma or point as a decimal delimiter
690
+
691
+ user_cs, object_cs = self.get_coordinates_systems_inputs()
692
+ if user_cs is None:
693
+ return
694
+
695
+ self.observable.action_observers({"configure_coordinates_systems": (user_cs, object_cs)})
696
+
697
+ def handle_fetch_coordinates_systems(self):
698
+ self.observable.action_observers({"fetch_coordinates_systems": True})
699
+
700
+
701
+ class ActuatorStates(QWidget):
702
+ """This Widget allows to view the state of all six actuators."""
703
+
704
+ def __init__(self, labels: List[str] = None):
705
+ super().__init__()
706
+
707
+ # initialize instance variables used by this class
708
+
709
+ self.status_labels = labels
710
+ self.leds: List[List] = []
711
+
712
+ vbox = QVBoxLayout()
713
+ vbox.addWidget(QLabel("Actuator States"))
714
+ vbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
715
+
716
+ self.create_states_widget = self.create_states()
717
+ vbox.addWidget(self.create_states_widget)
718
+ self.setLayout(vbox)
719
+
720
+ def create_states(self):
721
+ vbox = QVBoxLayout()
722
+
723
+ hbox = QHBoxLayout()
724
+ hbox.addWidget(QLabel(""))
725
+
726
+ for actuator_index in range(1, 7):
727
+ actuator_number = QLabel(str(actuator_index))
728
+ actuator_number.setAlignment(Qt.AlignHCenter)
729
+ actuator_number.setMinimumSize(20, 20)
730
+ actuator_number.setMaximumSize(20, 20)
731
+ # LED(self, size=QSize(20, 20), shape=ShapeEnum.SQUARE)
732
+ hbox.addWidget(actuator_number)
733
+ hbox.setSpacing(2)
734
+ vbox.addLayout(hbox)
735
+
736
+ for state in self.status_labels:
737
+ hbox = QHBoxLayout()
738
+ hbox.addWidget(QLabel(state))
739
+ actuator_leds = [LED(self, size=QSize(20, 20), shape=ShapeEnum.SQUARE) for _ in range(6)]
740
+ for led in actuator_leds:
741
+ hbox.addWidget(led)
742
+ hbox.setSpacing(2)
743
+ self.leds.append(actuator_leds)
744
+ vbox.addLayout(hbox)
745
+
746
+ create_states = QGroupBox()
747
+ create_states.setLayout(vbox)
748
+
749
+ return create_states
750
+
751
+ def set_states(self, states: List):
752
+ # States is a List of Lists, each inside list contains a dict with the states and
753
+ # another list with the states.
754
+
755
+ # zip(*states) will transpose the states
756
+
757
+ for leds, new_states in zip(self.leds, zip(*states)):
758
+ for led, state in zip(leds, new_states):
759
+ led.set_color(state)
760
+
761
+ def reset_states(self):
762
+ pass
763
+
764
+
765
+ def format_tooltip(value: Union[str, Dict, List] = None) -> str:
766
+ if value is None:
767
+ return ""
768
+
769
+ from rich.console import Console
770
+
771
+ console = Console(width=60, force_terminal=False, force_jupyter=False)
772
+ with console.capture() as capture:
773
+ console.print(value)
774
+
775
+ return capture.get()
776
+
777
+
778
+ class HexapodUIModel:
779
+ def __init__(self, connection_type: str, device):
780
+ self._connection_type = connection_type
781
+ self._device = device
782
+
783
+ @property
784
+ def connection_type(self):
785
+ return self._connection_type
786
+
787
+ @property
788
+ def device(self):
789
+ return self._device
790
+
791
+ def is_simulator(self):
792
+ return self.device.is_simulator()
793
+
794
+ def is_cs_connected(self):
795
+ return self.device.is_cs_connected() if self.connection_type == "proxy" else False
796
+
797
+ def is_device_connected(self):
798
+ return self.device.is_connected()
799
+
800
+ @deprecate(alternative="reconnect_device")
801
+ def reconnect(self):
802
+ self.reconnect_device()
803
+
804
+ def reconnect_device(self):
805
+ self.device.reconnect()
806
+ return self.device.is_connected()
807
+
808
+ def reconnect_cs(self):
809
+ self.device.reconnect_cs()
810
+ return self.device.is_cs_connected()
811
+
812
+ def disconnect(self):
813
+ self.device.disconnect()
814
+
815
+ def disconnect_cs(self):
816
+ self.device.disconnect_cs()
817
+
818
+ def has_commands(self):
819
+ if self.connection_type == "proxy":
820
+ return self.device.has_commands()
821
+ return True
822
+
823
+ def load_commands(self):
824
+ if self.connection_type == "proxy":
825
+ self.device.load_commands()
826
+
827
+ def get_states(self):
828
+ try:
829
+ _, states = self.device.get_general_state()
830
+ except TypeError:
831
+ states = None
832
+ return states
833
+
834
+ def configure_coordinates_systems(self, user_cs, object_cs):
835
+ return self.device.configure_coordinates_systems(*user_cs, *object_cs)
836
+
837
+ def get_coordinates_systems(self):
838
+ response = self.device.get_coordinates_systems()
839
+ user_cs = response[:6]
840
+ object_cs = response[6:]
841
+ return user_cs, object_cs
842
+
843
+ def get_user_positions(self):
844
+ return self.device.get_user_positions()
845
+
846
+ def get_machine_positions(self):
847
+ return self.device.get_machine_positions()
848
+
849
+ def get_actuator_length(self):
850
+ return self.device.get_actuator_length()
851
+
852
+ def get_actuator_states(self):
853
+ states = self.device.get_actuator_state()
854
+ states = [x[1] for x in states]
855
+ return states
856
+
857
+ def get_speed(self):
858
+ raise NotImplementedError
859
+
860
+ def activate_control_loop(self):
861
+ self.device.activate_control_loop()
862
+
863
+ def deactivate_control_loop(self):
864
+ self.device.deactivate_control_loop()
865
+
866
+ def check_absolute_movement(self, pos):
867
+ rc, rc_dict = self.device.check_absolute_movement(*pos)
868
+ MODULE_LOGGER.debug(rc)
869
+ MODULE_LOGGER.debug(rc_dict)
870
+ return rc_dict
871
+
872
+ def check_relative_object_movement(self, pos):
873
+ rc, rc_dict = self.device.check_relative_object_movement(*pos)
874
+ MODULE_LOGGER.debug(rc)
875
+ MODULE_LOGGER.debug(rc_dict)
876
+ return rc_dict
877
+
878
+ def check_relative_user_movement(self, pos):
879
+ rc, rc_dict = self.device.check_relative_user_movement(*pos)
880
+ MODULE_LOGGER.debug(rc)
881
+ MODULE_LOGGER.debug(rc_dict)
882
+ return rc_dict
883
+
884
+ def move_absolute(self, pos):
885
+ self.device.move_absolute(*pos)
886
+
887
+ def move_relative_object(self, pos):
888
+ self.device.move_relative_object(*pos)
889
+
890
+ def move_relative_user(self, pos):
891
+ self.device.move_relative_user(*pos)
892
+
893
+ def goto_zero_position(self):
894
+ self.device.goto_zero_position()
895
+
896
+ def goto_retracted_position(self):
897
+ self.device.goto_retracted_position()
898
+
899
+ def set_speed(self, tr_speed, rot_speed):
900
+ self.device.set_speed(tr_speed, rot_speed)
901
+
902
+ def homing(self):
903
+ self.device.homing()
904
+
905
+ def clear_error(self):
906
+ self.device.clear_error()
907
+
908
+ def reset(self):
909
+ self.device.reset()
910
+
911
+ def stop(self):
912
+ self.device.stop()
913
+
914
+
915
+ class HexapodUIController(Observer):
916
+ def __init__(self, model: HexapodUIModel, view):
917
+ self._model = model
918
+ self._view = view
919
+ self._view.add_observer(self)
920
+
921
+ self.states_capture_timer = None
922
+ self.timer_interval = 200
923
+ self.create_timer()
924
+
925
+ try:
926
+ if self.model.is_device_connected():
927
+ mode = self.model.connection_type.capitalize()
928
+ if self.model.is_simulator():
929
+ mode = f"{mode} [Simulator]"
930
+ self.view.update_status_bar(mode=mode)
931
+
932
+ self.view.check_device_action()
933
+ else:
934
+ self.view.uncheck_device_action()
935
+
936
+ if self.model.is_cs_connected():
937
+ self.view.check_cs_action()
938
+ else:
939
+ self.view.uncheck_cs_action()
940
+
941
+ MODULE_LOGGER.info(f"{self.model.is_device_connected()=}, {self.model.is_cs_connected()=}")
942
+
943
+ if model.connection_type in ["direct", "simulator"]:
944
+ view.disable_cs_action()
945
+
946
+ if self.has_connection():
947
+ self.view.set_connection_state("connected")
948
+ self.start_timer()
949
+ else:
950
+ self.view.set_connection_state("disconnected")
951
+ self.stop_timer()
952
+ except NotImplementedError:
953
+ MODULE_LOGGER.warning(
954
+ "There was no connection to the control server during startup, GUI starts in disconnected mode."
955
+ )
956
+ self.view.uncheck_cs_action()
957
+ self.view.uncheck_device_action()
958
+ self.view.set_connection_state("disconnected")
959
+
960
+ def has_connection(self):
961
+ """
962
+ Returns True if the controller has a connection to the device. This takes into account
963
+ that the control server might be disabled when the controller is directly connected to
964
+ the device or to a simulator.
965
+ """
966
+ if self.view.is_cs_action_enabled():
967
+ return bool(self.model.is_device_connected() and self.model.is_cs_connected())
968
+ else:
969
+ return bool(self.model.is_device_connected())
970
+
971
+ @property
972
+ def model(self):
973
+ return self._model
974
+
975
+ @property
976
+ def view(self):
977
+ return self._view
978
+
979
+ def create_timer(self):
980
+ """Create a Timer that will update the States every second."""
981
+
982
+ self.states_capture_timer = QTimer()
983
+ # This is only needed when the Timer needs to run in another Thread
984
+ # self.states_capture_timer.moveToThread(self)
985
+ self.states_capture_timer.timeout.connect(self.update_values)
986
+ self.states_capture_timer.setInterval(self.timer_interval)
987
+
988
+ def start_timer(self):
989
+ self.states_capture_timer.start()
990
+
991
+ def stop_timer(self):
992
+ self.states_capture_timer.stop()
993
+
994
+ def update_values(self):
995
+ """Updates the common view widgets."""
996
+
997
+ if not self.has_connection():
998
+ self.view.set_connection_state("disconnected")
999
+ self.stop_timer()
1000
+
1001
+ if not self.model.is_device_connected():
1002
+ self.view.disable_device_action()
1003
+ if not self.model.is_cs_connected():
1004
+ self.view.uncheck_cs_action()
1005
+
1006
+ return
1007
+
1008
+ states = self.model.get_states()
1009
+
1010
+ if states:
1011
+ self.view.updateStates(states)
1012
+
1013
+ actuator_states = self.model.get_actuator_states()
1014
+ self.view.update_actuator_states(actuator_states)
1015
+
1016
+ upos = self.model.get_user_positions()
1017
+ mpos = self.model.get_machine_positions()
1018
+ alen = self.model.get_actuator_length()
1019
+
1020
+ # the updatePositions() checks for None, no need to do that here
1021
+
1022
+ self.view.updatePositions(upos, mpos, alen)
1023
+
1024
+ def update(self, changed_object):
1025
+ text = changed_object.text()
1026
+
1027
+ if text == "STOP":
1028
+ self.model.stop()
1029
+
1030
+ if text == "INFO":
1031
+ self.help_window.show()
1032
+
1033
+ if text == "DEVICE-CONNECT":
1034
+ print(f"Pressed {text}")
1035
+
1036
+ if changed_object.is_selected():
1037
+ MODULE_LOGGER.debug("Reconnecting the Hexapod model.")
1038
+ if self.model.reconnect_device():
1039
+ self.view.set_connection_state("connected")
1040
+ if not self.model.has_commands():
1041
+ self.model.load_commands()
1042
+ self.start_timer()
1043
+ else:
1044
+ self.view.device_connection.set_selected(False)
1045
+ else:
1046
+ MODULE_LOGGER.debug("Disconnecting the Hexapod model.")
1047
+ self.stop_timer()
1048
+ self.model.disconnect()
1049
+ self.view.set_connection_state("disconnected")
1050
+ return
1051
+
1052
+ if text == "CS-CONNECT":
1053
+ if changed_object.is_selected():
1054
+ MODULE_LOGGER.debug("Reconnecting the Hexapod Control Server.")
1055
+ self.model.reconnect_cs()
1056
+ if not self.model.has_commands():
1057
+ self.model.load_commands()
1058
+ self.start_timer()
1059
+ if self.model.is_device_connected() and self.model.is_cs_connected():
1060
+ self.view.set_connection_state("connected")
1061
+ self.view.device_connection.enable()
1062
+ else:
1063
+ MODULE_LOGGER.debug("Disconnecting the Hexapod Control Server.")
1064
+ self.stop_timer()
1065
+ self.model.disconnect_cs()
1066
+ self.view.device_connection.disable()
1067
+ self.view.set_connection_state("disconnected")
1068
+
1069
+ if text == "CONTROL":
1070
+ if changed_object.is_selected():
1071
+ self.model.activate_control_loop()
1072
+ else:
1073
+ self.model.deactivate_control_loop()
1074
+
1075
+ if text == "HOMING":
1076
+ self.model.homing()
1077
+
1078
+ if text == "CLEAR-ERRORS":
1079
+ self.model.clear_error()
1080
+
1081
+ if text == "Reset":
1082
+ # FIXME: This causes a problem in the GUI EventLoop as the reset waits for 30 seconds
1083
+ # before it finishes. This will hang the EventLoop for 30 seconds with a spinning
1084
+ # wheel. Do we need to run the hexapod commands (or some) in a separate thread?
1085
+ # Other commands like move to position will also take some time and cause the GUI
1086
+ # to apear hanging...
1087
+ #
1088
+ # It is known that time.sleep() should not be used in GUI applications.
1089
+ # Use QTimer.singleShot() instead.
1090
+
1091
+ self.model.reset()
1092
+
1093
+ def do(self, actions):
1094
+ for action, value in actions.items():
1095
+ MODULE_LOGGER.debug(f"do {action} with {value}")
1096
+ if action == "move_absolute":
1097
+ self.model.move_absolute(value)
1098
+ self.view.update_status_bar(message=f"command: {action}{value}")
1099
+ elif action == "move_relative_object":
1100
+ self.model.move_relative_object(value)
1101
+ self.view.update_status_bar(message=f"command: {action}{value}")
1102
+ elif action == "move_relative_user":
1103
+ self.model.move_relative_user(value)
1104
+ self.view.update_status_bar(message=f"command: {action}{value}")
1105
+ elif action == "check_absolute_movement":
1106
+ rc_dict = self.model.check_absolute_movement(value)
1107
+ self.view.update_status_bar(message=f"command: {action}{value}")
1108
+ self.view.positioning.set_position_validation_icon(rc_dict)
1109
+ elif action == "check_relative_object_movement":
1110
+ rc_dict = self.model.check_relative_object_movement(value)
1111
+ self.view.update_status_bar(message=f"command: {action}{value}")
1112
+ self.view.positioning.set_position_validation_icon(rc_dict)
1113
+ elif action == "check_relative_user_movement":
1114
+ rc_dict = self.model.check_relative_user_movement(value)
1115
+ self.view.update_status_bar(message=f"command: {action}{value}")
1116
+ self.view.positioning.set_position_validation_icon(rc_dict)
1117
+ elif action == "set_speed":
1118
+ tr_speed, rot_speed = value
1119
+ MODULE_LOGGER.info(f"Set speed: {tr_speed=}, {rot_speed=}")
1120
+ self.model.set_speed(tr_speed, rot_speed)
1121
+ elif action == "fetch_speed":
1122
+ tr_speed, rot_speed = self.model.get_speed()
1123
+ self.view.set_speed(tr_speed, rot_speed)
1124
+ elif action == "goto_zero_position":
1125
+ self.model.goto_zero_position()
1126
+ elif action == "goto_retracted_position":
1127
+ self.model.goto_retracted_position()
1128
+ elif action == "configure_coordinates_systems":
1129
+ self.model.configure_coordinates_systems(*value)
1130
+ elif action == "fetch_coordinates_systems":
1131
+ user_cs, object_cs = self.model.get_coordinates_systems()
1132
+ self.view.set_coordinates_systems(user_cs, object_cs)
1133
+ else:
1134
+ MODULE_LOGGER.warning(f"Unknown action {action}")
1135
+
1136
+
1137
+ class HexapodUIView(QMainWindow, Observable):
1138
+ def __init__(self):
1139
+ super().__init__()
1140
+
1141
+ self.setGeometry(300, 300, 300, 200)
1142
+
1143
+ self.mode_label = QLabel("")
1144
+ self.user_positions = None
1145
+
1146
+ # Widget for Manual Positioning, Configuration TAB
1147
+
1148
+ self.positioning = None
1149
+ self.configuration = None
1150
+
1151
+ self.coordinate_systems: CoordinateSystems = None
1152
+ self.speed_settings = None
1153
+
1154
+ # Widget for the Advanced TAB
1155
+
1156
+ self.actuator_states = None
1157
+
1158
+ # Widget fot the temperature log TAB
1159
+
1160
+ self.temperature_log = None
1161
+
1162
+ def on_click(self, icon: Union[QIcon, bool]):
1163
+ sender = self.sender()
1164
+
1165
+ MODULE_LOGGER.log(0, f"type(sender) = {type(sender)}")
1166
+ MODULE_LOGGER.log(0, f"sender.text() = {sender.text()}")
1167
+ MODULE_LOGGER.log(0, f"sender.isCheckable() = {sender.isCheckable()}")
1168
+ MODULE_LOGGER.log(0, f"sender.isChecked() = {sender.isChecked()}")
1169
+ MODULE_LOGGER.log(0, f"type(icon) = {type(icon)}")
1170
+
1171
+ # This will trigger the update() method on all the observers
1172
+
1173
+ self.notify_observers(sender)
1174
+
1175
+ def create_status_bar(self):
1176
+ self.statusBar().setStyleSheet("border: 0; background-color: #FFF8DC;")
1177
+ self.statusBar().setStyleSheet("QStatusBar::item {border: none;}")
1178
+ self.statusBar().addPermanentWidget(VLine())
1179
+ self.statusBar().addPermanentWidget(self.mode_label)
1180
+
1181
+ def create_toolbar(self):
1182
+ # The Switch On/OFF is in this case used for the Control ON/OFF action.
1183
+
1184
+ self.control = ToggleButton(
1185
+ name="CONTROL",
1186
+ status_tip="enable-disable the control loop on the servo motors",
1187
+ selected=get_resource(":/icons/switch-on.svg"),
1188
+ not_selected=get_resource(":/icons/switch-off.svg"),
1189
+ disabled=get_resource(":/icons/switch-disabled.svg"),
1190
+ )
1191
+ self.control.clicked.connect(self.on_click)
1192
+
1193
+ # The Home action is used to command the Homing to the Hexapod.
1194
+
1195
+ self.homing = TouchButton(
1196
+ name="HOMING",
1197
+ status_tip="perform a homing operation",
1198
+ selected=get_resource(":/icons/home.svg"),
1199
+ disabled=get_resource(":/icons/home-disabled.svg"),
1200
+ )
1201
+ self.homing.clicked.connect(self.on_click)
1202
+
1203
+ # The Clear action is used to command the ClearErrors to the Hexapod.
1204
+
1205
+ self.clear_errors = TouchButton(
1206
+ name="CLEAR-ERRORS",
1207
+ status_tip="clear the error list on the controller",
1208
+ selected=get_resource(":/icons/erase.svg"),
1209
+ disabled=get_resource(":/icons/erase-disabled.svg"),
1210
+ )
1211
+ self.clear_errors.clicked.connect(self.on_click)
1212
+
1213
+ # The Reconnect action is used to reconnect to the control server
1214
+
1215
+ self.cs_connection = ToggleButton(
1216
+ name="CS-CONNECT",
1217
+ status_tip="connect-disconnect hexapod control server.",
1218
+ selected=get_resource(":/icons/cs-connected.svg"),
1219
+ not_selected=get_resource(":/icons/cs-not-connected.svg"),
1220
+ disabled=get_resource(":/icons/cs-connected-disabled.svg"),
1221
+ )
1222
+ self.cs_connection.clicked.connect(self.on_click)
1223
+
1224
+ # The Reconnect action is used to reconnect the device
1225
+
1226
+ self.device_connection = ToggleButton(
1227
+ name="DEVICE-CONNECT",
1228
+ status_tip="connect-disconnect the hexapod controller",
1229
+ selected=get_resource(":/icons/plugged.svg"),
1230
+ not_selected=get_resource(":/icons/unplugged.svg"),
1231
+ disabled=get_resource(":/icons/plugged-disabled.svg"),
1232
+ )
1233
+ self.device_connection.clicked.connect(self.on_click)
1234
+
1235
+ # The STOP button is used to immediately stop the current motion
1236
+
1237
+ stop_button = QIcon(str(get_resource(":/icons/stop.svg")))
1238
+
1239
+ self.stop_action = QAction(stop_button, "STOP", self)
1240
+ self.stop_action.setToolTip("STOP Movement")
1241
+ self.stop_action.triggered.connect(self.on_click)
1242
+
1243
+ # The HELP button is used to show the on-line help in a browser window
1244
+
1245
+ help_button = QIcon(str(get_resource(":/icons/info.svg")))
1246
+
1247
+ self.help_action = QAction(help_button, "INFO", self)
1248
+ self.help_action.setToolTip("Browse the on-line documentation")
1249
+ self.help_action.triggered.connect(self.on_click)
1250
+
1251
+ # spacer widget to help with aligning STOP button to the right
1252
+
1253
+ spacer = QWidget()
1254
+ spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1255
+
1256
+ self.toolbar = self.addToolBar("MainToolbar")
1257
+ self.toolbar.addWidget(self.control)
1258
+ self.toolbar.addWidget(self.homing)
1259
+ self.toolbar.addWidget(self.clear_errors)
1260
+ self.toolbar.addWidget(self.device_connection)
1261
+ self.toolbar.addWidget(self.cs_connection)
1262
+ self.toolbar.addWidget(spacer)
1263
+ self.toolbar.addAction(self.stop_action)
1264
+ self.toolbar.addAction(self.help_action)
1265
+
1266
+ return self.toolbar
1267
+
1268
+ def create_user_position_widget(self):
1269
+ vbox_labels = QVBoxLayout()
1270
+ vbox_values = QVBoxLayout()
1271
+ vbox_units = QVBoxLayout()
1272
+ hbox = QHBoxLayout()
1273
+
1274
+ self.user_positions = [
1275
+ [QLabel("X"), QLabel(), QLabel("mm")],
1276
+ [QLabel("Y"), QLabel(), QLabel("mm")],
1277
+ [QLabel("Z"), QLabel(), QLabel("mm")],
1278
+ [QLabel("Rx"), QLabel(), QLabel("deg")],
1279
+ [QLabel("Ry"), QLabel(), QLabel("deg")],
1280
+ [QLabel("Rz"), QLabel(), QLabel("deg")],
1281
+ ]
1282
+
1283
+ for upos in self.user_positions:
1284
+ vbox_labels.addWidget(upos[0])
1285
+ vbox_values.addWidget(upos[1])
1286
+ upos[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1287
+ upos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1288
+ upos[1].setMinimumWidth(80)
1289
+ vbox_units.addWidget(upos[2])
1290
+
1291
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1292
+ vbox_labels.addStretch(1)
1293
+ vbox_values.addStretch(1)
1294
+ vbox_units.addStretch(1)
1295
+
1296
+ hbox.addLayout(vbox_labels)
1297
+ hbox.addLayout(vbox_values)
1298
+ hbox.addLayout(vbox_units)
1299
+
1300
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1301
+ hbox.addStretch(1)
1302
+
1303
+ gbox_positions = QGroupBox("Object [in User]", self)
1304
+ gbox_positions.setLayout(hbox)
1305
+ gbox_positions.setToolTip("The position of the Object Coordinate System in the User Coordinate System.")
1306
+
1307
+ return gbox_positions
1308
+
1309
+ def create_machine_position_widget(self):
1310
+ vbox_labels = QVBoxLayout()
1311
+ vbox_values = QVBoxLayout()
1312
+ vbox_units = QVBoxLayout()
1313
+ hbox = QHBoxLayout()
1314
+
1315
+ self.mach_positions = [
1316
+ [QLabel("X"), QLabel(), QLabel("mm")],
1317
+ [QLabel("Y"), QLabel(), QLabel("mm")],
1318
+ [QLabel("Z"), QLabel(), QLabel("mm")],
1319
+ [QLabel("Rx"), QLabel(), QLabel("deg")],
1320
+ [QLabel("Ry"), QLabel(), QLabel("deg")],
1321
+ [QLabel("Rz"), QLabel(), QLabel("deg")],
1322
+ ]
1323
+
1324
+ for mpos in self.mach_positions:
1325
+ vbox_labels.addWidget(mpos[0])
1326
+ vbox_values.addWidget(mpos[1])
1327
+ mpos[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1328
+ mpos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1329
+ mpos[1].setMinimumWidth(80)
1330
+ vbox_units.addWidget(mpos[2])
1331
+
1332
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1333
+ vbox_labels.addStretch(1)
1334
+ vbox_values.addStretch(1)
1335
+ vbox_units.addStretch(1)
1336
+
1337
+ hbox.addLayout(vbox_labels)
1338
+ hbox.addLayout(vbox_values)
1339
+ hbox.addLayout(vbox_units)
1340
+
1341
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1342
+ hbox.addStretch(1)
1343
+
1344
+ gbox_positions = QGroupBox("Platform [in Machine]", self)
1345
+ gbox_positions.setLayout(hbox)
1346
+ gbox_positions.setToolTip("The position of the Platform Coordinate System in the Machine Coordinate System.")
1347
+
1348
+ return gbox_positions
1349
+
1350
+ def create_actuator_length_widget(self):
1351
+ vbox_labels = QVBoxLayout()
1352
+ vbox_values = QVBoxLayout()
1353
+ vbox_units = QVBoxLayout()
1354
+ hbox = QHBoxLayout()
1355
+
1356
+ self.actuator_lengths = [
1357
+ [QLabel("L1"), QLabel(), QLabel("mm")],
1358
+ [QLabel("L2"), QLabel(), QLabel("mm")],
1359
+ [QLabel("L3"), QLabel(), QLabel("mm")],
1360
+ [QLabel("L4"), QLabel(), QLabel("mm")],
1361
+ [QLabel("L5"), QLabel(), QLabel("mm")],
1362
+ [QLabel("L6"), QLabel(), QLabel("mm")],
1363
+ ]
1364
+
1365
+ for alength in self.actuator_lengths:
1366
+ vbox_labels.addWidget(alength[0])
1367
+ vbox_values.addWidget(alength[1])
1368
+ alength[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1369
+ alength[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1370
+ alength[1].setMinimumWidth(80)
1371
+ vbox_units.addWidget(alength[2])
1372
+
1373
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1374
+ vbox_labels.addStretch(1)
1375
+ vbox_values.addStretch(1)
1376
+ vbox_units.addStretch(1)
1377
+
1378
+ hbox.addLayout(vbox_labels)
1379
+ hbox.addLayout(vbox_values)
1380
+ hbox.addLayout(vbox_units)
1381
+
1382
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1383
+ hbox.addStretch(1)
1384
+
1385
+ gbox_lengths = QGroupBox("Actuator Length", self)
1386
+ gbox_lengths.setLayout(hbox)
1387
+
1388
+ return gbox_lengths
1389
+
1390
+ def create_tabbed_widget(self):
1391
+ self.tabs = QTabWidget()
1392
+ self.tabs.setTabsClosable(False)
1393
+ self.tabs.setMovable(False)
1394
+ self.tabs.setDocumentMode(True)
1395
+ self.tabs.setElideMode(Qt.ElideRight)
1396
+ self.tabs.setUsesScrollButtons(True)
1397
+
1398
+ self.positioning = Positioning(self, self)
1399
+ self.tabs.addTab(self.positioning, "Positions")
1400
+ self.coordinate_systems = CoordinateSystems(self, self)
1401
+ self.speed_settings = SpeedSettings(self, self)
1402
+ self.configuration = QWidget()
1403
+ vbox = QVBoxLayout()
1404
+ vbox.setSpacing(0)
1405
+ vbox.addWidget(self.coordinate_systems)
1406
+ vbox.addWidget(self.speed_settings)
1407
+ self.configuration.setLayout(vbox)
1408
+ self.tabs.addTab(self.configuration, "Configuration")
1409
+ self.tabs.currentChanged.connect(self.reload_settings_for_tab)
1410
+
1411
+ # Actuator states are initialised in the sub-class because the states are different
1412
+ # for the Alpha and Aplha+ controllers
1413
+
1414
+ self.tabs.addTab(self.actuator_states, "Advanced State")
1415
+
1416
+ self.tabs.addTab(self.temperature_log, "Temperature Log")
1417
+
1418
+ return self.tabs
1419
+
1420
+ def reload_settings_for_tab(self, tab_idx):
1421
+ MODULE_LOGGER.info(f"Reload for tab: {tab_idx}")
1422
+ if self.configuration is self.tabs.widget(tab_idx):
1423
+ self.coordinate_systems.handle_fetch_coordinates_systems()
1424
+ self.speed_settings.handle_fetch_speed_settings()
1425
+
1426
+ def set_coordinates_systems(self, user_cs, object_cs):
1427
+ self.coordinate_systems.set_coordinates_systems(user_cs, object_cs)
1428
+
1429
+ def set_speed(self, vt, vr):
1430
+ self.speed_settings.set_speed(vt, vr)
1431
+
1432
+ def is_cs_action_enabled(self):
1433
+ return self.cs_connection.isEnabled()
1434
+
1435
+ def disable_cs_action(self):
1436
+ self.cs_connection.disable()
1437
+
1438
+ def enable_cs_action(self):
1439
+ self.cs_connection.enable()
1440
+
1441
+ def check_cs_action(self):
1442
+ self.cs_connection.set_selected()
1443
+
1444
+ def uncheck_cs_action(self):
1445
+ self.cs_connection.set_selected(False)
1446
+
1447
+ def disable_device_action(self):
1448
+ self.device_connection.disable()
1449
+
1450
+ def enable_device_action(self):
1451
+ self.device_connection.enabled()
1452
+
1453
+ def check_device_action(self):
1454
+ self.device_connection.set_selected()
1455
+
1456
+ def uncheck_device_action(self):
1457
+ self.device_connection.set_selected(False)
1458
+
1459
+ def set_connection_state(self, state):
1460
+ # enable or disable all actions that involve a device or cs connection
1461
+ # don't change the action buttons for the device nor the cs, that is handled
1462
+ # in the caller because it might be a device connection loss that causes this state
1463
+ # or a control server, or both...
1464
+
1465
+ MODULE_LOGGER.info(f"{state=}")
1466
+
1467
+ if state == "connected":
1468
+ self.control.enable()
1469
+ self.homing.enable()
1470
+ self.clear_errors.enable()
1471
+ self.positioning.enable_movement()
1472
+ elif state == "disconnected":
1473
+ self.control.disable()
1474
+ self.homing.disable()
1475
+ self.clear_errors.disable()
1476
+ self.positioning.disable_movement()
1477
+ else:
1478
+ raise UnknownStateError(f"Unknown State ({state}), expected 'connected' or 'disconnected'.")
1479
+
1480
+ def update_actuator_states(self, states):
1481
+ if states is None:
1482
+ return
1483
+
1484
+ self.actuator_states.set_states(states)