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,846 @@
1
+ """
2
+ This module defines the device classes to be used to connect to and control the Hexapod ZONDA from Symétrie.
3
+ """
4
+
5
+ import math
6
+ import random
7
+ import time
8
+ from typing import Dict
9
+
10
+ from egse.device import DeviceInterface
11
+ from egse.hexapod import HexapodError
12
+ from egse.hexapod.symetrie import logger
13
+ from egse.hexapod.symetrie.alpha import AlphaPlusControllerInterface
14
+ from egse.hexapod.symetrie.hexapod import HexapodSimulator
15
+ from egse.hexapod.symetrie.zonda_devif import RETURN_CODES
16
+ from egse.hexapod.symetrie.zonda_devif import ZondaError
17
+ from egse.hexapod.symetrie.zonda_devif import ZondaTelnetInterface
18
+ from egse.hexapod.symetrie.zonda_devif import decode_command
19
+ from egse.proxy import Proxy
20
+ from egse.registry.client import RegistryClient
21
+ from egse.settings import Settings
22
+ from egse.system import Timer
23
+ from egse.system import wait_until
24
+ from egse.zmq_ser import connect_address
25
+ from numpy import loadtxt
26
+ from time import sleep
27
+
28
+ DEVICE_SETTINGS = Settings.load(filename="zonda.yaml")
29
+
30
+ HOME_COMPLETE = 6
31
+ IN_POSITION = 3
32
+ IN_MOTION = 4
33
+
34
+ GENERAL_STATE = [
35
+ "Error",
36
+ "System initialized",
37
+ "Control on",
38
+ "In position",
39
+ "Motion task running",
40
+ "Home task running",
41
+ "Home complete",
42
+ "Home virtual",
43
+ "Phase found",
44
+ "Brake on",
45
+ "Motion restricted",
46
+ "Power on encoders",
47
+ "Power on limit switches",
48
+ "Power on drives",
49
+ "Emergency stop",
50
+ ]
51
+
52
+ ACTUATOR_STATE = [
53
+ "Error",
54
+ "Control on",
55
+ "In position",
56
+ "Motion task running",
57
+ "Home task running",
58
+ "Home complete",
59
+ "Phase found",
60
+ "Brake on",
61
+ "Home hardware input",
62
+ "Negative hardware limit switch",
63
+ "Positive hardware limit switch",
64
+ "Software limit reached",
65
+ "Following error",
66
+ "Drive fault",
67
+ "Encoder error",
68
+ ]
69
+
70
+ VALIDATION_LIMITS = [
71
+ "Factory workspace limits",
72
+ "Machine workspace limits",
73
+ "User workspace limits",
74
+ "Actuator limits",
75
+ "Joints limits",
76
+ "Due to backlash compensation",
77
+ ]
78
+
79
+
80
+ class ZondaInterface(AlphaPlusControllerInterface, DeviceInterface):
81
+ """
82
+ Interface definition for the ZondaController, the ZondaProxy and the ZondaSimulator.
83
+ """
84
+
85
+
86
+ class ZondaController(ZondaInterface):
87
+ def __init__(self, hostname: str, port: int):
88
+ super().__init__()
89
+
90
+ logger.info(f"Initializing ZondaController with {hostname=} on {port=}")
91
+
92
+ self.v_pos = None
93
+
94
+ self.cm = ["absolute", "object relative", "user relative"]
95
+
96
+ self.speed = {"vt": 0, "vr": 0, "vtmin": 0, "vrmin": 0, "vtmax": 0, "vrmax": 0}
97
+
98
+ try:
99
+ self.hexapod = ZondaTelnetInterface(hostname=hostname, port=port)
100
+
101
+ except ZondaError as exc:
102
+ logger.warning(
103
+ f"HexapodError: Couldn't establish connection with the Hexapod ZONDA Hardware Controller: ({exc})"
104
+ )
105
+
106
+ def is_simulator(self):
107
+ return False
108
+
109
+ def is_connected(self):
110
+ return self.hexapod.is_connected()
111
+
112
+ def connect(self):
113
+ try:
114
+ self.hexapod.connect()
115
+ except ZondaError as exc:
116
+ logger.warning(f"ZondaError caught: Couldn't establish connection ({exc})")
117
+ raise HexapodError("Couldn't establish a connection with the Hexapod.") from exc
118
+
119
+ def disconnect(self):
120
+ try:
121
+ self.hexapod.disconnect()
122
+ except ZondaError as exc:
123
+ raise HexapodError("Couldn't disconnect from Hexapod.") from exc
124
+
125
+ def reconnect(self):
126
+ if self.is_connected():
127
+ self.disconnect()
128
+ self.connect()
129
+
130
+ def info(self):
131
+ try:
132
+ _version = self.hexapod.trans("VERS")
133
+ msg = "Info about the Hexapod ZONDA:\n"
134
+ msg += f"version = {_version}\n"
135
+ except ZondaError as exc:
136
+ raise HexapodError("Couldn't retrieve information from Hexapod ZONDA Hardware Controller.") from exc
137
+
138
+ return msg
139
+
140
+ def get_general_state(self):
141
+ """
142
+ Asks the general state of the hexapod on all the motors following the bits definition
143
+ presented below.
144
+
145
+ GENERAL_STATE =
146
+ 0: "Error",
147
+ 1: "System initialized",
148
+ 2: "Control on",
149
+ 3: "In position",
150
+ 4: "Motion task running",
151
+ 5: "Home task running",
152
+ 6: "Home complete",
153
+ 7: "Home virtual",
154
+ 8: "Phase found",
155
+ 9: "Brake on",
156
+ 10:"Motion restricted",
157
+ 11:"Power on encoders",
158
+ 12:"Power on limit switches",
159
+ 13:"Power on drives",
160
+ 14:"Emergency stop"
161
+
162
+ Returns:
163
+ A dictionary with the bits value of each parameter.
164
+ """
165
+ try:
166
+ rc = self.hexapod.trans("s_hexa")
167
+ rc = int(rc[0])
168
+
169
+ # from int to bit list of 15 elements corresponding to the hexapod state bits
170
+ # the bit list must be reversed to get lsb
171
+
172
+ s_hexa = [int(x) for x in f"{rc:015b}"[::-1]]
173
+ state = {k: v for k, v in zip(GENERAL_STATE, s_hexa)}
174
+ except ZondaError as exc:
175
+ raise HexapodError("Couldn't retrieve the state from Hexapod.") from exc
176
+
177
+ return [state, list(state.values())]
178
+
179
+ def stop(self):
180
+ try:
181
+ self.hexapod.trans("c_cmd=C_STOP")
182
+ sc = self.hexapod.check_command_status()
183
+ logger.warning(f"Stop command has been executed: {sc}")
184
+ except ZondaError as exc:
185
+ raise HexapodError("Couldn't disconnect from Hexapod.") from exc
186
+ return sc
187
+
188
+ def clear_error(self):
189
+ try:
190
+ print("Cleaning errors from buffer")
191
+ sc = self.hexapod.trans("c_cmd=C_CLEARERROR")
192
+ number = self.hexapod.trans("s_err_nr")
193
+ number = int(number[0])
194
+
195
+ if number == 0:
196
+ logger.info("All the errors have been cleared.")
197
+ else:
198
+ logger.warning("Couldn't clear all errors from Hexapod.")
199
+
200
+ except ZondaError as exc:
201
+ raise HexapodError("Couldn't clear all the errors from Hexapod.") from exc
202
+ return sc
203
+
204
+ def reset(self, wait=True):
205
+ try:
206
+ print("Resetting the Hexapod Controller")
207
+ print("STOP and CONTROLOFF commands are sent to the controller before resetting...")
208
+ self.stop()
209
+ print("Hexapod is stopped")
210
+ self.deactivate_control_loop()
211
+ print("Hexapod control loop has been deactivated")
212
+
213
+ print("Rebooting the controller: will take 2 min to initialize")
214
+
215
+ # FIXME: you might want to rethink this, because the reboot will close the Ethernet
216
+ # connection and therefore we need to reconnect after a two minutes waiting time.
217
+ # During the waiting time, the GUI should have all functions disabled and the
218
+ # connection icon should show a disconnected icon. Don't use `time.sleep()` as
219
+ # that will block the GUI, run a timer in a QThread signaling a reconnect when
220
+ # the sleep time is over.
221
+ self.hexapod.trans("system reboot")
222
+
223
+ except ZondaError as exc:
224
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
225
+ return None
226
+
227
+ def jog(self, axis: int, inc: float):
228
+ try:
229
+ self.hexapod.trans("c_ax={} c_par(0)={} c_cmd=C_JOG")
230
+ sc = self.hexapod.check_command_status()
231
+
232
+ except ZondaError as exc:
233
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
234
+ return sc
235
+
236
+ def perform_maintenance(self, axis):
237
+ try:
238
+ if axis == 10:
239
+ self.hexapod.trans("c_par(0)=3 c_cmd=C_MAINTENANCE")
240
+ else:
241
+ self.hexapod.trans("c_par(0)=2 c_par(1)={} c_cmd=C_MAINTENANCE".format(str(axis)))
242
+
243
+ sc = self.hexapod.check_command_status()
244
+ except ZondaError as exc:
245
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
246
+ return sc
247
+
248
+ def set_default(self):
249
+ try:
250
+ print("Resetting the hexapod to the factory default parameters...:")
251
+ self.hexapod.trans("c_cfg=1 c_cmd=C_CFG_DEFAULT")
252
+ sc = self.hexapod.check_command_status()
253
+
254
+ except ZondaError as exc:
255
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
256
+ return sc
257
+
258
+ def homing(self):
259
+ try:
260
+ print("Executing homing command")
261
+ self.hexapod.trans("c_cmd=C_HOME")
262
+ sc = self.hexapod.check_command_status()
263
+
264
+ except ZondaError as exc:
265
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
266
+ return sc
267
+
268
+ def goto_specific_position(self, pos):
269
+ try:
270
+ sc = self.hexapod.trans("c_par(0)={} c_cmd=C_MOVE_SPECIFICPOS".format(str(pos)))
271
+ print(sc)
272
+
273
+ except ZondaError as exc:
274
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
275
+
276
+ return sc
277
+
278
+ def goto_zero_position(self):
279
+ try:
280
+ sc = self.hexapod.trans("c_par(0)=1 c_cmd=C_MOVE_SPECIFICPOS")
281
+ print(sc)
282
+
283
+ except ZondaError as exc:
284
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
285
+
286
+ return sc
287
+
288
+ def goto_retracted_position(self):
289
+ try:
290
+ sc = self.hexapod.trans("c_par(0)=2 c_cmd=C_MOVE_SPECIFICPOS")
291
+ print(sc)
292
+
293
+ except ZondaError as exc:
294
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
295
+
296
+ return sc
297
+
298
+ def is_homing_done(self):
299
+ try:
300
+ sc = self.get_general_state()
301
+ homing = sc[1][HOME_COMPLETE]
302
+
303
+ except ZondaError as exc:
304
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
305
+ return homing
306
+
307
+ def is_in_position(self):
308
+ try:
309
+ sc = self.get_general_state()
310
+ in_position = sc[1][IN_POSITION]
311
+
312
+ except ZondaError as exc:
313
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
314
+ return in_position
315
+
316
+ def activate_control_loop(self):
317
+ try:
318
+ self.hexapod.trans("c_cmd=C_CONTROLON")
319
+ print("Executing activate_control_loop command")
320
+ sc = self.hexapod.check_command_status()
321
+ except ZondaError as exc:
322
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
323
+ return sc
324
+
325
+ def deactivate_control_loop(self):
326
+ try:
327
+ self.hexapod.trans("c_cmd=C_CONTROLOFF")
328
+ print("Executing deactivate_control_loop")
329
+ sc = self.hexapod.check_command_status()
330
+ except ZondaError as exc:
331
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
332
+ return sc
333
+
334
+ def machine_limit_enable(self, state: int):
335
+ try:
336
+ command = decode_command("CFG_LIMITENABLE", 1, state)
337
+ print(f"Executing machine_limit_enable command to state {state}. ")
338
+ self.hexapod.trans(command)
339
+ rc = self.hexapod.check_command_status()
340
+ except ZondaError as exc:
341
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
342
+
343
+ return rc
344
+
345
+ def machine_limit_set(self, tx_n, ty_n, tz_n, rx_n, ry_n, rz_n, tx_p, ty_p, tz_p, rx_p, ry_p, rz_p):
346
+ try:
347
+ name = "CFG_LIMIT"
348
+ arguments = [1, tx_n, ty_n, tz_n, rx_n, ry_n, rz_n, tx_p, ty_p, tz_p, rx_p, ry_p, rz_p]
349
+
350
+ cmd = decode_command(name, *arguments)
351
+
352
+ print("Executing machine_limit_set command {}...: ".format(cmd), end="")
353
+ self.hexapod.trans(cmd)
354
+ self.hexapod.check_command_status()
355
+
356
+ except ZondaError as exc:
357
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
358
+
359
+ def user_limit_enable(self, state: int):
360
+ try:
361
+ command = decode_command("CFG_LIMITENABLE", 2, state)
362
+ print(f"Executing user_limit_enable command to state {state}...: ")
363
+ self.hexapod.trans(command)
364
+ self.hexapod.check_command_status()
365
+ except ZondaError as exc:
366
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
367
+
368
+ return None
369
+
370
+ def user_limit_set(self, tx_n, ty_n, tz_n, rx_n, ry_n, rz_n, tx_p, ty_p, tz_p, rx_p, ry_p, rz_p):
371
+ try:
372
+ name = "CFG_LIMIT"
373
+ arguments = [2, tx_n, ty_n, tz_n, rx_n, ry_n, rz_n, tx_p, ty_p, tz_p, rx_p, ry_p, rz_p]
374
+
375
+ cmd = decode_command(name, *arguments)
376
+
377
+ print(f"Executing user_limit_set command {cmd}...: ")
378
+ self.hexapod.trans(cmd)
379
+ rc = self.hexapod.check_command_status()
380
+
381
+ except ZondaError as exc:
382
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
383
+
384
+ return rc
385
+
386
+ def __move(self, cm, tx, ty, tz, rx, ry, rz):
387
+ """
388
+ Ask the controller to perform the movement defined by the arguments and the command MOVE_PTP.
389
+
390
+ For all control modes cm, the rotation centre coincides with the Object
391
+ Coordinates System origin and the movements are controlled with translation
392
+ components at first (Tx, Ty, tZ) and then the rotation components (Rx, Ry, Rz).
393
+
394
+ Control mode cm:
395
+ * 0 = absolute control, object coordinate system position and orientation
396
+ expressed in the invariant user coordinate system
397
+ * 1 = object relative, motion expressed in the Object Coordinate System
398
+ * 2 = user relative, motion expressed in the User Coordinate System
399
+
400
+ Args:
401
+ cm (int): control mode
402
+ tx (float): position on the X-axis [mm]
403
+ ty (float): position on the Y-axis [mm]
404
+ tz (float): position on the Z-axis [mm]
405
+ rx (float): rotation around the X-axis [deg]
406
+ ry (float): rotation around the Y-axis [deg]
407
+ rz (float): rotation around the Z-axis [deg]
408
+ """
409
+
410
+ name = "MOVE_PTP"
411
+ arguments = [cm, tx, ty, tz, rx, ry, rz]
412
+
413
+ command = decode_command(name, *arguments)
414
+ print(f"Executing move_ptp command in {self.cm[cm]} mode ... {command}")
415
+ self.hexapod.trans(command)
416
+ return self.hexapod.check_command_status()
417
+
418
+ def validate_position(self, vm, cm, tx, ty, tz, rx, ry, rz):
419
+ # Currently only the vm=1 mode is developed by Symétrie
420
+ """
421
+ Ask the controller if the movement defined by the arguments is feasible.
422
+
423
+ Returns a tuple where the first element is an integer that represents the
424
+ bitfield encoding the errors. The second element is a dictionary with the
425
+ bit numbers that were (on) and the corresponding error description as
426
+ defined by VALIDATION_LIMITS.
427
+
428
+ Args:
429
+ tx (float): position on the X-axis [mm]
430
+ ty (float): position on the Y-axis [mm]
431
+ tz (float): position on the Z-axis [mm]
432
+ rx (float): rotation around the X-axis [deg]
433
+ ry (float): rotation around the Y-axis [deg]
434
+ rz (float): rotation around the Z-axis [deg]
435
+
436
+ """
437
+ name = "VALID_PTP"
438
+ arguments = [vm, cm, tx, ty, tz, rx, ry, rz]
439
+
440
+ command = decode_command(name, *arguments)
441
+
442
+ # print(f"Executing validate_position command: {command}")
443
+
444
+ self.hexapod.trans(command)
445
+ rc = self.hexapod.check_command_status()
446
+ pc = self.hexapod.get_pars(0)
447
+ pc = int(pc[0])
448
+
449
+ if pc == 0:
450
+ return 0, {}
451
+
452
+ if pc > 0:
453
+ d = decode_validation_error(pc)
454
+ return pc, d
455
+
456
+ # When coming here the command returned an error
457
+
458
+ msg = f"{RETURN_CODES.get(pc, 'unknown error code')}"
459
+ logger.error(f"Validate position: error code={pc} - {msg}")
460
+
461
+ return pc, {pc: msg}
462
+
463
+ def move_absolute(self, tx, ty, tz, rx, ry, rz):
464
+ try:
465
+ rc = self.__move(0, tx, ty, tz, rx, ry, rz)
466
+
467
+ except ZondaError as exc:
468
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
469
+ return rc
470
+
471
+ def move_relative_object(self, tx, ty, tz, rx, ry, rz):
472
+ try:
473
+ rc = self.__move(1, tx, ty, tz, rx, ry, rz)
474
+
475
+ except ZondaError as exc:
476
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
477
+ return rc
478
+
479
+ def move_relative_user(self, tx, ty, tz, rx, ry, rz):
480
+ try:
481
+ rc = self.__move(2, tx, ty, tz, rx, ry, rz)
482
+
483
+ except ZondaError as exc:
484
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
485
+ return rc
486
+
487
+ def check_absolute_movement(self, tx, ty, tz, rx, ry, rz):
488
+ # Currently only the vm=1 mode is developed by Symétrie
489
+ # Parameter cm = 0
490
+ return self.validate_position(1, 0, tx, ty, tz, rx, ry, rz)
491
+
492
+ def check_relative_object_movement(self, tx, ty, tz, rx, ry, rz):
493
+ # Currently only the vm=1 mode is developed by Symétrie
494
+ # Parameter cm = 1 for object
495
+ return self.validate_position(1, 1, tx, ty, tz, rx, ry, rz)
496
+
497
+ def check_relative_user_movement(self, tx, ty, tz, rx, ry, rz):
498
+ # Currently only the vm=1 mode is developed by Symétrie
499
+ # Parameter cm = 2 for user relative
500
+ return self.validate_position(1, 2, tx, ty, tz, rx, ry, rz)
501
+
502
+ def get_temperature(self):
503
+ # TODO: to be tested with the real Hexapod (the emulator does not implement this and only returns zeros)
504
+ try:
505
+ temp = self.hexapod.trans("s_ai_1,6,1")
506
+ temp = [float(x) for x in temp]
507
+
508
+ except ZondaError as exc:
509
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
510
+ return temp
511
+
512
+ def get_user_positions(self):
513
+ try:
514
+ uto = self.hexapod.trans("s_uto_tx,6,1")
515
+ uto = [float(x) for x in uto]
516
+
517
+ except ZondaError as exc:
518
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
519
+ return uto
520
+
521
+ def get_machine_positions(self):
522
+ try:
523
+ mtp = self.hexapod.trans("s_mtp_tx,6,1")
524
+ mtp = [float(x) for x in mtp]
525
+
526
+ except ZondaError as exc:
527
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
528
+ return mtp
529
+
530
+ def get_actuator_length(self):
531
+ try:
532
+ pos = self.hexapod.trans("s_pos_ax_1,6,1")
533
+ pos = [float(x) for x in pos]
534
+
535
+ except ZondaError as exc:
536
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
537
+ return pos
538
+
539
+ def get_actuator_state(self):
540
+ response = []
541
+ try:
542
+ actuator_states = self.hexapod.trans("s_ax_1,6,1")
543
+ actuator_states = [int(x) for x in actuator_states]
544
+
545
+ for idx, state in enumerate(actuator_states):
546
+ state_bits = [int(x) for x in f"{state:015b}"[::-1]]
547
+ state_dict = {k: v for k, v in zip(ACTUATOR_STATE, state_bits)}
548
+ response.append([state_dict, state_bits])
549
+
550
+ except ZondaError as exc:
551
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
552
+
553
+ return response
554
+
555
+ def get_coordinates_systems(self):
556
+ try:
557
+ command = decode_command("CFG_CS?")
558
+ print(f"Executing get_coordinate_system command: {command}")
559
+ self.hexapod.trans(command)
560
+ sc = self.hexapod.check_command_status()
561
+ cs = self.hexapod.get_pars(12)
562
+ cs = [float(x) for x in cs]
563
+
564
+ except ZondaError as exc:
565
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
566
+ return cs
567
+
568
+ 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):
569
+ try:
570
+ name = "CFG_CS"
571
+ arguments = [tx_u, ty_u, tz_u, rx_u, ry_u, rz_u, tx_o, ty_o, tz_o, rx_o, ry_o, rz_o]
572
+ command = decode_command(name, *arguments)
573
+ print(f"Executing configure_coordinates_systems: {command}")
574
+ self.hexapod.trans(command)
575
+ rc = self.hexapod.check_command_status()
576
+
577
+ except ZondaError as exc:
578
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
579
+ return rc
580
+
581
+ def get_limits_value(self, lim):
582
+ # Not implemented in Puna
583
+ # lim: int | 0 = Factory, 1 = machine cs limits, 2 = user cs limits
584
+ try:
585
+ cmd = decode_command("CFG_LIMIT?", lim)
586
+ print(f"Executing get_limits_value command: {cmd=}")
587
+ self.hexapod.trans(cmd)
588
+ self.hexapod.check_command_status()
589
+
590
+ pc = self.hexapod.get_pars(13)
591
+ pc = [float(x) for x in pc[1:]]
592
+
593
+ except ZondaError as exc:
594
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
595
+ return pc
596
+
597
+ def get_limits_state(self):
598
+ # not implemented in Puna
599
+ try:
600
+ command = decode_command("CFG_LIMITENABLE?")
601
+ print(f"Executing get_limits_state command: {command=}")
602
+ self.hexapod.trans(command)
603
+ sc = self.hexapod.check_command_status()
604
+
605
+ if sc[0] == 0:
606
+ pc = self.hexapod.get_pars(3)
607
+ pc = [int(i) for i in pc]
608
+ else:
609
+ pc = ["nan", "nan", "nan"]
610
+
611
+ keys = VALIDATION_LIMITS[:3]
612
+ ls = {k: v for k, v in zip(keys, pc)}
613
+
614
+ except ZondaError as exc:
615
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
616
+
617
+ return ls
618
+
619
+ def get_speed(self):
620
+ try:
621
+ command = decode_command("CFG_SPEED?")
622
+ print(f"Executing get_speed command: {command}")
623
+ self.hexapod.trans(command)
624
+ sc = self.hexapod.check_command_status()
625
+ speed = self.hexapod.get_pars(6)
626
+ speed = [float(x) for x in speed]
627
+ keys = list(self.speed.keys())
628
+
629
+ s = {k: v for k, v in zip(keys, speed)}
630
+
631
+ except ZondaError as exc:
632
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
633
+ return s
634
+
635
+ def set_speed(self, vt, vr):
636
+ try:
637
+ name = "CFG_SPEED"
638
+ arguments = [vt, vr]
639
+ command = decode_command(name, *arguments)
640
+ # The parameters are automatically limited by the controller between the factory
641
+ # configured min/max speeds
642
+ self.hexapod.trans(command)
643
+
644
+ except ZondaError as exc:
645
+ raise HexapodError("Couldn't Execute command on Hexapod.") from exc
646
+ return
647
+
648
+ def sequence(self, file_path: str, time_sleep: float):
649
+ file = file_path + ".txt"
650
+ SEQUENCE = loadtxt(file)
651
+ for step in SEQUENCE:
652
+ state = self.check_absolute_movement(step[0], step[1], step[2], step[3], step[4], step[5])
653
+ if state[0] != 0:
654
+ print("Error: Out of bounds! One point is out of the workspace!")
655
+ return
656
+ print("OK! The entire trajectory is reachable in the defined workspace.")
657
+ step_number = 0
658
+ for step in SEQUENCE:
659
+ step_number += 1
660
+ print("\nExecuting step number ", step_number, "over ", len(SEQUENCE), ": ", step)
661
+ self.move_absolute(step[0], step[1], step[2], step[3], step[4], step[5])
662
+ in_pos = self.get_general_state()[1][3]
663
+ while not (in_pos):
664
+ in_pos = self.get_general_state()[1][3]
665
+ print("Step ", step_number, "done.\nWaiting for ", time_sleep, "seconds.")
666
+ sleep(time_sleep)
667
+ print('Sequence "' + file_path + '" done with success!')
668
+
669
+
670
+ class ZondaSimulator(HexapodSimulator, DeviceInterface):
671
+ """
672
+ HexapodSimulator simulates the Symétrie Hexapod ZONDA. The class is heavily based on the
673
+ ReferenceFrames in the `egse.coordinates` package.
674
+
675
+ The simulator implements the same methods as the HexapodController class which acts on the
676
+ real hardware controller in either simulation mode or with a real Hexapod ZONDA connected.
677
+
678
+ Therefore, the HexapodSimulator can be used instead of the Hexapod class in test harnesses
679
+ and when the hardware is not available.
680
+
681
+ This class simulates all the movements and status of the Hexapod.
682
+ """
683
+
684
+ def __init__(self):
685
+ super().__init__()
686
+
687
+ def get_temperature(self) -> list[float]:
688
+ return [random.random() for _ in range(6)]
689
+
690
+
691
+ class ZondaProxy(Proxy, ZondaInterface):
692
+ """The ZondaProxy class is used to connect to the control server and send commands to the
693
+ Hexapod ZONDA remotely.
694
+
695
+ Args:
696
+ protocol: the transport protocol
697
+ hostname: location of the control server (IP address)
698
+ port: TCP port on which the control server is listening for commands
699
+
700
+ """
701
+
702
+ def __init__(self, protocol: str, hostname: str, port: int):
703
+ super().__init__(connect_address(protocol, hostname, port))
704
+
705
+ @classmethod
706
+ def from_identifier(cls, device_id: str):
707
+ with RegistryClient() as reg:
708
+ service = reg.discover_service(device_id)
709
+
710
+ if service:
711
+ protocol = service.get("protocol", "tcp")
712
+ hostname = service["host"]
713
+ port = service["port"]
714
+
715
+ else:
716
+ raise RuntimeError(f"No service registered as {device_id}")
717
+
718
+ logger.info(f"{protocol=}:{hostname=}:{port=}")
719
+
720
+ return cls(protocol, hostname, port)
721
+
722
+
723
+ def decode_validation_error(value) -> Dict:
724
+ """
725
+ Decode the bitfield variable that is returned by the VALID_PTP command.
726
+
727
+ Each bit in this variable represents a particular error in the validation of a movement.
728
+ Several errors can be combined into the given variable.
729
+
730
+ Returns a dictionary with the bit numbers that were (on) and the corresponding error description.
731
+ """
732
+
733
+ return {bit: VALIDATION_LIMITS[bit] for bit in range(6) if value >> bit & 0b01}
734
+
735
+
736
+ if __name__ == "__main__":
737
+ from rich import print as rp
738
+
739
+ zonda = ZondaController()
740
+ zonda.connect()
741
+
742
+ with Timer("ZondaController"):
743
+ rp(zonda.info())
744
+ rp(zonda.is_homing_done())
745
+ rp(zonda.is_in_position())
746
+ rp(zonda.activate_control_loop())
747
+ rp(zonda.get_general_state())
748
+ rp(zonda.get_actuator_state())
749
+ rp(zonda.deactivate_control_loop())
750
+ rp(zonda.get_general_state())
751
+ rp(zonda.get_actuator_state())
752
+ rp(zonda.stop())
753
+ rp(zonda.get_limits_value(0))
754
+ rp(zonda.get_limits_value(1))
755
+ rp(zonda.check_absolute_movement(1, 1, 1, 1, 1, 1))
756
+ rp(zonda.check_absolute_movement(51, 51, 51, 1, 1, 1))
757
+ rp(zonda.get_speed())
758
+ rp(zonda.set_speed(2.0, 1.0))
759
+ time.sleep(0.5) # if we do not sleep, the get_speed() will get the old values
760
+ speed = zonda.get_speed()
761
+
762
+ if not math.isclose(speed["vt"], 2.0):
763
+ rp(f"[red]{speed['vt']} != 2.0[/red]")
764
+ if not math.isclose(speed["vr"], 1.0):
765
+ rp(f"[red]{speed['vr']} != 1.0[/red]")
766
+
767
+ rp(zonda.get_actuator_length())
768
+
769
+ # rp(zonda.machine_limit_enable(0))
770
+ # rp(zonda.machine_limit_enable(1))
771
+ # rp(zonda.get_limits_state())
772
+ rp(zonda.get_coordinates_systems())
773
+ rp(
774
+ zonda.configure_coordinates_systems(
775
+ 0.033000,
776
+ -0.238000,
777
+ 230.205000,
778
+ 0.003282,
779
+ 0.005671,
780
+ 0.013930,
781
+ 0.000000,
782
+ 0.000000,
783
+ 0.000000,
784
+ 0.000000,
785
+ 0.000000,
786
+ 0.000000,
787
+ )
788
+ )
789
+ rp(zonda.get_coordinates_systems())
790
+ rp(zonda.get_machine_positions())
791
+ rp(zonda.get_user_positions())
792
+ rp(
793
+ zonda.configure_coordinates_systems(
794
+ 0.000000,
795
+ 0.000000,
796
+ 0.000000,
797
+ 0.000000,
798
+ 0.000000,
799
+ 0.000000,
800
+ 0.000000,
801
+ 0.000000,
802
+ 0.000000,
803
+ 0.000000,
804
+ 0.000000,
805
+ 0.000000,
806
+ )
807
+ )
808
+ rp(zonda.validate_position(1, 0, 0, 0, 0, 0, 0, 0))
809
+ rp(zonda.validate_position(1, 0, 0, 0, 50, 0, 0, 0))
810
+
811
+ rp(zonda.goto_zero_position())
812
+ rp(zonda.is_in_position())
813
+ if wait_until(zonda.is_in_position, interval=1, timeout=300):
814
+ rp("[red]Task zonda.is_in_position() timed out after 30s.[/red]")
815
+ rp(zonda.is_in_position())
816
+
817
+ rp(zonda.get_machine_positions())
818
+ rp(zonda.get_user_positions())
819
+
820
+ rp(zonda.move_absolute(0, 0, 12, 0, 0, 10))
821
+
822
+ rp(zonda.is_in_position())
823
+ if wait_until(zonda.is_in_position, interval=1, timeout=300):
824
+ rp("[red]Task zonda.is_in_position() timed out after 30s.[/red]")
825
+ rp(zonda.is_in_position())
826
+
827
+ rp(zonda.get_machine_positions())
828
+ rp(zonda.get_user_positions())
829
+
830
+ rp(zonda.move_absolute(0, 0, 0, 0, 0, 0))
831
+
832
+ rp(zonda.is_in_position())
833
+ if wait_until(zonda.is_in_position, interval=1, timeout=300):
834
+ rp("[red]Task zonda.is_in_position() timed out after 30s.[/red]")
835
+ rp(zonda.is_in_position())
836
+
837
+ rp(zonda.get_machine_positions())
838
+ rp(zonda.get_user_positions())
839
+
840
+ # zonda.reset()
841
+ zonda.disconnect()
842
+
843
+ # rp(0, decode_validation_error(0))
844
+ # rp(11, decode_validation_error(11))
845
+ # rp(8, decode_validation_error(8))
846
+ # rp(24, decode_validation_error(24))