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,433 @@
1
+ """
2
+ A Graphical User Interface for monitoring and commanding the Symétrie JORAN Hexapod.
3
+
4
+ Start the GUI from your terminal as follows:
5
+
6
+ joran_ui [--type proxy|direct|simulator]
7
+
8
+ This GUI is based on the SYM_positioning application from Symétrie. The intent
9
+ is to provide operators a user interface which is platform independent, but
10
+ familiar.
11
+
12
+ The application is completely written in Python/Qt5 and can therefore run on any
13
+ platform that supports Python and Qt5.
14
+
15
+ """
16
+
17
+ import argparse
18
+ import logging
19
+ import multiprocessing
20
+ import sys
21
+ import threading
22
+ from pathlib import Path
23
+ from typing import List
24
+
25
+ multiprocessing.current_process().name = "joran_ui"
26
+
27
+ import pyqtgraph as pg
28
+ from PyQt5.QtCore import QDateTime, QLockFile
29
+ from PyQt5.QtCore import Qt
30
+ from PyQt5.QtGui import QIcon
31
+ from PyQt5.QtWidgets import QApplication, QMessageBox
32
+ from PyQt5.QtWidgets import QFrame
33
+ from PyQt5.QtWidgets import QGroupBox
34
+ from PyQt5.QtWidgets import QHBoxLayout
35
+ from PyQt5.QtWidgets import QLabel
36
+ from PyQt5.QtWidgets import QLineEdit
37
+ from PyQt5.QtWidgets import QVBoxLayout
38
+ from PyQt5.QtWidgets import QWidget
39
+ from prometheus_client import start_http_server
40
+
41
+ from egse.gui import show_warning_message
42
+ from egse.gui.led import Indic
43
+ from egse.gui.states import States
44
+ from egse.gui.stripchart import StripChart
45
+ from egse.hexapod.symetrie.hexapod_ui import ActuatorStates
46
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIController
47
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIModel
48
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIView
49
+ from egse.hexapod.symetrie.joran import JoranController
50
+ from egse.hexapod.symetrie.joran import JoranProxy
51
+ from egse.hexapod.symetrie.joran_protocol import JoranSimulator
52
+ from egse.process import ProcessStatus
53
+ from egse.resource import get_resource
54
+ from egse.settings import Settings
55
+ from egse.system import do_every
56
+
57
+ MODULE_LOGGER = logging.getLogger(__name__)
58
+
59
+ # Status LEDs define the number of status leds (length of the list), the description and the
60
+ # default color when the LED is on.
61
+
62
+ STATUS_LEDS = [
63
+ ["Error", Indic.RED], # bit 0
64
+ ["System Initialized", Indic.GREEN], # bit 1
65
+ ["Control On", Indic.GREEN], # bit 2
66
+ ["In Position", Indic.GREEN], # bit 3
67
+ ["Motion Task Running", Indic.GREEN], # bit 4
68
+ ["Home Task Running", Indic.GREEN], # bit 5
69
+ ["Home Complete", Indic.GREEN], # bit 6
70
+ ["Home Virtual", Indic.GREEN], # bit 7
71
+ ["Phase Found", Indic.GREEN], # bit 8
72
+ ["Brake on", Indic.GREEN], # bit 9
73
+ ["Motion Restricted", Indic.RED], # bit 10
74
+ ["Power on Encoders", Indic.GREEN], # bit 11
75
+ ["Power on Limit switches", Indic.GREEN], # bit 12
76
+ ["Power on Drives", Indic.GREEN], # bit 13
77
+ ["Emergency Stop", Indic.RED], # bit 14
78
+ ]
79
+
80
+ # The index of the Control LED
81
+
82
+ CONTROL_ONOFF = 2
83
+
84
+ ACTUATOR_STATE_LABELS = [
85
+ "Error: ",
86
+ "Control On: ",
87
+ "In Position: ",
88
+ "Motion Task Running: ",
89
+ "Home task running: ",
90
+ "Home complete: ",
91
+ "Phase found: ",
92
+ "Brake on: ",
93
+ "Home HW input: ",
94
+ "Negative HW limit switch: ",
95
+ "Positive HW limit switch: ",
96
+ "SW limit reached: ",
97
+ "Following Error: ",
98
+ "Drive fault: ",
99
+ "Encoder error: ",
100
+ ]
101
+
102
+ SPECIFIC_POSITIONS = ["Position ZERO", "Position RETRACTED"]
103
+
104
+
105
+ class TemperatureLog(QWidget):
106
+ """This Widget allows to view the temperature value of all six actuators."""
107
+
108
+ def __init__(self, temp: List[str] = None):
109
+ super().__init__()
110
+
111
+ self.stripchart = None
112
+
113
+ self.temperatures = temp
114
+
115
+ # Switch to using white background and black foreground for pyqtgraph stripcharts
116
+
117
+ pg.setConfigOption("background", "w")
118
+ pg.setConfigOption("foreground", "k")
119
+
120
+ vbox = QVBoxLayout()
121
+ vbox.addWidget(QLabel("Temperature values in C"))
122
+ vbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
123
+
124
+ self.create_temperature_stripchart_widget = self.create_temperature_stripchart()
125
+ vbox.addWidget(self.create_temperature_stripchart_widget)
126
+
127
+ self.create_temperature_box_widget = self.create_temperature_box()
128
+ vbox.addWidget(self.create_temperature_box_widget)
129
+
130
+ self.setLayout(vbox)
131
+
132
+ def create_temperature_box(self):
133
+ vbox = QVBoxLayout()
134
+ vbox.setSpacing(0)
135
+
136
+ for box in range(6):
137
+ wbox = QHBoxLayout()
138
+ wbox.setSpacing(0)
139
+ wbox.addWidget(QLabel(f"Temp.{box + 1}: "))
140
+ wbox.setSpacing(0)
141
+ editbox = QLineEdit()
142
+ editbox.setReadOnly(True)
143
+ editbox.setFixedSize(80, 20)
144
+ wbox.setSpacing(0)
145
+ wbox.addWidget(editbox)
146
+ wbox.setSpacing(0)
147
+ vbox.addLayout(wbox)
148
+ vbox.setSpacing(0)
149
+
150
+ create_temperature_box = QGroupBox()
151
+ create_temperature_box.setLayout(vbox)
152
+
153
+ return create_temperature_box
154
+
155
+ def create_temperature_stripchart(self):
156
+ self.stripchart = StripChart(labels={"left": ("measure", "C"), "bottom": ("Time", "d hh:mm:ss")})
157
+ self.stripchart.setInterval(60 * 60 * 12) # 12h of data
158
+ self.stripchart.set_yrange(0, 40)
159
+
160
+ vbox = QVBoxLayout()
161
+ vbox.addStretch(1)
162
+ vbox.addWidget(self.stripchart)
163
+
164
+ create_temperature_stripchart = QGroupBox()
165
+ create_temperature_stripchart.setLayout(vbox)
166
+
167
+ return create_temperature_stripchart
168
+
169
+
170
+ class JoranUIView(HexapodUIView):
171
+ def __init__(self):
172
+ super().__init__()
173
+
174
+ self.setWindowTitle("Hexapod JORAN Controller")
175
+
176
+ self.actuator_states = ActuatorStates(labels=ACTUATOR_STATE_LABELS)
177
+
178
+ self.temperature_log = TemperatureLog()
179
+
180
+ self.temperature_values = self.temperature_log.create_temperature_box_widget
181
+ self.temperature_values = self.temperature_values.findChildren(QLineEdit)
182
+ for temp in range(len(self.temperature_values)):
183
+ self.temperature_values[temp].setText("0.00")
184
+
185
+ self.temperature_stripchart = self.temperature_log.create_temperature_stripchart_widget
186
+ self.temperature_stripchart = self.temperature_stripchart.findChildren(StripChart)
187
+ self.temperature_stripchart = self.temperature_stripchart[0]
188
+
189
+ self.init_gui()
190
+
191
+ def init_gui(self):
192
+ # The main frame in which all the other frames are located, the outer Application frame
193
+
194
+ app_frame = QFrame()
195
+ app_frame.setObjectName("AppFrame")
196
+
197
+ # The left part which shows the states and positions
198
+
199
+ status_frame = QFrame()
200
+ status_frame.setObjectName("StatusFrame")
201
+
202
+ # The right part which has tabs that allow settings, movements, maintenance etc.
203
+
204
+ tabs_frame = QFrame()
205
+ tabs_frame.setObjectName("TabsFrame")
206
+
207
+ # The states of the Hexapod (contains all the leds)
208
+
209
+ states_frame = QFrame()
210
+ states_frame.setObjectName("StatesFrame")
211
+
212
+ # The user, machine positions and actuator lengths
213
+
214
+ positions_frame = QFrame()
215
+ positions_frame.setObjectName("PositionsFrame")
216
+
217
+ hbox = QHBoxLayout()
218
+ vbox_left = QVBoxLayout()
219
+ vbox_right = QVBoxLayout()
220
+
221
+ self.create_toolbar()
222
+ self.create_status_bar()
223
+
224
+ self.states = States(STATUS_LEDS)
225
+
226
+ user_positions_widget = self.create_user_position_widget()
227
+ mach_positions_widget = self.create_machine_position_widget()
228
+ actuator_length_widget = self.create_actuator_length_widget()
229
+
230
+ vbox_right.addWidget(user_positions_widget)
231
+ vbox_right.addWidget(mach_positions_widget)
232
+ vbox_right.addWidget(actuator_length_widget)
233
+
234
+ positions_frame.setLayout(vbox_right)
235
+
236
+ vbox_left.addWidget(self.states)
237
+
238
+ states_frame.setLayout(vbox_left)
239
+
240
+ hbox.addWidget(states_frame)
241
+ hbox.addWidget(positions_frame)
242
+
243
+ status_frame.setLayout(hbox)
244
+
245
+ tabbed_widget = self.create_tabbed_widget()
246
+
247
+ hbox = QHBoxLayout()
248
+ hbox.addWidget(tabbed_widget)
249
+ tabs_frame.setLayout(hbox)
250
+
251
+ hbox = QHBoxLayout()
252
+ hbox.addWidget(status_frame)
253
+ hbox.addWidget(tabs_frame)
254
+
255
+ app_frame.setLayout(hbox)
256
+
257
+ self.setCentralWidget(app_frame)
258
+
259
+ def update_status_bar(self, message=None, mode=None, timeout=2000):
260
+ if message:
261
+ self.statusBar().showMessage(message, msecs=timeout)
262
+ if mode:
263
+ self.mode_label.setStyleSheet(f"border: 0; color: {'red' if 'Simulator' in mode else 'black'};")
264
+
265
+ self.mode_label.setText(f"mode: {mode}")
266
+ self.statusBar().repaint()
267
+
268
+ def updatePositions(self, user_positions, machine_positions, actuator_lengths):
269
+ if user_positions is None:
270
+ MODULE_LOGGER.warning("no userPositions passed into updatePositions(), returning.")
271
+ return
272
+
273
+ for upos in range(len(self.user_positions)):
274
+ try:
275
+ self.user_positions[upos][1].setText(f"{user_positions[upos]:10.4f}")
276
+ except IndexError:
277
+ MODULE_LOGGER.error(f"IndexError in user_positions, upos = {upos}")
278
+
279
+ if machine_positions is None:
280
+ MODULE_LOGGER.warning("no machinePositions passed into updatePositions(), returning.")
281
+ return
282
+
283
+ for mpos in range(len(self.mach_positions)):
284
+ self.mach_positions[mpos][1].setText(f"{machine_positions[mpos]:10.4f}")
285
+
286
+ if actuator_lengths is None:
287
+ MODULE_LOGGER.warning("no actuatorLengths passed into updatePositions(), returning.")
288
+ return
289
+
290
+ for idx, alen in enumerate(self.actuator_lengths):
291
+ alen[1].setText(f"{actuator_lengths[idx]:10.4f}")
292
+
293
+ def updateStates(self, states):
294
+ if states is None:
295
+ return
296
+
297
+ self.updateControlButton(states[CONTROL_ONOFF])
298
+ self.states.set_states(states)
299
+
300
+ def updateControlButton(self, flag):
301
+ self.control.set_selected(on=flag)
302
+
303
+ def updateTemperature(self, temp):
304
+ if temp is None:
305
+ MODULE_LOGGER.warning("no temperature passed into updateTemperature(), returning.")
306
+ return
307
+ else:
308
+ # TODO: How to add the 6 temperature values to the stripchart?
309
+ value = temp[0]
310
+ self.temperature_stripchart.update(QDateTime.currentMSecsSinceEpoch(), value)
311
+ for t in range(len(self.temperature_values)):
312
+ self.temperature_values[t].setText(f"{temp[t]:10.4f}")
313
+
314
+
315
+ class JoranUIModel(HexapodUIModel):
316
+ def __init__(self, connection_type: str):
317
+ if connection_type == "proxy":
318
+ device = JoranProxy()
319
+ elif connection_type == "direct":
320
+ device = JoranController()
321
+ device.connect()
322
+ elif connection_type == "simulator":
323
+ device = JoranSimulator()
324
+ else:
325
+ raise ValueError(f"Unknown type of Hexapod implementation passed into the model: {connection_type}")
326
+
327
+ super().__init__(connection_type, device)
328
+
329
+ if device is not None:
330
+ MODULE_LOGGER.debug(f"Hexapod initialized as {device.__class__.__name__}")
331
+
332
+ def get_speed(self):
333
+ speed_settings = self.device.get_speed()
334
+ return speed_settings["vt"], speed_settings["vr"]
335
+
336
+ def get_temperature(self):
337
+ temp = self.device.get_temperature()
338
+ return temp
339
+
340
+
341
+ class JoranUIController(HexapodUIController):
342
+ def __init__(self, model: JoranUIModel, view: JoranUIView):
343
+ super().__init__(model, view)
344
+
345
+ def update_values(self):
346
+ super().update_values()
347
+
348
+ # Add here any updates to JORAN specific widgets
349
+
350
+ temp = self.model.get_temperature()
351
+ self.view.updateTemperature(temp)
352
+
353
+
354
+ def parse_arguments():
355
+ """
356
+ Prepare the arguments that are specific for this application.
357
+ """
358
+ parser = argparse.ArgumentParser()
359
+ parser.add_argument(
360
+ "--type",
361
+ dest="type",
362
+ action="store",
363
+ choices={"proxy", "simulator", "direct"},
364
+ help="Specify Hexapod implementation you want to connect to.",
365
+ default="proxy",
366
+ )
367
+ parser.add_argument(
368
+ "--profile",
369
+ default=False,
370
+ action="store_true",
371
+ help="Enable info logging messages with method profile information.",
372
+ )
373
+ return parser.parse_args()
374
+
375
+
376
+ def main():
377
+ lock_file = QLockFile(str(Path("~/joran_ui.app.lock").expanduser()))
378
+
379
+ styles_location = get_resource(":/styles/default.qss")
380
+ app_logo = get_resource(":/icons/logo-joran.svg")
381
+
382
+ args = list(sys.argv)
383
+ args[1:1] = ["-stylesheet", str(styles_location)]
384
+ app = QApplication(args)
385
+ app.setWindowIcon(QIcon(str(app_logo)))
386
+
387
+ if lock_file.tryLock(100):
388
+ process_status = ProcessStatus()
389
+
390
+ timer_thread = threading.Thread(target=do_every, args=(10, process_status.update))
391
+ timer_thread.daemon = True
392
+ timer_thread.start()
393
+
394
+ args = parse_arguments()
395
+
396
+ if args.profile:
397
+ Settings.set_profiling(True)
398
+
399
+ if args.type == "proxy":
400
+ proxy = JoranProxy()
401
+ if not proxy.ping():
402
+ description = "Could not connect to Hexapod Control Server"
403
+ info_text = (
404
+ "The GUI will start, but the connection button will show a disconnected state. "
405
+ "Please check if the Control Server is running and start the server if needed. "
406
+ "Otherwise, check if the correct HOSTNAME for the control server is set in the "
407
+ "Settings.yaml "
408
+ "configuration file."
409
+ )
410
+
411
+ show_warning_message(description, info_text)
412
+
413
+ view = JoranUIView()
414
+ model = JoranUIModel(args.type)
415
+ JoranUIController(model, view)
416
+
417
+ view.show()
418
+
419
+ return app.exec_()
420
+ else:
421
+ error_message = QMessageBox()
422
+ error_message.setIcon(QMessageBox.Warning)
423
+ error_message.setWindowTitle("Error")
424
+ error_message.setText("The Joran GUI application is already running!")
425
+ error_message.setStandardButtons(QMessageBox.Ok)
426
+
427
+ return error_message.exec()
428
+
429
+
430
+ if __name__ == "__main__":
431
+ logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
432
+
433
+ sys.exit(main())