From 4b9dd1bc71dca966ff9e7e6ff6ed6b3e9f5dd7fe Mon Sep 17 00:00:00 2001 From: Patrick McDonagh Date: Fri, 14 Oct 2016 10:06:52 -0500 Subject: [PATCH] Instead of cloning pycomm (which may require installing git), just copy it and install it locally --- daq/Dockerfile | 3 +- daq/pycomm-master/.travis.yml | 12 + daq/pycomm-master/CHANGES | 39 + daq/pycomm-master/LICENSE | 22 + daq/pycomm-master/MANIFEST.in | 1 + daq/pycomm-master/README.rst | 171 ++++ daq/pycomm-master/examples/test_clx_comm.py | 42 + daq/pycomm-master/examples/test_slc_only.py | 72 ++ daq/pycomm-master/pycomm/__init__.py | 1 + daq/pycomm-master/pycomm/ab_comm/__init__.py | 2 + daq/pycomm-master/pycomm/ab_comm/clx.py | 873 +++++++++++++++++++ daq/pycomm-master/pycomm/ab_comm/slc.py | 574 ++++++++++++ daq/pycomm-master/pycomm/cip/__init__.py | 1 + daq/pycomm-master/pycomm/cip/cip_base.py | 864 ++++++++++++++++++ daq/pycomm-master/pycomm/cip/cip_const.py | 483 ++++++++++ daq/pycomm-master/pycomm/common.py | 8 + daq/pycomm-master/setup.py | 37 + 17 files changed, 3204 insertions(+), 1 deletion(-) create mode 100755 daq/pycomm-master/.travis.yml create mode 100755 daq/pycomm-master/CHANGES create mode 100755 daq/pycomm-master/LICENSE create mode 100755 daq/pycomm-master/MANIFEST.in create mode 100755 daq/pycomm-master/README.rst create mode 100755 daq/pycomm-master/examples/test_clx_comm.py create mode 100755 daq/pycomm-master/examples/test_slc_only.py create mode 100755 daq/pycomm-master/pycomm/__init__.py create mode 100755 daq/pycomm-master/pycomm/ab_comm/__init__.py create mode 100755 daq/pycomm-master/pycomm/ab_comm/clx.py create mode 100755 daq/pycomm-master/pycomm/ab_comm/slc.py create mode 100755 daq/pycomm-master/pycomm/cip/__init__.py create mode 100755 daq/pycomm-master/pycomm/cip/cip_base.py create mode 100755 daq/pycomm-master/pycomm/cip/cip_const.py create mode 100755 daq/pycomm-master/pycomm/common.py create mode 100755 daq/pycomm-master/setup.py diff --git a/daq/Dockerfile b/daq/Dockerfile index 7b4d22d..a44da60 100644 --- a/daq/Dockerfile +++ b/daq/Dockerfile @@ -4,11 +4,12 @@ RUN mkdir /root/tag-logger COPY taglogger.py /root/tag-logger/taglogger.py COPY sampleData.py /root/tag-logger/sampleData.py COPY tag /root/tag-logger/tag +COPY pycomm-master /tmp/pycomm # RUN wget https://bootstrap.pypa.io/get-pip.py # RUN python get-pip.py RUN pip install requests -RUN git clone https://github.com/ruscito/pycomm.git && cd pycomm && python setup.py install && cd .. +RUN cd /tmp/pycomm && python setup.py install && cd / CMD ["python", "/root/tag-logger/sampleData.py"] diff --git a/daq/pycomm-master/.travis.yml b/daq/pycomm-master/.travis.yml new file mode 100755 index 0000000..ba8ac2d --- /dev/null +++ b/daq/pycomm-master/.travis.yml @@ -0,0 +1,12 @@ +language: python + +python: +- "2.6" +- "2.7" +- "3.2" +- "3.3" +- "3.4" + +install: python setup.py install + +script: nosetests \ No newline at end of file diff --git a/daq/pycomm-master/CHANGES b/daq/pycomm-master/CHANGES new file mode 100755 index 0000000..5754287 --- /dev/null +++ b/daq/pycomm-master/CHANGES @@ -0,0 +1,39 @@ +CHANGES +======= + +1.0.8 +----- +Number 0001: +handling of raw values (hex) added to functions read_array and write_array: handling of raw values can be switched +on/off with additional parameter + +Number 0002: +is a bugfix when reading the tag_list from a PLC. If one tag is of datatype bool and it is part of a bool +array within an SINT, the tag type value contains also the bit position. + +Number 0003: +code is always logging into a file (pycomm.log) into working path. Code changed, so that it is possible to configure +the logging from the main application. + + + +1.0.6 +----- + +- Pypi posting + +1.0.0 +----- + +- Add support for SLC and PLC/05 plc + +0.2.0 +--- + +- Add CIP support class +- Add support for ControlLogix PLC + +0.1 +--- + +- Initial release. \ No newline at end of file diff --git a/daq/pycomm-master/LICENSE b/daq/pycomm-master/LICENSE new file mode 100755 index 0000000..62fcb0b --- /dev/null +++ b/daq/pycomm-master/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Agostino Ruscito + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/daq/pycomm-master/MANIFEST.in b/daq/pycomm-master/MANIFEST.in new file mode 100755 index 0000000..9561fb1 --- /dev/null +++ b/daq/pycomm-master/MANIFEST.in @@ -0,0 +1 @@ +include README.rst diff --git a/daq/pycomm-master/README.rst b/daq/pycomm-master/README.rst new file mode 100755 index 0000000..0eed45b --- /dev/null +++ b/daq/pycomm-master/README.rst @@ -0,0 +1,171 @@ +pycomm +====== +pycomm is a package that includes a collection of modules used to communicate with PLCs. +At the moment the first module in the package is ab_comm. + +Test +~~~~ +The library is currently test on Python 2.6, 2.7. + +.. image:: https://travis-ci.org/ruscito/pycomm.svg?branch=master + :target: https://travis-ci.org/ruscito/pycomm + +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 install pycomm + +ab_comm +~~~~~~~ +ab_comm is a module that contains a set of classes used to interface Rockwell PLCs using Ethernet/IP protocol. +The "clx" class can be used to communicate with Compactlogix, Controllogix PLCs +The "slc" can be used to communicate with Micrologix or SLC PLCs + +I tried to followCIP specifications volume 1 and 2 as well as `Rockwell Automation Publication 1756-PM020-EN-P - November 2012`_ . + +.. _Rockwell Automation Publication 1756-PM020-EN-P - November 2012: http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf + +See the following snippet for communication with a Controllogix PLC: + +:: + + from pycomm.ab_comm.clx import Driver as ClxDriver + import logging + + + if __name__ == '__main__': + logging.basicConfig( + filename="ClxDriver.log", + format="%(levelname)-10s %(asctime)s %(message)s", + level=logging.DEBUG + ) + c = ClxDriver() + + if c.open('172.16.2.161'): + + print(c.read_tag(['ControlWord'])) + print(c.read_tag(['parts', 'ControlWord', 'Counts'])) + + print(c.write_tag('Counts', -26, 'INT')) + print(c.write_tag(('Counts', 26, 'INT'))) + print(c.write_tag([('Counts', 26, 'INT')])) + print(c.write_tag([('Counts', -26, 'INT'), ('ControlWord', -30, 'DINT'), ('parts', 31, 'DINT')])) + + # To read an array + r_array = c.read_array("TotalCount", 1750) + for tag in r_array: + print (tag) + + # reset tha array to all 0 + w_array = [] + for i in xrange(1750): + w_array.append(0) + c.write_array("TotalCount", "SINT", w_array) + + c.close() + + + + +See the following snippet for communication with a Micrologix PLC: + + +:: + + from pycomm.ab_comm.slc import Driver as SlcDriver + import logging + + + if __name__ == '__main__': + logging.basicConfig( + filename="SlcDriver.log", + format="%(levelname)-10s %(asctime)s %(message)s", + level=logging.DEBUG + ) + c = SlcDriver() + if c.open('172.16.2.160'): + + print c.read_tag('S:1/5') + print c.read_tag('S:60', 2) + + print c.write_tag('N7:0', [-30, 32767, -32767]) + print c.write_tag('N7:0', 21) + print c.read_tag('N7:0', 10) + + print c.write_tag('F8:0', [3.1, 4.95, -32.89]) + print c.write_tag('F8:0', 21) + print c.read_tag('F8:0', 3) + + print c.write_tag('B3:100', [23, -1, 4, 9]) + print c.write_tag('B3:100', 21) + print c.read_tag('B3:100', 4) + + print c.write_tag('T4:3.PRE', 431) + print c.read_tag('T4:3.PRE') + print c.write_tag('C5:0.PRE', 501) + print c.read_tag('C5:0.PRE') + print c.write_tag('T4:3.ACC', 432) + print c.read_tag('T4:3.ACC') + print c.write_tag('C5:0.ACC', 502) + print c.read_tag('C5:0.ACC') + + c.write_tag('T4:2.EN', 0) + c.write_tag('T4:2.TT', 0) + c.write_tag('T4:2.DN', 0) + print c.read_tag('T4:2.EN', 1) + print c.read_tag('T4:2.TT', 1) + print c.read_tag('T4:2.DN',) + + c.write_tag('C5:0.CU', 1) + c.write_tag('C5:0.CD', 0) + c.write_tag('C5:0.DN', 1) + c.write_tag('C5:0.OV', 0) + c.write_tag('C5:0.UN', 1) + c.write_tag('C5:0.UA', 0) + print c.read_tag('C5:0.CU') + print c.read_tag('C5:0.CD') + print c.read_tag('C5:0.DN') + print c.read_tag('C5:0.OV') + print c.read_tag('C5:0.UN') + print c.read_tag('C5:0.UA') + + c.write_tag('B3:100', 1) + print c.read_tag('B3:100') + + c.write_tag('B3/3955', 1) + print c.read_tag('B3/3955') + + c.write_tag('N7:0/2', 1) + print c.read_tag('N7:0/2') + + print c.write_tag('O:0.0/4', 1) + print c.read_tag('O:0.0/4') + + c.close() + + +The Future +~~~~~~~~~~ +This package is under development. +The modules _ab_comm.clx_ and _ab_comm.slc_ are completed at moment but other drivers will be added in the future. + +Thanks +~~~~~~ +Thanks to patrickjmcd_ for the help with the Direct Connections and thanks in advance to anyone for feedback and suggestions. + +.. _patrickjmcd: https://github.com/patrickjmcd + +License +~~~~~~~ +pycomm is distributed under the MIT License \ No newline at end of file diff --git a/daq/pycomm-master/examples/test_clx_comm.py b/daq/pycomm-master/examples/test_clx_comm.py new file mode 100755 index 0000000..cd8c37d --- /dev/null +++ b/daq/pycomm-master/examples/test_clx_comm.py @@ -0,0 +1,42 @@ +from pycomm.ab_comm.clx import Driver as ClxDriver +import logging + +from time import sleep + + +if __name__ == '__main__': + + logging.basicConfig( + filename="ClxDriver.log", + format="%(levelname)-10s %(asctime)s %(message)s", + level=logging.DEBUG + ) + c = ClxDriver() + + print c['port'] + print c.__version__ + + + if c.open('172.16.2.161'): + while 1: + try: + print(c.read_tag(['ControlWord'])) + print(c.read_tag(['parts', 'ControlWord', 'Counts'])) + + print(c.write_tag('Counts', -26, 'INT')) + print(c.write_tag(('Counts', 26, 'INT'))) + print(c.write_tag([('Counts', 26, 'INT')])) + print(c.write_tag([('Counts', -26, 'INT'), ('ControlWord', -30, 'DINT'), ('parts', 31, 'DINT')])) + sleep(1) + except Exception as e: + err = c.get_status() + c.close() + print err + pass + + # To read an array + r_array = c.read_array("TotalCount", 1750) + for tag in r_array: + print (tag) + + c.close() diff --git a/daq/pycomm-master/examples/test_slc_only.py b/daq/pycomm-master/examples/test_slc_only.py new file mode 100755 index 0000000..f086efa --- /dev/null +++ b/daq/pycomm-master/examples/test_slc_only.py @@ -0,0 +1,72 @@ +__author__ = 'agostino' + +from pycomm.ab_comm.slc import Driver as SlcDriver + + +if __name__ == '__main__': + c = SlcDriver(True, 'delete_slc.log') + if c.open('172.16.2.160'): + + while 1: + try: + print c.read_tag('S:1/5') + print c.read_tag('S:60', 2) + + print c.write_tag('N7:0', [-30, 32767, -32767]) + print c.write_tag('N7:0', 21) + print c.read_tag('N7:0', 10) + + print c.write_tag('F8:0', [3.1, 4.95, -32.89]) + print c.write_tag('F8:0', 21) + print c.read_tag('F8:0', 3) + + print c.write_tag('B3:100', [23, -1, 4, 9]) + print c.write_tag('B3:100', 21) + print c.read_tag('B3:100', 4) + + print c.write_tag('T4:3.PRE', 431) + print c.read_tag('T4:3.PRE') + print c.write_tag('C5:0.PRE', 501) + print c.read_tag('C5:0.PRE') + print c.write_tag('T4:3.ACC', 432) + print c.read_tag('T4:3.ACC') + print c.write_tag('C5:0.ACC', 502) + print c.read_tag('C5:0.ACC') + + c.write_tag('T4:2.EN', 0) + c.write_tag('T4:2.TT', 0) + c.write_tag('T4:2.DN', 0) + print c.read_tag('T4:2.EN', 1) + print c.read_tag('T4:2.TT', 1) + print c.read_tag('T4:2.DN',) + + c.write_tag('C5:0.CU', 1) + c.write_tag('C5:0.CD', 0) + c.write_tag('C5:0.DN', 1) + c.write_tag('C5:0.OV', 0) + c.write_tag('C5:0.UN', 1) + c.write_tag('C5:0.UA', 0) + print c.read_tag('C5:0.CU') + print c.read_tag('C5:0.CD') + print c.read_tag('C5:0.DN') + print c.read_tag('C5:0.OV') + print c.read_tag('C5:0.UN') + print c.read_tag('C5:0.UA') + + c.write_tag('B3:100', 1) + print c.read_tag('B3:100') + + c.write_tag('B3/3955', 1) + print c.read_tag('B3/3955') + + c.write_tag('N7:0/2', 1) + print c.read_tag('N7:0/2') + + print c.write_tag('O:0.0/4', 1) + print c.read_tag('O:0.0/4') + except Exception as e: + err = c.get_status() + #c.close() + print err + pass + c.close() diff --git a/daq/pycomm-master/pycomm/__init__.py b/daq/pycomm-master/pycomm/__init__.py new file mode 100755 index 0000000..8c1f233 --- /dev/null +++ b/daq/pycomm-master/pycomm/__init__.py @@ -0,0 +1 @@ +__author__ = 'agostino' diff --git a/daq/pycomm-master/pycomm/ab_comm/__init__.py b/daq/pycomm-master/pycomm/ab_comm/__init__.py new file mode 100755 index 0000000..28c38a3 --- /dev/null +++ b/daq/pycomm-master/pycomm/ab_comm/__init__.py @@ -0,0 +1,2 @@ +__author__ = 'agostino' +import logging diff --git a/daq/pycomm-master/pycomm/ab_comm/clx.py b/daq/pycomm-master/pycomm/ab_comm/clx.py new file mode 100755 index 0000000..47ea813 --- /dev/null +++ b/daq/pycomm-master/pycomm/ab_comm/clx.py @@ -0,0 +1,873 @@ +# -*- coding: utf-8 -*- +# +# clx.py - Ethernet/IP Client for Rockwell PLCs +# +# +# Copyright (c) 2014 Agostino Ruscito +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +from pycomm.cip.cip_base import * +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logger = logging.getLogger(__name__) +logger.addHandler(NullHandler()) + + +class Driver(Base): + """ + This Ethernet/IP client is based on Rockwell specification. Please refer to the link below for details. + + http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf + + The following services have been implemented: + - Read Tag Service (0x4c) + - Read Tag Fragment Service (0x52) + - Write Tag Service (0x4d) + - Write Tag Fragment Service (0x53) + - Multiple Service Packet (0x0a) + + The client has been successfully tested with the following PLCs: + - CompactLogix 5330ERM + - CompactLogix 5370 + - ControlLogix 5572 and 1756-EN2T Module + +""" + + def __init__(self): + super(Driver, self).__init__() + + self._buffer = {} + self._get_template_in_progress = False + self.__version__ = '0.2' + + def get_last_tag_read(self): + """ Return the last tag read by a multi request read + + :return: A tuple (tag name, value, type) + """ + return self._last_tag_read + + def get_last_tag_write(self): + """ Return the last tag write by a multi request write + + :return: A tuple (tag name, 'GOOD') if the write was successful otherwise (tag name, 'BAD') + """ + return self._last_tag_write + + def _parse_instance_attribute_list(self, start_tag_ptr, status): + """ extract the tags list from the message received + + :param start_tag_ptr: The point in the message string where the tag list begin + :param status: The status of the message receives + """ + tags_returned = self._reply[start_tag_ptr:] + tags_returned_length = len(tags_returned) + idx = 0 + instance = 0 + count = 0 + try: + while idx < tags_returned_length: + instance = unpack_dint(tags_returned[idx:idx+4]) + idx += 4 + tag_length = unpack_uint(tags_returned[idx:idx+2]) + idx += 2 + tag_name = tags_returned[idx:idx+tag_length] + idx += tag_length + symbol_type = unpack_uint(tags_returned[idx:idx+2]) + idx += 2 + count += 1 + self._tag_list.append({'instance_id': instance, + 'tag_name': tag_name, + 'symbol_type': symbol_type}) + except Exception as e: + raise DataError(e) + + if status == SUCCESS: + self._last_instance = -1 + elif status == 0x06: + self._last_instance = instance + 1 + else: + self._status = (1, 'unknown status during _parse_tag_list') + self._last_instance = -1 + + def _parse_structure_makeup_attributes(self, start_tag_ptr, status): + """ extract the tags list from the message received + + :param start_tag_ptr: The point in the message string where the tag list begin + :param status: The status of the message receives + """ + self._buffer = {} + + if status != SUCCESS: + self._buffer['Error'] = status + return + + attribute = self._reply[start_tag_ptr:] + idx = 4 + try: + if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: + idx += 2 + self._buffer['object_definition_size'] = unpack_dint(attribute[idx:idx + 4]) + else: + self._buffer['Error'] = 'object_definition Error' + return + + idx += 6 + if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: + idx += 2 + self._buffer['structure_size'] = unpack_dint(attribute[idx:idx + 4]) + else: + self._buffer['Error'] = 'structure Error' + return + + idx += 6 + if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: + idx += 2 + self._buffer['member_count'] = unpack_uint(attribute[idx:idx + 2]) + else: + self._buffer['Error'] = 'member_count Error' + return + + idx += 4 + if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: + idx += 2 + self._buffer['structure_handle'] = unpack_uint(attribute[idx:idx + 2]) + else: + self._buffer['Error'] = 'structure_handle Error' + return + + return self._buffer + + except Exception as e: + raise DataError(e) + + def _parse_template(self, start_tag_ptr, status): + """ extract the tags list from the message received + + :param start_tag_ptr: The point in the message string where the tag list begin + :param status: The status of the message receives + """ + tags_returned = self._reply[start_tag_ptr:] + bytes_received = len(tags_returned) + + self._buffer += tags_returned + + if status == SUCCESS: + self._get_template_in_progress = False + + elif status == 0x06: + self._byte_offset += bytes_received + else: + self._status = (1, 'unknown status {0} during _parse_template'.format(status)) + logger.warning(self._status) + self._last_instance = -1 + + def _parse_fragment(self, start_ptr, status): + """ parse the fragment returned by a fragment service. + + :param start_ptr: Where the fragment start within the replay + :param status: status field used to decide if keep parsing or stop + """ + try: + 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) + + fragment_returned_length = len(fragment_returned) + idx = 0 + + while idx < fragment_returned_length: + try: + typ = I_DATA_TYPE[data_type] + if self._output_raw: + value = fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]] + else: + 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) + if self._output_raw: + self._tag_list += value + else: + self._tag_list.append((self._last_position, value)) + self._last_position += 1 + + if status == SUCCESS: + self._byte_offset = -1 + elif status == 0x06: + self._byte_offset += fragment_returned_length + else: + self._status = (2, 'unknown status during _parse_fragment') + self._byte_offset = -1 + + def _parse_multiple_request_read(self, tags): + """ 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. + Those information are appended to the tag list as tuple + + :return: the tag list + """ + offset = 50 + position = 50 + try: + 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]) + + if general_status == 0: + 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]) + else: + self._last_tag_read = (tags[index], None, None) + + tag_list.append(self._last_tag_read) + + return tag_list + except Exception as e: + raise DataError(e) + + def _parse_multiple_request_write(self, tags): + """ parse the message received from a multi request writ: + + For each tag parsed, the information extracted includes the tag name and the status of the writing. + Those information are appended to the tag list as tuple + + :return: the tag list + """ + offset = 50 + position = 50 + try: + 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]) + + if general_status == 0: + self._last_tag_write = (tags[index] + ('GOOD',)) + else: + self._last_tag_write = (tags[index] + ('BAD',)) + + tag_list.append(self._last_tag_write) + return tag_list + except Exception as e: + raise DataError(e) + + def _check_reply(self): + """ check the replayed message for error + + """ + self._more_packets_available = False + try: + if self._reply is None: + self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])]) + return False + # Get the type of command + typ = unpack_uint(self._reply[:2]) + + # Encapsulation status check + if unpack_dint(self._reply[8:12]) != SUCCESS: + self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ], + SERVICE_STATUS[unpack_dint(self._reply[8:12])])) + return False + + # Command Specific Status check + if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]): + status = unpack_usint(self._reply[42:43]) + if status != SUCCESS: + self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format( + SERVICE_STATUS[status], get_extended_status(self._reply, 42))) + return False + else: + return True + elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]): + status = unpack_usint(self._reply[48:49]) + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]: + self._parse_fragment(50, status) + return True + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]: + self._parse_instance_attribute_list(50, status) + return True + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Attributes"]: + self._parse_structure_makeup_attributes(50, status) + return True + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Template"] and \ + self._get_template_in_progress: + self._parse_template(50, status) + return True + if status == 0x06: + self._status = (3, "Insufficient Packet Space") + self._more_packets_available = True + elif status != SUCCESS: + self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format( + SERVICE_STATUS[status], get_extended_status(self._reply, 48))) + return False + else: + return True + + return True + except Exception as e: + raise DataError(e) + + def read_tag(self, tag): + """ read tag from a connected plc + + Possible combination can be passed to this method: + - ('Counts') a single tag name + - (['ControlWord']) a list with one tag or many + - (['parts', 'ControlWord', 'Counts']) + + At the moment there is not a strong validation for the argument passed. The user should verify + the correctness of the format passed. + + :return: None is returned in case of error otherwise the tag list is returned + """ + multi_requests = False + if isinstance(tag, list): + multi_requests = True + + if not self._target_is_connected: + if not self.forward_open(): + self._status = (6, "Target did not connected. read_tag will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. read_tag will not be executed.") + + 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)) + message_request = build_multiple_service(rp_list, Base._get_sequence()) + + else: + 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)) + return None + else: + # 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 + pack_uint(1) + ] + + 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 multi_requests: + return self._parse_multiple_request_read(tag) + else: + # Get the data type + 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] + except Exception as e: + raise DataError(e) + + def read_array(self, tag, counts, raw=False): + """ read array of atomic data type from a connected plc + + At the moment there is not a strong validation for the argument passed. The user should verify + the correctness of the format passed. + + :param tag: the name of the tag to read + :param counts: the number of element to read + :param raw: the value should output as raw-value (hex) + :return: None is returned in case of error otherwise the tag list is returned + """ + if not self._target_is_connected: + if not self.forward_open(): + self._status = (7, "Target did not connected. read_tag will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. read_tag will not be executed.") + + self._byte_offset = 0 + self._last_position = 0 + self._output_raw = raw + + if self._output_raw: + self._tag_list = '' + else: + self._tag_list = [] + while self._byte_offset != -1: + rp = create_tag_rp(tag) + if rp is None: + self._status = (7, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) + return None + else: + # 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 + pack_uint(counts), + pack_dint(self._byte_offset) + ] + + 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") + + return self._tag_list + + def write_tag(self, tag, value=None, typ=None): + """ write tag/tags from a connected plc + + Possible combination can be passed to this method: + - ('tag name', Value, data type) as single parameters or inside a tuple + - ([('tag name', Value, data type), ('tag name2', Value, data type)]) as array of tuples + + At the moment there is not a strong validation for the argument passed. The user should verify + the correctness of the format passed. + + The type accepted are: + - BOOL + - SINT + - INT' + - DINT + - REAL + - LINT + - BYTE + - WORD + - DWORD + - LWORD + + :param tag: tag name, or an array of tuple containing (tag name, value, data type) + :param value: the value to write or none if tag is an array of tuple or a tuple + :param typ: the type of the tag to write or none if tag is an array of tuple or a tuple + :return: None is returned in case of error otherwise the tag list is returned + """ + multi_requests = False + if isinstance(tag, list): + multi_requests = True + + if not self._target_is_connected: + if not self.forward_open(): + self._status = (8, "Target did not connected. write_tag will not be executed.") + logger.warning(self._status) + 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()) + + 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) + ] + + ret_val = self.send_unit_data( + build_common_packet_format( + DATA_ITEM['Connected'], + ''.join(message_request), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid, + ) + ) + + if multi_requests: + return self._parse_multiple_request_write(tag) + else: + if ret_val is None: + raise DataError("send_unit_data returned not valid data") + return ret_val + + def write_array(self, tag, data_type, values, raw=False): + """ write array of atomic data type from a connected plc + + At the moment there is not a strong validation for the argument passed. The user should verify + the correctness of the format passed. + + :param tag: the name of the tag to read + :param data_type: the type of tag to write + :param values: the array of values to write, if raw: the frame with bytes + :param raw: indicates that the values are given as raw values (hex) + """ + if not isinstance(values, list): + self._status = (9, "A list of tags must be passed to write_array.") + logger.warning(self._status) + raise DataError("A list of tags must be passed to write_array.") + + if not self._target_is_connected: + if not self.forward_open(): + self._status = (9, "Target did not connected. write_array will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. write_array will not be executed.") + + array_of_values = "" + 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) + byte_size += DATA_FUNCTION_SIZE[data_type] + + 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: + self._status = (9, "Cannot create tag {0} request packet. \ + write_array will not be executed.".format(tag)) + return None + else: + # 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 + pack_dint(byte_offset), + 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), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid, + )) is None: + raise DataError("send_unit_data returned not valid data") + array_of_values = "" + byte_size = 0 + + def _get_instance_attribute_list_service(self): + """ Step 1: Finding user-created controller scope tags in a Logix5000 controller + + This service returns instance IDs for each created instance of the symbol class, along with a list + of the attribute data associated with the requested attribute + """ + try: + 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.") + + self._last_instance = 0 + + self._get_template_in_progress = True + while self._last_instance != -1: + + # Creating the Message Request Packet + + message_request = [ + pack_uint(Base._get_sequence()), + chr(TAG_SERVICES_REQUEST['Get Instance Attributes List']), # STEP 1 + # the Request Path Size length in word + chr(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', + 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 + ] + + 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") + + self._get_template_in_progress = False + + except Exception as e: + raise DataError(e) + + 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.") + + 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 + ] + + 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") + + return self._buffer + + def _read_template(self, instance_id, object_definition_size): + """ get a list of the tags in the plc + + """ + 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.") + + self._byte_offset = 0 + self._buffer = "" + self._get_template_in_progress = True + + try: + while self._get_template_in_progress: + + # 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) + ] + + 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") + + self._get_template_in_progress = False + return self._buffer + + except Exception as e: + raise DataError(e) + + def _isolating_user_tag(self): + try: + 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' + 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 == 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}) + 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 + + if i != 0: + array_size = unpack_uint(buff[:2]) + try: + data_type = I_DATA_TYPE[unpack_uint(buff[2:4])] + except Exception: + data_type = "None" + + offset = unpack_dint(buff[4:8]) + type_list.append((array_size, data_type, offset)) + + buff = buff[8:] + + tag['udt']['data_type'] = type_list + except Exception as e: + raise DataError(e) + + def get_tag_list(self): + self._tag_list = [] + # Step 1 + self._get_instance_attribute_list_service() + + # Step 2 + self._isolating_user_tag() + + # Step 3 + 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 + + return self._tag_list diff --git a/daq/pycomm-master/pycomm/ab_comm/slc.py b/daq/pycomm-master/pycomm/ab_comm/slc.py new file mode 100755 index 0000000..bbb1091 --- /dev/null +++ b/daq/pycomm-master/pycomm/ab_comm/slc.py @@ -0,0 +1,574 @@ +# -*- coding: utf-8 -*- +# +# clx.py - Ethernet/IP Client for Rockwell PLCs +# +# +# Copyright (c) 2014 Agostino Ruscito +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +from pycomm.cip.cip_base import * +import re +import math +#import binascii + +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logger = logging.getLogger(__name__) +logger.addHandler(NullHandler()) + + +def parse_tag(tag): + t = re.search(r"(?P[CT])(?P\d{1,3})" + r"(:)(?P\d{1,3})" + r"(.)(?PACC|PRE|EN|DN|TT|CU|CD|DN|OV|UN|UA)", tag, flags=re.IGNORECASE) + if t: + if (1 <= int(t.group('file_number')) <= 255) \ + and (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'), + 'element_number': t.group('element_number'), + 'sub_element': PCCC_CT[t.group('sub_element').upper()], + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 3} + + t = re.search(r"(?P[LFBN])(?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: + if (1 <= int(t.group('file_number')) <= 255) \ + and (0 <= int(t.group('element_number')) <= 255) \ + 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'), + 'element_number': t.group('element_number'), + 'sub_element': t.group('sub_element'), + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 3} + else: + if (1 <= int(t.group('file_number')) <= 255) \ + and (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'), + 'element_number': t.group('element_number'), + 'sub_element': t.group('sub_element'), + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 2} + + 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: + if (0 <= int(t.group('file_number')) <= 255) \ + and (0 <= int(t.group('element_number')) <= 255) \ + 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'), + 'element_number': t.group('element_number'), + 'sub_element': t.group('sub_element'), + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 3} + else: + if (0 <= int(t.group('file_number')) <= 255) \ + and (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'), + 'element_number': t.group('element_number'), + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 2} + + t = re.search(r"(?PS)" + r"(:)(?P\d{1,3})" + r"(/(?P\d{1,2}))?", tag, flags=re.IGNORECASE) + if t: + if t.group('sub_element') is not None: + if (0 <= int(t.group('element_number')) <= 255) \ + and (0 <= int(t.group('sub_element')) <= 15): + return True, t.group(0), {'file_type': t.group('file_type').upper(), + 'file_number': '2', + 'element_number': t.group('element_number'), + 'sub_element': t.group('sub_element'), + 'read_func': '\xa2', + 'write_func': '\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', + 'address_field': 2} + + t = re.search(r"(?PB)(?P\d{1,3})" + r"(/)(?P\d{1,4})", + tag, flags=re.IGNORECASE) + if t: + if (1 <= int(t.group('file_number')) <= 255) \ + and (0 <= int(t.group('element_number')) <= 4095): + bit_position = int(t.group('element_number')) + element_number = bit_position / 16 + sub_element = bit_position - (element_number * 16) + return True, t.group(0), {'file_type': t.group('file_type').upper(), + 'file_number': t.group('file_number'), + 'element_number': element_number, + 'sub_element': sub_element, + 'read_func': '\xa2', + 'write_func': '\xab', + 'address_field': 3} + + return False, tag + + +class Driver(Base): + """ + SLC/PLC_5 Implementation + """ + def __init__(self): + super(Driver, self).__init__() + + self.__version__ = '0.1' + self._last_sequence = 0 + + def _check_reply(self): + """ + check the replayed message for error + """ + self._more_packets_available = False + try: + if self._reply is None: + self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])]) + return False + # Get the type of command + typ = unpack_uint(self._reply[:2]) + + # Encapsulation status check + if unpack_dint(self._reply[8:12]) != SUCCESS: + self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ], + SERVICE_STATUS[unpack_dint(self._reply[8:12])])) + return False + + # Command Specific Status check + if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]): + status = unpack_usint(self._reply[42:43]) + if status != SUCCESS: + self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format( + SERVICE_STATUS[status], get_extended_status(self._reply, 42))) + return False + else: + return True + + elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]): + status = unpack_usint(self._reply[48:49]) + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]: + self._parse_fragment(50, status) + return True + if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]: + self._parse_tag_list(50, status) + return True + if status == 0x06: + self._status = (3, "Insufficient Packet Space") + self._more_packets_available = True + elif status != SUCCESS: + self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format( + SERVICE_STATUS[status], get_extended_status(self._reply, 48))) + return False + else: + return True + + return True + except Exception as e: + raise DataError(e) + + def __queue_data_available(self, queue_number): + """ read the queue + + Possible combination can be passed to this method: + print c.read_tag('F8:0', 3) return a list of 3 registers starting from F8:0 + print c.read_tag('F8:0') return one value + + It is possible to read status bit + + :return: None is returned in case of error + """ + + # Creating the Message Request Packet + self._last_sequence = pack_uint(Base._get_sequence()) + + # PCCC_Cmd_Rd_w3_Q2 = [0x0f, 0x00, 0x30, 0x00, 0xa2, 0x6d, 0x00, 0xa5, 0x02, 0x00] + message_request = [ + self._last_sequence, + '\x4b', + '\x02', + CLASS_ID["8-bit"], + PATH["PCCC"], + '\x07', + self.attribs['vid'], + self.attribs['vsn'], + '\x0f', + '\x00', + self._last_sequence[1], + self._last_sequence[0], + '\xa2', # protected typed logical read with three address fields FNC + '\x6d', # Byte size to read = 109 + '\x00', # File Number + '\xa5', # File Type + pack_uint(queue_number) + ] + + if self.send_unit_data( + build_common_packet_format( + DATA_ITEM['Connected'], + ''.join(message_request), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid,)): + + sts = int(unpack_uint(self._reply[2:4])) + if sts == 146: + return True + else: + return False + else: + raise DataError("read_queue [send_unit_data] returned not valid data") + + def __save_record(self, filename): + with open(filename, "a") as csv_file: + logger.debug("SLC __save_record read:{0}".format(self._reply[61:])) + csv_file.write(self._reply[61:]+'\n') + csv_file.close() + + def __get_queue_size(self, queue_number): + """ get queue size + """ + # Creating the Message Request Packet + self._last_sequence = pack_uint(Base._get_sequence()) + + message_request = [ + self._last_sequence, + '\x4b', + '\x02', + CLASS_ID["8-bit"], + PATH["PCCC"], + '\x07', + self.attribs['vid'], + self.attribs['vsn'], + '\x0f', + '\x00', + self._last_sequence[1], + self._last_sequence[0], + # '\x30', + # '\x00', + '\xa1', # FNC to get the queue size + '\x06', # Byte size to read = 06 + '\x00', # File Number + '\xea', # File Type ???? + '\xff', # File Type ???? + pack_uint(queue_number) + ] + + if self.send_unit_data( + build_common_packet_format( + DATA_ITEM['Connected'], + ''.join(message_request), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid,)): + sts = int(unpack_uint(self._reply[65:67])) + logger.debug("SLC __get_queue_size({0}) returned {1}".format(queue_number, sts)) + return sts + else: + raise DataError("read_queue [send_unit_data] returned not valid data") + + def read_queue(self, queue_number, file_name): + """ read the queue + + """ + if not self._target_is_connected: + if not self.forward_open(): + self._status = (5, "Target did not connected. is_queue_available will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. is_queue_available will not be executed.") + + if self.__queue_data_available(queue_number): + logger.debug("SLC read_queue: Queue {0} has data".format(queue_number)) + self.__save_record(file_name + str(queue_number) + ".csv") + size = self.__get_queue_size(queue_number) + if size > 0: + for i in range(0, size): + if self.__queue_data_available(queue_number): + self.__save_record(file_name + str(queue_number) + ".csv") + + logger.debug("SLC read_queue: {0} record extract from queue {1}".format(size, queue_number)) + else: + logger.debug("SLC read_queue: Queue {0} has no data".format(queue_number)) + + def read_tag(self, tag, n=1): + """ read tag from a connected plc + + Possible combination can be passed to this method: + print c.read_tag('F8:0', 3) return a list of 3 registers starting from F8:0 + print c.read_tag('F8:0') return one value + + It is possible to read status bit + + :return: None is returned in case of error + """ + res = parse_tag(tag) + if not res[0]: + self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, n)) + logger.warning(self._status) + raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, n)) + + 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']) + + if not self._target_is_connected: + if not self.forward_open(): + self._status = (5, "Target did not connected. read_tag will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. read_tag will not be executed.") + + data_size = PCCC_DATA_SIZE[res[2]['file_type']] + + # Creating the Message Request Packet + self._last_sequence = pack_uint(Base._get_sequence()) + + message_request = [ + self._last_sequence, + '\x4b', + '\x02', + CLASS_ID["8-bit"], + PATH["PCCC"], + '\x07', + self.attribs['vid'], + self.attribs['vsn'], + '\x0f', + '\x00', + self._last_sequence[1], + 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) + ] + + 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), + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid,)): + sts = int(unpack_usint(self._reply[58])) + try: + if sts != 0: + sts_txt = PCCC_ERROR_CODE[sts] + self._status = (1000, "Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n)) + logger.warning(self._status) + raise DataError("Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n)) + + new_value = 61 + if bit_read: + if res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C': + if bit_position == PCCC_CT['PRE']: + return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( + self._reply[new_value+2:new_value+2+data_size]) + elif bit_position == PCCC_CT['ACC']: + return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( + self._reply[new_value+4:new_value+4+data_size]) + + tag_value = UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( + self._reply[new_value:new_value+data_size]) + return get_bit(tag_value, bit_position) + + else: + values_list = [] + while len(self._reply[new_value:]) >= data_size: + values_list.append( + UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](self._reply[new_value:new_value+data_size]) + ) + new_value = new_value+data_size + + if len(values_list) > 1: + return values_list + else: + return values_list[0] + + except Exception as e: + self._status = (1000, "Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n)) + logger.warning(self._status) + raise DataError("Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n)) + else: + raise DataError("send_unit_data returned not valid data") + + def write_tag(self, tag, value): + """ write tag from a connected plc + + Possible combination can be passed to this method: + c.write_tag('N7:0', [-30, 32767, -32767]) + c.write_tag('N7:0', 21) + c.read_tag('N7:0', 10) + + It is not possible to write status bit + + :return: None is returned in case of error + """ + res = parse_tag(tag) + if not res[0]: + self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, value)) + logger.warning(self._status) + raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, value)) + + if isinstance(value, list) and int(res[2]['address_field'] == 3): + self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value)) + logger.warning(self._status) + raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value)) + + if isinstance(value, list) and int(res[2]['address_field'] == 3): + self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value)) + logger.warning(self._status) + raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value)) + + bit_field = False + bit_position = 0 + sub_element = 0 + if int(res[2]['address_field'] == 3): + bit_field = True + bit_position = int(res[2]['sub_element']) + values_list = '' + else: + values_list = '\xff\xff' + + multi_requests = False + if isinstance(value, list): + multi_requests = True + + if not self._target_is_connected: + if not self.forward_open(): + self._status = (1000, "Target did not connected. write_tag will not be executed.") + logger.warning(self._status) + raise DataError("Target did not connected. write_tag will not be executed.") + + try: + n = 0 + if multi_requests: + data_size = PCCC_DATA_SIZE[res[2]['file_type']] + for v in value: + values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](v) + n += 1 + else: + n = 1 + if bit_field: + data_size = 2 + + if (res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C') \ + and (bit_position == PCCC_CT['PRE'] or bit_position == PCCC_CT['ACC']): + sub_element = bit_position + values_list = '\xff\xff' + PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value) + else: + sub_element = 0 + if value > 0: + values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(math.pow(2, bit_position)) + else: + values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(0) + + else: + values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value) + data_size = PCCC_DATA_SIZE[res[2]['file_type']] + + except Exception as e: + self._status = (1000, "Error({0}) packing the values to write to the" + "SLC write_tag({1},{2})".format(e, tag, value)) + logger.warning(self._status) + raise DataError("Error({0}) packing the values to write to the " + "SLC write_tag({1},{2})".format(e, tag, value)) + + data_to_write = values_list + + # Creating the Message Request Packet + self._last_sequence = pack_uint(Base._get_sequence()) + + message_request = [ + self._last_sequence, + '\x4b', + '\x02', + CLASS_ID["8-bit"], + PATH["PCCC"], + '\x07', + self.attribs['vid'], + self.attribs['vsn'], + '\x0f', + '\x00', + self._last_sequence[1], + 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) + ] + + 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, + ADDRESS_ITEM['Connection Based'], + addr_data=self._target_cid,)): + sts = int(unpack_usint(self._reply[58])) + try: + if sts != 0: + sts_txt = PCCC_ERROR_CODE[sts] + self._status = (1000, "Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value)) + logger.warning(self._status) + raise DataError("Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value)) + + return True + except Exception as e: + self._status = (1000, "Error({0}) parsing the data returned from " + "SLC write_tag({1},{2})".format(e, tag, value)) + logger.warning(self._status) + raise DataError("Error({0}) parsing the data returned from " + "SLC write_tag({1},{2})".format(e, tag, value)) + else: + raise DataError("send_unit_data returned not valid data") diff --git a/daq/pycomm-master/pycomm/cip/__init__.py b/daq/pycomm-master/pycomm/cip/__init__.py new file mode 100755 index 0000000..8c1f233 --- /dev/null +++ b/daq/pycomm-master/pycomm/cip/__init__.py @@ -0,0 +1 @@ +__author__ = 'agostino' diff --git a/daq/pycomm-master/pycomm/cip/cip_base.py b/daq/pycomm-master/pycomm/cip/cip_base.py new file mode 100755 index 0000000..91d0757 --- /dev/null +++ b/daq/pycomm-master/pycomm/cip/cip_base.py @@ -0,0 +1,864 @@ +# -*- coding: utf-8 -*- +# +# cip_base.py - A set of classes methods and structures used to implement Ethernet/IP +# +# +# Copyright (c) 2014 Agostino Ruscito +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import struct +import socket +import random + +from os import getpid +from pycomm.cip.cip_const import * +from pycomm.common import PycommError + + +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass +logger = logging.getLogger(__name__) +logger.addHandler(NullHandler()) + + +class CommError(PycommError): + pass + + +class DataError(PycommError): + pass + + +def pack_sint(n): + return struct.pack('b', n) + + +def pack_usint(n): + return struct.pack('B', n) + + +def pack_int(n): + """pack 16 bit into 2 bytes little endian""" + return struct.pack(' +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +ELEMENT_ID = { + "8-bit": '\x28', + "16-bit": '\x29', + "32-bit": '\x2a' +} + +CLASS_ID = { + "8-bit": '\x20', + "16-bit": '\x21', +} + +INSTANCE_ID = { + "8-bit": '\x24', + "16-bit": '\x25' +} + +ATTRIBUTE_ID = { + "8-bit": '\x30', + "16-bit": '\x31' +} + +# Path are combined as: +# 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' +} + +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' +} + +""" +When a tag is created, an instance of the Symbol Object (Class ID 0x6B) is created +inside the controller. + +When a UDT is created, an instance of the Template object (Class ID 0x6C) is +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 +} + +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' +} + +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, + "Get Instance Attributes List": 0x55, + "Get Attributes": 0x03, + "Read Template": 0x4c, +} + +TAG_SERVICES_REPLY = { + 0xcc: "Read Tag", + 0xd2: "Read Tag Fragmented", + 0xcd: "Write Tag", + 0xd3: "Write Tag Fragmented", + 0xce: "Read Modify Write Tag", + 0x8a: "Multiple Service Packet", + 0xd5: "Get Instance Attributes List", + 0x83: "Get Attributes", + 0xcc: "Read Template" +} + + +I_TAG_SERVICES_REPLY = { + "Read Tag": 0xcc, + "Read Tag Fragmented": 0xd2, + "Write Tag": 0xcd, + "Write Tag Fragmented": 0xd3, + "Read Modify Write Tag": 0xce, + "Multiple Service Packet": 0x8a, + "Get Instance Attributes List": 0xd5, + "Get Attributes": 0x83, + "Read Template": 0xcc +} + + +""" +EtherNet/IP Encapsulation Error Codes + +Standard CIP Encapsulation Error returned in the cip message header +""" +STATUS = { + 0x0000: "Success", + 0x0001: "The sender issued an invalid or unsupported encapsulation command", + 0x0002: "Insufficient memory", + 0x0003: "Poorly formed or incorrect data in the data portion", + 0x0064: "An originator used an invalid session handle when sending an encapsulation message to the target", + 0x0065: "The target received a message of invalid length", + 0x0069: "Unsupported Protocol Version" +} + +""" +MSG Error Codes: + +The following error codes have been taken from: + +Rockwell Automation Publication +1756-RM003P-EN-P - December 2014 +""" +SERVICE_STATUS = { + 0x01: "Connection failure (see extended status)", + 0x02: "Insufficient resource", + 0x03: "Invalid value", + 0x04: "IOI syntax error. A syntax error was detected decoding the Request Path (see extended status)", + 0x05: "Destination unknown, class unsupported, instance \nundefined or structure element undefined (see extended status)", + 0x06: "Insufficient Packet Space", + 0x07: "Connection lost", + 0x08: "Service not supported", + 0x09: "Error in data segment or invalid attribute value", + 0x0A: "Attribute list error", + 0x0B: "State already exist", + 0x0C: "Object state conflict", + 0x0D: "Object already exist", + 0x0E: "Attribute not settable", + 0x0F: "Permission denied", + 0x10: "Device state conflict", + 0x11: "Reply data too large", + 0x12: "Fragmentation of a primitive value", + 0x13: "Insufficient command data", + 0x14: "Attribute not supported", + 0x15: "Too much data", + 0x1A: "Bridge request too large", + 0x1B: "Bridge response too large", + 0x1C: "Attribute list shortage", + 0x1D: "Invalid attribute list", + 0x1E: "Request service error", + 0x1F: "Connection related failure (see extended status)", + 0x22: "Invalid reply received", + 0x25: "Key segment error", + 0x26: "Invalid IOI error", + 0x27: "Unexpected attribute in list", + 0x28: "DeviceNet error - invalid member ID", + 0x29: "DeviceNet error - member not settable", + 0xD1: "Module not in run state", + 0xFB: "Message port not supported", + 0xFC: "Message unsupported data type", + 0xFD: "Message uninitialized", + 0xFE: "Message timeout", + 0xff: "General Error (see extended status)" +} + +EXTEND_CODES = { + 0x01: { + 0x0100: "Connection in use", + 0x0103: "Transport not supported", + 0x0106: "Ownership conflict", + 0x0107: "Connection not found", + 0x0108: "Invalid connection type", + 0x0109: "Invalid connection size", + 0x0110: "Module not configured", + 0x0111: "EPR not supported", + 0x0114: "Wrong module", + 0x0115: "Wrong device type", + 0x0116: "Wrong revision", + 0x0118: "Invalid configuration format", + 0x011A: "Application out of connections", + 0x0203: "Connection timeout", + 0x0204: "Unconnected message timeout", + 0x0205: "Unconnected send parameter error", + 0x0206: "Message too large", + 0x0301: "No buffer memory", + 0x0302: "Bandwidth not available", + 0x0303: "No screeners available", + 0x0305: "Signature match", + 0x0311: "Port not available", + 0x0312: "Link address not available", + 0x0315: "Invalid segment type", + 0x0317: "Connection not scheduled" + }, + 0x04: { + 0x0000: "Extended status out of memory", + 0x0001: "Extended status out of instances" + }, + 0x05: { + 0x0000: "Extended status out of memory", + 0x0001: "Extended status out of instances" + }, + 0x1F: { + 0x0203: "Connection timeout" + }, + 0xff: { + 0x7: "Wrong data type", + 0x2001: "Excessive IOI", + 0x2002: "Bad parameter value", + 0x2018: "Semaphore reject", + 0x201B: "Size too small", + 0x201C: "Invalid size", + 0x2100: "Privilege failure", + 0x2101: "Invalid keyswitch position", + 0x2102: "Password invalid", + 0x2103: "No password issued", + 0x2104: "Address out of range", + 0x2105: "Address and how many out of range", + 0x2106: "Data in use", + 0x2107: "Type is invalid or not supported", + 0x2108: "Controller in upload or download mode", + 0x2109: "Attempt to change number of array dimensions", + 0x210A: "Invalid symbol name", + 0x210B: "Symbol does not exist", + 0x210E: "Search failed", + 0x210F: "Task cannot start", + 0x2110: "Unable to write", + 0x2111: "Unable to read", + 0x2112: "Shared routine not editable", + 0x2113: "Controller in faulted mode", + 0x2114: "Run mode inhibited" + + } +} +DATA_ITEM = { + 'Connected': '\xb1\x00', + 'Unconnected': '\xb2\x00' +} + +ADDRESS_ITEM = { + 'Connection Based': '\xa1\x00', + 'Null': '\x00\x00', + 'UCMM': '\x00\x00' +} + +UCMM = { + 'Interface Handle': 0, + 'Item Count': 2, + 'Address Type ID': 0, + 'Address Length': 0, + 'Data Type ID': 0x00b2 +} + +CONNECTION_SIZE = { + 'Backplane': '\x03', # CLX + 'Direct Network': '\x02' +} + +HEADER_SIZE = 24 +EXTENDED_SYMBOL = '\x91' +BOOL_ONE = 0xff +REQUEST_SERVICE = 0 +REQUEST_PATH_SIZE = 1 +REQUEST_PATH = 2 +SUCCESS = 0 +INSUFFICIENT_PACKETS = 6 +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 + +PADDING_BYTE = '\x00' +PRIORITY = '\x0a' +TIMEOUT_TICKS = '\x05' +TIMEOUT_MULTIPLIER = '\x01' +TRANSPORT_CLASS = '\xa3' + +CONNECTION_PARAMETER = { + 'PLC5': 0x4302, + 'SLC500': 0x4302, + 'CNET': 0x4320, + 'DHP': 0x4302, + 'Default': 0x43f8, +} + +""" +Atomic Data Type: + + Bit = Bool + Bit array = DWORD (32-bit boolean aray) + 8-bit integer = SINT +16-bit integer = UINT +32-bit integer = DINT + 32-bit float = REAL +64-bit integer = LINT + +From Rockwell Automation Publication 1756-PM020C-EN-P November 2012: +When reading a BOOL tag, the values returned for 0 and 1 are 0 and 0xff, respectively. +""" + +S_DATA_TYPE = { + 'BOOL': 0xc1, + 'SINT': 0xc2, # Signed 8-bit integer + 'INT': 0xc3, # Signed 16-bit integer + 'DINT': 0xc4, # Signed 32-bit integer + 'LINT': 0xc5, # Signed 64-bit integer + 'USINT': 0xc6, # Unsigned 8-bit integer + 'UINT': 0xc7, # Unsigned 16-bit integer + 'UDINT': 0xc8, # Unsigned 32-bit integer + 'ULINT': 0xc9, # Unsigned 64-bit integer + 'REAL': 0xca, # 32-bit floating point + 'LREAL': 0xcb, # 64-bit floating point + 'STIME': 0xcc, # Synchronous time + 'DATE': 0xcd, + 'TIME_OF_DAY': 0xce, + 'DATE_AND_TIME': 0xcf, + 'STRING': 0xd0, # character string (1 byte per character) + 'BYTE': 0xd1, # byte string 8-bits + 'WORD': 0xd2, # byte string 16-bits + 'DWORD': 0xd3, # byte string 32-bits + 'LWORD': 0xd4, # byte string 64-bits + 'STRING2': 0xd5, # character string (2 byte per character) + 'FTIME': 0xd6, # Duration high resolution + 'LTIME': 0xd7, # Duration long + 'ITIME': 0xd8, # Duration short + 'STRINGN': 0xd9, # character string (n byte per character) + 'SHORT_STRING': 0xda, # character string (1 byte per character, 1 byte length indicator) + 'TIME': 0xdb, # Duration in milliseconds + 'EPATH': 0xdc, # CIP Path segment + 'ENGUNIT': 0xdd, # Engineering Units + 'STRINGI': 0xde # International character string +} + +I_DATA_TYPE = { + 0xc1: 'BOOL', + 0xc2: 'SINT', # Signed 8-bit integer + 0xc3: 'INT', # Signed 16-bit integer + 0xc4: 'DINT', # Signed 32-bit integer + 0xc5: 'LINT', # Signed 64-bit integer + 0xc6: 'USINT', # Unsigned 8-bit integer + 0xc7: 'UINT', # Unsigned 16-bit integer + 0xc8: 'UDINT', # Unsigned 32-bit integer + 0xc9: 'ULINT', # Unsigned 64-bit integer + 0xca: 'REAL', # 32-bit floating point + 0xcb: 'LREAL', # 64-bit floating point + 0xcc: 'STIME', # Synchronous time + 0xcd: 'DATE', + 0xce: 'TIME_OF_DAY', + 0xcf: 'DATE_AND_TIME', + 0xd0: 'STRING', # character string (1 byte per character) + 0xd1: 'BYTE', # byte string 8-bits + 0xd2: 'WORD', # byte string 16-bits + 0xd3: 'DWORD', # byte string 32-bits + 0xd4: 'LWORD', # byte string 64-bits + 0xd5: 'STRING2', # character string (2 byte per character) + 0xd6: 'FTIME', # Duration high resolution + 0xd7: 'LTIME', # Duration long + 0xd8: 'ITIME', # Duration short + 0xd9: 'STRINGN', # character string (n byte per character) + 0xda: 'SHORT_STRING', # character string (1 byte per character, 1 byte length indicator) + 0xdb: 'TIME', # Duration in milliseconds + 0xdc: 'EPATH', # CIP Path segment + 0xdd: 'ENGUNIT', # Engineering Units + 0xde: 'STRINGI' # International character string +} + +REPLAY_INFO = { + 0x4e: 'FORWARD_CLOSE (4E,00)', + 0x52: 'UNCONNECTED_SEND (52,00)', + 0x54: 'FORWARD_OPEN (54,00)', + 0x6f: 'send_rr_data (6F,00)', + 0x70: 'send_unit_data (70,00)', + 0x00: 'nop', + 0x01: 'list_targets', + 0x04: 'list_services', + 0x63: 'list_identity', + 0x64: 'list_interfaces', + 0x65: 'register_session', + 0x66: 'unregister_session', +} + +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' +} + +PCCC_DATA_SIZE = { + 'N': 2, + # 'L': 4, + 'B': 2, + 'T': 6, + 'C': 6, + 'S': 2, + 'F': 4, + 'ST': 84, + 'A': 2, + 'R': 6, + 'O': 2, + 'I': 2 +} + +PCCC_CT = { + 'PRE': 1, + 'ACC': 2, + 'EN': 15, + 'TT': 14, + 'DN': 13, + 'CU': 15, + 'CD': 14, + 'OV': 12, + 'UN': 11, + 'UA': 10 +} + +PCCC_ERROR_CODE = { + -2: "Not Acknowledged (NAK)", + -3: "No Reponse, Check COM Settings", + -4: "Unknown Message from DataLink Layer", + -5: "Invalid Address", + -6: "Could Not Open Com Port", + -7: "No data specified to data link layer", + -8: "No data returned from PLC", + -20: "No Data Returned", + 16: "Illegal Command or Format, Address may not exist or not enough elements in data file", + 32: "PLC Has a Problem and Will Not Communicate", + 48: "Remote Node Host is Missing, Disconnected, or Shut Down", + 64: "Host Could Not Complete Function Due To Hardware Fault", + 80: "Addressing problem or Memory Protect Rungs", + 96: "Function not allows due to command protection selection", + 112: "Processor is in Program mode", + 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 diff --git a/daq/pycomm-master/pycomm/common.py b/daq/pycomm-master/pycomm/common.py new file mode 100755 index 0000000..1f0eb63 --- /dev/null +++ b/daq/pycomm-master/pycomm/common.py @@ -0,0 +1,8 @@ +__author__ = 'Agostino Ruscito' +__version__ = "1.0.8" +__date__ = "08 03 2015" + +class PycommError(Exception): + pass + + diff --git a/daq/pycomm-master/setup.py b/daq/pycomm-master/setup.py new file mode 100755 index 0000000..0f9f9ba --- /dev/null +++ b/daq/pycomm-master/setup.py @@ -0,0 +1,37 @@ +from distutils.core import setup +from pycomm import common +import os + + +def read(file_name): + return open(os.path.join(os.path.dirname(__file__), file_name)).read() + +setup( + name="pycomm", + author="Agostino Ruscito", + author_email="uscito@gmail.com", + version=common.__version__, + description="A PLC communication library for Python", + long_description=read('README.rst'), + license="MIT", + url="https://github.com/ruscito/pycomm", + packages=[ + "pycomm", + "pycomm.ab_comm", + "pycomm.cip" + ], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) \ No newline at end of file