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,410 @@
1
+ """
2
+ A Graphical User Interface for monitoring and commanding the Symétrie Hexapod.
3
+
4
+ Start the GUI from your terminal as follows:
5
+
6
+ puna_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 multiprocessing
19
+ import sys
20
+ import threading
21
+ from enum import IntEnum
22
+ from pathlib import Path
23
+
24
+ import typer
25
+ from PyQt5.QtCore import QLockFile
26
+ from PyQt5.QtGui import QIcon
27
+ from PyQt5.QtWidgets import QApplication
28
+ from PyQt5.QtWidgets import QFrame
29
+ from PyQt5.QtWidgets import QHBoxLayout
30
+ from PyQt5.QtWidgets import QMessageBox
31
+ from PyQt5.QtWidgets import QVBoxLayout
32
+
33
+ from egse.gui import show_warning_message
34
+ from egse.gui.led import Indic
35
+ from egse.gui.states import States
36
+ from egse.hexapod.symetrie import ControllerFactory
37
+ from egse.hexapod.symetrie import ProxyFactory
38
+ from egse.hexapod.symetrie import get_hexapod_controller_pars
39
+ from egse.hexapod.symetrie.hexapod_ui import ActuatorStates
40
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIController
41
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIModel
42
+ from egse.hexapod.symetrie.hexapod_ui import HexapodUIView
43
+ from egse.hexapod.symetrie.puna import PunaSimulator
44
+ from egse.hexapod.symetrie.punaplus import PunaPlusController
45
+ from egse.hexapod.symetrie.punaplus import PunaPlusProxy
46
+ from egse.log import logging
47
+ from egse.process import ProcessStatus
48
+ from egse.resource import get_resource
49
+ from egse.settings import Settings
50
+ from egse.system import do_every
51
+ from dotenv import load_dotenv
52
+
53
+ MODULE_LOGGER = logging.getLogger(__name__)
54
+
55
+ load_dotenv(override=True)
56
+
57
+
58
+ class DeviceControllerType(IntEnum):
59
+ ALPHA = 0
60
+ ALPHA_PLUS = 1
61
+
62
+
63
+ DCT = DeviceControllerType
64
+
65
+
66
+ # Status LEDs define the number of status leds (length of the list), the description and the
67
+ # default color when the LED is on.
68
+
69
+ STATUS_LEDS_ALPHA = [
70
+ ["Error", Indic.RED], # bit 0
71
+ ["System Initialized", Indic.GREEN], # bit 1
72
+ ["In position", Indic.GREEN], # bit 2
73
+ ["Amplifier enabled", Indic.GREEN], # bit 3
74
+ ["Homing done", Indic.GREEN], # bit 4
75
+ ["Brake on", Indic.GREEN], # bit 5
76
+ ["Emergency stop", Indic.ORANGE], # bit 6
77
+ ["Warning FE", Indic.ORANGE], # bit 7
78
+ ["Fatal FE", Indic.RED], # bit 8
79
+ ["Actuator Limit Error", Indic.RED], # bit 9
80
+ ["Amplifier Error", Indic.RED], # bit 10
81
+ ["Encoder error", Indic.RED], # bit 11
82
+ ["Phasing error", Indic.RED], # bit 12
83
+ ["Homing error", Indic.RED], # bit 13
84
+ ["Kinematic error", Indic.RED], # bit 14
85
+ ["Abort input error", Indic.RED], # bit 15
86
+ ["R/W memory error", Indic.RED], # bit 16
87
+ ["Temperature error", Indic.RED], # bit 17
88
+ ["Homing done (virtual)", Indic.ORANGE], # bit 18
89
+ ["Encoders power off", Indic.ORANGE], # bit 19
90
+ ["Limit switches power off", Indic.ORANGE], # bit 20
91
+ ["Reserved", Indic.BLACK], # bit 21
92
+ ["Reserved", Indic.BLACK], # bit 22
93
+ ["Reserved", Indic.BLACK], # bit 23
94
+ ]
95
+
96
+ STATUS_LEDS_ALPHA_PLUS = [
97
+ ["Error", Indic.RED], # bit 0
98
+ ["System Initialized", Indic.GREEN], # bit 1
99
+ ["Control On", Indic.GREEN], # bit 2
100
+ ["In Position", Indic.GREEN], # bit 3
101
+ ["Motion Task Running", Indic.GREEN], # bit 4
102
+ ["Home Task Running", Indic.GREEN], # bit 5
103
+ ["Home Complete", Indic.GREEN], # bit 6
104
+ ["Home Virtual", Indic.GREEN], # bit 7
105
+ ["Phase Found", Indic.GREEN], # bit 8
106
+ ["Brake on", Indic.GREEN], # bit 9
107
+ ["Motion Restricted", Indic.RED], # bit 10
108
+ ["Power on Encoders", Indic.GREEN], # bit 11
109
+ ["Power on Limit switches", Indic.GREEN], # bit 12
110
+ ["Power on Drives", Indic.GREEN], # bit 13
111
+ ["Emergency Stop", Indic.RED], # bit 14
112
+ ]
113
+
114
+ STATUS_LEDS = {DCT.ALPHA: STATUS_LEDS_ALPHA, DCT.ALPHA_PLUS: STATUS_LEDS_ALPHA_PLUS}
115
+
116
+ # The index of the Control LED
117
+
118
+ CONTROL_ONOFF = {DCT.ALPHA: 3, DCT.ALPHA_PLUS: 2}
119
+
120
+ ACTUATOR_STATE_LABELS_ALPHA = [
121
+ "In position",
122
+ "Control loop on servo motors active",
123
+ "Homing done",
124
+ "Input “Home switch”",
125
+ "Input “Positive limit switch”",
126
+ "Input “Negative limit switch”",
127
+ "Brake control output",
128
+ "Following error (warning)",
129
+ "Following error",
130
+ "Actuator out of bounds error",
131
+ "Amplifier error",
132
+ "Encoder error",
133
+ "Phasing error (brushless engine only)",
134
+ ]
135
+
136
+ ACTUATOR_STATE_LABELS_ALPHA_PLUS = [
137
+ "Error: ",
138
+ "Control On: ",
139
+ "In Position: ",
140
+ "Motion Task Running: ",
141
+ "Home task running: ",
142
+ "Home complete: ",
143
+ "Phase found: ",
144
+ "Brake on: ",
145
+ "Home HW input: ",
146
+ "Negative HW limit switch: ",
147
+ "Positive HW limit switch: ",
148
+ "SW limit reached: ",
149
+ "Following Error: ",
150
+ "Drive fault: ",
151
+ "Encoder error: ",
152
+ ]
153
+
154
+ ACTUATOR_STATE_LABELS = {DCT.ALPHA: ACTUATOR_STATE_LABELS_ALPHA, DCT.ALPHA_PLUS: ACTUATOR_STATE_LABELS_ALPHA_PLUS}
155
+
156
+
157
+ class PunaUIView(HexapodUIView):
158
+ def __init__(self, device_controller_type: DCT, device_id: str):
159
+ super().__init__()
160
+
161
+ self.dct = device_controller_type
162
+
163
+ if self.dct == DCT.ALPHA:
164
+ title = f"Hexapod PUNA Controller (Alpha) – {device_id}"
165
+ else:
166
+ title = f"Hexapod PUNA Controller (Alpha+) – {device_id}"
167
+
168
+ self.setWindowTitle(title)
169
+ self.actuator_states = ActuatorStates(labels=ACTUATOR_STATE_LABELS[self.dct])
170
+
171
+ self.init_gui()
172
+
173
+ def init_gui(self):
174
+ # The main frame in which all the other frames are located, the outer Application frame
175
+
176
+ app_frame = QFrame()
177
+ app_frame.setObjectName("AppFrame")
178
+
179
+ # The left part which shows the states and positions
180
+
181
+ status_frame = QFrame()
182
+ status_frame.setObjectName("StatusFrame")
183
+
184
+ # The right part which has tabs that allow settings, movements, maintenance etc.
185
+
186
+ tabs_frame = QFrame()
187
+ tabs_frame.setObjectName("TabsFrame")
188
+
189
+ # The states of the Hexapod (contains all the leds)
190
+
191
+ states_frame = QFrame()
192
+ states_frame.setObjectName("StatesFrame")
193
+
194
+ # The user, machine positions and actuator lengths
195
+
196
+ positions_frame = QFrame()
197
+ positions_frame.setObjectName("PositionsFrame")
198
+
199
+ hbox = QHBoxLayout()
200
+ vbox_left = QVBoxLayout()
201
+ vbox_right = QVBoxLayout()
202
+
203
+ self.create_toolbar()
204
+ self.create_status_bar()
205
+
206
+ self.states = States(STATUS_LEDS[self.dct])
207
+
208
+ user_positions_widget = self.create_user_position_widget()
209
+ mach_positions_widget = self.create_machine_position_widget()
210
+ actuator_length_widget = self.create_actuator_length_widget()
211
+
212
+ vbox_right.addWidget(user_positions_widget)
213
+ vbox_right.addWidget(mach_positions_widget)
214
+ vbox_right.addWidget(actuator_length_widget)
215
+
216
+ positions_frame.setLayout(vbox_right)
217
+
218
+ vbox_left.addWidget(self.states)
219
+
220
+ states_frame.setLayout(vbox_left)
221
+
222
+ hbox.addWidget(states_frame)
223
+ hbox.addWidget(positions_frame)
224
+
225
+ status_frame.setLayout(hbox)
226
+
227
+ tabbed_widget = self.create_tabbed_widget()
228
+
229
+ hbox = QHBoxLayout()
230
+ hbox.addWidget(tabbed_widget)
231
+ tabs_frame.setLayout(hbox)
232
+
233
+ hbox = QHBoxLayout()
234
+ hbox.addWidget(status_frame)
235
+ hbox.addWidget(tabs_frame)
236
+
237
+ app_frame.setLayout(hbox)
238
+
239
+ self.setCentralWidget(app_frame)
240
+
241
+ def update_status_bar(self, message=None, mode=None, timeout=2000):
242
+ if message:
243
+ self.statusBar().showMessage(message, msecs=timeout)
244
+ if mode:
245
+ self.mode_label.setStyleSheet(f"border: 0; color: {'red' if 'Simulator' in mode else 'black'};")
246
+ self.mode_label.setText(f"mode: {mode}")
247
+ self.statusBar().repaint()
248
+
249
+ def updatePositions(self, userPositions, machinePositions, actuatorLengths):
250
+ if userPositions is None:
251
+ MODULE_LOGGER.warning("no userPositions passed into updatePositions(), returning.")
252
+ return
253
+
254
+ for upos in range(len(self.user_positions)):
255
+ try:
256
+ self.user_positions[upos][1].setText(f"{userPositions[upos]:10.4f}")
257
+ except IndexError:
258
+ MODULE_LOGGER.error(f"IndexError in user_positions, upos = {upos}")
259
+
260
+ if machinePositions is None:
261
+ MODULE_LOGGER.warning("no machinePositions passed into updatePositions(), returning.")
262
+ return
263
+
264
+ for mpos in range(len(self.mach_positions)):
265
+ self.mach_positions[mpos][1].setText(f"{machinePositions[mpos]:10.4f}")
266
+
267
+ if actuatorLengths is None:
268
+ MODULE_LOGGER.warning("no actuatorLengths passed into updatePositions(), returning.")
269
+ return
270
+
271
+ for idx, alen in enumerate(self.actuator_lengths):
272
+ alen[1].setText(f"{actuatorLengths[idx]:10.4f}")
273
+
274
+ def updateStates(self, states):
275
+ if states is None:
276
+ return
277
+
278
+ self.updateControlButton(states[CONTROL_ONOFF[self.dct]])
279
+ self.states.set_states(states)
280
+
281
+ def updateControlButton(self, flag):
282
+ self.control.set_selected(on=flag)
283
+
284
+
285
+ class PunaUIModel(HexapodUIModel):
286
+ def __init__(self, connection_type, device_id):
287
+ hostname, port, dev_id, dev_name, *_ = get_hexapod_controller_pars(device_id)
288
+ if connection_type == "proxy":
289
+ device = ProxyFactory().create(dev_name, device_id=dev_id)
290
+ elif connection_type == "direct":
291
+ device = ControllerFactory().create(dev_name, device_id=dev_id)
292
+ device.connect()
293
+ elif connection_type == "simulator":
294
+ device = PunaSimulator()
295
+ else:
296
+ raise ValueError(f"Unknown type of Hexapod implementation passed into the model: {connection_type}")
297
+
298
+ super().__init__(connection_type, device)
299
+
300
+ self._device_id = dev_id
301
+
302
+ if device is not None:
303
+ MODULE_LOGGER.debug(f"Hexapod initialized as {device.__class__.__name__}")
304
+
305
+ @property
306
+ def device_id(self):
307
+ return self._device_id
308
+
309
+ def get_device_controller_type(self) -> DCT:
310
+ return DCT.ALPHA_PLUS if isinstance(self.device, (PunaPlusProxy, PunaPlusController)) else DCT.ALPHA
311
+
312
+ def get_speed(self):
313
+ vt, vr, vt_min, vr_min, vt_max, vr_max = self.device.get_speed()
314
+ return vt, vr
315
+
316
+
317
+ class PunaUIController(HexapodUIController):
318
+ def __init__(self, model: PunaUIModel, view: PunaUIView):
319
+ super().__init__(model, view)
320
+
321
+ def update_values(self):
322
+ super().update_values()
323
+
324
+ # Add here any updates to PUNA specific widgets
325
+
326
+
327
+ def parse_arguments():
328
+ """
329
+ Prepare the arguments that are specific for this application.
330
+ """
331
+ parser = argparse.ArgumentParser()
332
+ parser.add_argument(
333
+ "--type",
334
+ dest="type",
335
+ action="store",
336
+ choices={"proxy", "simulator", "direct"},
337
+ help="Specify Hexapod implementation you want to connect to.",
338
+ default="proxy",
339
+ )
340
+ parser.add_argument(
341
+ "--profile",
342
+ default=False,
343
+ action="store_true",
344
+ help="Enable info logging messages with method profile information.",
345
+ )
346
+ return parser.parse_args()
347
+
348
+
349
+ app = typer.Typer()
350
+
351
+
352
+ @app.command()
353
+ def main(device_id: str, device_type: str = "proxy", profile: bool = False):
354
+ multiprocessing.current_process().name = "puna_ui"
355
+
356
+ lock_file = QLockFile(str(Path("~/puna_ui.app.lock").expanduser()))
357
+
358
+ styles_location = get_resource(":/styles/default.qss")
359
+ app_logo = get_resource(":/icons/logo-puna.svg")
360
+
361
+ app = QApplication(["-stylesheet", str(styles_location)])
362
+ app.setWindowIcon(QIcon(str(app_logo)))
363
+
364
+ if lock_file.tryLock(100):
365
+ process_status = ProcessStatus()
366
+
367
+ timer_thread = threading.Thread(target=do_every, args=(10, process_status.update))
368
+ timer_thread.daemon = True
369
+ timer_thread.start()
370
+
371
+ if profile:
372
+ Settings.set_profiling(True)
373
+
374
+ if device_type == "proxy":
375
+ _, _, device_id, device_name, *_ = get_hexapod_controller_pars(device_id)
376
+ factory = ProxyFactory()
377
+ proxy = factory.create(device_name, device_id=device_id)
378
+ if not proxy.ping():
379
+ description = "Could not connect to Hexapod Control Server"
380
+ info_text = (
381
+ "The GUI will start, but the connection button will show a disconnected state. "
382
+ "Please check if the Control Server is running and start the server if needed. "
383
+ "Otherwise, check if the correct HOSTNAME for the control server is set in the "
384
+ "Settings.yaml "
385
+ "configuration file."
386
+ )
387
+
388
+ show_warning_message(description, info_text)
389
+
390
+ model = PunaUIModel(device_type, device_id)
391
+ view = PunaUIView(model.get_device_controller_type(), model.device_id)
392
+ PunaUIController(model, view)
393
+
394
+ view.show()
395
+
396
+ return app.exec_()
397
+ else:
398
+ error_message = QMessageBox()
399
+ error_message.setIcon(QMessageBox.Warning)
400
+ error_message.setWindowTitle("Error")
401
+ error_message.setText("The Puna GUI application is already running!")
402
+ error_message.setStandardButtons(QMessageBox.Ok)
403
+
404
+ return error_message.exec()
405
+
406
+
407
+ if __name__ == "__main__":
408
+ logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
409
+
410
+ sys.exit(app())
@@ -0,0 +1,115 @@
1
+ from egse.hexapod.symetrie import ControllerFactory
2
+ from egse.hexapod.symetrie import ProxyFactory
3
+ from egse.hexapod.symetrie import get_hexapod_controller_pars
4
+ from egse.hexapod.symetrie import logger
5
+ from egse.hexapod.symetrie.dynalpha import AlphaPlusControllerInterface
6
+ from egse.hexapod.symetrie.dynalpha import AlphaPlusTelnetInterface
7
+ from egse.mixin import DynamicCommandMixin
8
+ from egse.proxy import DynamicProxy
9
+ from egse.registry.client import RegistryClient
10
+ from egse.zmq_ser import connect_address
11
+
12
+
13
+ class PunaPlusInterface(AlphaPlusControllerInterface):
14
+ """
15
+ Interface definition for the PunaPlusController and the PunaPlusProxy.
16
+ """
17
+
18
+
19
+ class PunaPlusController(PunaPlusInterface, DynamicCommandMixin):
20
+ def __init__(self, hostname: str = "127.0.0.1", port: int = 23):
21
+ self.transport = self.device = AlphaPlusTelnetInterface(hostname, port)
22
+ self.hostname = hostname
23
+ self.port = port
24
+
25
+ super().__init__()
26
+
27
+ def get_controller_type(self):
28
+ return "ALPHA+"
29
+
30
+ def is_simulator(self):
31
+ return False
32
+
33
+ def is_connected(self):
34
+ return self.device.is_connected()
35
+
36
+ def connect(self):
37
+ self.device.connect()
38
+
39
+ def disconnect(self):
40
+ self.device.disconnect()
41
+
42
+ def reconnect(self):
43
+ if self.is_connected():
44
+ self.disconnect()
45
+ self.connect()
46
+
47
+
48
+ class PunaPlusProxy(DynamicProxy, PunaPlusInterface):
49
+ """
50
+ The PunaPlusProxy class is used to connect to the control server and send commands to the
51
+ Hexapod PUNA remotely. The device controller for that PUNA hexapod is an Alpha+ controller.
52
+
53
+ Args:
54
+ protocol: the transport protocol
55
+ hostname: location of the control server (IP address)
56
+ port: TCP port on which the control server is listening for commands
57
+
58
+ """
59
+
60
+ def __init__(self, protocol: str, hostname: str, port: int):
61
+ super().__init__(connect_address(protocol, hostname, port))
62
+
63
+ @classmethod
64
+ def from_identifier(cls, device_id: str):
65
+ with RegistryClient() as reg:
66
+ service = reg.discover_service(device_id)
67
+
68
+ if service:
69
+ protocol = service.get("protocol", "tcp")
70
+ hostname = service["host"]
71
+ port = service["port"]
72
+
73
+ else:
74
+ raise RuntimeError(f"No service registered as {device_id}")
75
+
76
+ logger.info(f"{protocol=}:{hostname=}:{port=}")
77
+
78
+ return cls(protocol, hostname, port)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ from egse.hexapod.symetrie.puna import PunaProxy
83
+
84
+ # The following imports are needed for the isinstance() to work
85
+ from egse.hexapod.symetrie.punaplus import PunaPlusProxy
86
+ from egse.hexapod.symetrie.punaplus import PunaPlusController
87
+
88
+ print()
89
+
90
+ *_, device_id, device_name, _ = get_hexapod_controller_pars()
91
+ print(f"{device_name = }, {device_id = }")
92
+
93
+ factory = ProxyFactory()
94
+ proxy = factory.create(device_name, device_id="1A")
95
+ assert isinstance(proxy, PunaProxy)
96
+
97
+ proxy = factory.create(device_name, device_id="2B")
98
+ assert isinstance(proxy, PunaPlusProxy)
99
+
100
+ print(proxy.info())
101
+
102
+ factory = ControllerFactory()
103
+
104
+ device = factory.create("PUNA", device_id="H_2B")
105
+ device.connect()
106
+ assert isinstance(device, PunaPlusController)
107
+
108
+ print(device.info())
109
+
110
+ device = factory.create("ZONDA")
111
+ device.connect()
112
+
113
+ print(device.info())
114
+
115
+ device.disconnect()