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