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.
- egse/hexapod/__init__.py +33 -0
- egse/hexapod/symetrie/__init__.py +182 -0
- egse/hexapod/symetrie/alpha.py +857 -0
- egse/hexapod/symetrie/dynalpha.py +1438 -0
- egse/hexapod/symetrie/hexapod.py +550 -0
- egse/hexapod/symetrie/hexapod_ui.py +1484 -0
- egse/hexapod/symetrie/joran.py +289 -0
- egse/hexapod/symetrie/joran.yaml +62 -0
- egse/hexapod/symetrie/joran_cs.py +179 -0
- egse/hexapod/symetrie/joran_protocol.py +117 -0
- egse/hexapod/symetrie/joran_ui.py +433 -0
- egse/hexapod/symetrie/pmac.py +1001 -0
- egse/hexapod/symetrie/pmac_regex.py +86 -0
- egse/hexapod/symetrie/puna.py +649 -0
- egse/hexapod/symetrie/puna.yaml +193 -0
- egse/hexapod/symetrie/puna_cs.py +241 -0
- egse/hexapod/symetrie/puna_protocol.py +126 -0
- egse/hexapod/symetrie/puna_ui.py +410 -0
- egse/hexapod/symetrie/punaplus.py +115 -0
- egse/hexapod/symetrie/zonda.py +846 -0
- egse/hexapod/symetrie/zonda.yaml +337 -0
- egse/hexapod/symetrie/zonda_cs.py +239 -0
- egse/hexapod/symetrie/zonda_devif.py +416 -0
- egse/hexapod/symetrie/zonda_protocol.py +118 -0
- egse/hexapod/symetrie/zonda_ui.py +434 -0
- symetrie_hexapod/__init__.py +0 -0
- symetrie_hexapod/cgse_explore.py +19 -0
- symetrie_hexapod/cgse_services.py +162 -0
- symetrie_hexapod/settings.yaml +15 -0
- symetrie_hexapod-0.17.3.dist-info/METADATA +21 -0
- symetrie_hexapod-0.17.3.dist-info/RECORD +33 -0
- symetrie_hexapod-0.17.3.dist-info/WHEEL +4 -0
- symetrie_hexapod-0.17.3.dist-info/entry_points.txt +23 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A Graphical User Interface for monitoring and commanding the Symétrie ZONDA Hexapod.
|
|
3
|
+
|
|
4
|
+
Start the GUI from your terminal as follows:
|
|
5
|
+
|
|
6
|
+
zonda_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
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import threading
|
|
24
|
+
from typing import List
|
|
25
|
+
|
|
26
|
+
multiprocessing.current_process().name = "zonda_ui"
|
|
27
|
+
|
|
28
|
+
import pyqtgraph as pg
|
|
29
|
+
from PyQt5.QtCore import QDateTime, QLockFile
|
|
30
|
+
from PyQt5.QtCore import Qt
|
|
31
|
+
from PyQt5.QtGui import QIcon
|
|
32
|
+
from PyQt5.QtWidgets import QApplication, QMessageBox
|
|
33
|
+
from PyQt5.QtWidgets import QFrame
|
|
34
|
+
from PyQt5.QtWidgets import QGroupBox
|
|
35
|
+
from PyQt5.QtWidgets import QHBoxLayout
|
|
36
|
+
from PyQt5.QtWidgets import QLabel
|
|
37
|
+
from PyQt5.QtWidgets import QLineEdit
|
|
38
|
+
from PyQt5.QtWidgets import QVBoxLayout
|
|
39
|
+
from PyQt5.QtWidgets import QWidget
|
|
40
|
+
from prometheus_client import start_http_server
|
|
41
|
+
|
|
42
|
+
from egse.gui import show_warning_message
|
|
43
|
+
from egse.gui.led import Indic
|
|
44
|
+
from egse.gui.states import States
|
|
45
|
+
from egse.gui.stripchart import StripChart
|
|
46
|
+
from egse.hexapod.symetrie.hexapod_ui import ActuatorStates
|
|
47
|
+
from egse.hexapod.symetrie.hexapod_ui import HexapodUIController
|
|
48
|
+
from egse.hexapod.symetrie.hexapod_ui import HexapodUIModel
|
|
49
|
+
from egse.hexapod.symetrie.hexapod_ui import HexapodUIView
|
|
50
|
+
from egse.hexapod.symetrie.zonda import ZondaController
|
|
51
|
+
from egse.hexapod.symetrie.zonda import ZondaProxy
|
|
52
|
+
from egse.hexapod.symetrie.zonda import ZondaSimulator
|
|
53
|
+
from egse.process import ProcessStatus
|
|
54
|
+
from egse.resource import get_resource
|
|
55
|
+
from egse.settings import Settings
|
|
56
|
+
from egse.system import do_every
|
|
57
|
+
|
|
58
|
+
MODULE_LOGGER = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
# Status LEDs define the number of status leds (length of the list), the description and the
|
|
61
|
+
# default color when the LED is on.
|
|
62
|
+
|
|
63
|
+
STATUS_LEDS = [
|
|
64
|
+
["Error", Indic.RED], # bit 0
|
|
65
|
+
["System Initialized", Indic.GREEN], # bit 1
|
|
66
|
+
["Control On", Indic.GREEN], # bit 2
|
|
67
|
+
["In Position", Indic.GREEN], # bit 3
|
|
68
|
+
["Motion Task Running", Indic.GREEN], # bit 4
|
|
69
|
+
["Home Task Running", Indic.GREEN], # bit 5
|
|
70
|
+
["Home Complete", Indic.GREEN], # bit 6
|
|
71
|
+
["Home Virtual", Indic.GREEN], # bit 7
|
|
72
|
+
["Phase Found", Indic.GREEN], # bit 8
|
|
73
|
+
["Brake on", Indic.GREEN], # bit 9
|
|
74
|
+
["Motion Restricted", Indic.RED], # bit 10
|
|
75
|
+
["Power on Encoders", Indic.GREEN], # bit 11
|
|
76
|
+
["Power on Limit switches", Indic.GREEN], # bit 12
|
|
77
|
+
["Power on Drives", Indic.GREEN], # bit 13
|
|
78
|
+
["Emergency Stop", Indic.RED], # bit 14
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# The index of the Control LED
|
|
82
|
+
|
|
83
|
+
CONTROL_ONOFF = 2
|
|
84
|
+
|
|
85
|
+
ACTUATOR_STATE_LABELS = [
|
|
86
|
+
"Error: ",
|
|
87
|
+
"Control On: ",
|
|
88
|
+
"In Position: ",
|
|
89
|
+
"Motion Task Running: ",
|
|
90
|
+
"Home task running: ",
|
|
91
|
+
"Home complete: ",
|
|
92
|
+
"Phase found: ",
|
|
93
|
+
"Brake on: ",
|
|
94
|
+
"Home HW input: ",
|
|
95
|
+
"Negative HW limit switch: ",
|
|
96
|
+
"Positive HW limit switch: ",
|
|
97
|
+
"SW limit reached: ",
|
|
98
|
+
"Following Error: ",
|
|
99
|
+
"Drive fault: ",
|
|
100
|
+
"Encoder error: ",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
SPECIFIC_POSITIONS = ["Position ZERO", "Position RETRACTED"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TemperatureLog(QWidget):
|
|
107
|
+
"""This Widget allows to view the temperature value of all six actuators."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, temp: List[str] = None):
|
|
110
|
+
super().__init__()
|
|
111
|
+
|
|
112
|
+
self.stripchart = None
|
|
113
|
+
|
|
114
|
+
self.temperatures = temp
|
|
115
|
+
|
|
116
|
+
# Switch to using white background and black foreground for pyqtgraph stripcharts
|
|
117
|
+
|
|
118
|
+
pg.setConfigOption("background", "w")
|
|
119
|
+
pg.setConfigOption("foreground", "k")
|
|
120
|
+
|
|
121
|
+
vbox = QVBoxLayout()
|
|
122
|
+
vbox.addWidget(QLabel("Temperature values in C"))
|
|
123
|
+
vbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
|
124
|
+
|
|
125
|
+
self.create_temperature_stripchart_widget = self.create_temperature_stripchart()
|
|
126
|
+
vbox.addWidget(self.create_temperature_stripchart_widget)
|
|
127
|
+
|
|
128
|
+
self.create_temperature_box_widget = self.create_temperature_box()
|
|
129
|
+
vbox.addWidget(self.create_temperature_box_widget)
|
|
130
|
+
|
|
131
|
+
self.setLayout(vbox)
|
|
132
|
+
|
|
133
|
+
def create_temperature_box(self):
|
|
134
|
+
vbox = QVBoxLayout()
|
|
135
|
+
vbox.setSpacing(0)
|
|
136
|
+
|
|
137
|
+
for box in range(6):
|
|
138
|
+
wbox = QHBoxLayout()
|
|
139
|
+
wbox.setSpacing(0)
|
|
140
|
+
wbox.addWidget(QLabel(f"Temp.{box + 1}: "))
|
|
141
|
+
wbox.setSpacing(0)
|
|
142
|
+
editbox = QLineEdit()
|
|
143
|
+
editbox.setReadOnly(True)
|
|
144
|
+
editbox.setFixedSize(80, 20)
|
|
145
|
+
wbox.setSpacing(0)
|
|
146
|
+
wbox.addWidget(editbox)
|
|
147
|
+
wbox.setSpacing(0)
|
|
148
|
+
vbox.addLayout(wbox)
|
|
149
|
+
vbox.setSpacing(0)
|
|
150
|
+
|
|
151
|
+
create_temperature_box = QGroupBox()
|
|
152
|
+
create_temperature_box.setLayout(vbox)
|
|
153
|
+
|
|
154
|
+
return create_temperature_box
|
|
155
|
+
|
|
156
|
+
def create_temperature_stripchart(self):
|
|
157
|
+
self.stripchart = StripChart(labels={"left": ("measure", "C"), "bottom": ("Time", "d hh:mm:ss")})
|
|
158
|
+
self.stripchart.setInterval(60 * 60 * 12) # 12h of data
|
|
159
|
+
self.stripchart.set_yrange(0, 40)
|
|
160
|
+
|
|
161
|
+
vbox = QVBoxLayout()
|
|
162
|
+
vbox.addStretch(1)
|
|
163
|
+
vbox.addWidget(self.stripchart)
|
|
164
|
+
|
|
165
|
+
create_temperature_stripchart = QGroupBox()
|
|
166
|
+
create_temperature_stripchart.setLayout(vbox)
|
|
167
|
+
|
|
168
|
+
return create_temperature_stripchart
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ZondaUIView(HexapodUIView):
|
|
172
|
+
def __init__(self):
|
|
173
|
+
super().__init__()
|
|
174
|
+
|
|
175
|
+
self.setWindowTitle("Hexapod ZONDA Controller")
|
|
176
|
+
|
|
177
|
+
self.actuator_states = ActuatorStates(labels=ACTUATOR_STATE_LABELS)
|
|
178
|
+
|
|
179
|
+
self.temperature_log = TemperatureLog()
|
|
180
|
+
|
|
181
|
+
self.temperature_values = self.temperature_log.create_temperature_box_widget
|
|
182
|
+
self.temperature_values = self.temperature_values.findChildren(QLineEdit)
|
|
183
|
+
for temp in range(len(self.temperature_values)):
|
|
184
|
+
self.temperature_values[temp].setText("0.00")
|
|
185
|
+
|
|
186
|
+
self.temperature_stripchart = self.temperature_log.create_temperature_stripchart_widget
|
|
187
|
+
self.temperature_stripchart = self.temperature_stripchart.findChildren(StripChart)
|
|
188
|
+
self.temperature_stripchart = self.temperature_stripchart[0]
|
|
189
|
+
|
|
190
|
+
self.init_gui()
|
|
191
|
+
|
|
192
|
+
def init_gui(self):
|
|
193
|
+
# The main frame in which all the other frames are located, the outer Application frame
|
|
194
|
+
|
|
195
|
+
app_frame = QFrame()
|
|
196
|
+
app_frame.setObjectName("AppFrame")
|
|
197
|
+
|
|
198
|
+
# The left part which shows the states and positions
|
|
199
|
+
|
|
200
|
+
status_frame = QFrame()
|
|
201
|
+
status_frame.setObjectName("StatusFrame")
|
|
202
|
+
|
|
203
|
+
# The right part which has tabs that allow settings, movements, maintenance etc.
|
|
204
|
+
|
|
205
|
+
tabs_frame = QFrame()
|
|
206
|
+
tabs_frame.setObjectName("TabsFrame")
|
|
207
|
+
|
|
208
|
+
# The states of the Hexapod (contains all the leds)
|
|
209
|
+
|
|
210
|
+
states_frame = QFrame()
|
|
211
|
+
states_frame.setObjectName("StatesFrame")
|
|
212
|
+
|
|
213
|
+
# The user, machine positions and actuator lengths
|
|
214
|
+
|
|
215
|
+
positions_frame = QFrame()
|
|
216
|
+
positions_frame.setObjectName("PositionsFrame")
|
|
217
|
+
|
|
218
|
+
hbox = QHBoxLayout()
|
|
219
|
+
vbox_left = QVBoxLayout()
|
|
220
|
+
vbox_right = QVBoxLayout()
|
|
221
|
+
|
|
222
|
+
self.create_toolbar()
|
|
223
|
+
self.create_status_bar()
|
|
224
|
+
|
|
225
|
+
self.states = States(STATUS_LEDS)
|
|
226
|
+
|
|
227
|
+
user_positions_widget = self.create_user_position_widget()
|
|
228
|
+
mach_positions_widget = self.create_machine_position_widget()
|
|
229
|
+
actuator_length_widget = self.create_actuator_length_widget()
|
|
230
|
+
|
|
231
|
+
vbox_right.addWidget(user_positions_widget)
|
|
232
|
+
vbox_right.addWidget(mach_positions_widget)
|
|
233
|
+
vbox_right.addWidget(actuator_length_widget)
|
|
234
|
+
|
|
235
|
+
positions_frame.setLayout(vbox_right)
|
|
236
|
+
|
|
237
|
+
vbox_left.addWidget(self.states)
|
|
238
|
+
|
|
239
|
+
states_frame.setLayout(vbox_left)
|
|
240
|
+
|
|
241
|
+
hbox.addWidget(states_frame)
|
|
242
|
+
hbox.addWidget(positions_frame)
|
|
243
|
+
|
|
244
|
+
status_frame.setLayout(hbox)
|
|
245
|
+
|
|
246
|
+
tabbed_widget = self.create_tabbed_widget()
|
|
247
|
+
|
|
248
|
+
hbox = QHBoxLayout()
|
|
249
|
+
hbox.addWidget(tabbed_widget)
|
|
250
|
+
tabs_frame.setLayout(hbox)
|
|
251
|
+
|
|
252
|
+
hbox = QHBoxLayout()
|
|
253
|
+
hbox.addWidget(status_frame)
|
|
254
|
+
hbox.addWidget(tabs_frame)
|
|
255
|
+
|
|
256
|
+
app_frame.setLayout(hbox)
|
|
257
|
+
|
|
258
|
+
self.setCentralWidget(app_frame)
|
|
259
|
+
|
|
260
|
+
def update_status_bar(self, message=None, mode=None, timeout=2000):
|
|
261
|
+
if message:
|
|
262
|
+
self.statusBar().showMessage(message, msecs=timeout)
|
|
263
|
+
if mode:
|
|
264
|
+
self.mode_label.setStyleSheet(f"border: 0; color: {'red' if 'Simulator' in mode else 'black'};")
|
|
265
|
+
|
|
266
|
+
self.mode_label.setText(f"mode: {mode}")
|
|
267
|
+
self.statusBar().repaint()
|
|
268
|
+
|
|
269
|
+
def updatePositions(self, userPositions, machinePositions, actuatorLengths):
|
|
270
|
+
if userPositions is None:
|
|
271
|
+
MODULE_LOGGER.warning("no userPositions passed into updatePositions(), returning.")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
for upos in range(len(self.user_positions)):
|
|
275
|
+
try:
|
|
276
|
+
self.user_positions[upos][1].setText(f"{userPositions[upos]:10.4f}")
|
|
277
|
+
except IndexError:
|
|
278
|
+
MODULE_LOGGER.error(f"IndexError in user_positions, upos = {upos}")
|
|
279
|
+
|
|
280
|
+
if machinePositions is None:
|
|
281
|
+
MODULE_LOGGER.warning("no machinePositions passed into updatePositions(), returning.")
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
for mpos in range(len(self.mach_positions)):
|
|
285
|
+
self.mach_positions[mpos][1].setText(f"{machinePositions[mpos]:10.4f}")
|
|
286
|
+
|
|
287
|
+
if actuatorLengths is None:
|
|
288
|
+
MODULE_LOGGER.warning("no actuatorLengths passed into updatePositions(), returning.")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
for idx, alen in enumerate(self.actuator_lengths):
|
|
292
|
+
alen[1].setText(f"{actuatorLengths[idx]:10.4f}")
|
|
293
|
+
|
|
294
|
+
def updateStates(self, states):
|
|
295
|
+
if states is None:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
self.updateControlButton(states[CONTROL_ONOFF])
|
|
299
|
+
self.states.set_states(states)
|
|
300
|
+
|
|
301
|
+
def updateControlButton(self, flag):
|
|
302
|
+
self.control.set_selected(on=flag)
|
|
303
|
+
|
|
304
|
+
def updateTemperature(self, temp):
|
|
305
|
+
if temp is None:
|
|
306
|
+
MODULE_LOGGER.warning("no temperature passed into updateTemperature(), returning.")
|
|
307
|
+
return
|
|
308
|
+
else:
|
|
309
|
+
# TODO: How to add the 6 temperature values to the stripchart?
|
|
310
|
+
value = temp[0]
|
|
311
|
+
self.temperature_stripchart.update(QDateTime.currentMSecsSinceEpoch(), value)
|
|
312
|
+
for t in range(len(self.temperature_values)):
|
|
313
|
+
self.temperature_values[t].setText(f"{temp[t]:10.4f}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ZondaUIModel(HexapodUIModel):
|
|
317
|
+
def __init__(self, connection_type):
|
|
318
|
+
if connection_type == "proxy":
|
|
319
|
+
device = ZondaProxy()
|
|
320
|
+
elif connection_type == "direct":
|
|
321
|
+
device = ZondaController()
|
|
322
|
+
device.connect()
|
|
323
|
+
elif connection_type == "simulator":
|
|
324
|
+
device = ZondaSimulator()
|
|
325
|
+
else:
|
|
326
|
+
raise ValueError(f"Unknown type of Hexapod implementation passed into the model: {connection_type}")
|
|
327
|
+
|
|
328
|
+
super().__init__(connection_type, device)
|
|
329
|
+
|
|
330
|
+
if device is not None:
|
|
331
|
+
MODULE_LOGGER.debug(f"Hexapod initialized as {device.__class__.__name__}")
|
|
332
|
+
|
|
333
|
+
def get_speed(self):
|
|
334
|
+
speed_settings = self.device.get_speed()
|
|
335
|
+
return speed_settings["vt"], speed_settings["vr"]
|
|
336
|
+
|
|
337
|
+
def get_temperature(self):
|
|
338
|
+
temp = self.device.get_temperature()
|
|
339
|
+
return temp
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class ZondaUIController(HexapodUIController):
|
|
343
|
+
def __init__(self, model: ZondaUIModel, view: ZondaUIView):
|
|
344
|
+
super().__init__(model, view)
|
|
345
|
+
|
|
346
|
+
def update_values(self):
|
|
347
|
+
super().update_values()
|
|
348
|
+
|
|
349
|
+
# Add here any updates to ZONDA specific widgets
|
|
350
|
+
|
|
351
|
+
temp = self.model.get_temperature()
|
|
352
|
+
self.view.updateTemperature(temp)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def parse_arguments():
|
|
356
|
+
"""
|
|
357
|
+
Prepare the arguments that are specific for this application.
|
|
358
|
+
"""
|
|
359
|
+
parser = argparse.ArgumentParser()
|
|
360
|
+
parser.add_argument(
|
|
361
|
+
"--type",
|
|
362
|
+
dest="type",
|
|
363
|
+
action="store",
|
|
364
|
+
choices={"proxy", "simulator", "direct"},
|
|
365
|
+
help="Specify Hexapod implementation you want to connect to.",
|
|
366
|
+
default="proxy",
|
|
367
|
+
)
|
|
368
|
+
parser.add_argument(
|
|
369
|
+
"--profile",
|
|
370
|
+
default=False,
|
|
371
|
+
action="store_true",
|
|
372
|
+
help="Enable info logging messages with method profile information.",
|
|
373
|
+
)
|
|
374
|
+
return parser.parse_args()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def main():
|
|
378
|
+
lock_file = QLockFile(str(Path("~/zonda_ui.app.lock").expanduser()))
|
|
379
|
+
|
|
380
|
+
styles_location = get_resource(":/styles/default.qss")
|
|
381
|
+
app_logo = get_resource(":/icons/logo-zonda.svg")
|
|
382
|
+
|
|
383
|
+
args = list(sys.argv)
|
|
384
|
+
args[1:1] = ["-stylesheet", str(styles_location)]
|
|
385
|
+
app = QApplication(args)
|
|
386
|
+
app.setWindowIcon(QIcon(str(app_logo)))
|
|
387
|
+
|
|
388
|
+
if lock_file.tryLock(100):
|
|
389
|
+
process_status = ProcessStatus()
|
|
390
|
+
|
|
391
|
+
timer_thread = threading.Thread(target=do_every, args=(10, process_status.update))
|
|
392
|
+
timer_thread.daemon = True
|
|
393
|
+
timer_thread.start()
|
|
394
|
+
|
|
395
|
+
args = parse_arguments()
|
|
396
|
+
|
|
397
|
+
if args.profile:
|
|
398
|
+
Settings.set_profiling(True)
|
|
399
|
+
|
|
400
|
+
if args.type == "proxy":
|
|
401
|
+
proxy = ZondaProxy()
|
|
402
|
+
if not proxy.ping():
|
|
403
|
+
description = "Could not connect to Hexapod Control Server"
|
|
404
|
+
info_text = (
|
|
405
|
+
"The GUI will start, but the connection button will show a disconnected state. "
|
|
406
|
+
"Please check if the Control Server is running and start the server if needed. "
|
|
407
|
+
"Otherwise, check if the correct HOSTNAME for the control server is set in the "
|
|
408
|
+
"Settings.yaml "
|
|
409
|
+
"configuration file."
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
show_warning_message(description, info_text)
|
|
413
|
+
|
|
414
|
+
view = ZondaUIView()
|
|
415
|
+
model = ZondaUIModel(args.type)
|
|
416
|
+
ZondaUIController(model, view)
|
|
417
|
+
|
|
418
|
+
view.show()
|
|
419
|
+
|
|
420
|
+
return app.exec_()
|
|
421
|
+
else:
|
|
422
|
+
error_message = QMessageBox()
|
|
423
|
+
error_message.setIcon(QMessageBox.Warning)
|
|
424
|
+
error_message.setWindowTitle("Error")
|
|
425
|
+
error_message.setText("The Zonda GUI application is already running!")
|
|
426
|
+
error_message.setStandardButtons(QMessageBox.Ok)
|
|
427
|
+
|
|
428
|
+
return error_message.exec()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
if __name__ == "__main__":
|
|
432
|
+
logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
|
|
433
|
+
|
|
434
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"show_processes",
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from egse.process import ProcessInfo
|
|
8
|
+
from egse.process import get_processes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def show_processes():
|
|
12
|
+
"""Returns of list of ProcessInfo data classes for matching processes from this package."""
|
|
13
|
+
|
|
14
|
+
def filter_procs(pi: ProcessInfo):
|
|
15
|
+
pattern = r"(joran|puna|zonda)_(ui|cs|sim)"
|
|
16
|
+
|
|
17
|
+
return re.search(pattern, pi.command)
|
|
18
|
+
|
|
19
|
+
return get_processes(filter_procs)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import textwrap
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import rich
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from egse.system import all_logging_disabled
|
|
10
|
+
from egse.system import redirect_output_to_log
|
|
11
|
+
|
|
12
|
+
puna = typer.Typer(name="puna", help="PUNA Positioning Hexapod, Symétrie")
|
|
13
|
+
|
|
14
|
+
zonda = typer.Typer(name="zonda", help="ZONDA Positioning Hexapod, Symétrie")
|
|
15
|
+
|
|
16
|
+
joran = typer.Typer(name="joran", help="JORAN Positioning Hexapod, Symétrie")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def start_hexapod_cs_process(device_name, device_id, simulator):
|
|
20
|
+
"""Generic function to start the hexapod control server in the background."""
|
|
21
|
+
|
|
22
|
+
rich.print(f"Starting the {device_name} hexapod control server for {device_id} – {simulator = }")
|
|
23
|
+
|
|
24
|
+
out = redirect_output_to_log(f".{device_name.lower()}_cs.{device_id.lower()}.start.log")
|
|
25
|
+
|
|
26
|
+
cmd = [sys.executable, "-m", f"egse.hexapod.symetrie.{device_name.lower()}_cs", "start", device_id]
|
|
27
|
+
if simulator:
|
|
28
|
+
cmd.append("--simulator")
|
|
29
|
+
|
|
30
|
+
subprocess.Popen(cmd, stdout=out, stderr=out, stdin=subprocess.DEVNULL, close_fds=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stop_hexapod_cs_process(device_name, device_id):
|
|
34
|
+
"""Generic function to stop the hexapod control server in the background."""
|
|
35
|
+
|
|
36
|
+
rich.print(f"Terminating hexapod {device_name} control server for {device_id}...")
|
|
37
|
+
|
|
38
|
+
out = redirect_output_to_log(f".{device_name.lower()}_cs.{device_id.lower()}.stop.log")
|
|
39
|
+
|
|
40
|
+
cmd = [sys.executable, "-m", f"egse.hexapod.symetrie.{device_name.lower()}_cs", "stop", device_id]
|
|
41
|
+
|
|
42
|
+
subprocess.Popen(cmd, stdout=out, stderr=out, stdin=subprocess.DEVNULL, close_fds=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------- PUNA Commands ---------------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@puna.command(name="start")
|
|
49
|
+
def start_puna(
|
|
50
|
+
device_id: Annotated[str, typer.Argument(help="the device identifier, identifies the hardware controller")],
|
|
51
|
+
simulator: Annotated[
|
|
52
|
+
bool, typer.Option("--simulator", "--sim", help="use a device simulator as the backend")
|
|
53
|
+
] = False,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Start the PUNA hexapod control server. The control server is always started in the background.
|
|
57
|
+
"""
|
|
58
|
+
start_hexapod_cs_process("PUNA", device_id, simulator)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@puna.command(name="stop")
|
|
62
|
+
def stop_puna(
|
|
63
|
+
device_id: Annotated[str, typer.Argument(help="the device identifier, identifies the hardware controller")],
|
|
64
|
+
):
|
|
65
|
+
"""Stop the PUNA hexapod control server."""
|
|
66
|
+
|
|
67
|
+
stop_hexapod_cs_process("PUNA", device_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@puna.command(name="status")
|
|
71
|
+
def status_puna(device_id: str):
|
|
72
|
+
"""Print status information on the PUNA hexapod control server."""
|
|
73
|
+
|
|
74
|
+
with all_logging_disabled():
|
|
75
|
+
from egse.hexapod.symetrie import puna_cs
|
|
76
|
+
|
|
77
|
+
puna_cs.status(device_id)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@puna.command(name="start-sim")
|
|
81
|
+
def start_puna_sim(device_id: str):
|
|
82
|
+
"""Start the PUNA Hexapod Simulator (the device simulator)."""
|
|
83
|
+
|
|
84
|
+
rich.print(
|
|
85
|
+
textwrap.dedent(
|
|
86
|
+
f"""\
|
|
87
|
+
[orange3]The PUNA simulator is in development, for now use the `--sim` option when starting the control
|
|
88
|
+
server.[/]
|
|
89
|
+
|
|
90
|
+
The `--sim` option will use a Controller clas that doesn't send commands to the device, but simulates
|
|
91
|
+
the requests by performing actions on the reference frames defined in the hexapod. This means you are
|
|
92
|
+
not exercising the actual device commanding, but the net result or outcome is the same.
|
|
93
|
+
|
|
94
|
+
usage: cgse puna start --sim {device_id}
|
|
95
|
+
"""
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
rich.print("Starting service PUNA Simulator")
|
|
101
|
+
|
|
102
|
+
out = redirect_output_to_log(f".puna_sim.{device_id.lower()}.start.log")
|
|
103
|
+
|
|
104
|
+
subprocess.Popen(
|
|
105
|
+
[sys.executable, "-m", "egse.hexapod.symetrie.puna_sim", "start", device_id],
|
|
106
|
+
stdout=out,
|
|
107
|
+
stderr=out,
|
|
108
|
+
stdin=subprocess.DEVNULL,
|
|
109
|
+
close_fds=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@puna.command(name="stop-sim")
|
|
114
|
+
def stop_puna_sim(device_id: str):
|
|
115
|
+
"""Stop the PUNA Hexapod Simulator."""
|
|
116
|
+
rich.print("Terminating the PUNA simulator.")
|
|
117
|
+
|
|
118
|
+
out = redirect_output_to_log(f".puna_sim.{device_id.lower()}.stop.log")
|
|
119
|
+
|
|
120
|
+
subprocess.Popen(
|
|
121
|
+
[sys.executable, "-m", "egse.hexapod.symetrie.puna_sim", "stop", device_id],
|
|
122
|
+
stdout=out,
|
|
123
|
+
stderr=out,
|
|
124
|
+
stdin=subprocess.DEVNULL,
|
|
125
|
+
close_fds=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------- ZONDA Commands --------------------------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@zonda.command(name="start")
|
|
133
|
+
def start_zonda(
|
|
134
|
+
device_id: Annotated[str, typer.Argument(help="the device identifier, identifies the hardware controller")],
|
|
135
|
+
simulator: Annotated[
|
|
136
|
+
bool, typer.Option("--simulator", "--sim", help="use a device simulator as the backend")
|
|
137
|
+
] = False,
|
|
138
|
+
):
|
|
139
|
+
"""
|
|
140
|
+
Start the ZONDA hexapod control server. The control server is always started in the background.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
start_hexapod_cs_process("ZONDA", device_id, simulator)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@zonda.command(name="stop")
|
|
147
|
+
def stop_zonda(
|
|
148
|
+
device_id: Annotated[str, typer.Argument(help="the device identifier, identifies the hardware controller")],
|
|
149
|
+
):
|
|
150
|
+
"""Stop the ZONDA hexapod control server."""
|
|
151
|
+
|
|
152
|
+
stop_hexapod_cs_process("ZONDA", device_id)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@zonda.command(name="status")
|
|
156
|
+
def status_zonda(device_id: str):
|
|
157
|
+
"""Print status information on the ZONDA hexapod control server."""
|
|
158
|
+
|
|
159
|
+
with all_logging_disabled():
|
|
160
|
+
from egse.hexapod.symetrie import zonda_cs
|
|
161
|
+
|
|
162
|
+
zonda_cs.status(device_id)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
PACKAGES:
|
|
2
|
+
SYMETRIE_HEXAPOD: Device driver for the Symétrie Hexapods PUNA, ZONDA, and JORAN
|
|
3
|
+
|
|
4
|
+
PUNA Alpha+ Controller:
|
|
5
|
+
user_name: provide username in local settings
|
|
6
|
+
password: provide password in local settings
|
|
7
|
+
|
|
8
|
+
Hexapod Control Server:
|
|
9
|
+
SERVICE_TYPE: puna
|
|
10
|
+
PROTOCOL: tcp
|
|
11
|
+
HOSTNAME: localhost # The hostname that client shall connect to, e.g. on the same machine
|
|
12
|
+
COMMANDING_PORT: 0 # The port on which the controller listens to commands - REQ-REP
|
|
13
|
+
MONITORING_PORT: 0 # The port on which the controller sends periodic status information of the device - PUB-SUB
|
|
14
|
+
SERVICE_PORT: 0 # The port on which the controller listens for configuration and administration - REQ-REP
|
|
15
|
+
METRICS_PORT: 0 # The HTTP port where Prometheus will connect to for retrieving metrics
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: symetrie-hexapod
|
|
3
|
+
Version: 0.17.3
|
|
4
|
+
Summary: Symetrie Hexapod implementation for CGSE
|
|
5
|
+
Author: IvS KU Leuven
|
|
6
|
+
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: CGSE,Common-EGSE,hardware testing,software framework
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: cgse-common
|
|
11
|
+
Requires-Dist: cgse-coordinates
|
|
12
|
+
Requires-Dist: cgse-core
|
|
13
|
+
Requires-Dist: cgse-gui
|
|
14
|
+
Requires-Dist: cgse-tools
|
|
15
|
+
Requires-Dist: invoke
|
|
16
|
+
Requires-Dist: paramiko
|
|
17
|
+
Requires-Dist: pyqt5>=5.15.11
|
|
18
|
+
Requires-Dist: pyqtgraph
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Drivers for the Symétrie Hexapods
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
egse/hexapod/__init__.py,sha256=a-pdJ5j5XcLPaiz-zLIjfyJjFFUnbl_sOdasMp0UwEI,918
|
|
2
|
+
egse/hexapod/symetrie/__init__.py,sha256=TzNC_EIL5wQTyVYFXh8N5sVSjQHdA8fY1NdDtT2ZtUE,6621
|
|
3
|
+
egse/hexapod/symetrie/alpha.py,sha256=cEuNtOd5BPW4zSo4HpKyCsvcDj_YBjZHhPcBa4UXnxw,33277
|
|
4
|
+
egse/hexapod/symetrie/dynalpha.py,sha256=HuDopBZygBaAPb5CGl9jmeTa-pN23EBiIIlx_IjPrMA,55145
|
|
5
|
+
egse/hexapod/symetrie/hexapod.py,sha256=YZggPY2xJrD-jOzNWz8EutUs6hS0qFLW02h42Ol5q3w,19023
|
|
6
|
+
egse/hexapod/symetrie/hexapod_ui.py,sha256=nOk8FR_-4FYuwlhyqGPFPORAVDX4aNS2QWa00iZCacs,52752
|
|
7
|
+
egse/hexapod/symetrie/joran.py,sha256=dilr0kl8aLivK8HOeb3sDAA_F5HsMEtmekflc91PoBE,8556
|
|
8
|
+
egse/hexapod/symetrie/joran.yaml,sha256=p97aPO3JnNJqCyz6Md8Odkj3lX0m5ODhtdqwbAsUpuo,2669
|
|
9
|
+
egse/hexapod/symetrie/joran_cs.py,sha256=MB_sNpD0WpXLs219OfsSD7RaMPt8QaVIqX2zeWqNaVU,5365
|
|
10
|
+
egse/hexapod/symetrie/joran_protocol.py,sha256=E4GX0uE7evXANChN0R3akTKP_ipjAw-Ecc2NlUDrfF0,4444
|
|
11
|
+
egse/hexapod/symetrie/joran_ui.py,sha256=b8IjBPSpkHOr8kpoKYIhfiYYnKVjlVx-IRwjFpno7L8,14162
|
|
12
|
+
egse/hexapod/symetrie/pmac.py,sha256=8I6UzDUZjT2MtlI4ImZB68Mz6nFEHVJAu1YpvGd2iVw,30266
|
|
13
|
+
egse/hexapod/symetrie/pmac_regex.py,sha256=hS5wzrdT5FzQEYYDS5suur3yoIZKxBX7cRri2sGPOoE,2593
|
|
14
|
+
egse/hexapod/symetrie/puna.py,sha256=99hhOgrDjVFW-Kr8nw2UVL6WXPQ_0MPNlfc8Ge5Letc,22727
|
|
15
|
+
egse/hexapod/symetrie/puna.yaml,sha256=u_oEgIfuxt6ebWoZaADtO4cFpSVuUgMjG0G7y1_Nc0w,8534
|
|
16
|
+
egse/hexapod/symetrie/puna_cs.py,sha256=4Se4RAYYrw_2QKW2q03h3rMyTcBQPr7hN5Lyt3tmJNw,8048
|
|
17
|
+
egse/hexapod/symetrie/puna_protocol.py,sha256=6EgKpmQQZXLyqelWVxQGZUrAjABwSkhEYsgGizxCsSs,5010
|
|
18
|
+
egse/hexapod/symetrie/puna_ui.py,sha256=GaeBRhBobHVgB9vDoygqsp7LbDXbNFh9RgEXCfqgHwg,13524
|
|
19
|
+
egse/hexapod/symetrie/punaplus.py,sha256=4O2uRi6rah_qRnHfuzoCsyc2fzkvSUxiDkRos47y2Mk,3428
|
|
20
|
+
egse/hexapod/symetrie/zonda.py,sha256=jgPQh6ajt_LUI8Co0vyxwrgnZ5oG3szNPu3eqanohWM,29509
|
|
21
|
+
egse/hexapod/symetrie/zonda.yaml,sha256=MC4kQ9k1USAUZOgDS1-Yi331LYq53JSRIDQh4VZXUNw,18737
|
|
22
|
+
egse/hexapod/symetrie/zonda_cs.py,sha256=5DVyo_iKhhHbG8VDnaaWMQ3VuNPXuHirIvuQZX3q4oQ,8054
|
|
23
|
+
egse/hexapod/symetrie/zonda_devif.py,sha256=F2cRW3zoBZc8lOfAa9cYkiCcp3sXTE7Ar2ypNfuXEas,14798
|
|
24
|
+
egse/hexapod/symetrie/zonda_protocol.py,sha256=fdR110zt0IWI1U7usZu7aajmGPvJoHjXZVGaY3NOu-s,4750
|
|
25
|
+
egse/hexapod/symetrie/zonda_ui.py,sha256=IYqymUL_sSmIgeJf8jY_pocsyfi36XzJeZCaMChhWaQ,14140
|
|
26
|
+
symetrie_hexapod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
symetrie_hexapod/cgse_explore.py,sha256=0Pnz4fwbTQBRHoliF_dIJclK-7AaHmDRsAvsOuzDoGg,423
|
|
28
|
+
symetrie_hexapod/cgse_services.py,sha256=krF8Lk1uTNiRdZIxuaWtEDt8Q1cxpXOd0qgT9yefbOE,5350
|
|
29
|
+
symetrie_hexapod/settings.yaml,sha256=7tz8INhWSXV7bPTLYkS_06fYuMeeRiKRLuIW1_LnpX8,946
|
|
30
|
+
symetrie_hexapod-0.17.3.dist-info/METADATA,sha256=ecIW_u33cUX1mWPR5UbPzmeYyj_b4r4hPTlUoRJzyR8,650
|
|
31
|
+
symetrie_hexapod-0.17.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
symetrie_hexapod-0.17.3.dist-info/entry_points.txt,sha256=e_AmaY0OtfoHHnxDS2M0dbkZmPY38juEp42ZTwQtQkY,664
|
|
33
|
+
symetrie_hexapod-0.17.3.dist-info/RECORD,,
|