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,649 @@
1
+ """
2
+ This module defines the device classes to be used to connect to and control the Hexapod PUNA from
3
+ Symétrie.
4
+
5
+ """
6
+
7
+ import time
8
+ from datetime import datetime
9
+ from datetime import timedelta
10
+
11
+ from egse.device import DeviceConnectionState
12
+ from egse.device import DeviceInterface
13
+ from egse.hexapod import HexapodError
14
+ from egse.hexapod.symetrie import logger
15
+ from egse.hexapod.symetrie import pmac
16
+ from egse.hexapod.symetrie.alpha import AlphaControllerInterface
17
+ from egse.hexapod.symetrie.hexapod import HexapodSimulator
18
+ from egse.hexapod.symetrie.pmac import PMACError
19
+ from egse.hexapod.symetrie.pmac import PmacEthernetInterface
20
+ from egse.hexapod.symetrie.pmac import decode_Q29
21
+ from egse.proxy import Proxy
22
+ from egse.registry.client import RegistryClient
23
+ from egse.settings import Settings
24
+ from egse.zmq_ser import connect_address
25
+
26
+ DEVICE_SETTINGS = Settings.load(filename="puna.yaml")
27
+
28
+ NUM_OF_DECIMALS = 6 # used for rounding numbers before sending to PMAC
29
+
30
+
31
+ class PunaInterface(AlphaControllerInterface, DeviceInterface):
32
+ """
33
+ Interface definition for the PunaController, the PunaProxy and the PunaSimulator.
34
+ """
35
+
36
+
37
+ class PunaController(PunaInterface):
38
+ """
39
+ The PunaController class allows controlling a Symétrie PUNA Hexapod through an Ethernet
40
+ interface that is connecting a Symétrie Controller.
41
+
42
+ The Symétrie Controller can be either in simulation mode or have a real Hexapod
43
+ connected.
44
+
45
+ **Synopsis**
46
+
47
+ from egse.hexapod.symetrie.puna import PunaController
48
+ hexapod = PunaController(hostname="10.33.178.145", port=1025)
49
+ try:
50
+ hexapod.connect()
51
+
52
+ # do some useful things here with the hexapod
53
+
54
+ except HexapodError as exc:
55
+ print(exc)
56
+ finally:
57
+ hexapod.disconnect()
58
+
59
+ The constructor also sets the connection parameters and tries to connect
60
+ to the controller. Make sure that you explicitly use the hexapod.disconnect()
61
+ command when no connection is needed anymore.
62
+
63
+ The controller can also be used as a context manager, in which case the `connect()`
64
+ and `disconnect()` methods should not be called:
65
+
66
+ with PunaController() as puna:
67
+ puna.info()
68
+
69
+ """
70
+
71
+ SPEC_POS_MAINTENANCE = 0
72
+ """Hexapod specific position, for maintenance or Jog only."""
73
+ SPEC_POS_ZERO = 1
74
+ """Hexapod zero position."""
75
+ SPEC_POS_RETRACTED = 2
76
+ """Hexapod retracted position."""
77
+
78
+ def __init__(self, hostname=None, port=None):
79
+ """
80
+ Opens a TCP/IP socket connection with the Hexapod PUNA Hardware Controller.
81
+
82
+ Args:
83
+ hostname (str): the IP address or fully qualified hostname of the Hexapod hardware
84
+ controller.
85
+ The default is defined in the ``settings.yaml`` configuration file.
86
+
87
+ port (int): the IP port number to connect to, by default set in the ``settings.yaml``
88
+ configuration file.
89
+
90
+ Raises:
91
+ HexapodError: when the connection could not be established for some reason.
92
+ """
93
+
94
+ super().__init__()
95
+
96
+ if hostname is None or port is None:
97
+ raise ValueError(f"Please provide both hostname and port for the PunaController, {hostname=}, {port=}")
98
+
99
+ logger.debug(f"Initializing PunaController with hostname={hostname} on port={port}")
100
+
101
+ try:
102
+ self.pmac = PmacEthernetInterface()
103
+ self.pmac.setConnectionParameters(hostname, port)
104
+ except PMACError as exc:
105
+ logger.warning(
106
+ f"HexapodError: Couldn't establish connection with the Hexapod PUNA Hardware Controller: ({exc})"
107
+ )
108
+
109
+ def is_simulator(self):
110
+ return False
111
+
112
+ def is_connected(self):
113
+ return self.pmac.isConnected()
114
+
115
+ def connect(self):
116
+ try:
117
+ self.pmac.connect()
118
+ except PMACError as exc:
119
+ logger.warning(f"PMACError caught: Couldn't establish connection ({exc})")
120
+ raise ConnectionError("Couldn't establish a connection with the Hexapod.") from exc
121
+
122
+ self.notify_observers(DeviceConnectionState.DEVICE_CONNECTED)
123
+
124
+ def disconnect(self):
125
+ try:
126
+ self.pmac.disconnect()
127
+ except PMACError as exc:
128
+ raise ConnectionError("Couldn't disconnect from Hexapod.") from exc
129
+
130
+ self.notify_observers(DeviceConnectionState.DEVICE_NOT_CONNECTED)
131
+
132
+ def reconnect(self):
133
+ if self.is_connected():
134
+ self.disconnect()
135
+ self.connect()
136
+
137
+ def is_in_position(self):
138
+ try:
139
+ out = self.pmac.getQVars(36, [0], int)
140
+ except PMACError as exc:
141
+ raise HexapodError("Couldn't retrieve information from Hexapod PUNA Hardware Controller.") from exc
142
+ return bool(out[0] & 0x04)
143
+
144
+ def info(self):
145
+ try:
146
+ msg = "Info about the Hexapod PUNA:\n"
147
+ msg += f"model = {self.pmac.getPmacModel()}\n"
148
+ msg += f"CID = {self.pmac.getCID()}\n"
149
+ msg += f"version = {self.pmac.getVersion()}\n"
150
+ msg += f"cpu = {self.pmac.getCPU()}\n"
151
+ msg += f"type = {self.pmac.getType()}\n"
152
+ msg += f"vendorID= {self.pmac.getVID()}\n"
153
+ msg += f"date = {self.pmac.getDate()}\n"
154
+ msg += f"time = {self.pmac.getTime()}\n"
155
+ msg += f"today = {self.pmac.getToday()}\n"
156
+ except PMACError as exc:
157
+ raise HexapodError("Couldn't retrieve information from Hexapod PUNA Hardware Controller.") from exc
158
+
159
+ return msg
160
+
161
+ def stop(self):
162
+ try:
163
+ rc = self.pmac.sendCommand(pmac.CMD_STOP)
164
+ except PMACError as exc:
165
+ raise HexapodError("Couldn't complete the STOP command.") from exc
166
+
167
+ return rc
168
+
169
+ def homing(self):
170
+ try:
171
+ rc = self.pmac.sendCommand(pmac.CMD_HOMING)
172
+ except PMACError as exc:
173
+ raise HexapodError("Couldn't complete the HOMING command.") from exc
174
+
175
+ logger.info("Homing: Command was successful")
176
+
177
+ return rc
178
+
179
+ def is_homing_done(self):
180
+ try:
181
+ rc = self.pmac.getQVars(26, [0], int)[0]
182
+ except PMACError as pmac_exc:
183
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
184
+ return False
185
+
186
+ msg = { # noqa: F841
187
+ 0: "Homing status is undefined.",
188
+ 1: "Homing is in progress",
189
+ 2: "Homing is done",
190
+ 3: "An error occurred during the Homing process.",
191
+ }
192
+
193
+ if rc == 2:
194
+ return True
195
+
196
+ return False
197
+
198
+ def set_virtual_homing(self, tx, ty, tz, rx, ry, rz):
199
+ try:
200
+ rc = self.pmac.sendCommand(pmac.CMD_VIRTUAL_HOMING, tx=tx, ty=ty, tz=tz, rx=rx, ry=ry, rz=rz)
201
+ except PMACError as exc:
202
+ raise HexapodError("Couldn't execute the virtual homing command.") from exc
203
+
204
+ logger.warning(
205
+ f"Virtual Homing successfully set to {tx:.6f}, {ty:.6f}, {tz:.6f}, {rx:.6f}, {ry:.6f}, {rz:.6f}."
206
+ )
207
+
208
+ return rc
209
+
210
+ def activate_control_loop(self):
211
+ try:
212
+ rc = self.pmac.sendCommand(pmac.CMD_CONTROLON)
213
+ except PMACError as exc:
214
+ raise HexapodError("Couldn't activate the control loop.") from exc
215
+
216
+ msg = { # noqa: F841
217
+ 0: "Command was successful",
218
+ -1: "Command was ignored",
219
+ -2: "Control of the servo motors has failed",
220
+ }
221
+
222
+ return rc
223
+
224
+ def deactivate_control_loop(self):
225
+ try:
226
+ rc = self.pmac.sendCommand(pmac.CMD_CONTROLOFF)
227
+ except PMACError as exc:
228
+ raise HexapodError("Couldn't de-activate the control loop.") from exc
229
+
230
+ return rc
231
+
232
+ def __move(self, cm, tx, ty, tz, rx, ry, rz):
233
+ """
234
+ Ask the controller to perform the movement defined by the arguments.
235
+
236
+ For all control modes cm, the rotation centre coincides with the Object
237
+ Coordinates System origin and the movements are controlled with translation
238
+ components at first (Tx, Ty, tZ) and then the rotation components (Rx, Ry, Rz).
239
+
240
+ Control mode cm:
241
+ * 0 = absolute control, object coordinate system position and orientation
242
+ expressed in the invariant user coordinate system
243
+ * 1 = object relative, motion expressed in the Object Coordinate System
244
+ * 2 = user relative, motion expressed in the User Coordinate System
245
+
246
+ Args:
247
+ cm (int): control mode
248
+ tx (float): position on the X-axis [mm]
249
+ ty (float): position on the Y-axis [mm]
250
+ tz (float): position on the Z-axis [mm]
251
+ rx (float): rotation around the X-axis [deg]
252
+ ry (float): rotation around the Y-axis [deg]
253
+ rz (float): rotation around the Z-axis [deg]
254
+
255
+ Returns:
256
+ 0 on success, -1 when ignored, -2 on error.
257
+
258
+ Raises:
259
+ PMACError: when the arguments do not match up or when there is a time out or when
260
+ there is a socket
261
+ communication error.
262
+
263
+ .. note:: When the command was not successful, this method will query the ``POSVALID?``
264
+ using the checkAbsolutePosition() and print a summary of the error messages
265
+ to the log file.
266
+ """
267
+
268
+ rc = self.pmac.sendCommand(pmac.CMD_MOVE, cm=cm, tx=tx, ty=ty, tz=tz, rx=rx, ry=ry, rz=rz)
269
+
270
+ error_code_msg = {
271
+ 0: "Command was successful",
272
+ -1: "Command was ignored",
273
+ -2: "Command was invalid, check with POSVALID?",
274
+ }
275
+
276
+ if rc < 0:
277
+ msg = f"Move command returned ({rc}: {error_code_msg[rc]})."
278
+
279
+ if rc == -2:
280
+ Q29, errors = self.__check_movement(cm, tx, ty, tz, rx, ry, rz)
281
+
282
+ msg += "\nError messages returned from POSVALID?:\n"
283
+ for key, value in errors.items():
284
+ msg += f" bit {key:<2d}: {value}\n"
285
+
286
+ logger.debug(msg)
287
+
288
+ return rc
289
+
290
+ def __check_movement(self, cm, tx, ty, tz, rx, ry, rz):
291
+ """
292
+ Ask the controller if the movement defined by the arguments is feasible.
293
+
294
+ Returns a tuple where the first element is an integer that represents the
295
+ bitfield encoding the errors. The second element is a dictionary with the
296
+ bit numbers that were (on) and the corresponding error description.
297
+
298
+ Args:
299
+ tx (float): position on the X-axis [mm]
300
+ ty (float): position on the Y-axis [mm]
301
+ tz (float): position on the Z-axis [mm]
302
+ rx (float): rotation around the X-axis [deg]
303
+ ry (float): rotation around the Y-axis [deg]
304
+ rz (float): rotation around the Z-axis [deg]
305
+
306
+ """
307
+ out = self.pmac.sendCommand(pmac.CMD_POSVALID_GET, cm=cm, tx=tx, ty=ty, tz=tz, rx=rx, ry=ry, rz=rz)
308
+ Q29 = decode_Q29(out[0])
309
+ return out[0], Q29
310
+
311
+ def move_absolute(self, tx, ty, tz, rx, ry, rz):
312
+ try:
313
+ rc = self.__move(0, tx, ty, tz, rx, ry, rz)
314
+ except PMACError as exc:
315
+ raise HexapodError("Couldn't execute the moveAbsolute command.") from exc
316
+
317
+ return rc
318
+
319
+ def check_absolute_movement(self, tx, ty, tz, rx, ry, rz):
320
+ return self.__check_movement(0, tx, ty, tz, rx, ry, rz)
321
+
322
+ def move_relative_object(self, tx, ty, tz, rx, ry, rz):
323
+ try:
324
+ rc = self.__move(1, tx, ty, tz, rx, ry, rz)
325
+ except PMACError as exc:
326
+ raise HexapodError("Couldn't execute the relative movement [object] command.") from exc
327
+
328
+ return rc
329
+
330
+ def check_relative_object_movement(self, tx, ty, tz, rx, ry, rz):
331
+ return self.__check_movement(1, tx, ty, tz, rx, ry, rz)
332
+
333
+ def move_relative_user(self, tx, ty, tz, rx, ry, rz):
334
+ try:
335
+ rc = self.__move(2, tx, ty, tz, rx, ry, rz)
336
+ except PMACError as exc:
337
+ raise HexapodError("Couldn't execute the relative movement [user] command.") from exc
338
+
339
+ return rc
340
+
341
+ def check_relative_user_movement(self, tx, ty, tz, rx, ry, rz):
342
+ return self.__check_movement(2, tx, ty, tz, rx, ry, rz)
343
+
344
+ def perform_maintenance(self, axis):
345
+ try:
346
+ rc = self.pmac.sendCommand(pmac.CMD_MAINTENANCE, axis=axis)
347
+ except PMACError as exc:
348
+ raise HexapodError("Couldn't perform maintenance cycle.") from exc
349
+
350
+ msg = {0: "Command was successfully executed", -1: "Command was ignored"} # noqa: F841
351
+
352
+ return rc
353
+
354
+ def goto_specific_position(self, pos):
355
+ try:
356
+ rc = self.pmac.sendCommand(pmac.CMD_SPECIFICPOS, pos=pos)
357
+ except PMACError as exc:
358
+ raise HexapodError(f"Couldn't goto specific position [pos={pos}].") from exc
359
+
360
+ msg = {
361
+ 0: "Command was successfully executed",
362
+ -1: "Command was ignored",
363
+ -2: "Invalid movement command",
364
+ }
365
+
366
+ logger.info(f"Goto Specific Position [{pos}]: {msg[rc]}")
367
+
368
+ if rc < 0:
369
+ try:
370
+ out = self.pmac.getQVars(0, [29], int)
371
+ except PMACError as exc:
372
+ raise HexapodError("Couldn't get a response from the Hexapod controller.") from exc
373
+ Q29 = decode_Q29(out[0])
374
+
375
+ msg = "Error messages returned in Q29:\n"
376
+ for key, value in Q29.items():
377
+ msg += f" {key:2d}: {value}\n"
378
+
379
+ logger.debug(msg)
380
+
381
+ return rc
382
+
383
+ def goto_retracted_position(self):
384
+ try:
385
+ rc = self.pmac.sendCommand(pmac.CMD_SPECIFICPOS, pos=self.SPEC_POS_RETRACTED)
386
+ except PMACError as exc:
387
+ raise HexapodError("Couldn't goto retracted position.") from exc
388
+
389
+ msg = {
390
+ 0: "Command was successfully executed",
391
+ -1: "Command was ignored",
392
+ -2: "Invalid movement command",
393
+ }
394
+
395
+ logger.info(f"Goto Retracted Position [2]: {msg[rc]}")
396
+
397
+ if rc < 0:
398
+ try:
399
+ out = self.pmac.getQVars(0, [29], int)
400
+ except PMACError as exc:
401
+ raise HexapodError("Couldn't get a response from the Hexapod controller.") from exc
402
+ Q29 = decode_Q29(out[0])
403
+
404
+ msg = "Error messages returned in Q29:\n"
405
+ for key, value in Q29.items():
406
+ msg += f" {key:2d}: {value}\n"
407
+
408
+ logger.debug(msg)
409
+
410
+ return rc
411
+
412
+ def goto_zero_position(self):
413
+ try:
414
+ rc = self.pmac.sendCommand(pmac.CMD_SPECIFICPOS, pos=self.SPEC_POS_ZERO)
415
+ except PMACError as exc:
416
+ raise HexapodError("Couldn't goto zero position.") from exc
417
+
418
+ msg = {
419
+ 0: "Command was successfully executed",
420
+ -1: "Command was ignored",
421
+ -2: "Invalid movement command",
422
+ }
423
+
424
+ logger.info(f"Goto Zero Position [1]: {msg[rc]}")
425
+
426
+ if rc < 0:
427
+ try:
428
+ out = self.pmac.getQVars(0, [29], int)
429
+ except PMACError as exc:
430
+ raise HexapodError("Couldn't get a response from the Hexapod controller.") from exc
431
+ Q29 = decode_Q29(out[0])
432
+
433
+ msg = "Error messages returned in Q29:\n"
434
+ for key, value in Q29.items():
435
+ msg += f" {key:2d}: {value}\n"
436
+
437
+ logger.debug(msg)
438
+
439
+ return rc
440
+
441
+ def get_buffer(self):
442
+ return_string = self.pmac.getBuffer()
443
+ return return_string
444
+
445
+ def clear_error(self):
446
+ try:
447
+ rc = self.pmac.sendCommand(pmac.CMD_CLEARERROR)
448
+ except PMACError as exc:
449
+ raise HexapodError("Couldn't clear errors in the controller software.") from exc
450
+
451
+ return rc
452
+
453
+ def jog(self, axis: int, inc: float) -> int:
454
+ if not (1 <= axis <= 6):
455
+ logger.error(f"The axis argument must be 1 <= axis <= 6, given {axis}.")
456
+ raise HexapodError("Illegal Argument Value: axis is {axis}, should be 1 <= axis <= 6.")
457
+
458
+ try:
459
+ rc = self.pmac.sendCommand(pmac.CMD_JOG, axis=axis, inc=inc)
460
+ except PMACError as exc:
461
+ raise HexapodError(f"Couldn't execute the jog command for axis={axis} with inc={inc} [mm].") from exc
462
+
463
+ msg = {0: "Command was successfully executed", -1: "Command was ignored"}
464
+
465
+ logger.info(f"JOG on axis [{axis}] of {inc} mm: {msg[rc]}")
466
+
467
+ return rc
468
+
469
+ def configure_coordinates_systems(self, tx_u, ty_u, tz_u, rx_u, ry_u, rz_u, tx_o, ty_o, tz_o, rx_o, ry_o, rz_o):
470
+ try:
471
+ rc = self.pmac.sendCommand(
472
+ pmac.CMD_CFG_CS,
473
+ tx_u=round(tx_u, NUM_OF_DECIMALS),
474
+ ty_u=round(ty_u, NUM_OF_DECIMALS),
475
+ tz_u=round(tz_u, NUM_OF_DECIMALS),
476
+ rx_u=round(rx_u, NUM_OF_DECIMALS),
477
+ ry_u=round(ry_u, NUM_OF_DECIMALS),
478
+ rz_u=round(rz_u, NUM_OF_DECIMALS),
479
+ tx_o=round(tx_o, NUM_OF_DECIMALS),
480
+ ty_o=round(ty_o, NUM_OF_DECIMALS),
481
+ tz_o=round(tz_o, NUM_OF_DECIMALS),
482
+ rx_o=round(rx_o, NUM_OF_DECIMALS),
483
+ ry_o=round(ry_o, NUM_OF_DECIMALS),
484
+ rz_o=round(rz_o, NUM_OF_DECIMALS),
485
+ )
486
+ except PMACError as exc:
487
+ raise HexapodError("Couldn't configure coordinate systems on the hexapod controller.") from exc
488
+
489
+ return rc
490
+
491
+ def get_coordinates_systems(self):
492
+ try:
493
+ out = self.pmac.sendCommand(pmac.CMD_CFG_CS_GET)
494
+ except PMACError as exc:
495
+ raise HexapodError("Couldn't get the coordinate systems information from the hexapod controller.") from exc
496
+
497
+ return out
498
+
499
+ def get_debug_info(self):
500
+ try:
501
+ out = self.pmac.sendCommand(pmac.CMD_STATE_DEBUG_GET)
502
+ except PMACError as exc:
503
+ raise HexapodError("Couldn't get the debugging information from the hexapod controller.") from exc
504
+
505
+ return out
506
+
507
+ def set_speed(self, vt, vr):
508
+ try:
509
+ rc = self.pmac.sendCommand(pmac.CMD_CFG_SPEED, vt=vt, vr=vr)
510
+ except PMACError as exc:
511
+ raise HexapodError("Couldn't set the speed for translation [{vt} mm/s] or rotation [{vr} deg/s].") from exc
512
+
513
+ return rc
514
+
515
+ def get_speed(self):
516
+ try:
517
+ out = self.pmac.sendCommand(pmac.CMD_CFG_SPEED_GET)
518
+ except PMACError as exc:
519
+ raise HexapodError("Couldn't get the speed settings from the hexapod controller.") from exc
520
+
521
+ return out
522
+
523
+ def get_general_state(self):
524
+ try:
525
+ out = self.pmac.getQVars(36, [0], int)
526
+ except PMACError as pmac_exc:
527
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
528
+ return None
529
+
530
+ return out[0], pmac.decode_Q36(out[0])
531
+
532
+ def get_actuator_state(self):
533
+ try:
534
+ out = self.pmac.getQVars(30, [0, 1, 2, 3, 4, 5], int)
535
+ except PMACError as pmac_exc:
536
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
537
+ return None
538
+
539
+ return [pmac.decode_Q30(value) for value in out]
540
+
541
+ def get_user_positions(self):
542
+ try:
543
+ # out = self.pmac.getQVars(53, [0, 1, 2, 3, 4, 5], float)
544
+ out = self.pmac.sendCommand(pmac.CMD_POSUSER_GET)
545
+ except PMACError as pmac_exc:
546
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
547
+ return None
548
+
549
+ return out
550
+
551
+ def get_machine_positions(self):
552
+ try:
553
+ out = self.pmac.getQVars(47, [0, 1, 2, 3, 4, 5], float)
554
+ except PMACError as pmac_exc:
555
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
556
+ return None
557
+
558
+ return out
559
+
560
+ def get_actuator_length(self):
561
+ try:
562
+ out = self.pmac.getQVars(41, [0, 1, 2, 3, 4, 5], float)
563
+ except PMACError as pmac_exc:
564
+ logger.error(f"PMAC Exception: {pmac_exc}", exc_info=True)
565
+ return None
566
+
567
+ return out
568
+
569
+ def reset(self, wait=True, verbose=False):
570
+ try:
571
+ self.pmac.sendCommand(pmac.CMD_RESETSOFT)
572
+ except PMACError as exc:
573
+ raise HexapodError("Couldn't (soft) reset the hexapod controller.") from exc
574
+
575
+ # How do I know when the RESETSOFT has finished and we can send further commands?
576
+
577
+ if wait:
578
+ logger.info("Sent a soft reset, this will take about 30 seconds to complete.")
579
+ self.__wait(30, verbose=verbose)
580
+
581
+ def __wait(self, duration, verbose=False):
582
+ """
583
+ Wait for a specific duration in seconds.
584
+ """
585
+ _timeout = timedelta(seconds=duration)
586
+ _start = datetime.now()
587
+
588
+ _rate = timedelta(seconds=5) # every _rate seconds print a message
589
+ _count = 0
590
+
591
+ logger.info(f"Just waiting {duration} seconds ...")
592
+
593
+ while datetime.now() - _start < _timeout:
594
+ if verbose and (datetime.now() - _start > _count * _rate):
595
+ _count += 1
596
+ logger.info(f"waited for {_count * _rate} of {_timeout} seconds, ")
597
+ print(f"waited for {_count * _rate} of {_timeout} seconds, ")
598
+
599
+ time.sleep(0.01)
600
+
601
+
602
+ class PunaSimulator(HexapodSimulator, DeviceInterface):
603
+ """
604
+ HexapodSimulator simulates the Symétrie Hexapod PUNA. The class is heavily based on the
605
+ ReferenceFrames in the `egse.coordinates` package.
606
+
607
+ The simulator implements the same methods as the HexapodController class which acts on the
608
+ real hardware controller in either simulation mode or with a real Hexapod PUNA connected.
609
+
610
+ Therefore, the HexapodSimulator can be used instead of the Hexapod class in test harnesses
611
+ and when the hardware is not available.
612
+
613
+ This class simulates all the movements and status of the Hexapod.
614
+ """
615
+
616
+ def __init__(self):
617
+ super().__init__()
618
+
619
+
620
+ class PunaProxy(Proxy, PunaInterface):
621
+ """The PunaProxy class is used to connect to the control server and send commands to the
622
+ Hexapod PUNA remotely.
623
+
624
+ Args:
625
+ protocol: the transport protocol
626
+ hostname: location of the control server (IP address)
627
+ port: TCP port on which the control server is listening for commands
628
+
629
+ """
630
+
631
+ def __init__(self, protocol: str, hostname: str, port: int):
632
+ super().__init__(connect_address(protocol, hostname, port))
633
+
634
+ @classmethod
635
+ def from_identifier(cls, device_id: str):
636
+ with RegistryClient() as reg:
637
+ service = reg.discover_service(device_id)
638
+
639
+ if service:
640
+ protocol = service.get("protocol", "tcp")
641
+ hostname = service["host"]
642
+ port = service["port"]
643
+
644
+ else:
645
+ raise RuntimeError(f"No service registered as {device_id}")
646
+
647
+ logger.info(f"{protocol=}:{hostname=}:{port=}")
648
+
649
+ return cls(protocol, hostname, port)