symetrie-hexapod 2023.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1509 @@
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
+
309
+ if error_codes:
310
+ self.validate_label.setFocus()
311
+ self.validate_label.invalidate()
312
+ self.validate_label.setToolTip(format_tooltip(tooltip or error_codes))
313
+ else:
314
+ self.validate_label.setFocus()
315
+ self.validate_label.validate()
316
+ self.validate_label.setToolTip("Movement command is valid.")
317
+
318
+ def get_manual_inputs(self):
319
+ """Returns the input positions as a list of floats."""
320
+ try:
321
+ pos = [
322
+ float(mm_pos[1].text().replace(",", ".")) for mm_pos in self.manual_mode_positions
323
+ ]
324
+ except ValueError as exc:
325
+ MODULE_LOGGER.error(f"Incorrect manual position input given: {exc}")
326
+
327
+ description = "Input errors in manual positions"
328
+ info_text = (
329
+ "Some of the values that you have filled into the manual position fields are "
330
+ "invalid. The fields can only contain floating point numbers, both '.' and ',"
331
+ "' are allowed."
332
+ )
333
+ show_warning_message(description, info_text)
334
+
335
+ return None
336
+
337
+ return pos
338
+
339
+ def handle_move_to_specific_position(self):
340
+
341
+ # Check which movement was requested
342
+
343
+ selected_text = self.combo_specific_position.currentText()
344
+ if "ZERO" in selected_text:
345
+ action = "goto_zero_position"
346
+ value = 1
347
+ if "RETRACTED" in selected_text:
348
+ action = "goto_retracted_position"
349
+ value = 2
350
+
351
+ self.observable.actionObservers({action: value})
352
+
353
+ def handle_movement(self):
354
+ # Read out the values in manual mode into an array of floats
355
+ # Users may use comma or point as a decimal delimiter
356
+
357
+ pos = self.get_manual_inputs()
358
+ if pos is None:
359
+ return
360
+
361
+ # Check which movement was requested
362
+
363
+ selected_text = self.combo_absolute_relative.currentText()
364
+ if selected_text == "Absolute":
365
+ movement = "move_absolute"
366
+ elif selected_text == "Relative object":
367
+ movement = "move_relative_object"
368
+ elif selected_text == "Relative user":
369
+ movement = "move_relative_user"
370
+
371
+ self.observable.actionObservers({movement: pos})
372
+
373
+ def handle_validate(self):
374
+ pos = self.get_manual_inputs()
375
+ if pos is None:
376
+ return
377
+
378
+ # Check which movement was requested
379
+
380
+ selected_text = self.combo_absolute_relative.currentText()
381
+ if selected_text == "Absolute":
382
+ validation = "check_absolute_movement"
383
+ elif selected_text == "Relative object":
384
+ validation = "check_relative_object_movement"
385
+ elif selected_text == "Relative user":
386
+ validation = "check_relative_user_movement"
387
+
388
+ self.observable.actionObservers({validation: pos})
389
+
390
+ def handle_copy_positions(self):
391
+ for pos, mm_pos in zip(self.view.user_positions, self.manual_mode_positions):
392
+ mm_pos[1].setText(pos[1].text())
393
+
394
+ self.validate_label.disable()
395
+ self.manual_mode_positions_widget.repaint()
396
+
397
+ def handle_clear_inputs(self):
398
+ for mm_pos in self.manual_mode_positions:
399
+ mm_pos[1].setText("0.0000")
400
+
401
+ self.validate_label.disable()
402
+ self.manual_mode_positions_widget.repaint()
403
+
404
+
405
+ class SpeedSettings(QWidget):
406
+
407
+ def __init__(self, view, observable):
408
+ super().__init__()
409
+ self.observable = observable
410
+ self.view = view
411
+
412
+ self.set_speed_widget: Optional[QGroupBox] = None
413
+ self.set_speed_widget = self.create_set_speed_widget()
414
+
415
+ vbox = QVBoxLayout()
416
+
417
+ hbox = QHBoxLayout()
418
+ hbox.addWidget(self.set_speed_widget)
419
+ hbox.addStretch()
420
+
421
+ # Add the Fetch and Apply for the Speed to the HBOX here
422
+
423
+ vbox_speed = QVBoxLayout()
424
+
425
+ fetch_button = QPushButton("Fetch")
426
+ fetch_button.setToolTip("Fetch speed settings from the Controller.")
427
+ fetch_button.clicked.connect(self.handle_fetch_speed_settings)
428
+
429
+ apply_button = QPushButton("Apply")
430
+ apply_button.setToolTip("Apply BOTH speed settings to the Controller.")
431
+ apply_button.clicked.connect(self.handle_apply_speed_settings)
432
+
433
+ vbox_speed.addWidget(fetch_button)
434
+ vbox_speed.addWidget(apply_button)
435
+
436
+ hbox.addLayout(vbox_speed)
437
+
438
+ vbox.addLayout(hbox)
439
+
440
+ self.setLayout(vbox)
441
+
442
+ def create_set_speed_widget(self):
443
+ """Creates the widget for the user set/get speed box."""
444
+
445
+ vbox = QVBoxLayout()
446
+
447
+ self.user_set_speed = [
448
+ [QLabel("Translation Speed (vt): "), QLineEdit(), QLabel("mm/s")],
449
+ [QLabel("Rotation Speed (vr): "), QLineEdit(), QLabel("°/s")]]
450
+
451
+ for speed in self.user_set_speed:
452
+ hbox = QHBoxLayout()
453
+ hbox.addWidget(speed[0])
454
+ hbox.addWidget(speed[1])
455
+ hbox.setSpacing(0)
456
+ hbox.addWidget(speed[2])
457
+ speed[0].setMinimumWidth(150)
458
+ speed[1].setText("0.0000")
459
+ speed[1].setStyleSheet("QLabel { background-color : LightGray; }")
460
+ speed[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
461
+ speed[1].setFixedWidth(100)
462
+ vbox.addLayout(hbox)
463
+
464
+ gbox_set_speed = QGroupBox("Set Speed parameters", self)
465
+ gbox_set_speed.setLayout(vbox)
466
+
467
+ return gbox_set_speed
468
+
469
+ def get_speed_settings_input(self):
470
+
471
+ tr_speed = float(self.user_set_speed[0][1].text())
472
+ rot_speed = float(self.user_set_speed[1][1].text())
473
+
474
+ return tr_speed, rot_speed
475
+
476
+ def set_speed(self, vt, vr):
477
+ self.user_set_speed[0][1].setText(str(vt))
478
+ self.user_set_speed[1][1].setText(str(vr))
479
+ self.set_speed_widget.repaint()
480
+
481
+ def handle_apply_speed_settings(self):
482
+
483
+ # Read out the values in the speed settings group
484
+
485
+ translation_speed, rotation_speed = self.get_speed_settings_input()
486
+
487
+ self.observable.actionObservers({"set_speed": (translation_speed, rotation_speed)})
488
+
489
+ def handle_fetch_speed_settings(self):
490
+
491
+ self.observable.actionObservers({"fetch_speed": True})
492
+
493
+
494
+ class CoordinateSystems(QWidget):
495
+ """This Widget allow to set the User and Object coordinate systems."""
496
+
497
+ def __init__(self, view, observable):
498
+ super().__init__()
499
+ self.observable = observable
500
+ self.view = view
501
+
502
+ # initialize instance variables used by this class
503
+
504
+ self.user_coordinates_system_widget: QGroupBox = None
505
+ self.user_coordinates_system: List = None
506
+
507
+ self.object_coordinates_system_widget: QGroupBox = None
508
+ self.object_coordinates_system: List = None
509
+
510
+ self.init_gui()
511
+
512
+ def init_gui(self):
513
+ """Initialize the main interface for this component."""
514
+
515
+ vbox = QVBoxLayout()
516
+
517
+ self.user_coordinates_system_widget = self.create_user_coordinates_system_widget()
518
+ self.object_coordinates_system_widget = self.create_object_coordinates_system_widget()
519
+
520
+ # The double arrow buttons are used to copy parameters from user to object coordinate
521
+ # system or vice versa.
522
+
523
+ copy_right_icon = QIcon(str(get_resource(":/icons/double-right-arrow.svg")))
524
+ copy_right_button = QPushButton()
525
+ copy_right_button.setIcon(copy_right_icon)
526
+ copy_right_button.setToolTip("Copy User coordinates to Object coordinates")
527
+ copy_right_button.clicked.connect(self.on_copy_right)
528
+
529
+ copy_left_icon = QIcon(str(get_resource(":/icons/double-left-arrow.svg")))
530
+ copy_left_button = QPushButton()
531
+ copy_left_button.setIcon(copy_left_icon)
532
+ copy_left_button.setToolTip("Copy Object coordinates to User coordinates")
533
+ copy_left_button.clicked.connect(self.on_copy_left)
534
+
535
+ vbox_icons = QVBoxLayout()
536
+ vbox_icons.addStretch()
537
+ vbox_icons.addWidget(copy_left_button)
538
+ vbox_icons.addWidget(copy_right_button)
539
+ vbox_icons.addStretch()
540
+
541
+ hbox = QHBoxLayout()
542
+
543
+ hbox.addWidget(self.user_coordinates_system_widget)
544
+ hbox.addLayout(vbox_icons)
545
+ hbox.addWidget(self.object_coordinates_system_widget)
546
+
547
+ vbox.addLayout(hbox)
548
+
549
+ apply_button = QPushButton("Apply")
550
+ apply_button.setToolTip("Apply the Coordinates Systems to the Controller.")
551
+ apply_button.clicked.connect(self.handle_apply_coordinates_systems)
552
+
553
+ fetch_button = QPushButton("Fetch")
554
+ fetch_button.setToolTip("Fetch the Coordinates Systems from the Controller.")
555
+ fetch_button.clicked.connect(self.handle_fetch_coordinates_systems)
556
+
557
+ hbox = QHBoxLayout()
558
+ hbox.addWidget(fetch_button)
559
+ hbox.addStretch()
560
+ hbox.addWidget(apply_button)
561
+ vbox.addLayout(hbox)
562
+
563
+ vbox.addStretch(1)
564
+
565
+ self.setLayout(vbox)
566
+
567
+ def set_coordinates_systems(self, user_cs, object_cs):
568
+ for usr, value in zip(self.user_coordinates_system, user_cs):
569
+ usr[1].setText(str(value))
570
+ for obj, value in zip(self.object_coordinates_system, object_cs):
571
+ obj[1].setText(str(value))
572
+ self.object_coordinates_system_widget.repaint()
573
+ self.user_coordinates_system_widget.repaint()
574
+
575
+ def create_user_coordinates_system_widget(self) -> QWidget:
576
+ """Creates the widget for the user coordinates system box."""
577
+
578
+ vbox = QVBoxLayout()
579
+
580
+ self.user_coordinates_system = [
581
+ [QLabel("X"), QLineEdit(), QLabel("mm")],
582
+ [QLabel("Y"), QLineEdit(), QLabel("mm")],
583
+ [QLabel("Z"), QLineEdit(), QLabel("mm")],
584
+ [QLabel("Rx"), QLineEdit(), QLabel("deg")],
585
+ [QLabel("Ry"), QLineEdit(), QLabel("deg")],
586
+ [QLabel("Rz"), QLineEdit(), QLabel("deg")],
587
+ ]
588
+
589
+ for mm_pos in self.user_coordinates_system:
590
+ hbox = QHBoxLayout()
591
+ hbox.addWidget(mm_pos[0])
592
+ hbox.addWidget(mm_pos[1])
593
+ hbox.addWidget(mm_pos[2])
594
+ mm_pos[0].setMinimumWidth(20)
595
+ mm_pos[1].setText("0.0000")
596
+ mm_pos[1].setStyleSheet("QLabel { background-color : LightGray; }")
597
+ mm_pos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
598
+ mm_pos[1].setMinimumWidth(50)
599
+ vbox.addLayout(hbox)
600
+
601
+ # Make sure the hboxes defined above stay nicely together when vertically resizing the
602
+ # Frame.
603
+
604
+ # vbox.addStretch()
605
+
606
+ gbox_user_coordinates_system = QGroupBox("User Coordinate System", self)
607
+ gbox_user_coordinates_system.setLayout(vbox)
608
+ gbox_user_coordinates_system.setToolTip("The User Coordinate System.")
609
+
610
+ return gbox_user_coordinates_system
611
+
612
+ def create_object_coordinates_system_widget(self) -> QWidget:
613
+ """Creates the widget for the object coordinates system box."""
614
+
615
+ vbox = QVBoxLayout()
616
+
617
+ self.object_coordinates_system = [
618
+ [QLabel("X"), QLineEdit(), QLabel("mm")],
619
+ [QLabel("Y"), QLineEdit(), QLabel("mm")],
620
+ [QLabel("Z"), QLineEdit(), QLabel("mm")],
621
+ [QLabel("Rx"), QLineEdit(), QLabel("deg")],
622
+ [QLabel("Ry"), QLineEdit(), QLabel("deg")],
623
+ [QLabel("Rz"), QLineEdit(), QLabel("deg")],
624
+ ]
625
+
626
+ for mm_pos in self.object_coordinates_system:
627
+ hbox = QHBoxLayout()
628
+ hbox.addWidget(mm_pos[0])
629
+ hbox.addWidget(mm_pos[1])
630
+ hbox.addWidget(mm_pos[2])
631
+ mm_pos[0].setMinimumWidth(20)
632
+ mm_pos[1].setText("0.0000")
633
+ mm_pos[1].setStyleSheet("QLabel { background-color : LightGray; }")
634
+ mm_pos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
635
+ mm_pos[1].setMinimumWidth(50)
636
+ vbox.addLayout(hbox)
637
+
638
+ # Make sure the hboxes defined above stay nicely together when vertically resizing the
639
+ # Frame.
640
+
641
+ # vbox.addStretch()
642
+
643
+ gbox_object_coordinates_system = QGroupBox("Object Coordinate System", self)
644
+ gbox_object_coordinates_system.setLayout(vbox)
645
+ gbox_object_coordinates_system.setToolTip("The Object Coordinate System.")
646
+
647
+ return gbox_object_coordinates_system
648
+
649
+ def on_copy_right(self, icon):
650
+ for usr, obj in zip(self.user_coordinates_system, self.object_coordinates_system):
651
+ obj[1].setText(usr[1].text())
652
+ self.object_coordinates_system_widget.repaint()
653
+
654
+ def on_copy_left(self, icon):
655
+ for usr, obj in zip(self.user_coordinates_system, self.object_coordinates_system):
656
+ usr[1].setText(obj[1].text())
657
+ self.user_coordinates_system_widget.repaint()
658
+
659
+ def get_coordinates_systems_inputs(self):
660
+ """Returns the values from the user and object coordinates systems as a tuple of two
661
+ lists of floats.
662
+
663
+ Returns:
664
+ A tuple containing two lists of floats, the first list for the user coordinates
665
+ system, the second list for the object coordinates system.
666
+ """
667
+ try:
668
+ user_cs = [
669
+ float(pos[1].text().replace(",", ".")) for pos in self.user_coordinates_system
670
+ ]
671
+ object_cs = [
672
+ float(pos[1].text().replace(",", ".")) for pos in self.object_coordinates_system
673
+ ]
674
+ except ValueError as exc:
675
+ MODULE_LOGGER.error(
676
+ f"Incorrect manual input given for user or object coordinates " f"system: {exc}"
677
+ )
678
+
679
+ description = "Input errors for coordinates systems"
680
+ info_text = (
681
+ "Some of the values that you have filled into the fields for the coordinates "
682
+ "systems are invalid. The fields can only contain floating point numbers, "
683
+ "both '.' and ',' are allowed."
684
+ )
685
+ show_warning_message(description, info_text)
686
+
687
+ return None, None
688
+
689
+ return user_cs, object_cs
690
+
691
+ def handle_apply_coordinates_systems(self):
692
+
693
+ # Read out the values in manual mode into an array of floats
694
+ # Users may use comma or point as a decimal delimiter
695
+
696
+ user_cs, object_cs = self.get_coordinates_systems_inputs()
697
+ if user_cs is None:
698
+ return
699
+
700
+ self.observable.actionObservers({"configure_coordinates_systems": (user_cs, object_cs)})
701
+
702
+ def handle_fetch_coordinates_systems(self):
703
+
704
+ self.observable.actionObservers({"fetch_coordinates_systems": True})
705
+
706
+
707
+ class ActuatorStates(QWidget):
708
+ """This Widget allows to view the state of all six actuators."""
709
+ def __init__(self, labels: List[str] = None):
710
+ super().__init__()
711
+
712
+ # initialize instance variables used by this class
713
+
714
+ self.status_labels = labels
715
+ self.leds: List[List] = []
716
+
717
+ vbox = QVBoxLayout()
718
+ vbox.addWidget(QLabel("Actuator States"))
719
+ vbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
720
+
721
+ self.create_states_widget = self.create_states()
722
+ vbox.addWidget(self.create_states_widget)
723
+ self.setLayout(vbox)
724
+
725
+ def create_states(self):
726
+ vbox = QVBoxLayout()
727
+
728
+ hbox = QHBoxLayout()
729
+ hbox.addWidget(QLabel(""))
730
+
731
+ for actuator_index in range(1, 7):
732
+
733
+ actuator_number = QLabel(str(actuator_index))
734
+ actuator_number.setAlignment(Qt.AlignHCenter)
735
+ actuator_number.setMinimumSize(20, 20)
736
+ actuator_number.setMaximumSize(20, 20)
737
+ # LED(self, size=QSize(20, 20), shape=ShapeEnum.SQUARE)
738
+ hbox.addWidget(actuator_number)
739
+ hbox.setSpacing(2)
740
+ vbox.addLayout(hbox)
741
+
742
+ for state in self.status_labels:
743
+ hbox = QHBoxLayout()
744
+ hbox.addWidget(QLabel(state))
745
+ actuator_leds = [
746
+ LED(self, size=QSize(20, 20), shape=ShapeEnum.SQUARE)
747
+ for _ in range(6)
748
+ ]
749
+ for led in actuator_leds:
750
+ hbox.addWidget(led)
751
+ hbox.setSpacing(2)
752
+ self.leds.append(actuator_leds)
753
+ vbox.addLayout(hbox)
754
+
755
+ create_states = QGroupBox()
756
+ create_states.setLayout(vbox)
757
+
758
+ return create_states
759
+
760
+ def set_states(self, states: List):
761
+
762
+ # States is a List of Lists, each inside list contains a dict with the states and
763
+ # another list with the states.
764
+
765
+ # zip(*states) will transpose the states
766
+
767
+ for leds, new_states in zip(self.leds, zip(*states)):
768
+ for led, state in zip(leds, new_states):
769
+ led.set_color(state)
770
+
771
+ def reset_states(self):
772
+ pass
773
+
774
+
775
+ def format_tooltip(value: Union[str, Dict, List] = None) -> str:
776
+ if value is None:
777
+ return ""
778
+
779
+ from rich.console import Console
780
+
781
+ console = Console(width=60, force_terminal=False, force_jupyter=False)
782
+ with console.capture() as capture:
783
+ console.print(value)
784
+
785
+ return capture.get()
786
+
787
+
788
+ class HexapodUIModel:
789
+ def __init__(self, connection_type: str, device):
790
+ self._connection_type = connection_type
791
+ self._device = device
792
+
793
+ @property
794
+ def connection_type(self):
795
+ return self._connection_type
796
+
797
+ @property
798
+ def device(self):
799
+ return self._device
800
+
801
+ def is_simulator(self):
802
+ return self.device.is_simulator()
803
+
804
+ def is_cs_connected(self):
805
+ return self.device.is_cs_connected() if self.connection_type == "proxy" else False
806
+
807
+ def is_device_connected(self):
808
+ return self.device.is_connected()
809
+
810
+ @deprecate(alternative="reconnect_device")
811
+ def reconnect(self):
812
+ self.reconnect_device()
813
+
814
+ def reconnect_device(self):
815
+ self.device.reconnect()
816
+ return self.device.is_connected()
817
+
818
+ def reconnect_cs(self):
819
+ self.device.reconnect_cs()
820
+ return self.device.is_cs_connected()
821
+
822
+ def disconnect(self):
823
+ self.device.disconnect()
824
+
825
+ def disconnect_cs(self):
826
+ self.device.disconnect_cs()
827
+
828
+ def has_commands(self):
829
+ if self.connection_type == "proxy":
830
+ return self.device.has_commands()
831
+ return True
832
+
833
+ def load_commands(self):
834
+ if self.connection_type == "proxy":
835
+ self.device.load_commands()
836
+
837
+ def get_states(self):
838
+ try:
839
+ _, states = self.device.get_general_state()
840
+ except TypeError:
841
+ states = None
842
+ return states
843
+
844
+ def configure_coordinates_systems(self, user_cs, object_cs):
845
+ return self.device.configure_coordinates_systems(*user_cs, *object_cs)
846
+
847
+ def get_coordinates_systems(self):
848
+ response = self.device.get_coordinates_systems()
849
+ user_cs = response[:6]
850
+ object_cs = response[6:]
851
+ return user_cs, object_cs
852
+
853
+ def get_user_positions(self):
854
+ return self.device.get_user_positions()
855
+
856
+ def get_machine_positions(self):
857
+ return self.device.get_machine_positions()
858
+
859
+ def get_actuator_length(self):
860
+ return self.device.get_actuator_length()
861
+
862
+ def get_actuator_states(self):
863
+ states = self.device.get_actuator_state()
864
+ states = [x[1] for x in states]
865
+ return states
866
+
867
+ def get_speed(self):
868
+ raise NotImplementedError
869
+
870
+ def activate_control_loop(self):
871
+ self.device.activate_control_loop()
872
+
873
+ def deactivate_control_loop(self):
874
+ self.device.deactivate_control_loop()
875
+
876
+ def check_absolute_movement(self, pos):
877
+ rc, rc_dict = self.device.check_absolute_movement(*pos)
878
+ MODULE_LOGGER.debug(rc)
879
+ MODULE_LOGGER.debug(rc_dict)
880
+ return rc_dict
881
+
882
+ def check_relative_object_movement(self, pos):
883
+ rc, rc_dict = self.device.check_relative_object_movement(*pos)
884
+ MODULE_LOGGER.debug(rc)
885
+ MODULE_LOGGER.debug(rc_dict)
886
+ return rc_dict
887
+
888
+ def check_relative_user_movement(self, pos):
889
+ rc, rc_dict = self.device.check_relative_user_movement(*pos)
890
+ MODULE_LOGGER.debug(rc)
891
+ MODULE_LOGGER.debug(rc_dict)
892
+ return rc_dict
893
+
894
+ def move_absolute(self, pos):
895
+ self.device.move_absolute(*pos)
896
+
897
+ def move_relative_object(self, pos):
898
+ self.device.move_relative_object(*pos)
899
+
900
+ def move_relative_user(self, pos):
901
+ self.device.move_relative_user(*pos)
902
+
903
+ def goto_zero_position(self):
904
+ self.device.goto_zero_position()
905
+
906
+ def goto_retracted_position(self):
907
+ self.device.goto_retracted_position()
908
+
909
+ def set_speed(self, tr_speed, rot_speed):
910
+ self.device.set_speed(tr_speed, rot_speed)
911
+
912
+ def homing(self):
913
+ self.device.homing()
914
+
915
+ def clear_error(self):
916
+ self.device.clear_error()
917
+
918
+ def reset(self):
919
+ self.device.reset()
920
+
921
+ def stop(self):
922
+ self.device.stop()
923
+
924
+
925
+ class HexapodUIController(Observer):
926
+ def __init__(self, model: HexapodUIModel, view):
927
+ self._model = model
928
+ self._view = view
929
+ self._view.addObserver(self)
930
+
931
+ self.states_capture_timer = None
932
+ self.timer_interval = 200
933
+ self.create_timer()
934
+
935
+ try:
936
+ if self.model.is_device_connected():
937
+ mode = self.model.connection_type.capitalize()
938
+ if self.model.is_simulator():
939
+ mode = f"{mode} [Simulator]"
940
+ self.view.update_status_bar(mode=mode)
941
+
942
+ self.view.check_device_action()
943
+ else:
944
+ self.view.uncheck_device_action()
945
+
946
+ if self.model.is_cs_connected():
947
+ self.view.check_cs_action()
948
+ else:
949
+ self.view.uncheck_cs_action()
950
+
951
+ MODULE_LOGGER.info(f"{self.model.is_device_connected()=}, {self.model.is_cs_connected()=}")
952
+
953
+ if model.connection_type in ["direct", "simulator"]:
954
+ view.disable_cs_action()
955
+
956
+ if self.has_connection():
957
+ self.view.set_connection_state("connected")
958
+ self.start_timer()
959
+ else:
960
+ self.view.set_connection_state("disconnected")
961
+ self.stop_timer()
962
+ except NotImplementedError:
963
+ MODULE_LOGGER.warning(
964
+ "There was no connection to the control server during startup, "
965
+ "GUI starts in disconnected mode."
966
+ )
967
+ self.view.uncheck_cs_action()
968
+ self.view.uncheck_device_action()
969
+ self.view.set_connection_state("disconnected")
970
+
971
+ def has_connection(self):
972
+ """
973
+ Returns True if the controller has a connection to the device. This takes into account
974
+ that the control server might be disabled when the controller is directly connected to
975
+ the device or to a simulator.
976
+ """
977
+ if self.view.is_cs_action_enabled():
978
+ return bool(self.model.is_device_connected() and self.model.is_cs_connected())
979
+ else:
980
+ return bool(self.model.is_device_connected())
981
+
982
+ @property
983
+ def model(self):
984
+ return self._model
985
+
986
+ @property
987
+ def view(self):
988
+ return self._view
989
+
990
+ def create_timer(self):
991
+ """Create a Timer that will update the States every second."""
992
+
993
+ self.states_capture_timer = QTimer()
994
+ # This is only needed when the Timer needs to run in another Thread
995
+ # self.states_capture_timer.moveToThread(self)
996
+ self.states_capture_timer.timeout.connect(self.update_values)
997
+ self.states_capture_timer.setInterval(self.timer_interval)
998
+
999
+ def start_timer(self):
1000
+ self.states_capture_timer.start()
1001
+
1002
+ def stop_timer(self):
1003
+ self.states_capture_timer.stop()
1004
+
1005
+ def update_values(self):
1006
+ """Updates the common view widgets."""
1007
+
1008
+ if not self.has_connection():
1009
+ self.view.set_connection_state("disconnected")
1010
+ self.stop_timer()
1011
+
1012
+ if not self.model.is_device_connected(): self.view.disable_device_action()
1013
+ if not self.model.is_cs_connected(): self.view.uncheck_cs_action()
1014
+
1015
+ return
1016
+
1017
+ states = self.model.get_states()
1018
+
1019
+ if states:
1020
+ self.view.updateStates(states)
1021
+
1022
+ actuator_states = self.model.get_actuator_states()
1023
+ self.view.update_actuator_states(actuator_states)
1024
+
1025
+ upos = self.model.get_user_positions()
1026
+ mpos = self.model.get_machine_positions()
1027
+ alen = self.model.get_actuator_length()
1028
+
1029
+ # the updatePositions() checks for None, no need to do that here
1030
+
1031
+ self.view.updatePositions(upos, mpos, alen)
1032
+
1033
+ def update(self, changed_object):
1034
+
1035
+ text = changed_object.text()
1036
+
1037
+ if text == "STOP":
1038
+ self.model.stop()
1039
+
1040
+ if text == "INFO":
1041
+ self.help_window.show()
1042
+
1043
+ if text == "DEVICE-CONNECT":
1044
+ print(f"Pressed {text}")
1045
+
1046
+ if changed_object.is_selected():
1047
+ MODULE_LOGGER.debug("Reconnecting the Hexapod model.")
1048
+ if self.model.reconnect_device():
1049
+ self.view.set_connection_state("connected")
1050
+ if not self.model.has_commands():
1051
+ self.model.load_commands()
1052
+ self.start_timer()
1053
+ else:
1054
+ self.view.device_connection.set_selected(False)
1055
+ else:
1056
+ MODULE_LOGGER.debug("Disconnecting the Hexapod model.")
1057
+ self.stop_timer()
1058
+ self.model.disconnect()
1059
+ self.view.set_connection_state("disconnected")
1060
+ return
1061
+
1062
+ if text == "CS-CONNECT":
1063
+ if changed_object.is_selected():
1064
+ MODULE_LOGGER.debug("Reconnecting the Hexapod Control Server.")
1065
+ self.model.reconnect_cs()
1066
+ if not self.model.has_commands():
1067
+ self.model.load_commands()
1068
+ self.start_timer()
1069
+ if self.model.is_device_connected() and self.model.is_cs_connected():
1070
+ self.view.set_connection_state("connected")
1071
+ self.view.device_connection.enable()
1072
+ else:
1073
+ MODULE_LOGGER.debug("Disconnecting the Hexapod Control Server.")
1074
+ self.stop_timer()
1075
+ self.model.disconnect_cs()
1076
+ self.view.device_connection.disable()
1077
+ self.view.set_connection_state("disconnected")
1078
+
1079
+ if text == "CONTROL":
1080
+ if changed_object.is_selected():
1081
+ self.model.activate_control_loop()
1082
+ else:
1083
+ self.model.deactivate_control_loop()
1084
+
1085
+ if text == "HOMING":
1086
+ self.model.homing()
1087
+
1088
+ if text == "CLEAR-ERRORS":
1089
+ self.model.clear_error()
1090
+
1091
+ if text == "Reset":
1092
+ # FIXME: This causes a problem in the GUI EventLoop as the reset waits for 30 seconds
1093
+ # before it finishes. This will hang the EventLoop for 30 seconds with a spinning
1094
+ # wheel. Do we need to run the hexapod commands (or some) in a separate thread?
1095
+ # Other commands like move to position will also take some time and cause the GUI
1096
+ # to apear hanging...
1097
+ #
1098
+ # It is known that time.sleep() should not be used in GUI applications.
1099
+ # Use QTimer.singleShot() instead.
1100
+
1101
+ self.model.reset()
1102
+
1103
+ def do(self, actions):
1104
+
1105
+ for action, value in actions.items():
1106
+ MODULE_LOGGER.debug(f"do {action} with {value}")
1107
+ if action == "move_absolute":
1108
+ self.model.move_absolute(value)
1109
+ self.view.update_status_bar(message=f"command: {action}{value}")
1110
+ elif action == "move_relative_object":
1111
+ self.model.move_relative_object(value)
1112
+ self.view.update_status_bar(message=f"command: {action}{value}")
1113
+ elif action == "move_relative_user":
1114
+ self.model.move_relative_user(value)
1115
+ self.view.update_status_bar(message=f"command: {action}{value}")
1116
+ elif action == "check_absolute_movement":
1117
+ rc_dict = self.model.check_absolute_movement(value)
1118
+ self.view.update_status_bar(message=f"command: {action}{value}")
1119
+ self.view.positioning.set_position_validation_icon(rc_dict)
1120
+ elif action == "check_relative_object_movement":
1121
+ rc_dict = self.model.check_relative_object_movement(value)
1122
+ self.view.update_status_bar(message=f"command: {action}{value}")
1123
+ self.view.positioning.set_position_validation_icon(rc_dict)
1124
+ elif action == "check_relative_user_movement":
1125
+ rc_dict = self.model.check_relative_user_movement(value)
1126
+ self.view.update_status_bar(message=f"command: {action}{value}")
1127
+ self.view.positioning.set_position_validation_icon(rc_dict)
1128
+ elif action == "set_speed":
1129
+ tr_speed, rot_speed = value
1130
+ MODULE_LOGGER.info(f"Set speed: {tr_speed=}, {rot_speed=}")
1131
+ self.model.set_speed(tr_speed, rot_speed)
1132
+ elif action == "fetch_speed":
1133
+ tr_speed, rot_speed = self.model.get_speed()
1134
+ self.view.set_speed(tr_speed, rot_speed)
1135
+ elif action == "goto_zero_position":
1136
+ self.model.goto_zero_position()
1137
+ elif action == "goto_retracted_position":
1138
+ self.model.goto_retracted_position()
1139
+ elif action == "configure_coordinates_systems":
1140
+ self.model.configure_coordinates_systems(*value)
1141
+ elif action == "fetch_coordinates_systems":
1142
+ user_cs, object_cs = self.model.get_coordinates_systems()
1143
+ self.view.set_coordinates_systems(user_cs, object_cs)
1144
+ else:
1145
+ MODULE_LOGGER.warning(f"Unknown action {action}")
1146
+
1147
+
1148
+ class HexapodUIView(QMainWindow, Observable):
1149
+ def __init__(self):
1150
+ super().__init__()
1151
+
1152
+ self.setGeometry(300, 300, 300, 200)
1153
+
1154
+ self.mode_label = QLabel("")
1155
+ self.user_positions = None
1156
+
1157
+ # Widget for Manual Positioning, Configuration TAB
1158
+
1159
+ self.positioning = None
1160
+ self.configuration = None
1161
+
1162
+ self.coordinate_systems: CoordinateSystems = None
1163
+ self.speed_settings = None
1164
+
1165
+ # Widget for the Advanced TAB
1166
+
1167
+ self.actuator_states = None
1168
+
1169
+ # Widget fot the temperature log TAB
1170
+
1171
+ self.temperature_log = None
1172
+
1173
+ def onClick(self, icon: Union[QIcon, bool]):
1174
+
1175
+ sender = self.sender()
1176
+
1177
+ MODULE_LOGGER.log(0, f"type(sender) = {type(sender)}")
1178
+ MODULE_LOGGER.log(0, f"sender.text() = {sender.text()}")
1179
+ MODULE_LOGGER.log(0, f"sender.isCheckable() = {sender.isCheckable()}")
1180
+ MODULE_LOGGER.log(0, f"sender.isChecked() = {sender.isChecked()}")
1181
+ MODULE_LOGGER.log(0, f"type(icon) = {type(icon)}")
1182
+
1183
+ # This will trigger the update() method on all the observers
1184
+
1185
+ self.notifyObservers(sender)
1186
+
1187
+ def createStatusBar(self):
1188
+
1189
+ self.statusBar().setStyleSheet("border: 0; background-color: #FFF8DC;")
1190
+ self.statusBar().setStyleSheet("QStatusBar::item {border: none;}")
1191
+ self.statusBar().addPermanentWidget(VLine())
1192
+ self.statusBar().addPermanentWidget(self.mode_label)
1193
+
1194
+ def createToolbar(self):
1195
+
1196
+ # The Switch On/OFF is in this case used for the Control ON/OFF action.
1197
+
1198
+ self.control = ToggleButton(
1199
+ name="CONTROL",
1200
+ status_tip="enable-disable the control loop on the servo motors",
1201
+ selected=get_resource(":/icons/switch-on.svg"),
1202
+ not_selected=get_resource(":/icons/switch-off.svg"),
1203
+ disabled=get_resource(":/icons/switch-disabled.svg")
1204
+ )
1205
+ self.control.clicked.connect(self.onClick)
1206
+
1207
+ # The Home action is used to command the Homing to the Hexapod.
1208
+
1209
+ self.homing = TouchButton(
1210
+ name="HOMING",
1211
+ status_tip="perform a homing operation",
1212
+ selected=get_resource(":/icons/home.svg"),
1213
+ disabled=get_resource(":/icons/home-disabled.svg"),
1214
+ )
1215
+ self.homing.clicked.connect(self.onClick)
1216
+
1217
+ # The Clear action is used to command the ClearErrors to the Hexapod.
1218
+
1219
+ self.clear_errors = TouchButton(
1220
+ name="CLEAR-ERRORS",
1221
+ status_tip="clear the error list on the controller",
1222
+ selected=get_resource(":/icons/erase.svg"),
1223
+ disabled=get_resource(":/icons/erase-disabled.svg"),
1224
+ )
1225
+ self.clear_errors.clicked.connect(self.onClick)
1226
+
1227
+ # The Reconnect action is used to reconnect to the control server
1228
+
1229
+ self.cs_connection = ToggleButton(
1230
+ name="CS-CONNECT",
1231
+ status_tip="connect-disconnect hexapod control server.",
1232
+ selected=get_resource(":/icons/cs-connected.svg"),
1233
+ not_selected=get_resource(":/icons/cs-not-connected.svg"),
1234
+ disabled=get_resource(":/icons/cs-connected-disabled.svg")
1235
+ )
1236
+ self.cs_connection.clicked.connect(self.onClick)
1237
+
1238
+ # The Reconnect action is used to reconnect the device
1239
+
1240
+ self.device_connection = ToggleButton(
1241
+ name="DEVICE-CONNECT",
1242
+ status_tip="connect-disconnect the hexapod controller",
1243
+ selected=get_resource(":/icons/plugged.svg"),
1244
+ not_selected=get_resource(":/icons/unplugged.svg"),
1245
+ disabled=get_resource(":/icons/plugged-disabled.svg")
1246
+ )
1247
+ self.device_connection.clicked.connect(self.onClick)
1248
+
1249
+
1250
+ # The STOP button is used to immediately stop the current motion
1251
+
1252
+ stop_button = QIcon(str(get_resource(":/icons/stop.svg")))
1253
+
1254
+ self.stop_action = QAction(stop_button, "STOP", self)
1255
+ self.stop_action.setToolTip("STOP Movement")
1256
+ self.stop_action.triggered.connect(self.onClick)
1257
+
1258
+ # The HELP button is used to show the on-line help in a browser window
1259
+
1260
+ help_button = QIcon(str(get_resource(":/icons/info.svg")))
1261
+
1262
+ self.help_action = QAction(help_button, "INFO", self)
1263
+ self.help_action.setToolTip("Browse the on-line documentation")
1264
+ self.help_action.triggered.connect(self.onClick)
1265
+
1266
+ # spacer widget to help with aligning STOP button to the right
1267
+
1268
+ spacer = QWidget()
1269
+ spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1270
+
1271
+ self.toolbar = self.addToolBar("MainToolbar")
1272
+ self.toolbar.addWidget(self.control)
1273
+ self.toolbar.addWidget(self.homing)
1274
+ self.toolbar.addWidget(self.clear_errors)
1275
+ self.toolbar.addWidget(self.device_connection)
1276
+ self.toolbar.addWidget(self.cs_connection)
1277
+ self.toolbar.addWidget(spacer)
1278
+ self.toolbar.addAction(self.stop_action)
1279
+ self.toolbar.addAction(self.help_action)
1280
+
1281
+ return self.toolbar
1282
+
1283
+ def createUserPositionWidget(self):
1284
+
1285
+ vbox_labels = QVBoxLayout()
1286
+ vbox_values = QVBoxLayout()
1287
+ vbox_units = QVBoxLayout()
1288
+ hbox = QHBoxLayout()
1289
+
1290
+ self.user_positions = [
1291
+ [QLabel("X"), QLabel(), QLabel("mm")],
1292
+ [QLabel("Y"), QLabel(), QLabel("mm")],
1293
+ [QLabel("Z"), QLabel(), QLabel("mm")],
1294
+ [QLabel("Rx"), QLabel(), QLabel("deg")],
1295
+ [QLabel("Ry"), QLabel(), QLabel("deg")],
1296
+ [QLabel("Rz"), QLabel(), QLabel("deg")],
1297
+ ]
1298
+
1299
+ for upos in self.user_positions:
1300
+ vbox_labels.addWidget(upos[0])
1301
+ vbox_values.addWidget(upos[1])
1302
+ upos[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1303
+ upos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1304
+ upos[1].setMinimumWidth(80)
1305
+ vbox_units.addWidget(upos[2])
1306
+
1307
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1308
+ vbox_labels.addStretch(1)
1309
+ vbox_values.addStretch(1)
1310
+ vbox_units.addStretch(1)
1311
+
1312
+ hbox.addLayout(vbox_labels)
1313
+ hbox.addLayout(vbox_values)
1314
+ hbox.addLayout(vbox_units)
1315
+
1316
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1317
+ hbox.addStretch(1)
1318
+
1319
+ gbox_positions = QGroupBox("Object [in User]", self)
1320
+ gbox_positions.setLayout(hbox)
1321
+ gbox_positions.setToolTip(
1322
+ "The position of the Object Coordinate System in the User Coordinate System."
1323
+ )
1324
+
1325
+ return gbox_positions
1326
+
1327
+ def createMachinePositionWidget(self):
1328
+
1329
+ vbox_labels = QVBoxLayout()
1330
+ vbox_values = QVBoxLayout()
1331
+ vbox_units = QVBoxLayout()
1332
+ hbox = QHBoxLayout()
1333
+
1334
+ self.mach_positions = [
1335
+ [QLabel("X"), QLabel(), QLabel("mm")],
1336
+ [QLabel("Y"), QLabel(), QLabel("mm")],
1337
+ [QLabel("Z"), QLabel(), QLabel("mm")],
1338
+ [QLabel("Rx"), QLabel(), QLabel("deg")],
1339
+ [QLabel("Ry"), QLabel(), QLabel("deg")],
1340
+ [QLabel("Rz"), QLabel(), QLabel("deg")],
1341
+ ]
1342
+
1343
+ for mpos in self.mach_positions:
1344
+ vbox_labels.addWidget(mpos[0])
1345
+ vbox_values.addWidget(mpos[1])
1346
+ mpos[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1347
+ mpos[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1348
+ mpos[1].setMinimumWidth(80)
1349
+ vbox_units.addWidget(mpos[2])
1350
+
1351
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1352
+ vbox_labels.addStretch(1)
1353
+ vbox_values.addStretch(1)
1354
+ vbox_units.addStretch(1)
1355
+
1356
+ hbox.addLayout(vbox_labels)
1357
+ hbox.addLayout(vbox_values)
1358
+ hbox.addLayout(vbox_units)
1359
+
1360
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1361
+ hbox.addStretch(1)
1362
+
1363
+ gbox_positions = QGroupBox("Platform [in Machine]", self)
1364
+ gbox_positions.setLayout(hbox)
1365
+ gbox_positions.setToolTip(
1366
+ "The position of the Platform Coordinate System in the Machine Coordinate System."
1367
+ )
1368
+
1369
+ return gbox_positions
1370
+
1371
+ def createActuatorLengthWidget(self):
1372
+
1373
+ vbox_labels = QVBoxLayout()
1374
+ vbox_values = QVBoxLayout()
1375
+ vbox_units = QVBoxLayout()
1376
+ hbox = QHBoxLayout()
1377
+
1378
+ self.actuator_lengths = [
1379
+ [QLabel("L1"), QLabel(), QLabel("mm")],
1380
+ [QLabel("L2"), QLabel(), QLabel("mm")],
1381
+ [QLabel("L3"), QLabel(), QLabel("mm")],
1382
+ [QLabel("L4"), QLabel(), QLabel("mm")],
1383
+ [QLabel("L5"), QLabel(), QLabel("mm")],
1384
+ [QLabel("L6"), QLabel(), QLabel("mm")],
1385
+ ]
1386
+
1387
+ for alength in self.actuator_lengths:
1388
+ vbox_labels.addWidget(alength[0])
1389
+ vbox_values.addWidget(alength[1])
1390
+ alength[1].setStyleSheet("QLabel { background-color : LightGrey; }")
1391
+ alength[1].setAlignment(Qt.AlignRight | Qt.AlignVCenter)
1392
+ alength[1].setMinimumWidth(80)
1393
+ vbox_units.addWidget(alength[2])
1394
+
1395
+ # Make sure the labels stay nicely together when vertically resizing the Frame.
1396
+ vbox_labels.addStretch(1)
1397
+ vbox_values.addStretch(1)
1398
+ vbox_units.addStretch(1)
1399
+
1400
+ hbox.addLayout(vbox_labels)
1401
+ hbox.addLayout(vbox_values)
1402
+ hbox.addLayout(vbox_units)
1403
+
1404
+ # Make sure the leds and labels stay nicely together when horizontally resizing the Frame.
1405
+ hbox.addStretch(1)
1406
+
1407
+ gbox_lengths = QGroupBox("Actuator Length", self)
1408
+ gbox_lengths.setLayout(hbox)
1409
+
1410
+ return gbox_lengths
1411
+
1412
+ def create_tabbed_widget(self):
1413
+
1414
+ self.tabs = QTabWidget()
1415
+ self.tabs.setTabsClosable(False)
1416
+ self.tabs.setMovable(False)
1417
+ self.tabs.setDocumentMode(True)
1418
+ self.tabs.setElideMode(Qt.ElideRight)
1419
+ self.tabs.setUsesScrollButtons(True)
1420
+
1421
+ self.positioning = Positioning(self, self)
1422
+ self.tabs.addTab(self.positioning, "Positions")
1423
+ self.coordinate_systems = CoordinateSystems(self, self)
1424
+ self.speed_settings = SpeedSettings(self, self)
1425
+ self.configuration = QWidget()
1426
+ vbox = QVBoxLayout()
1427
+ vbox.setSpacing(0)
1428
+ vbox.addWidget(self.coordinate_systems)
1429
+ vbox.addWidget(self.speed_settings)
1430
+ self.configuration.setLayout(vbox)
1431
+ self.tabs.addTab(self.configuration, "Configuration")
1432
+ self.tabs.currentChanged.connect(self.reload_settings_for_tab)
1433
+
1434
+ # Actuator states are initialised in the sub-class because the states are different
1435
+ # for the Alpha and Aplha+ controllers
1436
+
1437
+ self.tabs.addTab(self.actuator_states, "Advanced State")
1438
+
1439
+ self.tabs.addTab(self.temperature_log, "Temperature Log")
1440
+
1441
+ return self.tabs
1442
+
1443
+ def reload_settings_for_tab(self, tab_idx):
1444
+ MODULE_LOGGER.info(f"Reload for tab: {tab_idx}")
1445
+ if self.configuration is self.tabs.widget(tab_idx):
1446
+ self.coordinate_systems.handle_fetch_coordinates_systems()
1447
+ self.speed_settings.handle_fetch_speed_settings()
1448
+
1449
+ def set_coordinates_systems(self, user_cs, object_cs):
1450
+ self.coordinate_systems.set_coordinates_systems(user_cs, object_cs)
1451
+
1452
+ def set_speed(self, vt, vr):
1453
+ self.speed_settings.set_speed(vt, vr)
1454
+
1455
+ def is_cs_action_enabled(self):
1456
+ return self.cs_connection.isEnabled()
1457
+
1458
+ def disable_cs_action(self):
1459
+ self.cs_connection.disable()
1460
+
1461
+ def enable_cs_action(self):
1462
+ self.cs_connection.enable()
1463
+
1464
+ def check_cs_action(self):
1465
+ self.cs_connection.set_selected()
1466
+
1467
+ def uncheck_cs_action(self):
1468
+ self.cs_connection.set_selected(False)
1469
+
1470
+ def disable_device_action(self):
1471
+ self.device_connection.disable()
1472
+
1473
+ def enable_device_action(self):
1474
+ self.device_connection.enabled()
1475
+
1476
+ def check_device_action(self):
1477
+ self.device_connection.set_selected()
1478
+
1479
+ def uncheck_device_action(self):
1480
+ self.device_connection.set_selected(False)
1481
+
1482
+ def set_connection_state(self, state):
1483
+ # enable or disable all actions that involve a device or cs connection
1484
+ # don't change the action buttons for the device nor the cs, that is handled
1485
+ # in the caller because it might be a device connection loss that causes this state
1486
+ # or a control server, or both...
1487
+
1488
+ MODULE_LOGGER.info(f"{state=}")
1489
+
1490
+ if state == "connected":
1491
+ self.control.enable()
1492
+ self.homing.enable()
1493
+ self.clear_errors.enable()
1494
+ self.positioning.enable_movement()
1495
+ elif state == "disconnected":
1496
+ self.control.disable()
1497
+ self.homing.disable()
1498
+ self.clear_errors.disable()
1499
+ self.positioning.disable_movement()
1500
+ else:
1501
+ raise UnknownStateError(
1502
+ f"Unknown State ({state}), expected 'connected' or 'disconnected'."
1503
+ )
1504
+
1505
+ def update_actuator_states(self, states):
1506
+ if states is None:
1507
+ return
1508
+
1509
+ self.actuator_states.set_states(states)