diff --git a/.gitignore b/.gitignore index dd73af3..cb936d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # C extensions *.so +*.sw[a-z] + # Distribution / packaging .Python env/ @@ -54,4 +56,10 @@ docs/_build/ target/ # pycharm editor -.idea/ \ No newline at end of file +.idea/ + +#Eclipse +.project +.settings/ +.pydevproject +.vscode/settings.json diff --git a/.travis.yml b/.travis.yml index ba8ac2d..8c8bf59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: python python: -- "2.6" -- "2.7" - "3.2" - "3.3" - "3.4" +- "3.5" +- "3.6" install: python setup.py install -script: nosetests \ No newline at end of file +script: nosetests diff --git a/README.rst b/README.rst index 0c26a64..586a6a3 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,12 @@ Test ~~~~ The library is currently test on Python 2.6, 2.7. +Library is basically tested on python3.5 for SLC parts: + - Open a connection + - Read Tag + - Close the connection +(write_tag is updated to python3 but it's not tested) + .. image:: https://travis-ci.org/ruscito/pycomm.svg?branch=master :target: https://travis-ci.org/ruscito/pycomm @@ -14,18 +20,10 @@ Setup ~~~~~ The package can be installed from -GitHub: -:: - - git clone https://github.com/ruscito/pycomm.git - cd pycomm - sudo python setup.py install - - -PyPi: +PIP: :: - pip install pycomm + pip install git+https://github.com/bpaterni/pycomm.git@pycomm3 ab_comm ~~~~~~~ @@ -174,4 +172,4 @@ Thanks to patrickjmcd_ for the help with the Direct Connections and thanks in ad License ~~~~~~~ -pycomm is distributed under the MIT License \ No newline at end of file +pycomm is distributed under the MIT License diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 049532b..a373ade 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -24,7 +24,9 @@ # SOFTWARE. # from pycomm.cip.cip_base import * +from collections import defaultdict import logging + try: # Python 2.7+ from logging import NullHandler except ImportError: @@ -65,6 +67,10 @@ def __init__(self): self._get_template_in_progress = False self.__version__ = '0.2' + self._struct_cache = {} + self._template_cache = {} + self._udt_cache = {} + def get_last_tag_read(self): """ Return the last tag read by a multi request read @@ -92,13 +98,13 @@ def _parse_instance_attribute_list(self, start_tag_ptr, status): count = 0 try: while idx < tags_returned_length: - instance = unpack_dint(tags_returned[idx:idx+4]) + instance = unpack_dint(tags_returned[idx:idx + 4]) idx += 4 - tag_length = unpack_uint(tags_returned[idx:idx+2]) + tag_length = unpack_uint(tags_returned[idx:idx + 2]) idx += 2 - tag_name = tags_returned[idx:idx+tag_length] + tag_name = tags_returned[idx:idx + tag_length] idx += tag_length - symbol_type = unpack_uint(tags_returned[idx:idx+2]) + symbol_type = unpack_uint(tags_returned[idx:idx + 2]) idx += 2 count += 1 self._tag_list.append({'instance_id': instance, @@ -195,8 +201,8 @@ def _parse_fragment(self, start_ptr, status): """ try: - data_type = unpack_uint(self._reply[start_ptr:start_ptr+2]) - fragment_returned = self._reply[start_ptr+2:] + data_type = unpack_uint(self._reply[start_ptr:start_ptr + 2]) + fragment_returned = self._reply[start_ptr + 2:] except Exception as e: raise DataError(e) @@ -207,9 +213,9 @@ def _parse_fragment(self, start_ptr, status): try: typ = I_DATA_TYPE[data_type] if self._output_raw: - value = fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]] + value = fragment_returned[idx:idx + DATA_FUNCTION_SIZE[typ]] else: - value = UNPACK_DATA_FUNCTION[typ](fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]]) + value = UNPACK_DATA_FUNCTION[typ](fragment_returned[idx:idx + DATA_FUNCTION_SIZE[typ]]) idx += DATA_FUNCTION_SIZE[typ] except Exception as e: raise DataError(e) @@ -228,7 +234,7 @@ def _parse_fragment(self, start_ptr, status): logger.warning(self._status) self._byte_offset = -1 - def _parse_multiple_request_read(self, tags): + def _parse_multiple_request_read(self, tags, tag_bits=None): """ parse the message received from a multi request read: For each tag parsed, the information extracted includes the tag name, the value read and the data type. @@ -238,25 +244,31 @@ def _parse_multiple_request_read(self, tags): """ offset = 50 position = 50 + tag_bits = tag_bits or {} try: - number_of_service_replies = unpack_uint(self._reply[offset:offset+2]) + number_of_service_replies = unpack_uint(self._reply[offset:offset + 2]) tag_list = [] for index in range(number_of_service_replies): position += 2 - start = offset + unpack_uint(self._reply[position:position+2]) - general_status = unpack_usint(self._reply[start+2:start+3]) - + start = offset + unpack_uint(self._reply[position:position + 2]) + general_status = unpack_usint(self._reply[start + 2:start + 3]) + tag = tags[index] if general_status == 0: - data_type = unpack_uint(self._reply[start+4:start+6]) + typ = I_DATA_TYPE[unpack_uint(self._reply[start + 4:start + 6])] value_begin = start + 6 - value_end = value_begin + DATA_FUNCTION_SIZE[I_DATA_TYPE[data_type]] - value = self._reply[value_begin:value_end] - self._last_tag_read = (tags[index], UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](value), - I_DATA_TYPE[data_type]) + value_end = value_begin + DATA_FUNCTION_SIZE[typ] + value = UNPACK_DATA_FUNCTION[typ](self._reply[value_begin:value_end]) + if tag in tag_bits: + for bit in tag_bits[tag]: + name = f'{tag}.{bit}' + value = bool(value & (1 << bit)) if bit < BITS_PER_INT_TYPE[typ] else None + tag_list.append((name, value, 'BOOL')) + else: + self._last_tag_read = (tag, value, typ) + tag_list.append(self._last_tag_read) else: - self._last_tag_read = (tags[index], None, None) - - tag_list.append(self._last_tag_read) + self._last_tag_read = (tag, None, None) + tag_list.append(self._last_tag_read) return tag_list except Exception as e: @@ -272,13 +284,14 @@ def _parse_multiple_request_write(self, tags): """ offset = 50 position = 50 + try: - number_of_service_replies = unpack_uint(self._reply[offset:offset+2]) + number_of_service_replies = unpack_uint(self._reply[offset:offset + 2]) tag_list = [] for index in range(number_of_service_replies): position += 2 - start = offset + unpack_uint(self._reply[position:position+2]) - general_status = unpack_usint(self._reply[start+2:start+3]) + start = offset + unpack_uint(self._reply[position:position + 2]) + general_status = unpack_usint(self._reply[start + 2:start + 3]) if general_status == 0: self._last_tag_write = (tags[index] + ('GOOD',)) @@ -361,9 +374,9 @@ def read_tag(self, tag): :return: None is returned in case of error otherwise the tag list is returned """ self.clear() - multi_requests = False - if isinstance(tag, list): - multi_requests = True + multi_requests = isinstance(tag, (list, tuple)) + tag_bits = defaultdict(list) + tags_read = [] if not self._target_is_connected: if not self.forward_open(): @@ -374,15 +387,26 @@ def read_tag(self, tag): if multi_requests: rp_list = [] for t in tag: - rp = create_tag_rp(t, multi_requests=True) - if rp is None: - self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) - raise DataError("Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) - else: - rp_list.append(chr(TAG_SERVICES_REQUEST['Read Tag']) + rp + pack_uint(1)) + t, bit = self._prep_bools(t, 'BOOL', bits_only=True) + read = bit is None or t not in tag_bits + if bit is not None: + tag_bits[t].append(bit) + if read: + tags_read.append(t) + rp = create_tag_rp(t, multi_requests=True) + if rp is None: + self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) + raise DataError("Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) + else: + rp_list.append( + bytes([TAG_SERVICES_REQUEST['Read Tag']]) + + rp + + pack_uint(1)) + message_request = build_multiple_service(rp_list, Base._get_sequence()) else: + tag, bit = self._prep_bools(tag, 'BOOL', bits_only=True) rp = create_tag_rp(tag) if rp is None: self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) @@ -391,29 +415,33 @@ def read_tag(self, tag): # Creating the Message Request Packet message_request = [ pack_uint(Base._get_sequence()), - chr(TAG_SERVICES_REQUEST['Read Tag']), # the Request Service - chr(len(rp) / 2), # the Request Path Size length in word - rp, # the request path + bytes([TAG_SERVICES_REQUEST['Read Tag']]), # the Request Service + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path pack_uint(1) ] if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )) is None: raise DataError("send_unit_data returned not valid data") if multi_requests: - return self._parse_multiple_request_read(tag) + return self._parse_multiple_request_read(tags_read, tag_bits) else: # Get the data type if self._status[0] == SUCCESS: data_type = unpack_uint(self._reply[50:52]) + typ = I_DATA_TYPE[data_type] try: - return UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](self._reply[52:]), I_DATA_TYPE[data_type] + value = UNPACK_DATA_FUNCTION[typ](self._reply[52:]) + if bit is not None: + value = bool(value & (1 << bit)) if bit < BITS_PER_INT_TYPE[typ] else None + return value, typ except Exception as e: raise DataError(e) else: @@ -454,9 +482,9 @@ def read_array(self, tag, counts, raw=False): # Creating the Message Request Packet message_request = [ pack_uint(Base._get_sequence()), - chr(TAG_SERVICES_REQUEST["Read Tag Fragmented"]), # the Request Service - chr(len(rp) / 2), # the Request Path Size length in word - rp, # the request path + bytes([TAG_SERVICES_REQUEST["Read Tag Fragmented"]]), # the Request Service + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path pack_uint(counts), pack_dint(self._byte_offset) ] @@ -464,7 +492,7 @@ def read_array(self, tag, counts, raw=False): if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )) is None: @@ -472,6 +500,115 @@ def read_array(self, tag, counts, raw=False): return self._tag_list + @staticmethod + def _prep_bools(tag, typ, bits_only=True): + """ + if tag is a bool and a bit of an integer, returns the base tag and the bit value, + else returns the tag name and None + + """ + if typ != 'BOOL': + return tag, None + if not bits_only and tag.endswith(']'): + try: + base, idx = tag[:-1].rsplit(sep='[', maxsplit=1) + idx = int(idx) + base = f'{base}[{idx//32}]' + return base, idx + except Exception: + return tag, None + else: + try: + base, bit = tag.rsplit('.', maxsplit=1) + bit = int(bit) + return base, bit + except Exception: + return tag, None + + def _write_tag_multi_write(self, tags): + rp_list = [] + tags_added = [] + for name, value, typ in tags: + name, bit = self._prep_bools(name, typ, bits_only=False) # check if we're writing a bit of a integer rather than a BOOL + # Create the request path to wrap the tag name + rp = create_tag_rp(name, multi_requests=True) + if rp is None: + self._status = (8, "Cannot create tag{0} req. packet. write_tag will not be executed".format(tags)) + return None + else: + try: + if bit is not None: + rp = create_tag_rp(name, multi_requests=True) + request = bytes([TAG_SERVICES_REQUEST["Read Modify Write Tag"]]) + rp + request += b''.join(self._make_write_bit_data(bit, value, bool_ary='[' in name)) + name = f'{name}.{bit}' + else: + request = (bytes([TAG_SERVICES_REQUEST["Write Tag"]]) + + rp + + pack_uint(S_DATA_TYPE[typ]) + + b'\x01\x00' + + PACK_DATA_FUNCTION[typ](value)) + + rp_list.append(request) + except (LookupError, struct.error) as e: + self._status = (8, "Tag:{0} type:{1} removed from write list. Error:{2}.".format(name, typ, e)) + + # The tag in idx position need to be removed from the rp list because has some kind of error + else: + tags_added.append((name, value, typ)) + + # Create the message request + message_request = build_multiple_service(rp_list, Base._get_sequence()) + return message_request, tags_added + + def _write_tag_single_write(self, tag, value, typ): + name, bit = self._prep_bools(tag, typ, bits_only=False) # check if we're writing a bit of a integer rather than a BOOL + + rp = create_tag_rp(name) + if rp is None: + self._status = (8, "Cannot create tag {0} request packet. write_tag will not be executed.".format(tag)) + logger.warning(self._status) + return None + else: + # Creating the Message Request Packet + message_request = [ + pack_uint(Base._get_sequence()), + bytes([TAG_SERVICES_REQUEST["Read Modify Write Tag"] + if bit is not None else TAG_SERVICES_REQUEST["Write Tag"]]), + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path + ] + if bit is not None: + try: + message_request += self._make_write_bit_data(bit, value, bool_ary='[' in name) + except Exception as err: + raise DataError('Unable to write bit, invalid bit number' + repr(err)) + else: + message_request += [ + pack_uint(S_DATA_TYPE[typ]), # data type + pack_uint(1), # Add the number of tag to write + PACK_DATA_FUNCTION[typ](value) + ] + + return message_request + + @staticmethod + def _make_write_bit_data(bit, value, bool_ary=False): + or_mask, and_mask = 0b0000_0000_0000_0000, 0b1111_1111_1111_1111 + + if bool_ary: + mask_size = 4 + bit = bit % 32 + else: + mask_size = 1 if bit < 8 else 2 if bit < 16 else 4 + + if value: + or_mask |= (1 << bit) + else: + and_mask &= ~(1 << bit) + + return [pack_uint(mask_size), pack_udint(or_mask)[:mask_size], pack_udint(and_mask)[:mask_size]] + def write_tag(self, tag, value=None, typ=None): """ write tag/tags from a connected plc @@ -485,7 +622,7 @@ def write_tag(self, tag, value=None, typ=None): The type accepted are: - BOOL - SINT - - INT' + - INT - DINT - REAL - LINT @@ -500,9 +637,7 @@ def write_tag(self, tag, value=None, typ=None): :return: None is returned in case of error otherwise the tag list is returned """ self.clear() # cleanup error string - multi_requests = False - if isinstance(tag, list): - multi_requests = True + multi_requests = isinstance(tag, (list, tuple)) if not self._target_is_connected: if not self.forward_open(): @@ -511,70 +646,22 @@ def write_tag(self, tag, value=None, typ=None): raise DataError("Target did not connected. write_tag will not be executed.") if multi_requests: - rp_list = [] - tag_to_remove = [] - idx = 0 - for name, value, typ in tag: - # Create the request path to wrap the tag name - rp = create_tag_rp(name, multi_requests=True) - if rp is None: - self._status = (8, "Cannot create tag{0} req. packet. write_tag will not be executed".format(tag)) - return None - else: - try: # Trying to add the rp to the request path list - val = PACK_DATA_FUNCTION[typ](value) - rp_list.append( - chr(TAG_SERVICES_REQUEST['Write Tag']) - + rp - + pack_uint(S_DATA_TYPE[typ]) - + pack_uint(1) - + val - ) - idx += 1 - except (LookupError, struct.error) as e: - self._status = (8, "Tag:{0} type:{1} removed from write list. Error:{2}.".format(name, typ, e)) - - # The tag in idx position need to be removed from the rp list because has some kind of error - tag_to_remove.append(idx) - - # Remove the tags that have not been inserted in the request path list - for position in tag_to_remove: - del tag[position] - # Create the message request - message_request = build_multiple_service(rp_list, Base._get_sequence()) - + message_request, tag = self._write_tag_multi_write(tag) else: if isinstance(tag, tuple): name, value, typ = tag else: name = tag - - rp = create_tag_rp(name) - if rp is None: - self._status = (8, "Cannot create tag {0} request packet. write_tag will not be executed.".format(tag)) - logger.warning(self._status) - return None - else: - # Creating the Message Request Packet - message_request = [ - pack_uint(Base._get_sequence()), - chr(TAG_SERVICES_REQUEST["Write Tag"]), # the Request Service - chr(len(rp) / 2), # the Request Path Size length in word - rp, # the request path - pack_uint(S_DATA_TYPE[typ]), # data type - pack_uint(1), # Add the number of tag to write - PACK_DATA_FUNCTION[typ](value) - ] + message_request = self._write_tag_single_write(name, value, typ) ret_val = self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, ) ) - if multi_requests: return self._parse_multiple_request_write(tag) else: @@ -603,18 +690,15 @@ def write_array(self, tag, values, data_type, raw=False): logger.warning(self._status) raise DataError("Target did not connected. write_array will not be executed.") - array_of_values = "" + array_of_values = b'' byte_size = 0 byte_offset = 0 for i, value in enumerate(values): - if raw: - array_of_values += value - else: - array_of_values += PACK_DATA_FUNCTION[data_type](value) + array_of_values += value if raw else PACK_DATA_FUNCTION[data_type](value) byte_size += DATA_FUNCTION_SIZE[data_type] - if byte_size >= 450 or i == len(values)-1: + if byte_size >= 450 or i == len(values) - 1: # create the message and send the fragment rp = create_tag_rp(tag) if rp is None: @@ -625,25 +709,25 @@ def write_array(self, tag, values, data_type, raw=False): # Creating the Message Request Packet message_request = [ pack_uint(Base._get_sequence()), - chr(TAG_SERVICES_REQUEST["Write Tag Fragmented"]), # the Request Service - chr(len(rp) / 2), # the Request Path Size length in word - rp, # the request path - pack_uint(S_DATA_TYPE[data_type]), # Data type to write - pack_uint(len(values)), # Number of elements to write + bytes([TAG_SERVICES_REQUEST["Write Tag Fragmented"]]), # the Request Service + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path + pack_uint(S_DATA_TYPE[data_type]), # Data type to write + pack_uint(len(values)), # Number of elements to write pack_dint(byte_offset), - array_of_values # Fragment of elements to write + array_of_values # Fragment of elements to write ] byte_offset += byte_size if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )) is None: raise DataError("send_unit_data returned not valid data") - array_of_values = "" + array_of_values = b'' byte_size = 0 def _get_instance_attribute_list_service(self): @@ -668,25 +752,25 @@ def _get_instance_attribute_list_service(self): message_request = [ pack_uint(Base._get_sequence()), - chr(TAG_SERVICES_REQUEST['Get Instance Attributes List']), # STEP 1 + bytes([TAG_SERVICES_REQUEST['Get Instance Attributes List']]), # STEP 1 # the Request Path Size length in word - chr(3), + bytes([3]), # Request Path ( 20 6B 25 00 Instance ) - CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 + CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 CLASS_CODE["Symbol Object"], # Logical segment: Symbolic Object 0x6B - INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 - '\x00', - pack_uint(self._last_instance), # The instance + INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 + b'\x00', + pack_uint(self._last_instance), # The instance # Request Data - pack_uint(2), # Number of attributes to retrieve - pack_uint(1), # Attribute 1: Symbol name - pack_uint(2) # Attribute 2: Symbol type + pack_uint(2), # Number of attributes to retrieve + pack_uint(1), # Attribute 1: Symbol name + pack_uint(2) # Attribute 2: Symbol type ] if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )) is None: @@ -701,35 +785,37 @@ def _get_structure_makeup(self, instance_id): """ get the structure makeup for a specific structure """ - if not self._target_is_connected: - if not self.forward_open(): - self._status = (10, "Target did not connected. get_tag_list will not be executed.") - logger.warning(self._status) - raise DataError("Target did not connected. get_tag_list will not be executed.") + if instance_id not in self._struct_cache: + if not self._target_is_connected: + if not self.forward_open(): + self._status = (10, "Target did not connected. get_tag_list will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. get_tag_list will not be executed.") - message_request = [ - pack_uint(self._get_sequence()), - chr(TAG_SERVICES_REQUEST['Get Attributes']), - chr(3), # Request Path ( 20 6B 25 00 Instance ) - CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 - CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C - INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 - '\x00', - pack_uint(instance_id), - pack_uint(4), # Number of attributes - pack_uint(4), # Template Object Definition Size UDINT - pack_uint(5), # Template Structure Size UDINT - pack_uint(2), # Template Member Count UINT - pack_uint(1) # Structure Handle We can use this to read and write UINT - ] + message_request = [ + pack_uint(self._get_sequence()), + bytes([TAG_SERVICES_REQUEST['Get Attributes']]), + bytes([3]), # Request Path ( 20 6B 25 00 Instance ) + CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 + CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C + INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 + b'\x00', + pack_uint(instance_id), + pack_uint(4), # Number of attributes + pack_uint(4), # Template Object Definition Size UDINT + pack_uint(5), # Template Structure Size UDINT + pack_uint(2), # Template Member Count UINT + pack_uint(1) # Structure Handle We can use this to read and write UINT + ] - if self.send_unit_data( - build_common_packet_format(DATA_ITEM['Connected'], - ''.join(message_request), ADDRESS_ITEM['Connection Based'], - addr_data=self._target_cid,)) is None: - raise DataError("send_unit_data returned not valid data") + if self.send_unit_data( + build_common_packet_format(DATA_ITEM['Connected'], + b''.join(message_request), ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid, )) is None: + raise DataError("send_unit_data returned not valid data") + self._struct_cache[instance_id] = self._buffer - return self._buffer + return self._struct_cache[instance_id] def _read_template(self, instance_id, object_definition_size): """ get a list of the tags in the plc @@ -741,124 +827,140 @@ def _read_template(self, instance_id, object_definition_size): logger.warning(self._status) raise DataError("Target did not connected. get_tag_list will not be executed.") - self._byte_offset = 0 - self._buffer = "" - self._get_template_in_progress = True + if instance_id not in self._template_cache: + self._byte_offset = 0 + self._buffer = b'' + self._get_template_in_progress = True - try: - while self._get_template_in_progress: + try: + while self._get_template_in_progress: - # Creating the Message Request Packet + # Creating the Message Request Packet - message_request = [ - pack_uint(self._get_sequence()), - chr(TAG_SERVICES_REQUEST['Read Template']), - chr(3), # Request Path ( 20 6B 25 00 Instance ) - CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 - CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C - INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 - '\x00', - pack_uint(instance_id), - pack_dint(self._byte_offset), # Offset - pack_uint(((object_definition_size * 4)-23) - self._byte_offset) - ] + message_request = [ + pack_uint(self._get_sequence()), + bytes([TAG_SERVICES_REQUEST['Read Template']]), + bytes([3]), # Request Path ( 20 6B 25 00 Instance ) + CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 + CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C + INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 + b'\x00', + pack_uint(instance_id), + pack_dint(self._byte_offset), # Offset + pack_uint(((object_definition_size * 4) - 21) - self._byte_offset) + ] - if not self.send_unit_data( - build_common_packet_format(DATA_ITEM['Connected'], ''.join(message_request), - ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): - raise DataError("send_unit_data returned not valid data") + if not self.send_unit_data( + build_common_packet_format( + DATA_ITEM['Connected'], + b''.join(message_request), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid, )): + raise DataError("send_unit_data returned not valid data") - self._get_template_in_progress = False - return self._buffer + self._get_template_in_progress = False + self._template_cache[instance_id] = self._buffer - except Exception as e: - raise DataError(e) + except Exception as e: + raise DataError(e) + return self._template_cache[instance_id] def _isolating_user_tag(self): try: - lst = self._tag_list - self._tag_list = [] + lst, self._tag_list = self._tag_list, [] for tag in lst: - if tag['tag_name'].find(':') != -1 or tag['tag_name'].find('__') != -1: - continue - if tag['symbol_type'] & 0b0001000000000000: - continue - dimension = (tag['symbol_type'] & 0b0110000000000000) >> 13 - - if tag['symbol_type'] & 0b1000000000000000 : - template_instance_id = tag['symbol_type'] & 0b0000111111111111 - tag_type = 'struct' - data_type = 'user-created' + tag['tag_name'] = tag['tag_name'].decode() + if ':' in tag['tag_name'] or '__' in tag['tag_name']: + continue + if tag['symbol_type'] & 0b0001000000000000: + continue + dimension = (tag['symbol_type'] & 0b0110000000000000) >> 13 + + if tag['symbol_type'] & 0b1000000000000000: + template_instance_id = tag['symbol_type'] & 0b0000111111111111 + tag_type = 'struct' + data_type = 'user-created' + self._tag_list.append({'instance_id': tag['instance_id'], + 'template_instance_id': template_instance_id, + 'tag_name': tag['tag_name'], + 'dim': dimension, + 'tag_type': tag_type, + 'data_type': data_type, + 'template': {}, + 'udt': {}}) + else: + tag_type = 'atomic' + datatype = tag['symbol_type'] & 0b0000000011111111 + data_type = I_DATA_TYPE[datatype] + if datatype == S_DATA_TYPE['BOOL']: + bit_position = (tag['symbol_type'] & 0b0000011100000000) >> 8 self._tag_list.append({'instance_id': tag['instance_id'], - 'template_instance_id': template_instance_id, 'tag_name': tag['tag_name'], 'dim': dimension, 'tag_type': tag_type, 'data_type': data_type, - 'template': {}, - 'udt': {}}) + 'bit_position': bit_position}) else: - tag_type = 'atomic' - datatype = tag['symbol_type'] & 0b0000000011111111 - data_type = I_DATA_TYPE[datatype] - if datatype == 0xc1: - bit_position = (tag['symbol_type'] & 0b0000011100000000) >> 8 - self._tag_list.append({'instance_id': tag['instance_id'], - 'tag_name': tag['tag_name'], - 'dim': dimension, - 'tag_type': tag_type, - 'data_type': data_type, - 'bit_position' : bit_position}) - else: - self._tag_list.append({'instance_id': tag['instance_id'], - 'tag_name': tag['tag_name'], - 'dim': dimension, - 'tag_type': tag_type, - 'data_type': data_type}) + self._tag_list.append({'instance_id': tag['instance_id'], + 'tag_name': tag['tag_name'], + 'dim': dimension, + 'tag_type': tag_type, + 'data_type': data_type}) except Exception as e: raise DataError(e) - def _parse_udt_raw(self, tag): - try: - buff = self._read_template(tag['template_instance_id'], tag['template']['object_definition_size']) - member_count = tag['template']['member_count'] - names = buff.split('\00') - lst = [] - - tag['udt']['name'] = 'Not an user defined structure' - for name in names: - if len(name) > 1: - - if name.find(';') != -1: - tag['udt']['name'] = name[:name.find(';')] - elif name.find('ZZZZZZZZZZ') != -1: - continue - elif name.isalpha(): - lst.append(name) - else: - continue - tag['udt']['internal_tags'] = lst - - type_list = [] - - for i in xrange(member_count): - # skip member 1 + def _build_udt(self, data, member_count): + udt = {'name': None, + 'internal_tags': [], 'data_type': []} + names = (x.decode(errors='replace') for x in data.split(b'\x00') if len(x) > 1) + for name in names: + if ';' in name and udt['name'] is None: + udt['name'] = name[:name.find(';')] + elif 'ZZZZZZZZZZ' in name: + continue + elif name.isalnum(): + udt['internal_tags'].append(name) + else: + continue + if udt['name'] is None: + udt['name'] = 'Not a user define structure' - if i != 0: - array_size = unpack_uint(buff[:2]) + for _ in range(member_count): + array_size = unpack_uint(data[:2]) + try: + data_type = I_DATA_TYPE[unpack_uint(data[2:4])] + except Exception: + dtval = unpack_uint(data[2:4]) + instance_id = dtval & 0b0000111111111111 + if instance_id in I_DATA_TYPE: + data_type = I_DATA_TYPE[instance_id] + else: try: - data_type = I_DATA_TYPE[unpack_uint(buff[2:4])] + template = self._get_structure_makeup(instance_id) + if not template.get('Error'): + _data = self._read_template(instance_id, template['object_definition_size']) + data_type = self._build_udt(_data, template['member_count']) + else: + data_type = 'None' except Exception: - data_type = "None" + data_type = 'None' - offset = unpack_dint(buff[4:8]) - type_list.append((array_size, data_type, offset)) + offset = unpack_dint(data[4:8]) + udt['data_type'].append((array_size, data_type, offset)) - buff = buff[8:] + data = data[8:] + return udt - tag['udt']['data_type'] = type_list - except Exception as e: - raise DataError(e) + def _parse_udt_raw(self, tag): + if tag['template_instance_id'] not in self._udt_cache: + try: + buff = self._read_template(tag['template_instance_id'], tag['template']['object_definition_size']) + member_count = tag['template']['member_count'] + self._udt_cache[tag['template_instance_id']] = self._build_udt(buff, member_count) + except Exception as e: + raise DataError(e) + + return self._udt_cache[tag['template_instance_id']] def get_tag_list(self): self._tag_list = [] @@ -872,13 +974,7 @@ def get_tag_list(self): for tag in self._tag_list: if tag['tag_type'] == 'struct': tag['template'] = self._get_structure_makeup(tag['template_instance_id']) - - for idx, tag in enumerate(self._tag_list): - # print (tag) - if tag['tag_type'] == 'struct': - self._parse_udt_raw(tag) - - # Step 4 + tag['udt'] = self._parse_udt_raw(tag) return self._tag_list @@ -888,25 +984,38 @@ def write_string(self, tag, value, size=82): STRING STRING_12 STRING_16 STRING_20 STRING_40 STRING_8 by default we assume size 82 (STRING) """ - if size not in string_sizes: - raise DataError("String size is incorrect") - data_tag = ".".join((tag, "DATA")) len_tag = ".".join((tag, "LEN")) # create an empty array data_to_send = [0] * size for idx, val in enumerate(value): - data_to_send[idx] = ord(val) + try: + unsigned = ord(val) + data_to_send[idx] = unsigned - 256 if unsigned > 127 else unsigned + except IndexError: + break + + str_len = len(value) + if str_len > size: + str_len = size - self.write_tag(len_tag, len(value), 'DINT') + self.write_tag(len_tag, str_len, 'DINT') self.write_array(data_tag, data_to_send, 'SINT') - def read_string(self, tag): - data_tag = ".".join((tag, "DATA")) - len_tag = ".".join((tag, "LEN")) - length = self.read_tag(len_tag) - values = self.read_array(data_tag, length[0]) - values = zip(*values)[1] - char_array = [chr(ch) for ch in values] - return ''.join(char_array) \ No newline at end of file + def read_string(self, tag, str_len=None): + data_tag = f'{tag}.DATA' + if str_len is None: + len_tag = f'{tag}.LEN' + tmp = self.read_tag(len_tag) + length, _ = tmp or (None, None) + else: + length = str_len + + if length: + values = self.read_array(data_tag, length) + if values: + _, values = zip(*values) + chars = [chr(v + 256) if v < 0 else chr(v) for v in values] + return ''.join(ch for ch in chars if ch != '\x00') + return None diff --git a/pycomm/ab_comm/slc.py b/pycomm/ab_comm/slc.py index 834cd7c..75116ee 100644 --- a/pycomm/ab_comm/slc.py +++ b/pycomm/ab_comm/slc.py @@ -51,8 +51,8 @@ def parse_tag(tag): 'file_number': t.group('file_number'), 'element_number': t.group('element_number'), 'sub_element': PCCC_CT[t.group('sub_element').upper()], - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 3} t = re.search(r"(?P[LFBN])(?P\d{1,3})" @@ -69,8 +69,8 @@ def parse_tag(tag): 'file_number': t.group('file_number'), 'element_number': t.group('element_number'), 'sub_element': t.group('sub_element'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 3} else: if (1 <= int(t.group('file_number')) <= 255) \ @@ -80,12 +80,12 @@ def parse_tag(tag): 'file_number': t.group('file_number'), 'element_number': t.group('element_number'), 'sub_element': t.group('sub_element'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 2} - t = re.search(r"(?P[IO])(:)(?P\d{1,3})" - r"(.)(?P\d{1,3})" + t = re.search(r"(?P[IO])(:)(?P\d{1,3})" + r"(.)(?P\d{1,3})" r"(/(?P\d{1,2}))?", tag, flags=re.IGNORECASE) if t: if t.group('sub_element') is not None: @@ -94,21 +94,22 @@ def parse_tag(tag): and (0 <= int(t.group('sub_element')) <= 15): return True, t.group(0), {'file_type': t.group('file_type').upper(), - 'file_number': t.group('file_number'), + 'file_number': '0', 'element_number': t.group('element_number'), + 'pos_number': t.group('position_number'), 'sub_element': t.group('sub_element'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 3} else: - if (0 <= int(t.group('file_number')) <= 255) \ - and (0 <= int(t.group('element_number')) <= 255): + if (0 <= int(t.group('element_number')) <= 255): return True, t.group(0), {'file_type': t.group('file_type').upper(), - 'file_number': t.group('file_number'), + 'file_number': '0', 'element_number': t.group('element_number'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'pos_number': t.group('position_number'), + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 2} t = re.search(r"(?PS)" @@ -122,16 +123,16 @@ def parse_tag(tag): 'file_number': '2', 'element_number': t.group('element_number'), 'sub_element': t.group('sub_element'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 3} else: if 0 <= int(t.group('element_number')) <= 255: return True, t.group(0), {'file_type': t.group('file_type').upper(), 'file_number': '2', 'element_number': t.group('element_number'), - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 2} t = re.search(r"(?PB)(?P\d{1,3})" @@ -147,8 +148,8 @@ def parse_tag(tag): 'file_number': t.group('file_number'), 'element_number': element_number, 'sub_element': sub_element, - 'read_func': '\xa2', - 'write_func': '\xab', + 'read_func': b'\xa2', + 'write_func': b'\xab', 'address_field': 3} return False, tag @@ -253,7 +254,7 @@ def __queue_data_available(self, queue_number): if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): @@ -303,7 +304,7 @@ def __get_queue_size(self, queue_number): if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): sts = int(unpack_uint(self._reply[65:67])) @@ -354,7 +355,6 @@ def read_tag(self, tag, n=1): bit_read = False bit_position = 0 - sub_element = 0 if int(res[2]['address_field'] == 3): bit_read = True bit_position = int(res[2]['sub_element']) @@ -369,36 +369,36 @@ def read_tag(self, tag, n=1): # Creating the Message Request Packet self._last_sequence = pack_uint(Base._get_sequence()) - + message_request = [ self._last_sequence, - '\x4b', - '\x02', + b'\x4b', + b'\x02', CLASS_ID["8-bit"], PATH["PCCC"], - '\x07', + b'\x07', self.attribs['vid'], self.attribs['vsn'], - '\x0f', - '\x00', - self._last_sequence[1], - self._last_sequence[0], + b'\x0f', + b'\x00', + pack_usint(self._last_sequence[1]), + pack_usint(self._last_sequence[0]), res[2]['read_func'], pack_usint(data_size * n), pack_usint(int(res[2]['file_number'])), PCCC_DATA_TYPE[res[2]['file_type']], pack_usint(int(res[2]['element_number'])), - pack_usint(sub_element) + b'\x00' if 'pos_number' not in res[2] else pack_usint(int(res[2]['pos_number'])) ] logger.debug("SLC read_tag({0},{1})".format(tag, n)) if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request), + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): - sts = int(unpack_usint(self._reply[58])) + sts = int(self._reply[58]) try: if sts != 0: sts_txt = PCCC_ERROR_CODE[sts] @@ -529,30 +529,30 @@ def write_tag(self, tag, value): message_request = [ self._last_sequence, - '\x4b', - '\x02', + b'\x4b', + b'\x02', CLASS_ID["8-bit"], PATH["PCCC"], - '\x07', + b'\x07', self.attribs['vid'], self.attribs['vsn'], - '\x0f', - '\x00', - self._last_sequence[1], - self._last_sequence[0], + b'\x0f', + b'\x00', + pack_usint(self._last_sequence[1]), + pack_usint(self._last_sequence[0]), res[2]['write_func'], pack_usint(data_size * n), pack_usint(int(res[2]['file_number'])), PCCC_DATA_TYPE[res[2]['file_type']], pack_usint(int(res[2]['element_number'])), - pack_usint(sub_element) + b'\x00' if 'pos_number' not in res[2] else pack_usint(int(res[2]['pos_number'])) ] logger.debug("SLC write_tag({0},{1})".format(tag, value)) if self.send_unit_data( build_common_packet_format( DATA_ITEM['Connected'], - ''.join(message_request) + data_to_write, + b''.join(message_request) + data_to_write, ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): sts = int(unpack_usint(self._reply[58])) diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 81757ae..572bf15 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -26,9 +26,8 @@ import struct import socket -import random -from os import getpid +from os import getpid, urandom from pycomm.cip.cip_const import * from pycomm.common import PycommError @@ -75,6 +74,11 @@ def pack_dint(n): return struct.pack(' 0x20, 0x67, 0x24, 0x01. PATH = { - 'Connection Manager': '\x06\x24\x01', - 'Router': '\x02\x24\x01', - 'Backplane Data Type': '\x66\x24\x01', - 'PCCC': '\x67\x24\x01', - 'DHCP Channel A': '\xa6\x24\x01\x01\x2c\x01', - 'DHCP Channel B': '\xa6\x24\x01\x02\x2c\x01' + 'Connection Manager': b'\x06\x24\x01', + 'Router': b'\x02\x24\x01', + 'Backplane Data Type': b'\x66\x24\x01', + 'PCCC': b'\x67\x24\x01', + 'DHCP Channel A': b'\xa6\x24\x01\x01\x2c\x01', + 'DHCP Channel B': b'\xa6\x24\x01\x02\x2c\x01' } ENCAPSULATION_COMMAND = { # Volume 2: 2-3.2 Command Field UINT 2 byte - "nop": '\x00\x00', - "list_targets": '\x01\x00', - "list_services": '\x04\x00', - "list_identity": '\x63\x00', - "list_interfaces": '\x64\x00', - "register_session": '\x65\x00', - "unregister_session": '\x66\x00', - "send_rr_data": '\x6F\x00', - "send_unit_data": '\x70\x00' + "nop": b'\x00\x00', + "list_targets": b'\x01\x00', + "list_services": b'\x04\x00', + "list_identity": b'\x63\x00', + "list_interfaces": b'\x64\x00', + "register_session": b'\x65\x00', + "unregister_session": b'\x66\x00', + "send_rr_data": b'\x6F\x00', + "send_unit_data": b'\x70\x00' } """ @@ -77,33 +77,33 @@ created to hold information about the structure makeup. """ CLASS_CODE = { - "Message Router": '\x02', # Volume 1: 5-1 - "Symbol Object": '\x6b', - "Template Object": '\x6c', - "Connection Manager": '\x06' # Volume 1: 3-5 + "Message Router": b'\x02', # Volume 1: 5-1 + "Symbol Object": b'\x6b', + "Template Object": b'\x6c', + "Connection Manager": b'\x06' # Volume 1: 3-5 } CONNECTION_MANAGER_INSTANCE = { - 'Open Request': '\x01', - 'Open Format Rejected': '\x02', - 'Open Resource Rejected': '\x03', - 'Open Other Rejected': '\x04', - 'Close Request': '\x05', - 'Close Format Request': '\x06', - 'Close Other Request': '\x07', - 'Connection Timeout': '\x08' + 'Open Request': b'\x01', + 'Open Format Rejected': b'\x02', + 'Open Resource Rejected': b'\x03', + 'Open Other Rejected': b'\x04', + 'Close Request': b'\x05', + 'Close Format Request': b'\x06', + 'Close Other Request': b'\x07', + 'Connection Timeout': b'\x08' } TAG_SERVICES_REQUEST = { - "Read Tag": 0x4c, - "Read Tag Fragmented": 0x52, - "Write Tag": 0x4d, - "Write Tag Fragmented": 0x53, - "Read Modify Write Tag": 0x4e, - "Multiple Service Packet": 0x0a, + "Read Tag": 0x4c, + "Read Tag Fragmented": 0x52, + "Write Tag": 0x4d, + "Write Tag Fragmented": 0x53, + "Read Modify Write Tag": 0x4e, + "Multiple Service Packet": 0x0a, "Get Instance Attributes List": 0x55, - "Get Attributes": 0x03, - "Read Template": 0x4c, + "Get Attributes": 0x03, + "Read Template": 0x4c, } TAG_SERVICES_REPLY = { @@ -177,6 +177,7 @@ 0x13: "Insufficient command data", 0x14: "Attribute not supported", 0x15: "Too much data", + 0x16: "Object does not exist", 0x1A: "Bridge request too large", 0x1B: "Bridge response too large", 0x1C: "Attribute list shortage", @@ -266,14 +267,14 @@ } } DATA_ITEM = { - 'Connected': '\xb1\x00', - 'Unconnected': '\xb2\x00' + 'Connected': b'\xb1\x00', + 'Unconnected': b'\xb2\x00' } ADDRESS_ITEM = { - 'Connection Based': '\xa1\x00', - 'Null': '\x00\x00', - 'UCMM': '\x00\x00' + 'Connection Based': b'\xa1\x00', + 'Null': b'\x00\x00', + 'UCMM': b'\x00\x00' } UCMM = { @@ -285,12 +286,12 @@ } CONNECTION_SIZE = { - 'Backplane': '\x03', # CLX - 'Direct Network': '\x02' + 'Backplane': b'\x03', # CLX + 'Direct Network': b'\x02' } HEADER_SIZE = 24 -EXTENDED_SYMBOL = '\x91' +EXTENDED_SYMBOL = b'\x91' BOOL_ONE = 0xff REQUEST_SERVICE = 0 REQUEST_PATH_SIZE = 1 @@ -300,20 +301,20 @@ OFFSET_MESSAGE_REQUEST = 40 -FORWARD_CLOSE = '\x4e' -UNCONNECTED_SEND = '\x52' -FORWARD_OPEN = '\x54' -LARGE_FORWARD_OPEN = '\x5b' -GET_CONNECTION_DATA = '\x56' -SEARCH_CONNECTION_DATA = '\x57' -GET_CONNECTION_OWNER = '\x5a' -MR_SERVICE_SIZE = 2 +FORWARD_CLOSE = b'\x4e' +UNCONNECTED_SEND = b'\x52' +FORWARD_OPEN = b'\x54' +LARGE_FORWARD_OPEN = b'\x5b' +GET_CONNECTION_DATA = b'\x56' +SEARCH_CONNECTION_DATA = b'\x57' +GET_CONNECTION_OWNER = b'\x5a' +MR_SERVICE_SIZE = 2 -PADDING_BYTE = '\x00' -PRIORITY = '\x0a' -TIMEOUT_TICKS = '\x05' -TIMEOUT_MULTIPLIER = '\x01' -TRANSPORT_CLASS = '\xa3' +PADDING_BYTE = b'\x00' +PRIORITY = b'\x0a' +TIMEOUT_TICKS = b'\x05' +TIMEOUT_MULTIPLIER = b'\x01' +TRANSPORT_CLASS = b'\xa3' CONNECTION_PARAMETER = { 'PLC5': 0x4302, @@ -338,6 +339,19 @@ When reading a BOOL tag, the values returned for 0 and 1 are 0 and 0xff, respectively. """ +BITS_PER_INT_TYPE = { + 'SINT': 8, # Signed 8-bit integer + 'INT': 16, # Signed 16-bit integer + 'DINT': 32, # Signed 32-bit integer + 'LINT': 64, # Signed 64-bit integer + 'USINT': 8, # Unsigned 8-bit integer + 'UINT': 16, # Unsigned 16-bit integer + 'UDINT': 32, # Unsigned 32-bit integer + 'ULINT': 64, # Unsigned 64-bit integer + 'WORD': 16, # byte string 16-bits + 'DWORD': 32, # byte string 32-bits + 'LWORD': 64, # byte string 64-bits +} S_DATA_TYPE = { 'BOOL': 0xc1, 'SINT': 0xc2, # Signed 8-bit integer @@ -420,17 +434,17 @@ } PCCC_DATA_TYPE = { - 'N': '\x89', - 'B': '\x85', - 'T': '\x86', - 'C': '\x87', - 'S': '\x84', - 'F': '\x8a', - 'ST': '\x8d', - 'A': '\x8e', - 'R': '\x88', - 'O': '\x8b', - 'I': '\x8c' + 'N': b'\x89', + 'B': b'\x85', + 'T': b'\x86', + 'C': b'\x87', + 'S': b'\x84', + 'F': b'\x8a', + 'ST': b'\x8d', + 'A': b'\x8e', + 'R': b'\x88', + 'O': b'\x8b', + 'I': b'\x8c' } PCCC_DATA_SIZE = { @@ -480,4 +494,4 @@ 128: "Compatibility mode file missing or communication zone problem", 144: "Remote node cannot buffer command", 240: "Error code in EXT STS Byte" -} \ No newline at end of file +}