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,1001 @@
1
+ """
2
+ PMAC Interface to Hexapod Hardware Controller from Symétrie.
3
+
4
+ Some of the code below is based on the dls_pmaclib Python library that was made available
5
+ as open source by Diamond Control under the LGPL v3.x. For more info and the original
6
+ source code, please go to http://controls.diamond.ac.uk/downloads/python/index.php].
7
+
8
+ Author: Rik Huygen
9
+ """
10
+
11
+ import socket
12
+ import struct
13
+ import threading
14
+ from datetime import datetime
15
+ from datetime import timedelta
16
+ from typing import List
17
+
18
+ from egse.env import bool_env
19
+ from egse.hexapod.symetrie import logger
20
+ from egse.hexapod.symetrie.pmac_regex import match_float_response
21
+ from egse.hexapod.symetrie.pmac_regex import match_int_response
22
+ from egse.hexapod.symetrie.pmac_regex import match_string_response
23
+
24
+ VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
25
+
26
+ # Command set Request
27
+
28
+ VR_PMAC_SENDLINE = 0xB0
29
+ VR_PMAC_GETLINE = 0xB1
30
+ VR_PMAC_FLUSH = 0xB3
31
+ VR_PMAC_GETMEM = 0xB4
32
+ VR_PMAC_SETMEM = 0xB5
33
+ VR_PMAC_SETBIT = 0xBA
34
+ VR_PMAC_SETBITS = 0xBB
35
+ VR_PMAC_PORT = 0xBE
36
+ VR_PMAC_GETRESPONSE = 0xBF
37
+ VR_PMAC_READREADY = 0xC2
38
+ VR_CTRL_RESPONSE = 0xC4
39
+ VR_PMAC_GETBUFFER = 0xC5
40
+ VR_PMAC_WRITEBUFFER = 0xC6
41
+ VR_PMAC_WRITEERROR = 0xC7
42
+ VR_FWDOWNLOAD = 0xCB
43
+ VR_IPADDRESS = 0xE0
44
+
45
+ # Request Types
46
+
47
+ VR_DOWNLOAD = 0x40 # Command send to the device
48
+ VR_UPLOAD = 0xC0 # Command send to host
49
+
50
+ # API Function Error Codes
51
+
52
+ ERR_DISCONNECTED = -1
53
+ ERR_NOT_READY_2_BE_READ = -2
54
+ ERR_NO_CMD_MATCHING = -3
55
+ ERR_WRITE = -4
56
+ ERR_READ = -5
57
+ ERR_CONNECTION = -6
58
+ ERR_CMD_TIMEOUT = -7
59
+
60
+ # API Returns
61
+
62
+ RETURN_SUCCESS = 0
63
+ RETURN_IGNORED = -1
64
+ RETURN_FAILURE = -2
65
+
66
+ # The States of Q20 which are checked after the command has executed.
67
+
68
+ Q20_0: List[int] = [] # no checking of Q20 to be done
69
+ Q20_1: List[int] = [RETURN_SUCCESS] # wait until success
70
+ Q20_2: List[int] = [RETURN_SUCCESS, RETURN_IGNORED]
71
+ Q20_3: List[int] = [RETURN_SUCCESS, RETURN_IGNORED, RETURN_FAILURE]
72
+
73
+ # Each command is defined as a dictionary with the following structure:
74
+ #
75
+ # {
76
+ # 'name' : [string] Human readable name of the command
77
+ # 'cmd' : [string] Actual command string for the controller.
78
+ # Can contain format replacement fields for commands that take input values.
79
+ # 'in' : [int] Number of input values. This should match up with the number of replacement fields in 'cmd'
80
+ # 'out' : [type] Number of output variable. The type of the output variables is defined by 'out_type'
81
+ # 'return' : [string] The command to retreive and check the output.
82
+ # 'check' : [iterable] Iterable containing the output values that are expected.
83
+ # 'out_type': [function] A function object that is used to convert the output variables from string to <type func>.
84
+ # }
85
+ #
86
+ #
87
+
88
+ CMD_STOP = {
89
+ "name": "STOP",
90
+ "cmd": "&2 Q20=2",
91
+ "in": 0,
92
+ "out": 0,
93
+ "return": "&2 Q20",
94
+ "check": Q20_1,
95
+ "out_type": None,
96
+ }
97
+ CMD_HOMING = {
98
+ "name": "HOMING",
99
+ "cmd": "&2 Q20=1",
100
+ "in": 0,
101
+ "out": 0,
102
+ "return": "&2 Q20",
103
+ "check": Q20_1,
104
+ "out_type": None,
105
+ }
106
+ CMD_VIRTUAL_HOMING = {
107
+ "name": "HOMINGVIRTUAL",
108
+ "cmd": "&2 Q71={tx} Q72={ty} Q73={tz} Q74={rx} Q75={ry} Q76={rz} Q20=42",
109
+ "in": 6,
110
+ "out": 0,
111
+ "return": "&2 Q20",
112
+ "check": Q20_2,
113
+ "out_type": None,
114
+ }
115
+ CMD_CONTROLON = {
116
+ "name": "CONTROL_ON",
117
+ "cmd": "&2 Q20=3",
118
+ "in": 0,
119
+ "out": 0,
120
+ "return": "&2 Q20",
121
+ "check": Q20_3,
122
+ "out_type": None,
123
+ }
124
+ CMD_CONTROLOFF = {
125
+ "name": "CONTROL_OFF",
126
+ "cmd": "&2 Q20=4",
127
+ "in": 0,
128
+ "out": 0,
129
+ "return": "&2 Q20",
130
+ "check": Q20_1,
131
+ "out_type": None,
132
+ }
133
+ CMD_MOVE = {
134
+ "name": "MOVE",
135
+ "cmd": "&2 Q70={cm} Q71={tx:.6f} Q72={ty:.6f} Q73={tz:.6f} Q74={rx:.6f} Q75={ry:.6f} Q76={rz:.6f} Q20=11",
136
+ "in": 7,
137
+ "out": 0,
138
+ "return": "&2 Q20",
139
+ "check": Q20_3,
140
+ "out_type": None,
141
+ }
142
+ CMD_MAINTENANCE = {
143
+ "name": "MAINTENANCE",
144
+ "cmd": "&2 Q80={axis} Q20=12",
145
+ "in": 1,
146
+ "out": 0,
147
+ "return": "&2 Q20",
148
+ "check": Q20_2,
149
+ "out_type": None,
150
+ }
151
+ CMD_SPECIFICPOS = {
152
+ "name": "SPECIFICPOS",
153
+ "cmd": "&2 Q80={pos} Q20=13",
154
+ "in": 1,
155
+ "out": 0,
156
+ "return": "&2 Q20",
157
+ "check": Q20_3,
158
+ "out_type": None,
159
+ }
160
+ CMD_CLEARERROR = {
161
+ "name": "CLEARERROR",
162
+ "cmd": "&2 Q20=15",
163
+ "in": 0,
164
+ "out": 0,
165
+ "return": "&2 Q20",
166
+ "check": Q20_1,
167
+ "out_type": None,
168
+ }
169
+ CMD_RESETSOFT = {
170
+ "name": "RESETSOFT",
171
+ "cmd": "$$$",
172
+ "in": 0,
173
+ "out": 0,
174
+ "return": None,
175
+ "check": None,
176
+ "out_type": None,
177
+ }
178
+ CMD_JOG = {
179
+ "name": "JOG",
180
+ "cmd": "&2 Q68={axis} Q69={inc} Q20=41",
181
+ "in": 0,
182
+ "out": 0,
183
+ "return": "&2 Q20",
184
+ "check": Q20_2,
185
+ "out_type": None,
186
+ }
187
+
188
+ CMD_CFG_CS = {
189
+ "name": "CFG#CS",
190
+ "cmd": (
191
+ "&2 Q80={tx_u} Q81={ty_u} Q82={tz_u} Q83={rx_u} Q84={ry_u} Q85={rz_u}"
192
+ " Q86={tx_o} Q87={ty_o} Q88={tz_o} Q89={rx_o} Q90={ry_o} Q91={rz_o} Q20=21"
193
+ ),
194
+ "in": 12,
195
+ "out": 0,
196
+ "return": "&2 Q20",
197
+ "check": Q20_2,
198
+ "out_type": None,
199
+ }
200
+ CMD_CFG_SPEED = {
201
+ "name": "CFG#SPEED",
202
+ "cmd": "&2 Q80={vt} Q81={vr} Q20=25",
203
+ "in": 2,
204
+ "out": 0,
205
+ "return": "&2 Q20",
206
+ "check": Q20_2,
207
+ "out_type": None,
208
+ }
209
+
210
+ CMD_Q20_GET = {
211
+ "name": "Q20?",
212
+ "cmd": "&2 Q20",
213
+ "in": 0,
214
+ "out": 0,
215
+ "return": None,
216
+ "check": None,
217
+ "out_type": None,
218
+ }
219
+ CMD_Q29_GET = {
220
+ "name": "Q29?",
221
+ "cmd": "&2 Q29",
222
+ "in": 0,
223
+ "out": 0,
224
+ "return": None,
225
+ "check": None,
226
+ "out_type": None,
227
+ }
228
+ CMD_STATE_HEXA_GET = {
229
+ "name": "STATE#HEXA?",
230
+ "cmd": "&2 Q36",
231
+ "in": 0,
232
+ "out": 0,
233
+ "return": None,
234
+ "check": None,
235
+ "out_type": None,
236
+ }
237
+ CMD_STATE_ERROR_GET = {
238
+ "name": "STATE#ERROR?",
239
+ "cmd": "&2 Q38",
240
+ "in": 0,
241
+ "out": 0,
242
+ "return": None,
243
+ "check": None,
244
+ "out_type": None,
245
+ }
246
+ CMD_STATE_DEBUG_GET = {
247
+ "name": "STATE#DEBUG?",
248
+ "cmd": "&2 Q26,3,1 Q20",
249
+ "in": 0,
250
+ "out": 4,
251
+ "return": None,
252
+ "check": None,
253
+ "out_type": int,
254
+ }
255
+ CMD_VERSION_GET = {
256
+ "name": "VERSION?",
257
+ "cmd": "&2 Q20=55",
258
+ "in": 0,
259
+ "out": 4,
260
+ "return": "&2 Q20 Q80,4,1",
261
+ "check": Q20_1,
262
+ "out_type": bytes.decode,
263
+ }
264
+ CMD_POSUSER_GET = {
265
+ "name": "POSUSER?",
266
+ "cmd": "&2 Q53,6,1",
267
+ "in": 0,
268
+ "out": 6,
269
+ "return": None,
270
+ "check": None,
271
+ "out_type": float,
272
+ }
273
+ CMD_POSVALID_GET = {
274
+ "name": "POSVALID?",
275
+ "cmd": "&2 Q70={cm} Q71={tx:.6f} Q72={ty:.6f} Q73={tz:.6f} Q74={rx:.6f} Q75={ry:.6f} Q76={rz:.6f} Q20=10",
276
+ "in": 7,
277
+ "out": 1,
278
+ "return": "&2 Q20 Q29",
279
+ "check": Q20_1,
280
+ "out_type": int,
281
+ }
282
+ CMD_CFG_CS_GET = {
283
+ "name": "CFG#CS?",
284
+ "cmd": "&2 Q20=31",
285
+ "in": 0,
286
+ "out": 12,
287
+ "return": "&2 Q20 Q80,12,1",
288
+ "check": Q20_1,
289
+ "out_type": float,
290
+ }
291
+ CMD_CFG_SPEED_GET = {
292
+ "name": "CFG#SPEED?",
293
+ "cmd": "&2 Q20=35",
294
+ "in": 0,
295
+ "out": 6,
296
+ "return": "&2 Q20 Q80,6,1",
297
+ "check": Q20_1,
298
+ "out_type": float,
299
+ }
300
+
301
+ CMD_STRING_CID = "CID" # Card ID number
302
+ CMD_STRING_VER = "VERSION" # Firmware Version (Revision Level)
303
+ CMD_STRING_CPU = "CPU"
304
+ CMD_STRING_TYPE = "TYPE"
305
+ CMD_STRING_VID = "VID" # Vendor ID
306
+ CMD_STRING_DATE = "DATE"
307
+ CMD_STRING_TIME = "TIME"
308
+ CMD_STRING_TODAY = "TODAY"
309
+
310
+ DEF_TPMAC_CMD_TIMEOUT = timedelta(seconds=5)
311
+ DEF_TPMAC_READ_TIMEOUT = timedelta(seconds=2)
312
+ DEF_TPMAC_READ_FREQ = timedelta(milliseconds=100)
313
+ DEF_TPMAC_STRING_SIZE = 1400
314
+
315
+ DEF_STRING_SIZE = 256
316
+
317
+
318
+ # #### Pure Python API interface #######################################################################################
319
+
320
+
321
+ class PMACError(Exception):
322
+ pass
323
+
324
+
325
+ # #### Helper functions and classes ####################################################################################
326
+
327
+
328
+ def extractOutput(sourceStr, nr, func=bytes.decode):
329
+ """
330
+ Extract values from the source string argument.
331
+
332
+ The output variables all have the same type and are converted using
333
+ the func argument, e.g. int or float.
334
+
335
+ :return: an array with the output variables.
336
+ """
337
+ # Remove the acknowledgement from the sourceStr
338
+
339
+ sourceStr = sourceStr.rstrip(b"\r\x06")
340
+
341
+ # Split the string in it's parts separated by \r
342
+
343
+ parts = sourceStr.split(b"\r")
344
+
345
+ # extract the values from the part and put them in out
346
+
347
+ try:
348
+ out = [func(part) for part in parts]
349
+ except (ValueError, TypeError) as exc:
350
+ raise PMACError(f'extractOutput(): Could not parse individual parts of "{sourceStr}" with {func}.') from exc
351
+
352
+ return out
353
+
354
+
355
+ def extractQ20AndOutput(sourceStr, nr, func=bytes.decode):
356
+ """
357
+ Extract values from the source string argument.
358
+
359
+ The first value is always the Q20 variable and is of integer type.
360
+
361
+ The other output variables all have the same type and are converted using
362
+ the func argument, e.g. int or float.
363
+
364
+ Returns a tuple containing the Q20 and an array with the output variables.
365
+ """
366
+ # Remove the acknowledgement from the sourceStr
367
+
368
+ sourceStr = sourceStr.rstrip(b"\r\x06")
369
+
370
+ # Split the string in it's parts separated by \r
371
+
372
+ parts = sourceStr.split(b"\r")
373
+
374
+ # First value is always Q20, then the other output values
375
+
376
+ try:
377
+ Q20 = int(parts[0])
378
+ out = [func(part) for part in parts[1:]]
379
+ except (ValueError, TypeError) as ex:
380
+ raise PMACError(
381
+ f'extractQ20AndOutput(): Could not parse individual parts of "{sourceStr}" with {func}.'
382
+ ) from ex
383
+
384
+ return (Q20, out)
385
+
386
+
387
+ def decode_Q29(value):
388
+ """
389
+ Decode the bitfield variable Q29.
390
+
391
+ Each bit in this variable represents a particular error in the validation of a movement.
392
+ Several errors can be combined into the Q29 variable.
393
+
394
+ Returns a dictionary with the bit numbers that were (on) and the corresponding error description.
395
+ """
396
+
397
+ # This description is taken from the API PUNA Hexapod Controller Manual, version A, section 4.5.4 POSVALID?
398
+
399
+ description = [
400
+ "Homing is not done",
401
+ "Coordinate systems definition not realized",
402
+ "Kinematic error",
403
+ "Out of SYMETRIE (factory) workspace",
404
+ "Out of machine workspace",
405
+ "Out of user workspace",
406
+ "Actuator out of limits",
407
+ "Joints out of limits",
408
+ "Out of limits due to backlash compensation",
409
+ '"Abort" input enabled',
410
+ '"Safety Sensor" inputs enabled',
411
+ ]
412
+
413
+ error_msgs = {}
414
+
415
+ for bit in range(24):
416
+ if value >> bit & 0x01:
417
+ error_msgs[bit] = description[bit] if bit < 11 else "Reserved"
418
+
419
+ return error_msgs
420
+
421
+
422
+ def decode_Q30(value):
423
+ """
424
+ Decode the bitfield variable Q30.
425
+
426
+ Each bit in this variable represents a particular system setting of the actuator.
427
+
428
+ Returns a dictionary with the bit numbers that were (on) and the corresponding error description.
429
+ """
430
+
431
+ # This description is taken from the API PUNA Hexapod Controller Manual, version A, section 4.5.5 STATE#ACTUATOR?
432
+
433
+ description = [
434
+ "In position",
435
+ "Control loop on servo motors active",
436
+ "Homing done",
437
+ 'Input "Home switch"',
438
+ 'Input "Positive limit switch"',
439
+ 'Input "Negative limit switch"',
440
+ "Brake control output",
441
+ "Following error (warning)",
442
+ "Following error",
443
+ "Actuator out of bounds error",
444
+ "Amplifier error",
445
+ "Encoder error",
446
+ "Phasing error (brushless engine only)",
447
+ ]
448
+
449
+ error_msgs = {bit: description[bit] if bit < 13 else "Reserved" for bit in range(24) if value >> bit & 0x01}
450
+
451
+ states = [int(x) for x in f"{value:024b}"[::-1]]
452
+
453
+ return error_msgs, states
454
+
455
+
456
+ def decode_Q36(value):
457
+ """
458
+ Decode the bitfield variable Q36.
459
+
460
+ Each bit in this variable represents the status of a particular system setting of the hexapod.
461
+
462
+ Returns an array with the value (True/False) of the individual bits.
463
+ """
464
+
465
+ # This description is taken from the API PUNA Hexapod Controller Manual, version A, section 4.5.6 STATE#HEXA?
466
+
467
+ description = [ # noqa: F841
468
+ "Error (OR)",
469
+ "System Initialized",
470
+ "In position",
471
+ "Control loop on servo motors active",
472
+ "Homing done",
473
+ "Brake control output",
474
+ "Emergency stop button engaged",
475
+ "Following error (warning)",
476
+ "Following error",
477
+ "Actuator out of bounds error",
478
+ "Amplifier Error",
479
+ "Encoder error",
480
+ "Phasing error (brushless motors only)",
481
+ "Homing error",
482
+ "Kinematic error",
483
+ '"Abort" input error',
484
+ "R/W flash memory error",
485
+ "Temperature error on one or several motors",
486
+ "Home done (virtual)",
487
+ "Encoders power off",
488
+ "Limit switches power off",
489
+ "Reserved",
490
+ "Reserved",
491
+ "Reserved",
492
+ ]
493
+
494
+ bit_values = [False for x in range(24)]
495
+
496
+ for bit in range(24):
497
+ bit_values[bit] = True if value >> bit & 0x01 else False
498
+
499
+ return bit_values
500
+
501
+
502
+ class EthernetCommand:
503
+ def __init__(self, requestType, request, value, index):
504
+ self.requestType = requestType # type is byte
505
+ self.request = request # type is byte
506
+ self.value = value # type is word (= 2 bytes), content request specific
507
+ self.index = index # type is word
508
+ self.length = None # type is word, length of the data part
509
+ self.data = [] # type is byte, max length = 1492 bytes
510
+
511
+ def getCommandPacket(self, command=None):
512
+ """
513
+ Pack and return the header and the command in a bytes packet.
514
+ """
515
+ if command is None:
516
+ command = ""
517
+
518
+ assert type(command) == str
519
+
520
+ headerStr = struct.pack(">BBHHH", self.requestType, self.request, self.value, self.index, len(command))
521
+ wrappedCommand = headerStr + command.encode()
522
+
523
+ if VERBOSE_DEBUG:
524
+ logger.debug(f"Command Packet generated: {wrappedCommand}")
525
+
526
+ return wrappedCommand
527
+
528
+
529
+ class PmacEthernetInterface(object):
530
+ """
531
+ This class provides an interface to connect to a remote PMAC over an Ethernet interface.
532
+ It provides methods to connect to the PMAC, to disconnect, and to issue commands.
533
+ """
534
+
535
+ def __init__(self, numAxes=None):
536
+ """
537
+ Initialize the PMAC Ethernet interface.
538
+ """
539
+ self.modelName = None # Will be initialized by the getPmacModel() call
540
+
541
+ # Basic connection settings
542
+
543
+ self.hostname = ""
544
+ self.port = None
545
+
546
+ # Access-to-the-connection semaphore.
547
+ # Use this to lock/unlock I/O access to the connection (whatever type it is) in child classes.
548
+
549
+ self.semaphore = threading.Semaphore()
550
+
551
+ self.isConnectionOpen = False
552
+
553
+ # Use the getter self.getNumberOfAxes() to access this. The value is None if uninitialised.
554
+
555
+ if numAxes is not None:
556
+ self._numAxes = int(numAxes)
557
+ else:
558
+ self._numAxes = None
559
+
560
+ # Define a number of often used commands
561
+
562
+ self.getResponseCommand = EthernetCommand(VR_DOWNLOAD, VR_PMAC_GETRESPONSE, 0, 0)
563
+ self.getBufferCommand = EthernetCommand(VR_UPLOAD, VR_PMAC_GETBUFFER, 0, 0)
564
+
565
+ def setConnectionParameters(self, host="localhost", port=None):
566
+ self.hostname = str(host)
567
+ if port is not None:
568
+ self.port = int(str(port))
569
+
570
+ # Attempts to open a connection to a remote PMAC.
571
+ # Returns None on success, or an error message string on failure.
572
+
573
+ def connect(self):
574
+ # Sanity checks
575
+
576
+ if self.isConnectionOpen:
577
+ raise PMACError("Socket is already open")
578
+ if self.hostname in (None, ""):
579
+ raise PMACError("ERROR: hostname not initialized")
580
+ if self.port in (None, 0):
581
+ raise PMACError("ERROR: port number not initialized")
582
+
583
+ # Create a new socket instance
584
+
585
+ try:
586
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
587
+ self.sock.setblocking(1)
588
+ self.sock.settimeout(3)
589
+ except socket.error as e_socket:
590
+ raise PMACError("ERROR: Failed to create socket.") from e_socket
591
+
592
+ # Attempt to establish a connection to the remote host
593
+
594
+ try:
595
+ logger.debug(f'Connecting a socket to host "{self.hostname}" using port {self.port}')
596
+ self.sock.connect((self.hostname, self.port))
597
+ except ConnectionRefusedError as e_cr:
598
+ raise PMACError(f"ERROR: Connection refused to {self.hostname}") from e_cr
599
+ except socket.gaierror as e_gai:
600
+ raise PMACError(f"ERROR: socket address info error for {self.hostname}") from e_gai
601
+ except socket.herror as e_h:
602
+ raise PMACError(f"ERROR: socket host address error for {self.hostname}") from e_h
603
+ except socket.timeout as e_timeout:
604
+ raise PMACError(f"ERROR: socket timeout error for {self.hostname}") from e_timeout
605
+ except OSError as ose:
606
+ raise PMACError(f"ERROR: OSError caught ({ose}).") from ose
607
+
608
+ self.isConnectionOpen = True
609
+
610
+ # Check that we are connected to a pmac by issuing the "VERSION" command -- expecting the 1.947 in return.
611
+ # If we don't get the right response, then disconnect automatically
612
+
613
+ if not self.isConnected():
614
+ raise PMACError("Device is not connected, check logging messages for the cause.")
615
+
616
+ def disconnect(self):
617
+ """
618
+ Disconnect from the Ethernet connection.
619
+
620
+ Raises a PMACError on failure.
621
+ """
622
+ try:
623
+ if self.isConnectionOpen:
624
+ logger.debug(f"Disconnecting from {self.hostname}")
625
+ self.semaphore.acquire()
626
+ self.sock.close()
627
+ self.semaphore.release()
628
+ self.isConnectionOpen = False
629
+ except Exception as e_exc:
630
+ raise PMACError(f"Could not close socket to {self.hostname}") from e_exc
631
+
632
+ def isConnected(self):
633
+ """
634
+ Check if the PMAC Ethernet connection is open. This will send a 'version' command to the
635
+ PMAC interface and try to parse the return value. If no connection was established or
636
+ no version could be parsed from the return value, False is returned.
637
+ """
638
+ if not self.isConnectionOpen:
639
+ return False
640
+
641
+ try:
642
+ response = self.getResponse(CMD_STRING_VER)
643
+ except PMACError as e_pmac:
644
+ if len(e_pmac.args) >= 2 and e_pmac.args[1] == ERR_CONNECTION:
645
+ logger.error(f"While trying to talk to the device the following exception occurred, exception={e_pmac}")
646
+ logger.error("Most probably the client connection was closed. Disconnecting...")
647
+ self.disconnect()
648
+ return False
649
+ else:
650
+ logger.error(f"While trying to talk to the device the following exception occured, exception={e_pmac}")
651
+ self.disconnect()
652
+ return False
653
+ finally:
654
+ pass
655
+
656
+ version = match_float_response(response)
657
+
658
+ if not version:
659
+ # if the response was not matching the regex then we're not talking to a PMAC!
660
+ logger.error(
661
+ f'Device did not respond correctly to a "VERSION" command, response={response}. Disconnecting...'
662
+ )
663
+ self.disconnect()
664
+ return False
665
+
666
+ return True
667
+
668
+ def getBuffer(self, shouldWait=True):
669
+ """"""
670
+
671
+ if not self.isConnectionOpen:
672
+ raise PMACError("Device is not connected, reconnect or check logging messages.")
673
+
674
+ try:
675
+ if shouldWait:
676
+ self.semaphore.acquire()
677
+
678
+ # Attempt to send the complete command to PMAC
679
+
680
+ self.sock.sendall(self.getBufferCommand.getCommandPacket())
681
+ logger.debug("Sent out get buffer request to PMAC")
682
+
683
+ # wait for, read and return the response from PMAC (will be at most 1400 chars)
684
+
685
+ returnStr = self.sock.recv(2048)
686
+ logger.debug(f"Received from PMAC: {returnStr}")
687
+
688
+ return returnStr
689
+
690
+ except socket.timeout as e_timeout:
691
+ raise PMACError("Socket timeout error", ERR_CMD_TIMEOUT) from e_timeout
692
+ except socket.error as e_socket:
693
+ # Interpret any socket-related error as an I/O error
694
+ raise PMACError("Socket communication error.", ERR_CONNECTION) from e_socket
695
+ except OSError as e_os:
696
+ raise PMACError(f"OS Error: {e_os}") from e_os
697
+ finally:
698
+ if shouldWait:
699
+ self.semaphore.release()
700
+
701
+ def getResponse(self, command, shouldWait=True):
702
+ """
703
+ Send a single command to the controller and block until a response from the controller.
704
+
705
+ Args:
706
+ command (str): is the command to be sent
707
+
708
+ shouldWait (bool, optional, default True): whether to wait on the semaphore.
709
+
710
+ This should normally be left default. If we have acquired the semaphore manually,
711
+ then specify shouldWait = False (and don't forget to release the semaphore eventually).
712
+
713
+ Returns:
714
+ response (str): is either a string returned by the PMAC (on success), or an error message (on failure)
715
+
716
+ Raises:
717
+ PMACError when there was an I/O problem during comm with the PMAC or the response does not have
718
+ recognised terminators.
719
+
720
+ Note that PMAC may still return an "ERRxxx" code; this function will still consider that
721
+ a successful transmission.
722
+
723
+ """
724
+
725
+ if not self.isConnectionOpen:
726
+ raise PMACError("Device is not connected, reconnect or check logging messages.")
727
+
728
+ command = str(command)
729
+
730
+ try:
731
+ if shouldWait:
732
+ self.semaphore.acquire()
733
+
734
+ # Attempt to send the complete command to PMAC
735
+
736
+ if VERBOSE_DEBUG:
737
+ logger.debug(f"Sending out to PMAC: {command}")
738
+
739
+ self.sock.sendall(self.getResponseCommand.getCommandPacket(command))
740
+
741
+ # wait for, read and return the response from PMAC (will be at most 1400 chars)
742
+
743
+ returnStr = self.sock.recv(2048)
744
+
745
+ if VERBOSE_DEBUG:
746
+ logger.debug(f"Received from PMAC: {returnStr}")
747
+
748
+ return returnStr
749
+
750
+ except OSError as e_os:
751
+ raise PMACError(f"OS Error: {e_os}") from e_os
752
+ except socket.timeout as e_timeout:
753
+ raise PMACError("Socket timeout error", ERR_CMD_TIMEOUT) from e_timeout
754
+ except socket.error as e_socket:
755
+ # Interpret any socket-related error as an I/O error
756
+ raise PMACError("Socket communication error.", ERR_CONNECTION) from e_socket
757
+ finally:
758
+ if shouldWait:
759
+ self.semaphore.release()
760
+
761
+ def waitFor(self, cmd, values):
762
+ start = datetime.now()
763
+ count = 0
764
+
765
+ assert isinstance(values, list), "Expected list argument"
766
+
767
+ while datetime.now() - start < DEF_TPMAC_CMD_TIMEOUT:
768
+ # Only read every DEF_TPMAC_READ_FREQ
769
+
770
+ if datetime.now() - start > count * DEF_TPMAC_READ_FREQ:
771
+ count += 1
772
+ retStr = self.getResponse(cmd)
773
+ key = match_int_response(retStr)
774
+ if key in values:
775
+ return key
776
+
777
+ raise PMACError(f"Timeout while waiting for {cmd} to be {values}")
778
+
779
+ def waitForOutput(self, cmd, values, nr, func):
780
+ start = datetime.now()
781
+ count = 0
782
+
783
+ assert isinstance(values, list), "Expected list argument"
784
+
785
+ while datetime.now() - start < DEF_TPMAC_CMD_TIMEOUT:
786
+ # Only read every DEF_TPMAC_READ_FREQ
787
+
788
+ if datetime.now() - start > count * DEF_TPMAC_READ_FREQ:
789
+ count += 1
790
+ retStr = self.getResponse(cmd)
791
+ Q20, out = extractQ20AndOutput(retStr, nr, func)
792
+ if Q20 not in values:
793
+ continue
794
+ return out
795
+
796
+ raise PMACError(f"Timeout while waiting for {cmd} to be {values}")
797
+
798
+ def getQVars(self, base, offsets, func=bytes.decode):
799
+ """
800
+ Get a list of values of Qxx variables, where xx = base + offset (for each offset).
801
+
802
+ Throws a PMACError when no response received from Hexapod Hardware Controller.
803
+ """
804
+ qVars = map(lambda x: base + x, offsets)
805
+ cmd = ""
806
+ for qVar in qVars:
807
+ cmd += f"Q{qVar:02} "
808
+ retStr = self.getResponse(cmd)
809
+
810
+ if VERBOSE_DEBUG:
811
+ logger.debug(f"retStr={retStr} of type {type(retStr)}")
812
+
813
+ if retStr == b"\x00":
814
+ raise PMACError(f"No response received for {cmd}, return value is {retStr}")
815
+
816
+ # Remove the acknowledgement from the sourceStr
817
+
818
+ retStr = retStr.rstrip(b"\r\x06")
819
+
820
+ # Split the string in it's parts separated by \r
821
+
822
+ qVarsResult = [func(x) for x in retStr.split(b"\r")]
823
+
824
+ return qVarsResult
825
+
826
+ def getMVars(self, base, offsets, func=bytes.decode):
827
+ """
828
+ Get a list of values of Mxx variables, where xx = base + offset (for each offset).
829
+
830
+ The 'M' variables are Symétrie internal variables.
831
+ The following M-Vars are known:
832
+
833
+ * M5282 - check if a motion program is running, return 1 when it is running, 0 if no motion program is running
834
+
835
+ Throws a PMACError when no response received from Hexapod Hardware Controller.
836
+ """
837
+ mVars = map(lambda x: base + x, offsets)
838
+ cmd = ""
839
+ for mVar in mVars:
840
+ cmd += f"M{mVar:02} "
841
+ retStr = self.getResponse(cmd)
842
+ logger.debug(f"retStr={retStr} of type {type(retStr)}")
843
+
844
+ if retStr == b"\x00":
845
+ raise PMACError(f"No response received for {cmd}, return value is {retStr}")
846
+
847
+ # Remove the acknowledgement from the sourceStr
848
+
849
+ retStr = retStr.rstrip(b"\r\x06")
850
+
851
+ # Split the string in it's parts separated by \r
852
+
853
+ mVarsResult = [func(x) for x in retStr.split(b"\r")]
854
+
855
+ return mVarsResult
856
+
857
+ def getIVars(self, base, offsets, func=bytes.decode):
858
+ """
859
+ Get a list of values of Ixxxx variables, where xxxx = base + offset (for each offset).
860
+ """
861
+ iVars = map(lambda x: base + x, offsets)
862
+ cmd = ""
863
+ for iVar in iVars:
864
+ cmd += "i%d " % iVar
865
+ retStr = self.getResponse(cmd)
866
+ logger.debug(f"retStr={retStr} of type {type(retStr)}")
867
+
868
+ # Remove the acknowledgement from the sourceStr
869
+
870
+ retStr = retStr.rstrip(b"\r\x06")
871
+
872
+ # Split the string in it's parts separated by \r
873
+
874
+ iVarsResult = [func(x) for x in retStr.split(b"\r")]
875
+
876
+ return iVarsResult
877
+
878
+ def getPmacModel(self):
879
+ """
880
+ Return a string designating which PMAC model this is.
881
+
882
+ Raise a PMACError if the model is unknown.
883
+ """
884
+
885
+ if self.modelName:
886
+ return self.modelName
887
+
888
+ # Ask for pmac model, returns an integer
889
+
890
+ modelCode = self.getCID()
891
+
892
+ # Return a model designation based on model code
893
+
894
+ modelNamesByCode = {602413: "Turbo PMAC2-VME", 603382: "Geo Brick (3U Turbo PMAC2)"}
895
+ try:
896
+ modelName = modelNamesByCode[modelCode]
897
+ except KeyError as e_key:
898
+ raise PMACError(f"Unsupported PMAC model: modelCode={modelCode}") from e_key
899
+
900
+ self.modelName = modelName
901
+
902
+ return modelName
903
+
904
+ def getCID(self):
905
+ retStr = self.getResponse(CMD_STRING_CID)
906
+ return match_int_response(retStr)
907
+
908
+ def getVersion(self):
909
+ retStr = self.getResponse(CMD_STRING_VER)
910
+ return match_float_response(retStr)
911
+
912
+ def getCPU(self):
913
+ retStr = self.getResponse(CMD_STRING_CPU)
914
+ return match_string_response(retStr)
915
+
916
+ def getType(self):
917
+ retStr = self.getResponse(CMD_STRING_TYPE)
918
+ return match_string_response(retStr)
919
+
920
+ def getVID(self):
921
+ retStr = self.getResponse(CMD_STRING_VID)
922
+ return match_string_response(retStr)
923
+
924
+ def getDate(self):
925
+ retStr = self.getResponse(CMD_STRING_DATE)
926
+ return match_string_response(retStr)
927
+
928
+ def getTime(self):
929
+ retStr = self.getResponse(CMD_STRING_TIME)
930
+ return match_string_response(retStr)
931
+
932
+ def getToday(self):
933
+ retStr = self.getResponse(CMD_STRING_TODAY)
934
+ return match_string_response(retStr)
935
+
936
+ def sendCommand(self, cmd, **kwargs):
937
+ """
938
+ Sends a command string to the TPMAC.
939
+
940
+ Once the acknowledgment is received, it interrogates the TPMAC Q20 variable
941
+ each DEF_TPMAC_READ_FREQ microseconds until Q20 = 0 or -1 or -2.
942
+
943
+ Throws a PMACError:
944
+ * when the arguments do not match up
945
+ * when there is a Time-Out
946
+ * when there is a socket communication error
947
+
948
+ Returns the Q20 value as an integer.
949
+ """
950
+
951
+ # When we expect input parameters, make sure the number of **kwargs matches the cmd['in']
952
+ # and format the command with the given keyword arguments.raise
953
+
954
+ if cmd["in"] > 0:
955
+ if len(kwargs) == cmd["in"]:
956
+ fullCommand = cmd["cmd"].format(**kwargs)
957
+ else:
958
+ raise PMACError(f"Expected {cmd['in']} keyword arguments to cmd['name'], got {len(kwargs)}")
959
+ else:
960
+ fullCommand = cmd["cmd"]
961
+
962
+ if VERBOSE_DEBUG:
963
+ logger.debug(f"Sending the {cmd['name']} command.")
964
+
965
+ retStr = self.getResponse(fullCommand)
966
+
967
+ if VERBOSE_DEBUG:
968
+ logger.debug(f"Command '{cmd['name']}' returned \"{retStr}\"")
969
+
970
+ # Check the return code (usually Q20)
971
+
972
+ if cmd["out"] == 0 and cmd["return"] is not None:
973
+ # The following method can throw a PMACError on Timeout
974
+
975
+ rc = self.waitFor(cmd["return"], cmd["check"])
976
+ logger.debug(f"waitFor returned {rc}")
977
+
978
+ return rc
979
+
980
+ # Get and return the output parameters
981
+
982
+ if cmd["out"] > 0 and cmd["return"] is not None:
983
+ # The following method can throw a PMACError on Timeout
984
+
985
+ out = self.waitForOutput(cmd["return"], cmd["check"], cmd["out"], cmd["out_type"])
986
+ logger.debug(f"waitForAndOutput returned {out}")
987
+
988
+ return out
989
+
990
+ # Get the output parameters out of the retStr
991
+
992
+ if cmd["out"] > 0 and cmd["return"] is None:
993
+ out = extractOutput(retStr, cmd["out"], cmd["out_type"])
994
+
995
+ return out
996
+ #
997
+ return None
998
+
999
+
1000
+ if __name__ == "__main__":
1001
+ pass