From e861dae4b1bc887db6cf0aa2b1c03630784a4479 Mon Sep 17 00:00:00 2001 From: Brian Paterni Date: Sat, 6 May 2017 16:18:06 -0500 Subject: [PATCH 01/33] gitignore: add vim swp files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dd73af3..900c00c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # C extensions *.so +*.sw[a-z] + # Distribution / packaging .Python env/ @@ -54,4 +56,4 @@ docs/_build/ target/ # pycharm editor -.idea/ \ No newline at end of file +.idea/ From e833153b639c74c03290c4a177f5e577b57dce84 Mon Sep 17 00:00:00 2001 From: Brian Paterni Date: Sat, 6 May 2017 16:18:30 -0500 Subject: [PATCH 02/33] initial work to get pycomm running on python 3.5 --- pycomm/ab_comm/clx.py | 63 ++++++++++++---------- pycomm/cip/cip_base.py | 74 +++++++++++++++---------- pycomm/cip/cip_const.py | 116 ++++++++++++++++++++-------------------- 3 files changed, 139 insertions(+), 114 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 049532b..4a33b9c 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -379,7 +379,10 @@ def read_tag(self, tag): 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)) + rp_list.append( + bytes([TAG_SERVICES_REQUEST['Read Tag']]) + + rp + + pack_uint(1)) message_request = build_multiple_service(rp_list, Base._get_sequence()) else: @@ -391,8 +394,8 @@ 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 + 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) ] @@ -400,7 +403,7 @@ def read_tag(self, tag): 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: @@ -558,8 +561,8 @@ def write_tag(self, tag, value=None, typ=None): # 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 + bytes([TAG_SERVICES_REQUEST["Write Tag"]]), # the Request Service + bytes([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 @@ -569,7 +572,7 @@ def write_tag(self, tag, value=None, typ=None): 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, ) @@ -668,14 +671,14 @@ 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_CODE["Symbol Object"], # Logical segment: Symbolic Object 0x6B INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 - '\x00', + b'\x00', pack_uint(self._last_instance), # The instance # Request Data pack_uint(2), # Number of attributes to retrieve @@ -686,7 +689,7 @@ def _get_instance_attribute_list_service(self): 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: @@ -709,12 +712,12 @@ def _get_structure_makeup(self, instance_id): message_request = [ pack_uint(self._get_sequence()), - chr(TAG_SERVICES_REQUEST['Get Attributes']), - chr(3), # Request Path ( 20 6B 25 00 Instance ) + 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 - '\x00', + b'\x00', pack_uint(instance_id), pack_uint(4), # Number of attributes pack_uint(4), # Template Object Definition Size UDINT @@ -725,7 +728,7 @@ def _get_structure_makeup(self, instance_id): if self.send_unit_data( build_common_packet_format(DATA_ITEM['Connected'], - ''.join(message_request), ADDRESS_ITEM['Connection Based'], + b''.join(message_request), ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)) is None: raise DataError("send_unit_data returned not valid data") @@ -742,7 +745,7 @@ def _read_template(self, instance_id, object_definition_size): raise DataError("Target did not connected. get_tag_list will not be executed.") self._byte_offset = 0 - self._buffer = "" + self._buffer = b'' self._get_template_in_progress = True try: @@ -752,20 +755,23 @@ def _read_template(self, instance_id, object_definition_size): message_request = [ pack_uint(self._get_sequence()), - chr(TAG_SERVICES_REQUEST['Read Template']), - chr(3), # Request Path ( 20 6B 25 00 Instance ) + 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 - '\x00', + b'\x00', pack_uint(instance_id), pack_dint(self._byte_offset), # Offset pack_uint(((object_definition_size * 4)-23) - 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,)): + 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 @@ -779,7 +785,8 @@ def _isolating_user_tag(self): lst = self._tag_list self._tag_list = [] for tag in lst: - if tag['tag_name'].find(':') != -1 or tag['tag_name'].find('__') != -1: + #if tag['tag_name'].find(':') != -1 or tag['tag_name'].find('__') != -1: + if b':' in tag['tag_name'] or b'__' in tag['tag_name']: continue if tag['symbol_type'] & 0b0001000000000000: continue @@ -822,16 +829,18 @@ 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') + names = buff.split(b'\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: + #if name.find(';') != -1: + if b':' in name: + tag['udt']['name'] = name[:name.find(b';')] + #elif name.find('ZZZZZZZZZZ') != -1: + elif b'ZZZZZZZZZZ' in name: continue elif name.isalpha(): lst.append(name) @@ -841,7 +850,7 @@ def _parse_udt_raw(self, tag): type_list = [] - for i in xrange(member_count): + for i in range(member_count): # skip member 1 if i != 0: diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 81757ae..53c083b 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -28,7 +28,7 @@ import socket import random -from os import getpid +from os import getpid, urandom from pycomm.cip.cip_const import * from pycomm.common import PycommError @@ -96,7 +96,7 @@ def unpack_sint(st): def unpack_usint(st): - return int(struct.unpack('B', st[0])[0]) + return int(struct.unpack('B', bytes([st[0]]))[0]) def unpack_int(st): @@ -205,7 +205,7 @@ def get_bit(value, idx): def print_bytes_line(msg): out = '' for ch in msg: - out += "{:0>2x}".format(ord(ch)) + out += "{:0>2x}".format(ch) return out @@ -218,7 +218,7 @@ def print_bytes_msg(msg, info=''): if new_line: out += "\n({:0>4d}) ".format(line * 10) new_line = False - out += "{:0>2x} ".format(ord(ch)) + out += "{:0>2x} ".format(ch) if column == 9: new_line = True column = 0 @@ -263,32 +263,33 @@ def create_tag_rp(tag, multi_requests=False): It returns the request packed wrapped around the tag passed. If any error it returns none """ - tags = tag.split('.') + tags = tag.encode().split(b'.') rp = [] index = [] for tag in tags: add_index = False # Check if is an array tag - if tag.find('[') != -1: + #if tag.find(b'[') != -1: + if b'[' in tag: # Remove the last square bracket tag = tag[:len(tag)-1] # Isolate the value inside bracket - inside_value = tag[tag.find('[')+1:] + inside_value = tag[tag.find(b'[')+1:] # Now split the inside value in case part of multidimensional array - index = inside_value.split(',') + index = inside_value.split(b',') # Flag the existence of one o more index add_index = True # Get only the tag part - tag = tag[:tag.find('[')] + tag = tag[:tag.find(b'[')] tag_length = len(tag) # Create the request path rp.append(EXTENDED_SYMBOL) # ANSI Ext. symbolic segment - rp.append(chr(tag_length)) # Length of the tag + rp.append(bytes([tag_length])) # Length of the tag # Add the tag to the Request path for char in tag: - rp.append(char) + rp.append(bytes([char])) # Add pad byte because total length of Request path must be word-aligned if tag_length % 2: rp.append(PADDING_BYTE) @@ -311,9 +312,9 @@ def create_tag_rp(tag, multi_requests=False): # At this point the Request Path is completed, if multi_requests: - request_path = chr(len(rp)/2) + ''.join(rp) + request_path = bytes([len(rp)//2]) + b''.join(rp) else: - request_path = ''.join(rp) + request_path = b''.join(rp) return request_path @@ -344,7 +345,7 @@ def build_multiple_service(rp_list, sequence=None): if sequence is not None: mr.append(pack_uint(sequence)) - mr.append(chr(TAG_SERVICES_REQUEST["Multiple Service Packet"])) # the Request Service + mr.append(bytes([TAG_SERVICES_REQUEST["Multiple Service Packet"]])) # the Request Service mr.append(pack_usint(2)) # the Request Path Size length in word mr.append(CLASS_ID["8-bit"]) mr.append(CLASS_CODE["Message Router"]) @@ -457,7 +458,7 @@ def receive(self, timeout=0): bytes_recd += len(chunk) except socket.error as e: raise CommError(e) - return ''.join(chunks) + return b''.join(chunks) def close(self): self.sock.close() @@ -506,9 +507,21 @@ def __init__(self): self._status = (0, "") self._output_raw = False # indicating value should be output as raw (hex) - self.attribs = {'context': '_pycomm_', 'protocol version': 1, 'rpi': 5000, 'port': 0xAF12, 'timeout': 10, - 'backplane': 1, 'cpu slot': 0, 'option': 0, 'cid': '\x27\x04\x19\x71', 'csn': '\x27\x04', - 'vid': '\x09\x10', 'vsn': '\x09\x10\x19\x71', 'name': 'Base', 'ip address': None} + self.attribs = { + 'context': '_pycomm_', + 'protocol version': 1, + 'rpi': 5000, + 'port': 0xAF12, + 'timeout': 10, + 'backplane': 1, + 'cpu slot': 0, + 'option': 0, + 'cid': b'\x27\x04\x19\x71', + 'csn': b'\x27\x04', + 'vid': b'\x09\x10', + 'vsn': b'\x09\x10\x19\x71', + 'name': 'Base', + 'ip address': None} def __len__(self): return len(self.attribs) @@ -558,12 +571,14 @@ def __repr__(self): return self._device_description def generate_cid(self): - self.attribs['cid'] = '{0}{1}{2}{3}'.format(chr(random.randint(0, 255)), chr(random.randint(0, 255)) - , chr(random.randint(0, 255)), chr(random.randint(0, 255))) + #self.attribs['cid'] = '{0}{1}{2}{3}'.format(chr(random.randint(0, 255)), chr(random.randint(0, 255)) + # , chr(random.randint(0, 255)), chr(random.randint(0, 255))) + self.attribs['cid'] = urandom(4) def generate_vsn(self): - self.attribs['vsn'] = '{0}{1}{2}{3}'.format(chr(random.randint(0, 255)), chr(random.randint(0, 255)) - , chr(random.randint(0, 255)), chr(random.randint(0, 255))) + #self.attribs['vsn'] = '{0}{1}{2}{3}'.format(chr(random.randint(0, 255)), chr(random.randint(0, 255)) + # , chr(random.randint(0, 255)), chr(random.randint(0, 255))) + self.attribs['vsn'] = urandom(4) def description(self): return self._device_description @@ -631,11 +646,11 @@ def build_header(self, command, length): :return: the headre """ try: - h = command # Command UINT + h = command h += pack_uint(length) # Length UINT h += pack_dint(self._session) # Session Handle UDINT h += pack_dint(0) # Status UDINT - h += self.attribs['context'] # Sender Context 8 bytes + h += self.attribs['context'].encode() # Sender Context 8 bytes h += pack_dint(self.attribs['option']) # Option UDINT return h except Exception as e: @@ -671,6 +686,7 @@ def forward_open(self): :return: False if any error in the replayed message """ + if self._session == 0: self._status = (4, "A session need to be registered before to call forward_open.") raise CommError("A session need to be registered before to call forward open") @@ -690,7 +706,7 @@ def forward_open(self): self.attribs['vid'], self.attribs['vsn'], TIMEOUT_MULTIPLIER, - '\x00\x00\x00', + b'\x00\x00\x00', pack_dint(self.attribs['rpi'] * 1000), pack_uint(CONNECTION_PARAMETER['Default']), pack_dint(self.attribs['rpi'] * 1000), @@ -717,7 +733,7 @@ def forward_open(self): ] if self.send_rr_data( - build_common_packet_format(DATA_ITEM['Unconnected'], ''.join(forward_open_msg), ADDRESS_ITEM['UCMM'],)): + build_common_packet_format(DATA_ITEM['Unconnected'], b''.join(forward_open_msg), ADDRESS_ITEM['UCMM'],)): self._target_cid = self._reply[44:48] self._target_is_connected = True return True @@ -762,18 +778,18 @@ def forward_close(self): if self.__direct_connections: forward_close_msg[11:2] = [ CONNECTION_SIZE['Direct Network'], - '\x00' + b'\x00' ] else: forward_close_msg[11:4] = [ CONNECTION_SIZE['Backplane'], - '\x00', + b'\x00', pack_usint(self.attribs['backplane']), pack_usint(self.attribs['cpu slot']) ] if self.send_rr_data( - build_common_packet_format(DATA_ITEM['Unconnected'], ''.join(forward_close_msg), ADDRESS_ITEM['UCMM'])): + build_common_packet_format(DATA_ITEM['Unconnected'], b''.join(forward_close_msg), ADDRESS_ITEM['UCMM'])): self._target_is_connected = False return True self._status = (5, "forward_close returned False") diff --git a/pycomm/cip/cip_const.py b/pycomm/cip/cip_const.py index 4f24ad5..846e639 100644 --- a/pycomm/cip/cip_const.py +++ b/pycomm/cip/cip_const.py @@ -25,19 +25,19 @@ # ELEMENT_ID = { - "8-bit": '\x28', - "16-bit": '\x29', - "32-bit": '\x2a' + "8-bit": b'\x28', + "16-bit": b'\x29', + "32-bit": b'\x2a' } CLASS_ID = { - "8-bit": '\x20', - "16-bit": '\x21', + "8-bit": b'\x20', + "16-bit": b'\x21', } INSTANCE_ID = { - "8-bit": '\x24', - "16-bit": '\x25' + "8-bit": b'\x24', + "16-bit": b'\x25' } ATTRIBUTE_ID = { @@ -58,15 +58,15 @@ } 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 = { @@ -266,14 +266,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 +285,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 +300,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, @@ -480,4 +480,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 +} From 07522925cef4d9a999907b05d0f4880154544554 Mon Sep 17 00:00:00 2001 From: Salvo Musumeci Date: Mon, 21 Aug 2017 17:06:33 +0200 Subject: [PATCH 03/33] Update cip_const.py convert values to bytes --- pycomm/cip/cip_const.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pycomm/cip/cip_const.py b/pycomm/cip/cip_const.py index 846e639..91da862 100644 --- a/pycomm/cip/cip_const.py +++ b/pycomm/cip/cip_const.py @@ -49,12 +49,12 @@ # CLASS_ID + PATHS # For example PCCC path is CLASS_ID["8-bit"]+PATH["PCCC"] -> 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 @@ -420,17 +420,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 = { From 7b0a8473393c2f19f770f9570f6a24ea7809f913 Mon Sep 17 00:00:00 2001 From: Salvo Musumeci Date: Mon, 21 Aug 2017 17:11:58 +0200 Subject: [PATCH 04/33] Update slc.py updated for python3 --- pycomm/ab_comm/slc.py | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pycomm/ab_comm/slc.py b/pycomm/ab_comm/slc.py index 834cd7c..311429d 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,8 +80,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': 2} t = re.search(r"(?P[IO])(:)(?P\d{1,3})" @@ -97,8 +97,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 (0 <= int(t.group('file_number')) <= 255) \ @@ -107,8 +107,8 @@ def parse_tag(tag): return True, t.group(0), {'file_type': t.group('file_type').upper(), 'file_number': t.group('file_number'), '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"(?PS)" @@ -122,16 +122,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 +147,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 +253,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 +303,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])) @@ -372,17 +372,17 @@ def read_tag(self, tag, n=1): 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'])), @@ -395,10 +395,10 @@ def read_tag(self, tag, n=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,)): - sts = int(unpack_usint(self._reply[58])) + sts = int(self._reply[58]) try: if sts != 0: sts_txt = PCCC_ERROR_CODE[sts] @@ -552,7 +552,7 @@ def write_tag(self, 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])) From 6aa33e1e92c170a287893ad5f8b4697b46dab69f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 21 Aug 2017 21:34:04 +0200 Subject: [PATCH 05/33] Ignore Eclipse Project --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 900c00c..9a79bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,8 @@ target/ # pycharm editor .idea/ + +#Eclipse +.project +.settings/ +.pydevproject From d118e1328b6af86a04c762056e0840d53254b60d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 21 Aug 2017 21:36:45 +0200 Subject: [PATCH 06/33] Bugfix in Tag addressing --- pycomm/ab_comm/slc.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pycomm/ab_comm/slc.py b/pycomm/ab_comm/slc.py index 311429d..75116ee 100644 --- a/pycomm/ab_comm/slc.py +++ b/pycomm/ab_comm/slc.py @@ -84,8 +84,8 @@ def parse_tag(tag): '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,19 +94,20 @@ 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': 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'), + 'pos_number': t.group('position_number'), 'read_func': b'\xa2', 'write_func': b'\xab', 'address_field': 2} @@ -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,7 +369,7 @@ 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, b'\x4b', @@ -388,7 +388,7 @@ def read_tag(self, tag, n=1): 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)) @@ -529,23 +529,23 @@ 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)) From 84d4c117a7c7400eb846c479f27e8e06205edf97 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 21 Aug 2017 21:37:08 +0200 Subject: [PATCH 07/33] Updated README with Python3 tests --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 0c26a64..dd86031 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 From 197529362e749e05e1fdcc7ce7ba08665cbd389a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 21 Aug 2017 21:51:45 +0200 Subject: [PATCH 08/33] Updated travis --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 6d55f5522b9012b523db30d00f0771c129a7c9bc Mon Sep 17 00:00:00 2001 From: Brian Paterni Date: Sun, 11 Feb 2018 17:07:14 -0600 Subject: [PATCH 09/33] cip_base: more compact unpack_bool() * not sure if struct.unpack() is necessary, but direct comparison seems to work for now --- pycomm/cip/cip_base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 53c083b..baf9d51 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -86,10 +86,7 @@ def pack_lint(l): def unpack_bool(st): - if not (int(struct.unpack('B', st[0])[0]) == 0): - return 1 - return 0 - + return 1 if not st[0] == 0 else 0 def unpack_sint(st): return int(struct.unpack('b', st[0])[0]) From 5933756a93d1356b7dadc9e54dbc3c7646ae39f2 Mon Sep 17 00:00:00 2001 From: Tom Young Date: Mon, 9 Apr 2018 10:26:17 -0600 Subject: [PATCH 10/33] remove extra forward_close call --- pycomm/cip/cip_base.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index baf9d51..1e32b53 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -847,9 +847,6 @@ def open(self, ip_address, direct_connection=False): if self.register_session() is None: self._status = (13, "Session not registered") return False - - # not sure but maybe I can remove this because is used to clean up any previous unclosed connection - self.forward_close() return True except Exception as e: # self.clean_up() @@ -883,22 +880,6 @@ def close(self): if error_string: raise CommError(error_string) - - - - - - - - - - - - - - - - def clean_up(self): self.__sock = None self._target_is_connected = False From 3d1724f0e1a3b82e2d73c4a8effadba9002f9796 Mon Sep 17 00:00:00 2001 From: Tom Young Date: Mon, 9 Apr 2018 10:26:35 -0600 Subject: [PATCH 11/33] add status code 0x16 --- pycomm/cip/cip_const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pycomm/cip/cip_const.py b/pycomm/cip/cip_const.py index 91da862..9feb0b9 100644 --- a/pycomm/cip/cip_const.py +++ b/pycomm/cip/cip_const.py @@ -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", From 3e7b328b58ab816f848c140ae8f8a4c639716ec2 Mon Sep 17 00:00:00 2001 From: Tom Young Date: Fri, 15 Jun 2018 15:59:18 -0600 Subject: [PATCH 12/33] fix error in multirequest write --- pycomm/ab_comm/clx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 4a33b9c..86cc517 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -527,7 +527,7 @@ def write_tag(self, tag, value=None, typ=None): 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']) + bytes([TAG_SERVICES_REQUEST['Write Tag']]) + rp + pack_uint(S_DATA_TYPE[typ]) + pack_uint(1) From e786476d73489acc0de91588688702a3f727a3f1 Mon Sep 17 00:00:00 2001 From: bshep00 <40313276+bshep00@users.noreply.github.com> Date: Sat, 16 Jun 2018 05:07:47 -0400 Subject: [PATCH 13/33] Fix read_array functionality in Python 3.5 --- pycomm/ab_comm/clx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 86cc517..d702f41 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -457,8 +457,8 @@ 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 + byte([TAG_SERVICES_REQUEST["Read Tag Fragmented"]]), # the Request Service + byte([len(rp) // 2)], # the Request Path Size length in word rp, # the request path pack_uint(counts), pack_dint(self._byte_offset) @@ -467,7 +467,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: @@ -918,4 +918,4 @@ def read_string(self, 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 + return ''.join(char_array) From dc1aa87f5ac677e1a35bf38fb458925d30bc6b64 Mon Sep 17 00:00:00 2001 From: Tom Young Date: Tue, 19 Jun 2018 11:33:14 -0600 Subject: [PATCH 14/33] fix syntax error --- pycomm/ab_comm/clx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index d702f41..2c47fa8 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -458,7 +458,7 @@ def read_array(self, tag, counts, raw=False): message_request = [ pack_uint(Base._get_sequence()), byte([TAG_SERVICES_REQUEST["Read Tag Fragmented"]]), # the Request Service - byte([len(rp) // 2)], # the Request Path Size length in word + byte([len(rp) // 2]), # the Request Path Size length in word rp, # the request path pack_uint(counts), pack_dint(self._byte_offset) From 00e640696dfd710d52e0a5bd6629c4927a3caf18 Mon Sep 17 00:00:00 2001 From: Lil-Jening Date: Tue, 31 Jul 2018 13:05:46 -0400 Subject: [PATCH 15/33] Pip for Pycomm3 Fork Changed readme to add command for Pip for the Pycomm3 Fork. --- README.rst | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index dd86031..586a6a3 100644 --- a/README.rst +++ b/README.rst @@ -20,18 +20,10 @@ Setup ~~~~~ The package can be installed from -GitHub: +PIP: :: - git clone https://github.com/ruscito/pycomm.git - cd pycomm - sudo python setup.py install - - -PyPi: -:: - - pip install pycomm + pip install git+https://github.com/bpaterni/pycomm.git@pycomm3 ab_comm ~~~~~~~ @@ -180,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 From 46e7eb8d9fc6a944291f1e869216b0f4a35a4622 Mon Sep 17 00:00:00 2001 From: Lil-Jening Date: Fri, 3 Aug 2018 12:56:27 -0400 Subject: [PATCH 16/33] Bandaid fix for exception error handling in python 3 Exception handling has changed a bit in python 3. when using exception as E, it no longer has the attribute e.message. now the message string is fully in e. This is a bandaid fix that I did when editing the file during error checking. It may not be the correct way to do it. I saw this when I was forcibly disconnecting the script after a c.open was called. Here is the log bit that called out this section and caused my script to not auto reconnect. File "C:\Python36\lib\site-packages\pycomm\cip\cip_base.py", line 867, in close error_string += "Error on close() -> session Err: %s" % e.message AttributeError: 'CommError' object has no attribute 'message' --- pycomm/cip/cip_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 1e32b53..80aacb4 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -864,7 +864,7 @@ def close(self): if self._session != 0: self.un_register_session() except Exception as e: - error_string += "Error on close() -> session Err: %s" % e.message + error_string += "Error on close() -> session Err: %s" % e logger.warning(error_string) # %GLA must do a cleanup __sock.close() @@ -872,7 +872,7 @@ def close(self): if self.__sock: self.__sock.close() except Exception as e: - error_string += "; close() -> __sock.close Err: %s" % e.message + error_string += "; close() -> __sock.close Err: %s" % e logger.warning(error_string) self.clean_up() From 027ec25aa5cb8bb8d765390274ff5b9edf2e2408 Mon Sep 17 00:00:00 2001 From: ottowayi Date: Mon, 6 Aug 2018 09:12:29 -0500 Subject: [PATCH 17/33] fix read and write array methods --- pycomm/ab_comm/clx.py | 19 ++++++++----------- pycomm/cip/cip_base.py | 3 ++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 2c47fa8..3925e4c 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -457,8 +457,8 @@ def read_array(self, tag, counts, raw=False): # Creating the Message Request Packet message_request = [ pack_uint(Base._get_sequence()), - byte([TAG_SERVICES_REQUEST["Read Tag Fragmented"]]), # the Request Service - byte([len(rp) // 2]), # the Request Path Size length in word + 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) @@ -606,15 +606,12 @@ 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: @@ -628,8 +625,8 @@ 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 + 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 @@ -641,12 +638,12 @@ def write_array(self, tag, values, data_type, 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: 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): diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 1e32b53..4422c48 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -88,8 +88,9 @@ def pack_lint(l): def unpack_bool(st): return 1 if not st[0] == 0 else 0 + def unpack_sint(st): - return int(struct.unpack('b', st[0])[0]) + return int(struct.unpack('b', bytes([st[0]]))[0]) def unpack_usint(st): From e3ec733c1b94eb27007f8608293a9329f756b085 Mon Sep 17 00:00:00 2001 From: ottowayi Date: Mon, 6 Aug 2018 09:13:09 -0500 Subject: [PATCH 18/33] fixed read_string --- pycomm/ab_comm/clx.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 3925e4c..3d25227 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -911,8 +911,11 @@ def write_string(self, tag, value, size=82): 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) + length, _ = self.read_tag(len_tag) + if length: + values = self.read_array(data_tag, length) + _, values = zip(*values) + char_array = [chr(ch) for ch in values] + return ''.join(char_array) + else: + return '' From b4991f61cd18fefd06fbdd2f93b304d67cbda18a Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 26 Mar 2019 18:27:21 -0500 Subject: [PATCH 19/33] fixed issue with udt name field not being parsed correctly --- pycomm/ab_comm/clx.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 3d25227..c7a5db3 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -826,17 +826,14 @@ 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(b'\00') + names = buff.split(b'\x00') lst = [] tag['udt']['name'] = 'Not an user defined structure' for name in names: if len(name) > 1: - - #if name.find(';') != -1: - if b':' in name: + if b';' in name: tag['udt']['name'] = name[:name.find(b';')] - #elif name.find('ZZZZZZZZZZ') != -1: elif b'ZZZZZZZZZZ' in name: continue elif name.isalpha(): From 2244ec2a35a2cbf3e85e2beed87eb8099ff62689 Mon Sep 17 00:00:00 2001 From: ottowayi Date: Thu, 2 May 2019 21:05:58 -0500 Subject: [PATCH 20/33] fixed issue with get_tag_list chopping off last 2 chars of the last tag in a udt added caching to the struct/template/udt values instead of reading them every time --- pycomm/ab_comm/clx.py | 270 ++++++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 129 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index c7a5db3..4bed9a5 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -25,6 +25,7 @@ # from pycomm.cip.cip_base import * import logging + try: # Python 2.7+ from logging import NullHandler except ImportError: @@ -65,6 +66,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 @@ -701,35 +706,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()), - 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 - ] + 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'], - 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 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,127 +748,138 @@ 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 = b'' - 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()), - 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)-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'], - b''.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: - if b':' in tag['tag_name'] or b'__' 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' + 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(b'\x00') - lst = [] - - tag['udt']['name'] = 'Not an user defined structure' - for name in names: - if len(name) > 1: - if b';' in name: - tag['udt']['name'] = name[:name.find(b';')] - elif b'ZZZZZZZZZZ' in name: - continue - elif name.isalpha(): - lst.append(name) - else: - continue - tag['udt']['internal_tags'] = lst - - type_list = [] - - for i in range(member_count): - # skip member 1 + def _build_udt(self, data, member_count): + udt = {'name': 'Not a user defined structure', + '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: + udt['name'] = name[:name.find(';')] + elif 'ZZZZZZZZZZ' in name: + continue + elif name.isalnum(): + udt['internal_tags'].append(name) + else: + continue - 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 = [] @@ -875,13 +893,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 From 36a94fc66d72fdb723080627541c5b3b23575cad Mon Sep 17 00:00:00 2001 From: ottowayi Date: Thu, 2 May 2019 21:29:35 -0500 Subject: [PATCH 21/33] allow writing of any length string truncate strings on write to string length fixed issue when reading/writing some characters in strings added option to read a string with a known length. this can be used to read the full length of the string DATA array, without reading the .LEN. It removes any null chars from the result. --- pycomm/ab_comm/clx.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 4bed9a5..bf1333a 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -903,28 +903,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 - self.write_tag(len_tag, len(value), 'DINT') + str_len = len(value) + if str_len > size: + str_len = size + + 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) + 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) - _, values = zip(*values) - char_array = [chr(ch) for ch in values] - return ''.join(char_array) - else: - return '' + 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 From 5f03f16448e7c19ccb7a7c1d460adca926510cad Mon Sep 17 00:00:00 2001 From: ottowayi Date: Thu, 2 May 2019 21:41:18 -0500 Subject: [PATCH 22/33] added initial support for Extended Forward Open. Changes based on Pylogix's implementation. Enabled by setting `client.attribs['large forward open'] = True` This may break the direction connection stuff. I have not tested it beyond my use case. Confirmed working on L62 v20 and L74 v31 using Python 3.7 --- pycomm/cip/cip_base.py | 67 +++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/pycomm/cip/cip_base.py b/pycomm/cip/cip_base.py index 6b0947f..e33ed27 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -26,7 +26,6 @@ import struct import socket -import random from os import getpid, urandom from pycomm.cip.cip_const import * @@ -75,6 +74,11 @@ def pack_dint(n): return struct.pack(' Date: Mon, 6 May 2019 14:41:25 -0400 Subject: [PATCH 23/33] autoformatted code --- pycomm/ab_comm/clx.py | 74 +++++++++++++++++++++--------------------- pycomm/cip/cip_base.py | 3 +- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index bf1333a..2978364 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -97,13 +97,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, @@ -200,8 +200,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) @@ -212,9 +212,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) @@ -277,13 +277,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',)) @@ -463,8 +464,8 @@ def read_array(self, tag, counts, raw=False): message_request = [ pack_uint(Base._get_sequence()), 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 + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path pack_uint(counts), pack_dint(self._byte_offset) ] @@ -582,7 +583,6 @@ def write_tag(self, tag, value=None, typ=None): addr_data=self._target_cid, ) ) - if multi_requests: return self._parse_multiple_request_write(tag) else: @@ -619,7 +619,7 @@ def write_array(self, tag, values, data_type, raw=False): 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: @@ -631,12 +631,12 @@ def write_array(self, tag, values, data_type, raw=False): message_request = [ pack_uint(Base._get_sequence()), 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 + 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 @@ -677,15 +677,15 @@ def _get_instance_attribute_list_service(self): # the Request Path Size length in word 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 + INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 b'\x00', - pack_uint(self._last_instance), # The instance + 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( @@ -716,23 +716,23 @@ def _get_structure_makeup(self, instance_id): 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 + 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 + 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 + 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'], b''.join(message_request), ADDRESS_ITEM['Connection Based'], - addr_data=self._target_cid,)) is None: + addr_data=self._target_cid, )) is None: raise DataError("send_unit_data returned not valid data") self._struct_cache[instance_id] = self._buffer @@ -761,14 +761,14 @@ def _read_template(self, instance_id, object_definition_size): 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 + 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 + 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) + pack_uint(((object_definition_size * 4) - 21) - self._byte_offset) ] if not self.send_unit_data( @@ -776,7 +776,7 @@ def _read_template(self, instance_id, object_definition_size): DATA_ITEM['Connected'], b''.join(message_request), ADDRESS_ITEM['Connection Based'], - addr_data=self._target_cid,)): + addr_data=self._target_cid, )): raise DataError("send_unit_data returned not valid data") self._get_template_in_progress = False @@ -935,6 +935,6 @@ def read_string(self, tag, str_len=None): 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] + 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/cip/cip_base.py b/pycomm/cip/cip_base.py index e33ed27..572bf15 100644 --- a/pycomm/cip/cip_base.py +++ b/pycomm/cip/cip_base.py @@ -290,8 +290,7 @@ def create_tag_rp(tag, multi_requests=False): rp.append(bytes([tag_length])) # Length of the tag # Add the tag to the Request path - for char in tag: - rp.append(bytes([char])) + rp += [bytes([char]) for char in tag] # Add pad byte because total length of Request path must be word-aligned if tag_length % 2: rp.append(PADDING_BYTE) From e2e49404026de990f7b0b4a4f4a3e5888fe2a9ee Mon Sep 17 00:00:00 2001 From: ottowayi Date: Mon, 6 May 2019 15:03:34 -0400 Subject: [PATCH 24/33] added support to `read_tag` to allow reading bits of integers automatically, this will do a single read of the base tag but will show individual results for the requested bits added support to `write_tag` for the _Read Modify Write Tag Service_ to allow writing bits of integers w/o needing to read the current value first and calculating the new value. The multi-tag write function was broken as well, I believe this fixed it. --- pycomm/ab_comm/clx.py | 218 +++++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 75 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 2978364..2d594a7 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -233,7 +233,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. @@ -243,25 +243,30 @@ 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]: + value = bool(value & 1 << bit) if bit < BITS_PER_INT_TYPE[typ] else None + tag_list.append((f'{tag}.{bit}', 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: @@ -367,9 +372,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(): @@ -380,18 +385,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( + t, bit = self._bool_is_bit(t, 'BOOL') + 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._bool_is_bit(tag, 'BOOL') 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)) @@ -401,8 +414,8 @@ def read_tag(self, tag): message_request = [ pack_uint(Base._get_sequence()), bytes([TAG_SERVICES_REQUEST['Read Tag']]), # the Request Service - bytes([len(rp) // 2]), # the Request Path Size length in word - rp, # the request path + bytes([len(rp) // 2]), # the Request Path Size length in word + rp, # the request path pack_uint(1) ] @@ -416,13 +429,20 @@ def read_tag(self, tag): 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]) try: - return UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](self._reply[52:]), I_DATA_TYPE[data_type] + typ = I_DATA_TYPE[data_type] + value = UNPACK_DATA_FUNCTION[typ](self._reply[52:]) + if bit is not None: + if bit < BITS_PER_INT_TYPE[typ]: + value = bool(value & (1 << bit)) + else: + raise DataError(f'Invalid bit value {bit} for data type {typ}') + return value except Exception as e: raise DataError(e) else: @@ -481,6 +501,101 @@ def read_array(self, tag, counts, raw=False): return self._tag_list + @staticmethod + def _bool_is_bit(tag, typ): + """ + 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 + try: + base, bit = tag.rsplit('.') + 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._bool_is_bit(name, typ) # 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)) + 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._bool_is_bit(tag, typ) # 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) + 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): + mask_size = 1 if bit < 8 else 2 if bit < 16 else 4 + or_mask, and_mask = 0b0000_0000_0000_0000, 0b1111_1111_1111_1111 + + 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 @@ -520,60 +635,13 @@ 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( - bytes([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()), - bytes([TAG_SERVICES_REQUEST["Write Tag"]]), # the Request Service - bytes([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( From bc9513eacee5658add5af37ab2d57413b052315b Mon Sep 17 00:00:00 2001 From: ottowayi Date: Mon, 6 May 2019 15:04:34 -0400 Subject: [PATCH 25/33] added some constants for bit length of different data types --- pycomm/cip/cip_const.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pycomm/cip/cip_const.py b/pycomm/cip/cip_const.py index 9feb0b9..c26e424 100644 --- a/pycomm/cip/cip_const.py +++ b/pycomm/cip/cip_const.py @@ -339,6 +339,16 @@ 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 +} S_DATA_TYPE = { 'BOOL': 0xc1, 'SINT': 0xc2, # Signed 8-bit integer From 5a9360bac994b7d6749744cb970546891004af9d Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 7 May 2019 08:11:04 -0400 Subject: [PATCH 26/33] minor changes to how bits are handled fixed issue when parsing bits of ints in UDTs (udt.int.0) --- pycomm/ab_comm/clx.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 2d594a7..dc36bd6 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -259,10 +259,11 @@ def _parse_multiple_request_read(self, tags, tag_bits=None): value = UNPACK_DATA_FUNCTION[typ](self._reply[value_begin:value_end]) if tag in tag_bits: for bit in tag_bits[tag]: - value = bool(value & 1 << bit) if bit < BITS_PER_INT_TYPE[typ] else None - tag_list.append((f'{tag}.{bit}', value, 'BOOL')) + name = f'{tag}.{bit}' + value = bool(val & (1 << bit)) if bit < BITS_PER_TYPE[typ] else None + tag_list.append((name, value, 'BOOL')) else: - self._last_tag_read = (tag, value, typ) + self._last_tag_read = (tag, val, typ) tag_list.append(self._last_tag_read) else: self._last_tag_read = (tag, None, None) @@ -385,7 +386,7 @@ def read_tag(self, tag): if multi_requests: rp_list = [] for t in tag: - t, bit = self._bool_is_bit(t, 'BOOL') + t, bit = self._parse_bits(t, 'BOOL') read = bit is None or t not in tag_bits if bit is not None: tag_bits[t].append(bit) @@ -404,7 +405,7 @@ def read_tag(self, tag): message_request = build_multiple_service(rp_list, Base._get_sequence()) else: - tag, bit = self._bool_is_bit(tag, 'BOOL') + tag, bit = self._parse_bits(tag, 'BOOL') 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)) @@ -438,10 +439,7 @@ def read_tag(self, tag): typ = I_DATA_TYPE[data_type] value = UNPACK_DATA_FUNCTION[typ](self._reply[52:]) if bit is not None: - if bit < BITS_PER_INT_TYPE[typ]: - value = bool(value & (1 << bit)) - else: - raise DataError(f'Invalid bit value {bit} for data type {typ}') + value = bool(value & (1 << bit)) if bit < BITS_PER_TYPE[typ] else None return value except Exception as e: raise DataError(e) @@ -502,7 +500,7 @@ def read_array(self, tag, counts, raw=False): return self._tag_list @staticmethod - def _bool_is_bit(tag, typ): + def _parse_bits(tag, typ): """ 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 @@ -511,7 +509,7 @@ def _bool_is_bit(tag, typ): if typ != 'BOOL': return tag, None try: - base, bit = tag.rsplit('.') + base, bit = tag.rsplit('.', maxsplit=1) bit = int(bit) return base, bit except Exception: @@ -521,7 +519,7 @@ def _write_tag_multi_write(self, tags): rp_list = [] tags_added = [] for name, value, typ in tags: - name, bit = self._bool_is_bit(name, typ) # check if we're writing a bit of a integer rather than a BOOL + name, bit = self._parse_bits(name, typ) # 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: @@ -554,7 +552,7 @@ def _write_tag_multi_write(self, tags): return message_request, tags_added def _write_tag_single_write(self, tag, value, typ): - name, bit = self._bool_is_bit(tag, typ) # check if we're writing a bit of a integer rather than a BOOL + name, bit = self._parse_bits(tag, typ) # check if we're writing a bit of a integer rather than a BOOL rp = create_tag_rp(name) if rp is None: @@ -586,8 +584,8 @@ def _write_tag_single_write(self, tag, value, typ): @staticmethod def _make_write_bit_data(bit, value): - mask_size = 1 if bit < 8 else 2 if bit < 16 else 4 or_mask, and_mask = 0b0000_0000_0000_0000, 0b1111_1111_1111_1111 + mask_size = 1 if bit < 8 else 2 if bit < 16 else 4 if value: or_mask |= (1 << bit) From 7e19684964032c9df391feae2cd96b3a1ed6bb3c Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 7 May 2019 08:54:53 -0400 Subject: [PATCH 27/33] added fix so to allow writing bool array elements --- pycomm/ab_comm/clx.py | 50 +++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index dc36bd6..ff22839 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -386,7 +386,7 @@ def read_tag(self, tag): if multi_requests: rp_list = [] for t in tag: - t, bit = self._parse_bits(t, 'BOOL') + 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) @@ -405,7 +405,7 @@ def read_tag(self, tag): message_request = build_multiple_service(rp_list, Base._get_sequence()) else: - tag, bit = self._parse_bits(tag, 'BOOL') + 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)) @@ -500,7 +500,7 @@ def read_array(self, tag, counts, raw=False): return self._tag_list @staticmethod - def _parse_bits(tag, typ): + 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 @@ -508,18 +508,27 @@ def _parse_bits(tag, typ): """ if typ != 'BOOL': return tag, None - try: - base, bit = tag.rsplit('.', maxsplit=1) - bit = int(bit) - return base, bit - except Exception: - 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._parse_bits(name, typ) # check if we're writing a bit of a integer rather than a BOOL + 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: @@ -530,7 +539,7 @@ def _write_tag_multi_write(self, tags): 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)) + 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"]]) + @@ -552,7 +561,7 @@ def _write_tag_multi_write(self, tags): return message_request, tags_added def _write_tag_single_write(self, tag, value, typ): - name, bit = self._parse_bits(tag, typ) # check if we're writing a bit of a integer rather than a BOOL + 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: @@ -570,7 +579,7 @@ def _write_tag_single_write(self, tag, value, typ): ] if bit is not None: try: - message_request += self._make_write_bit_data(bit, value) + 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: @@ -583,9 +592,14 @@ def _write_tag_single_write(self, tag, value, typ): return message_request @staticmethod - def _make_write_bit_data(bit, value): + def _make_write_bit_data(bit, value, bool_ary=False): or_mask, and_mask = 0b0000_0000_0000_0000, 0b1111_1111_1111_1111 - mask_size = 1 if bit < 8 else 2 if bit < 16 else 4 + + 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) @@ -607,7 +621,7 @@ def write_tag(self, tag, value=None, typ=None): The type accepted are: - BOOL - SINT - - INT' + - INT - DINT - REAL - LINT @@ -622,9 +636,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(): From 97d10f30ea2d7aab3973e241a7f07de74f2f86f3 Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 7 May 2019 10:22:07 -0400 Subject: [PATCH 28/33] forgot to have read_tag return the type, not just the value --- pycomm/ab_comm/clx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index ff22839..7575078 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -435,12 +435,12 @@ def read_tag(self, tag): # Get the data type if self._status[0] == SUCCESS: data_type = unpack_uint(self._reply[50:52]) + typ = DATA_TYPE[data_type] try: - typ = 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_TYPE[typ] else None - return value + return value, typ except Exception as e: raise DataError(e) else: From 61908bbf18721f6cb4039439ce8efb7409b72daf Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 28 May 2019 11:20:16 -0500 Subject: [PATCH 29/33] fixed a couple typos from last pull request. --- pycomm/ab_comm/clx.py | 9 +++++---- pycomm/cip/cip_const.py | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 7575078..a03f9f7 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -24,6 +24,7 @@ # SOFTWARE. # from pycomm.cip.cip_base import * +from collections import defaultdict import logging try: # Python 2.7+ @@ -260,10 +261,10 @@ def _parse_multiple_request_read(self, tags, tag_bits=None): if tag in tag_bits: for bit in tag_bits[tag]: name = f'{tag}.{bit}' - value = bool(val & (1 << bit)) if bit < BITS_PER_TYPE[typ] else None + 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, val, typ) + self._last_tag_read = (tag, value, typ) tag_list.append(self._last_tag_read) else: self._last_tag_read = (tag, None, None) @@ -435,11 +436,11 @@ def read_tag(self, tag): # Get the data type if self._status[0] == SUCCESS: data_type = unpack_uint(self._reply[50:52]) - typ = DATA_TYPE[data_type] + typ = I_DATA_TYPE[data_type] try: value = UNPACK_DATA_FUNCTION[typ](self._reply[52:]) if bit is not None: - value = bool(value & (1 << bit)) if bit < BITS_PER_TYPE[typ] else 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) diff --git a/pycomm/cip/cip_const.py b/pycomm/cip/cip_const.py index c26e424..4342c57 100644 --- a/pycomm/cip/cip_const.py +++ b/pycomm/cip/cip_const.py @@ -348,6 +348,9 @@ '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, From c4823d8215e0664f5a53ed277b6a34d65a1be7c5 Mon Sep 17 00:00:00 2001 From: ottowayi Date: Tue, 28 May 2019 11:22:40 -0500 Subject: [PATCH 30/33] fixed issue in v31 when getting data type names for UDTs --- pycomm/ab_comm/clx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index a03f9f7..a373ade 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -910,11 +910,11 @@ def _isolating_user_tag(self): raise DataError(e) def _build_udt(self, data, member_count): - udt = {'name': 'Not a user defined structure', + 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: + if ';' in name and udt['name'] is None: udt['name'] = name[:name.find(';')] elif 'ZZZZZZZZZZ' in name: continue @@ -922,6 +922,8 @@ def _build_udt(self, data, member_count): udt['internal_tags'].append(name) else: continue + if udt['name'] is None: + udt['name'] = 'Not a user define structure' for _ in range(member_count): array_size = unpack_uint(data[:2]) From b7aa3db11a1853e5b20a2f23b1b5605ca4a79a1f Mon Sep 17 00:00:00 2001 From: Nathan Baker Date: Mon, 8 Jul 2019 17:20:40 -0500 Subject: [PATCH 31/33] ignore vscode settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9a79bc1..cb936d3 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ target/ .project .settings/ .pydevproject +.vscode/settings.json From 440b62086f4fd650438edcbee4901d990729f7a3 Mon Sep 17 00:00:00 2001 From: Nathan Baker Date: Mon, 8 Jul 2019 17:20:50 -0500 Subject: [PATCH 32/33] import defaultdict --- pycomm/ab_comm/clx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 7575078..0c3a7d4 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -24,6 +24,7 @@ # SOFTWARE. # from pycomm.cip.cip_base import * +from collections import defaultdict import logging try: # Python 2.7+ From 4f9480dd973aa6f63355e377f45a06d3ec5eb690 Mon Sep 17 00:00:00 2001 From: Nathan Baker Date: Mon, 8 Jul 2019 17:48:52 -0500 Subject: [PATCH 33/33] fix typo? --- pycomm/ab_comm/clx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycomm/ab_comm/clx.py b/pycomm/ab_comm/clx.py index 0c3a7d4..c4e3139 100644 --- a/pycomm/ab_comm/clx.py +++ b/pycomm/ab_comm/clx.py @@ -436,7 +436,7 @@ def read_tag(self, tag): # Get the data type if self._status[0] == SUCCESS: data_type = unpack_uint(self._reply[50:52]) - typ = DATA_TYPE[data_type] + typ = I_DATA_TYPE[data_type] try: value = UNPACK_DATA_FUNCTION[typ](self._reply[52:]) if bit is not None: