├── .gitignore ├── .travis.yml ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── README.rst ├── examples ├── test_clx_comm.py └── test_slc_only.py ├── pycomm ├── __init__.py ├── ab_comm │ ├── __init__.py │ ├── clx.py │ └── slc.py ├── cip │ ├── __init__.py │ ├── cip_base.py │ └── cip_const.py └── common.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # pycharm editor 57 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | 7 | install: python setup.py install 8 | 9 | script: nosetests 10 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 1.0.8 5 | ----- 6 | Number 0001: 7 | handling of raw values (hex) added to functions read_array and write_array: handling of raw values can be switched 8 | on/off with additional parameter 9 | 10 | Number 0002: 11 | 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 12 | array within an SINT, the tag type value contains also the bit position. 13 | 14 | Number 0003: 15 | code is always logging into a file (pycomm.log) into working path. Code changed, so that it is possible to configure 16 | the logging from the main application. 17 | 18 | 19 | 20 | 1.0.6 21 | ----- 22 | 23 | - Pypi posting 24 | 25 | 1.0.0 26 | ----- 27 | 28 | - Add support for SLC and PLC/05 plc 29 | 30 | 0.2.0 31 | --- 32 | 33 | - Add CIP support class 34 | - Add support for ControlLogix PLC 35 | 36 | 0.1 37 | --- 38 | 39 | - Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Agostino Ruscito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pycomm 2 | ====== 3 | pycomm is a package that includes a collection of modules used to communicate with PLCs. 4 | At the moment the first module in the package is ab_comm. 5 | 6 | Test 7 | ~~~~ 8 | The library is currently test on Python 2.6, 2.7. 9 | 10 | .. image:: https://travis-ci.org/ruscito/pycomm.svg?branch=master 11 | :target: https://travis-ci.org/ruscito/pycomm 12 | 13 | Setup 14 | ~~~~~ 15 | The package can be installed from 16 | 17 | GitHub: 18 | :: 19 | 20 | git clone https://github.com/ruscito/pycomm.git 21 | cd pycomm 22 | sudo python setup.py install 23 | 24 | 25 | PyPi: 26 | :: 27 | 28 | pip install pycomm 29 | 30 | ab_comm 31 | ~~~~~~~ 32 | ab_comm is a module that contains a set of classes used to interface Rockwell PLCs using Ethernet/IP protocol. 33 | The "clx" class can be used to communicate with Compactlogix, Controllogix PLCs 34 | The "slc" can be used to communicate with Micrologix or SLC PLCs 35 | 36 | I tried to followCIP specifications volume 1 and 2 as well as `Rockwell Automation Publication 1756-PM020-EN-P - November 2012`_ . 37 | 38 | .. _Rockwell Automation Publication 1756-PM020-EN-P - November 2012: http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf 39 | 40 | See the following snippet for communication with a Controllogix PLC: 41 | 42 | :: 43 | 44 | from pycomm.ab_comm.clx import Driver as ClxDriver 45 | import logging 46 | 47 | 48 | if __name__ == '__main__': 49 | logging.basicConfig( 50 | filename="ClxDriver.log", 51 | format="%(levelname)-10s %(asctime)s %(message)s", 52 | level=logging.DEBUG 53 | ) 54 | c = ClxDriver() 55 | 56 | if c.open('172.16.2.161'): 57 | 58 | print(c.read_tag(['ControlWord'])) 59 | print(c.read_tag(['parts', 'ControlWord', 'Counts'])) 60 | 61 | print(c.write_tag('Counts', -26, 'INT')) 62 | print(c.write_tag(('Counts', 26, 'INT'))) 63 | print(c.write_tag([('Counts', 26, 'INT')])) 64 | print(c.write_tag([('Counts', -26, 'INT'), ('ControlWord', -30, 'DINT'), ('parts', 31, 'DINT')])) 65 | 66 | # To read an array 67 | r_array = c.read_array("TotalCount", 1750) 68 | for tag in r_array: 69 | print (tag) 70 | 71 | 72 | # To read string 73 | c.write_string('TEMP_STRING', 'my_value') 74 | c.read_string('TEMP_STRING') 75 | 76 | # reset tha array to all 0 77 | w_array = [] 78 | for i in xrange(1750): 79 | w_array.append(0) 80 | c.write_array("TotalCount", w_array, "SINT") 81 | 82 | c.close() 83 | 84 | 85 | 86 | 87 | See the following snippet for communication with a Micrologix PLC: 88 | 89 | 90 | :: 91 | 92 | from pycomm.ab_comm.slc import Driver as SlcDriver 93 | import logging 94 | 95 | 96 | if __name__ == '__main__': 97 | logging.basicConfig( 98 | filename="SlcDriver.log", 99 | format="%(levelname)-10s %(asctime)s %(message)s", 100 | level=logging.DEBUG 101 | ) 102 | c = SlcDriver() 103 | if c.open('172.16.2.160'): 104 | 105 | print c.read_tag('S:1/5') 106 | print c.read_tag('S:60', 2) 107 | 108 | print c.write_tag('N7:0', [-30, 32767, -32767]) 109 | print c.write_tag('N7:0', 21) 110 | print c.read_tag('N7:0', 10) 111 | 112 | print c.write_tag('F8:0', [3.1, 4.95, -32.89]) 113 | print c.write_tag('F8:0', 21) 114 | print c.read_tag('F8:0', 3) 115 | 116 | print c.write_tag('B3:100', [23, -1, 4, 9]) 117 | print c.write_tag('B3:100', 21) 118 | print c.read_tag('B3:100', 4) 119 | 120 | print c.write_tag('T4:3.PRE', 431) 121 | print c.read_tag('T4:3.PRE') 122 | print c.write_tag('C5:0.PRE', 501) 123 | print c.read_tag('C5:0.PRE') 124 | print c.write_tag('T4:3.ACC', 432) 125 | print c.read_tag('T4:3.ACC') 126 | print c.write_tag('C5:0.ACC', 502) 127 | print c.read_tag('C5:0.ACC') 128 | 129 | c.write_tag('T4:2.EN', 0) 130 | c.write_tag('T4:2.TT', 0) 131 | c.write_tag('T4:2.DN', 0) 132 | print c.read_tag('T4:2.EN', 1) 133 | print c.read_tag('T4:2.TT', 1) 134 | print c.read_tag('T4:2.DN',) 135 | 136 | c.write_tag('C5:0.CU', 1) 137 | c.write_tag('C5:0.CD', 0) 138 | c.write_tag('C5:0.DN', 1) 139 | c.write_tag('C5:0.OV', 0) 140 | c.write_tag('C5:0.UN', 1) 141 | c.write_tag('C5:0.UA', 0) 142 | print c.read_tag('C5:0.CU') 143 | print c.read_tag('C5:0.CD') 144 | print c.read_tag('C5:0.DN') 145 | print c.read_tag('C5:0.OV') 146 | print c.read_tag('C5:0.UN') 147 | print c.read_tag('C5:0.UA') 148 | 149 | c.write_tag('B3:100', 1) 150 | print c.read_tag('B3:100') 151 | 152 | c.write_tag('B3/3955', 1) 153 | print c.read_tag('B3/3955') 154 | 155 | c.write_tag('N7:0/2', 1) 156 | print c.read_tag('N7:0/2') 157 | 158 | print c.write_tag('O:0.0/4', 1) 159 | print c.read_tag('O:0.0/4') 160 | 161 | c.close() 162 | 163 | 164 | The Future 165 | ~~~~~~~~~~ 166 | This package is under development. 167 | The modules _ab_comm.clx_ and _ab_comm.slc_ are completed at moment but other drivers will be added in the future. 168 | 169 | Thanks 170 | ~~~~~~ 171 | Thanks to patrickjmcd_ for the help with the Direct Connections and thanks in advance to anyone for feedback and suggestions. 172 | 173 | .. _patrickjmcd: https://github.com/patrickjmcd 174 | 175 | License 176 | ~~~~~~~ 177 | pycomm is distributed under the MIT License -------------------------------------------------------------------------------- /examples/test_clx_comm.py: -------------------------------------------------------------------------------- 1 | from pycomm.ab_comm.clx import Driver as ClxDriver 2 | import logging 3 | 4 | from time import sleep 5 | 6 | 7 | if __name__ == '__main__': 8 | 9 | logging.basicConfig( 10 | filename="ClxDriver.log", 11 | format="%(levelname)-10s %(asctime)s %(message)s", 12 | level=logging.DEBUG 13 | ) 14 | c = ClxDriver() 15 | 16 | print c['port'] 17 | print c.__version__ 18 | 19 | 20 | if c.open('172.16.2.161'): 21 | while 1: 22 | try: 23 | print(c.read_tag(['ControlWord'])) 24 | print(c.read_tag(['parts', 'ControlWord', 'Counts'])) 25 | 26 | print(c.write_tag('Counts', -26, 'INT')) 27 | print(c.write_tag(('Counts', 26, 'INT'))) 28 | print(c.write_tag([('Counts', 26, 'INT')])) 29 | print(c.write_tag([('Counts', -26, 'INT'), ('ControlWord', -30, 'DINT'), ('parts', 31, 'DINT')])) 30 | sleep(1) 31 | except Exception as e: 32 | c.close() 33 | print e 34 | pass 35 | 36 | # To read an array 37 | r_array = c.read_array("TotalCount", 1750) 38 | for tag in r_array: 39 | print (tag) 40 | 41 | c.close() 42 | -------------------------------------------------------------------------------- /examples/test_slc_only.py: -------------------------------------------------------------------------------- 1 | __author__ = 'agostino' 2 | 3 | from pycomm.ab_comm.slc import Driver as SlcDriver 4 | import logging 5 | 6 | if __name__ == '__main__': 7 | logging.basicConfig( 8 | filename="SlcDriver.log", 9 | format="%(levelname)-10s %(asctime)s %(message)s", 10 | level=logging.DEBUG 11 | ) 12 | c = SlcDriver() 13 | if c.open('192.168.1.15'): 14 | 15 | while 1: 16 | try: 17 | print c.read_tag('S:1/5') 18 | print c.read_tag('S:60', 2) 19 | 20 | print c.write_tag('N7:0', [-30, 32767, -32767]) 21 | print c.write_tag('N7:0', 21) 22 | print c.read_tag('N7:0', 10) 23 | 24 | print c.write_tag('F8:0', [3.1, 4.95, -32.89]) 25 | print c.write_tag('F8:0', 21) 26 | print c.read_tag('F8:0', 3) 27 | 28 | print c.write_tag('B3:100', [23, -1, 4, 9]) 29 | print c.write_tag('B3:100', 21) 30 | print c.read_tag('B3:100', 4) 31 | 32 | print c.write_tag('T4:3.PRE', 431) 33 | print c.read_tag('T4:3.PRE') 34 | print c.write_tag('C5:0.PRE', 501) 35 | print c.read_tag('C5:0.PRE') 36 | print c.write_tag('T4:3.ACC', 432) 37 | print c.read_tag('T4:3.ACC') 38 | print c.write_tag('C5:0.ACC', 502) 39 | print c.read_tag('C5:0.ACC') 40 | 41 | c.write_tag('T4:2.EN', 0) 42 | c.write_tag('T4:2.TT', 0) 43 | c.write_tag('T4:2.DN', 0) 44 | print c.read_tag('T4:2.EN', 1) 45 | print c.read_tag('T4:2.TT', 1) 46 | print c.read_tag('T4:2.DN',) 47 | 48 | c.write_tag('C5:0.CU', 1) 49 | c.write_tag('C5:0.CD', 0) 50 | c.write_tag('C5:0.DN', 1) 51 | c.write_tag('C5:0.OV', 0) 52 | c.write_tag('C5:0.UN', 1) 53 | c.write_tag('C5:0.UA', 0) 54 | print c.read_tag('C5:0.CU') 55 | print c.read_tag('C5:0.CD') 56 | print c.read_tag('C5:0.DN') 57 | print c.read_tag('C5:0.OV') 58 | print c.read_tag('C5:0.UN') 59 | print c.read_tag('C5:0.UA') 60 | 61 | c.write_tag('B3:100', 1) 62 | print c.read_tag('B3:100') 63 | 64 | c.write_tag('B3/3955', 1) 65 | print c.read_tag('B3/3955') 66 | 67 | c.write_tag('N7:0/2', 1) 68 | print c.read_tag('N7:0/2') 69 | 70 | print c.write_tag('O:0.0/4', 1) 71 | print c.read_tag('O:0.0/4') 72 | except Exception as e: 73 | print e 74 | pass 75 | c.close() 76 | -------------------------------------------------------------------------------- /pycomm/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'agostino' 2 | -------------------------------------------------------------------------------- /pycomm/ab_comm/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'agostino' 2 | import logging 3 | -------------------------------------------------------------------------------- /pycomm/ab_comm/clx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # clx.py - Ethernet/IP Client for Rockwell PLCs 4 | # 5 | # 6 | # Copyright (c) 2014 Agostino Ruscito 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | from pycomm.cip.cip_base import * 27 | import logging 28 | try: # Python 2.7+ 29 | from logging import NullHandler 30 | except ImportError: 31 | class NullHandler(logging.Handler): 32 | def emit(self, record): 33 | pass 34 | 35 | logger = logging.getLogger(__name__) 36 | logger.addHandler(NullHandler()) 37 | 38 | string_sizes = [82, 12, 16, 20, 40, 8] 39 | 40 | 41 | class Driver(Base): 42 | """ 43 | This Ethernet/IP client is based on Rockwell specification. Please refer to the link below for details. 44 | 45 | http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf 46 | 47 | The following services have been implemented: 48 | - Read Tag Service (0x4c) 49 | - Read Tag Fragment Service (0x52) 50 | - Write Tag Service (0x4d) 51 | - Write Tag Fragment Service (0x53) 52 | - Multiple Service Packet (0x0a) 53 | 54 | The client has been successfully tested with the following PLCs: 55 | - CompactLogix 5330ERM 56 | - CompactLogix 5370 57 | - ControlLogix 5572 and 1756-EN2T Module 58 | 59 | """ 60 | 61 | def __init__(self): 62 | super(Driver, self).__init__() 63 | 64 | self._buffer = {} 65 | self._get_template_in_progress = False 66 | self.__version__ = '0.2' 67 | 68 | def get_last_tag_read(self): 69 | """ Return the last tag read by a multi request read 70 | 71 | :return: A tuple (tag name, value, type) 72 | """ 73 | return self._last_tag_read 74 | 75 | def get_last_tag_write(self): 76 | """ Return the last tag write by a multi request write 77 | 78 | :return: A tuple (tag name, 'GOOD') if the write was successful otherwise (tag name, 'BAD') 79 | """ 80 | return self._last_tag_write 81 | 82 | def _parse_instance_attribute_list(self, start_tag_ptr, status): 83 | """ extract the tags list from the message received 84 | 85 | :param start_tag_ptr: The point in the message string where the tag list begin 86 | :param status: The status of the message receives 87 | """ 88 | tags_returned = self._reply[start_tag_ptr:] 89 | tags_returned_length = len(tags_returned) 90 | idx = 0 91 | instance = 0 92 | count = 0 93 | try: 94 | while idx < tags_returned_length: 95 | instance = unpack_dint(tags_returned[idx:idx+4]) 96 | idx += 4 97 | tag_length = unpack_uint(tags_returned[idx:idx+2]) 98 | idx += 2 99 | tag_name = tags_returned[idx:idx+tag_length] 100 | idx += tag_length 101 | symbol_type = unpack_uint(tags_returned[idx:idx+2]) 102 | idx += 2 103 | count += 1 104 | self._tag_list.append({'instance_id': instance, 105 | 'tag_name': tag_name, 106 | 'symbol_type': symbol_type}) 107 | except Exception as e: 108 | raise DataError(e) 109 | 110 | if status == SUCCESS: 111 | self._last_instance = -1 112 | elif status == 0x06: 113 | self._last_instance = instance + 1 114 | else: 115 | self._status = (1, 'unknown status during _parse_tag_list') 116 | self._last_instance = -1 117 | 118 | def _parse_structure_makeup_attributes(self, start_tag_ptr, status): 119 | """ extract the tags list from the message received 120 | 121 | :param start_tag_ptr: The point in the message string where the tag list begin 122 | :param status: The status of the message receives 123 | """ 124 | self._buffer = {} 125 | 126 | if status != SUCCESS: 127 | self._buffer['Error'] = status 128 | return 129 | 130 | attribute = self._reply[start_tag_ptr:] 131 | idx = 4 132 | try: 133 | if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: 134 | idx += 2 135 | self._buffer['object_definition_size'] = unpack_dint(attribute[idx:idx + 4]) 136 | else: 137 | self._buffer['Error'] = 'object_definition Error' 138 | return 139 | 140 | idx += 6 141 | if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: 142 | idx += 2 143 | self._buffer['structure_size'] = unpack_dint(attribute[idx:idx + 4]) 144 | else: 145 | self._buffer['Error'] = 'structure Error' 146 | return 147 | 148 | idx += 6 149 | if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: 150 | idx += 2 151 | self._buffer['member_count'] = unpack_uint(attribute[idx:idx + 2]) 152 | else: 153 | self._buffer['Error'] = 'member_count Error' 154 | return 155 | 156 | idx += 4 157 | if unpack_uint(attribute[idx:idx + 2]) == SUCCESS: 158 | idx += 2 159 | self._buffer['structure_handle'] = unpack_uint(attribute[idx:idx + 2]) 160 | else: 161 | self._buffer['Error'] = 'structure_handle Error' 162 | return 163 | 164 | return self._buffer 165 | 166 | except Exception as e: 167 | raise DataError(e) 168 | 169 | def _parse_template(self, start_tag_ptr, status): 170 | """ extract the tags list from the message received 171 | 172 | :param start_tag_ptr: The point in the message string where the tag list begin 173 | :param status: The status of the message receives 174 | """ 175 | tags_returned = self._reply[start_tag_ptr:] 176 | bytes_received = len(tags_returned) 177 | 178 | self._buffer += tags_returned 179 | 180 | if status == SUCCESS: 181 | self._get_template_in_progress = False 182 | 183 | elif status == 0x06: 184 | self._byte_offset += bytes_received 185 | else: 186 | self._status = (1, 'unknown status {0} during _parse_template'.format(status)) 187 | logger.warning(self._status) 188 | self._last_instance = -1 189 | 190 | def _parse_fragment(self, start_ptr, status): 191 | """ parse the fragment returned by a fragment service. 192 | 193 | :param start_ptr: Where the fragment start within the replay 194 | :param status: status field used to decide if keep parsing or stop 195 | """ 196 | 197 | try: 198 | data_type = unpack_uint(self._reply[start_ptr:start_ptr+2]) 199 | fragment_returned = self._reply[start_ptr+2:] 200 | except Exception as e: 201 | raise DataError(e) 202 | 203 | fragment_returned_length = len(fragment_returned) 204 | idx = 0 205 | 206 | while idx < fragment_returned_length: 207 | try: 208 | typ = I_DATA_TYPE[data_type] 209 | if self._output_raw: 210 | value = fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]] 211 | else: 212 | value = UNPACK_DATA_FUNCTION[typ](fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]]) 213 | idx += DATA_FUNCTION_SIZE[typ] 214 | except Exception as e: 215 | raise DataError(e) 216 | if self._output_raw: 217 | self._tag_list += value 218 | else: 219 | self._tag_list.append((self._last_position, value)) 220 | self._last_position += 1 221 | 222 | if status == SUCCESS: 223 | self._byte_offset = -1 224 | elif status == 0x06: 225 | self._byte_offset += fragment_returned_length 226 | else: 227 | self._status = (2, '{0}: {1}'.format(SERVICE_STATUS[status], get_extended_status(self._reply, 48))) 228 | logger.warning(self._status) 229 | self._byte_offset = -1 230 | 231 | def _parse_multiple_request_read(self, tags): 232 | """ parse the message received from a multi request read: 233 | 234 | For each tag parsed, the information extracted includes the tag name, the value read and the data type. 235 | Those information are appended to the tag list as tuple 236 | 237 | :return: the tag list 238 | """ 239 | offset = 50 240 | position = 50 241 | try: 242 | number_of_service_replies = unpack_uint(self._reply[offset:offset+2]) 243 | tag_list = [] 244 | for index in range(number_of_service_replies): 245 | position += 2 246 | start = offset + unpack_uint(self._reply[position:position+2]) 247 | general_status = unpack_usint(self._reply[start+2:start+3]) 248 | 249 | if general_status == 0: 250 | data_type = unpack_uint(self._reply[start+4:start+6]) 251 | value_begin = start + 6 252 | value_end = value_begin + DATA_FUNCTION_SIZE[I_DATA_TYPE[data_type]] 253 | value = self._reply[value_begin:value_end] 254 | self._last_tag_read = (tags[index], UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](value), 255 | I_DATA_TYPE[data_type]) 256 | else: 257 | self._last_tag_read = (tags[index], None, None) 258 | 259 | tag_list.append(self._last_tag_read) 260 | 261 | return tag_list 262 | except Exception as e: 263 | raise DataError(e) 264 | 265 | def _parse_multiple_request_write(self, tags): 266 | """ parse the message received from a multi request writ: 267 | 268 | For each tag parsed, the information extracted includes the tag name and the status of the writing. 269 | Those information are appended to the tag list as tuple 270 | 271 | :return: the tag list 272 | """ 273 | offset = 50 274 | position = 50 275 | try: 276 | number_of_service_replies = unpack_uint(self._reply[offset:offset+2]) 277 | tag_list = [] 278 | for index in range(number_of_service_replies): 279 | position += 2 280 | start = offset + unpack_uint(self._reply[position:position+2]) 281 | general_status = unpack_usint(self._reply[start+2:start+3]) 282 | 283 | if general_status == 0: 284 | self._last_tag_write = (tags[index] + ('GOOD',)) 285 | else: 286 | self._last_tag_write = (tags[index] + ('BAD',)) 287 | 288 | tag_list.append(self._last_tag_write) 289 | return tag_list 290 | except Exception as e: 291 | raise DataError(e) 292 | 293 | def _check_reply(self): 294 | """ check the replayed message for error 295 | 296 | """ 297 | self._more_packets_available = False 298 | try: 299 | if self._reply is None: 300 | self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])]) 301 | return False 302 | # Get the type of command 303 | typ = unpack_uint(self._reply[:2]) 304 | 305 | # Encapsulation status check 306 | if unpack_dint(self._reply[8:12]) != SUCCESS: 307 | self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ], 308 | SERVICE_STATUS[unpack_dint(self._reply[8:12])])) 309 | return False 310 | 311 | # Command Specific Status check 312 | if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]): 313 | status = unpack_usint(self._reply[42:43]) 314 | if status != SUCCESS: 315 | self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format( 316 | SERVICE_STATUS[status], get_extended_status(self._reply, 42))) 317 | return False 318 | else: 319 | return True 320 | elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]): 321 | status = unpack_usint(self._reply[48:49]) 322 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]: 323 | self._parse_fragment(50, status) 324 | return True 325 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]: 326 | self._parse_instance_attribute_list(50, status) 327 | return True 328 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Attributes"]: 329 | self._parse_structure_makeup_attributes(50, status) 330 | return True 331 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Template"] and \ 332 | self._get_template_in_progress: 333 | self._parse_template(50, status) 334 | return True 335 | if status == 0x06: 336 | self._status = (3, "Insufficient Packet Space") 337 | self._more_packets_available = True 338 | elif status != SUCCESS: 339 | self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format( 340 | SERVICE_STATUS[status], get_extended_status(self._reply, 48))) 341 | logger.warning(self._status) 342 | return False 343 | else: 344 | return True 345 | 346 | return True 347 | except Exception as e: 348 | raise DataError(e) 349 | 350 | def read_tag(self, tag): 351 | """ read tag from a connected plc 352 | 353 | Possible combination can be passed to this method: 354 | - ('Counts') a single tag name 355 | - (['ControlWord']) a list with one tag or many 356 | - (['parts', 'ControlWord', 'Counts']) 357 | 358 | At the moment there is not a strong validation for the argument passed. The user should verify 359 | the correctness of the format passed. 360 | 361 | :return: None is returned in case of error otherwise the tag list is returned 362 | """ 363 | self.clear() 364 | multi_requests = False 365 | if isinstance(tag, list): 366 | multi_requests = True 367 | 368 | if not self._target_is_connected: 369 | if not self.forward_open(): 370 | self._status = (6, "Target did not connected. read_tag will not be executed.") 371 | logger.warning(self._status) 372 | raise DataError("Target did not connected. read_tag will not be executed.") 373 | 374 | if multi_requests: 375 | rp_list = [] 376 | for t in tag: 377 | rp = create_tag_rp(t, multi_requests=True) 378 | if rp is None: 379 | self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) 380 | raise DataError("Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) 381 | else: 382 | rp_list.append(chr(TAG_SERVICES_REQUEST['Read Tag']) + rp + pack_uint(1)) 383 | message_request = build_multiple_service(rp_list, Base._get_sequence()) 384 | 385 | else: 386 | rp = create_tag_rp(tag) 387 | if rp is None: 388 | self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) 389 | return None 390 | else: 391 | # Creating the Message Request Packet 392 | message_request = [ 393 | pack_uint(Base._get_sequence()), 394 | chr(TAG_SERVICES_REQUEST['Read Tag']), # the Request Service 395 | chr(len(rp) / 2), # the Request Path Size length in word 396 | rp, # the request path 397 | pack_uint(1) 398 | ] 399 | 400 | if self.send_unit_data( 401 | build_common_packet_format( 402 | DATA_ITEM['Connected'], 403 | ''.join(message_request), 404 | ADDRESS_ITEM['Connection Based'], 405 | addr_data=self._target_cid, 406 | )) is None: 407 | raise DataError("send_unit_data returned not valid data") 408 | 409 | if multi_requests: 410 | return self._parse_multiple_request_read(tag) 411 | else: 412 | # Get the data type 413 | if self._status[0] == SUCCESS: 414 | data_type = unpack_uint(self._reply[50:52]) 415 | try: 416 | return UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](self._reply[52:]), I_DATA_TYPE[data_type] 417 | except Exception as e: 418 | raise DataError(e) 419 | else: 420 | return None 421 | 422 | def read_array(self, tag, counts, raw=False): 423 | """ read array of atomic data type from a connected plc 424 | 425 | At the moment there is not a strong validation for the argument passed. The user should verify 426 | the correctness of the format passed. 427 | 428 | :param tag: the name of the tag to read 429 | :param counts: the number of element to read 430 | :param raw: the value should output as raw-value (hex) 431 | :return: None is returned in case of error otherwise the tag list is returned 432 | """ 433 | self.clear() 434 | if not self._target_is_connected: 435 | if not self.forward_open(): 436 | self._status = (7, "Target did not connected. read_tag will not be executed.") 437 | logger.warning(self._status) 438 | raise DataError("Target did not connected. read_tag will not be executed.") 439 | 440 | self._byte_offset = 0 441 | self._last_position = 0 442 | self._output_raw = raw 443 | 444 | if self._output_raw: 445 | self._tag_list = '' 446 | else: 447 | self._tag_list = [] 448 | while self._byte_offset != -1: 449 | rp = create_tag_rp(tag) 450 | if rp is None: 451 | self._status = (7, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag)) 452 | return None 453 | else: 454 | # Creating the Message Request Packet 455 | message_request = [ 456 | pack_uint(Base._get_sequence()), 457 | chr(TAG_SERVICES_REQUEST["Read Tag Fragmented"]), # the Request Service 458 | chr(len(rp) / 2), # the Request Path Size length in word 459 | rp, # the request path 460 | pack_uint(counts), 461 | pack_dint(self._byte_offset) 462 | ] 463 | 464 | if self.send_unit_data( 465 | build_common_packet_format( 466 | DATA_ITEM['Connected'], 467 | ''.join(message_request), 468 | ADDRESS_ITEM['Connection Based'], 469 | addr_data=self._target_cid, 470 | )) is None: 471 | raise DataError("send_unit_data returned not valid data") 472 | 473 | return self._tag_list 474 | 475 | def write_tag(self, tag, value=None, typ=None): 476 | """ write tag/tags from a connected plc 477 | 478 | Possible combination can be passed to this method: 479 | - ('tag name', Value, data type) as single parameters or inside a tuple 480 | - ([('tag name', Value, data type), ('tag name2', Value, data type)]) as array of tuples 481 | 482 | At the moment there is not a strong validation for the argument passed. The user should verify 483 | the correctness of the format passed. 484 | 485 | The type accepted are: 486 | - BOOL 487 | - SINT 488 | - INT' 489 | - DINT 490 | - REAL 491 | - LINT 492 | - BYTE 493 | - WORD 494 | - DWORD 495 | - LWORD 496 | 497 | :param tag: tag name, or an array of tuple containing (tag name, value, data type) 498 | :param value: the value to write or none if tag is an array of tuple or a tuple 499 | :param typ: the type of the tag to write or none if tag is an array of tuple or a tuple 500 | :return: None is returned in case of error otherwise the tag list is returned 501 | """ 502 | self.clear() # cleanup error string 503 | multi_requests = False 504 | if isinstance(tag, list): 505 | multi_requests = True 506 | 507 | if not self._target_is_connected: 508 | if not self.forward_open(): 509 | self._status = (8, "Target did not connected. write_tag will not be executed.") 510 | logger.warning(self._status) 511 | raise DataError("Target did not connected. write_tag will not be executed.") 512 | 513 | if multi_requests: 514 | rp_list = [] 515 | tag_to_remove = [] 516 | idx = 0 517 | for name, value, typ in tag: 518 | # Create the request path to wrap the tag name 519 | rp = create_tag_rp(name, multi_requests=True) 520 | if rp is None: 521 | self._status = (8, "Cannot create tag{0} req. packet. write_tag will not be executed".format(tag)) 522 | return None 523 | else: 524 | try: # Trying to add the rp to the request path list 525 | val = PACK_DATA_FUNCTION[typ](value) 526 | rp_list.append( 527 | chr(TAG_SERVICES_REQUEST['Write Tag']) 528 | + rp 529 | + pack_uint(S_DATA_TYPE[typ]) 530 | + pack_uint(1) 531 | + val 532 | ) 533 | idx += 1 534 | except (LookupError, struct.error) as e: 535 | self._status = (8, "Tag:{0} type:{1} removed from write list. Error:{2}.".format(name, typ, e)) 536 | 537 | # The tag in idx position need to be removed from the rp list because has some kind of error 538 | tag_to_remove.append(idx) 539 | 540 | # Remove the tags that have not been inserted in the request path list 541 | for position in tag_to_remove: 542 | del tag[position] 543 | # Create the message request 544 | message_request = build_multiple_service(rp_list, Base._get_sequence()) 545 | 546 | else: 547 | if isinstance(tag, tuple): 548 | name, value, typ = tag 549 | else: 550 | name = tag 551 | 552 | rp = create_tag_rp(name) 553 | if rp is None: 554 | self._status = (8, "Cannot create tag {0} request packet. write_tag will not be executed.".format(tag)) 555 | logger.warning(self._status) 556 | return None 557 | else: 558 | # Creating the Message Request Packet 559 | message_request = [ 560 | pack_uint(Base._get_sequence()), 561 | chr(TAG_SERVICES_REQUEST["Write Tag"]), # the Request Service 562 | chr(len(rp) / 2), # the Request Path Size length in word 563 | rp, # the request path 564 | pack_uint(S_DATA_TYPE[typ]), # data type 565 | pack_uint(1), # Add the number of tag to write 566 | PACK_DATA_FUNCTION[typ](value) 567 | ] 568 | 569 | ret_val = self.send_unit_data( 570 | build_common_packet_format( 571 | DATA_ITEM['Connected'], 572 | ''.join(message_request), 573 | ADDRESS_ITEM['Connection Based'], 574 | addr_data=self._target_cid, 575 | ) 576 | ) 577 | 578 | if multi_requests: 579 | return self._parse_multiple_request_write(tag) 580 | else: 581 | if ret_val is None: 582 | raise DataError("send_unit_data returned not valid data") 583 | return ret_val 584 | 585 | def write_array(self, tag, values, data_type, raw=False): 586 | """ write array of atomic data type from a connected plc 587 | At the moment there is not a strong validation for the argument passed. The user should verify 588 | the correctness of the format passed. 589 | :param tag: the name of the tag to read 590 | :param data_type: the type of tag to write 591 | :param values: the array of values to write, if raw: the frame with bytes 592 | :param raw: indicates that the values are given as raw values (hex) 593 | """ 594 | self.clear() 595 | if not isinstance(values, list): 596 | self._status = (9, "A list of tags must be passed to write_array.") 597 | logger.warning(self._status) 598 | raise DataError("A list of tags must be passed to write_array.") 599 | 600 | if not self._target_is_connected: 601 | if not self.forward_open(): 602 | self._status = (9, "Target did not connected. write_array will not be executed.") 603 | logger.warning(self._status) 604 | raise DataError("Target did not connected. write_array will not be executed.") 605 | 606 | array_of_values = "" 607 | byte_size = 0 608 | byte_offset = 0 609 | 610 | for i, value in enumerate(values): 611 | if raw: 612 | array_of_values += value 613 | else: 614 | array_of_values += PACK_DATA_FUNCTION[data_type](value) 615 | byte_size += DATA_FUNCTION_SIZE[data_type] 616 | 617 | if byte_size >= 450 or i == len(values)-1: 618 | # create the message and send the fragment 619 | rp = create_tag_rp(tag) 620 | if rp is None: 621 | self._status = (9, "Cannot create tag {0} request packet. \ 622 | write_array will not be executed.".format(tag)) 623 | return None 624 | else: 625 | # Creating the Message Request Packet 626 | message_request = [ 627 | pack_uint(Base._get_sequence()), 628 | chr(TAG_SERVICES_REQUEST["Write Tag Fragmented"]), # the Request Service 629 | chr(len(rp) / 2), # the Request Path Size length in word 630 | rp, # the request path 631 | pack_uint(S_DATA_TYPE[data_type]), # Data type to write 632 | pack_uint(len(values)), # Number of elements to write 633 | pack_dint(byte_offset), 634 | array_of_values # Fragment of elements to write 635 | ] 636 | byte_offset += byte_size 637 | 638 | if self.send_unit_data( 639 | build_common_packet_format( 640 | DATA_ITEM['Connected'], 641 | ''.join(message_request), 642 | ADDRESS_ITEM['Connection Based'], 643 | addr_data=self._target_cid, 644 | )) is None: 645 | raise DataError("send_unit_data returned not valid data") 646 | array_of_values = "" 647 | byte_size = 0 648 | 649 | def _get_instance_attribute_list_service(self): 650 | """ Step 1: Finding user-created controller scope tags in a Logix5000 controller 651 | 652 | This service returns instance IDs for each created instance of the symbol class, along with a list 653 | of the attribute data associated with the requested attribute 654 | """ 655 | try: 656 | if not self._target_is_connected: 657 | if not self.forward_open(): 658 | self._status = (10, "Target did not connected. get_tag_list will not be executed.") 659 | logger.warning(self._status) 660 | raise DataError("Target did not connected. get_tag_list will not be executed.") 661 | 662 | self._last_instance = 0 663 | 664 | self._get_template_in_progress = True 665 | while self._last_instance != -1: 666 | 667 | # Creating the Message Request Packet 668 | 669 | message_request = [ 670 | pack_uint(Base._get_sequence()), 671 | chr(TAG_SERVICES_REQUEST['Get Instance Attributes List']), # STEP 1 672 | # the Request Path Size length in word 673 | chr(3), 674 | # Request Path ( 20 6B 25 00 Instance ) 675 | CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 676 | CLASS_CODE["Symbol Object"], # Logical segment: Symbolic Object 0x6B 677 | INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 678 | '\x00', 679 | pack_uint(self._last_instance), # The instance 680 | # Request Data 681 | pack_uint(2), # Number of attributes to retrieve 682 | pack_uint(1), # Attribute 1: Symbol name 683 | pack_uint(2) # Attribute 2: Symbol type 684 | ] 685 | 686 | if self.send_unit_data( 687 | build_common_packet_format( 688 | DATA_ITEM['Connected'], 689 | ''.join(message_request), 690 | ADDRESS_ITEM['Connection Based'], 691 | addr_data=self._target_cid, 692 | )) is None: 693 | raise DataError("send_unit_data returned not valid data") 694 | 695 | self._get_template_in_progress = False 696 | 697 | except Exception as e: 698 | raise DataError(e) 699 | 700 | def _get_structure_makeup(self, instance_id): 701 | """ 702 | get the structure makeup for a specific structure 703 | """ 704 | if not self._target_is_connected: 705 | if not self.forward_open(): 706 | self._status = (10, "Target did not connected. get_tag_list will not be executed.") 707 | logger.warning(self._status) 708 | raise DataError("Target did not connected. get_tag_list will not be executed.") 709 | 710 | message_request = [ 711 | pack_uint(self._get_sequence()), 712 | chr(TAG_SERVICES_REQUEST['Get Attributes']), 713 | chr(3), # Request Path ( 20 6B 25 00 Instance ) 714 | CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 715 | CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C 716 | INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 717 | '\x00', 718 | pack_uint(instance_id), 719 | pack_uint(4), # Number of attributes 720 | pack_uint(4), # Template Object Definition Size UDINT 721 | pack_uint(5), # Template Structure Size UDINT 722 | pack_uint(2), # Template Member Count UINT 723 | pack_uint(1) # Structure Handle We can use this to read and write UINT 724 | ] 725 | 726 | if self.send_unit_data( 727 | build_common_packet_format(DATA_ITEM['Connected'], 728 | ''.join(message_request), ADDRESS_ITEM['Connection Based'], 729 | addr_data=self._target_cid,)) is None: 730 | raise DataError("send_unit_data returned not valid data") 731 | 732 | return self._buffer 733 | 734 | def _read_template(self, instance_id, object_definition_size): 735 | """ get a list of the tags in the plc 736 | 737 | """ 738 | if not self._target_is_connected: 739 | if not self.forward_open(): 740 | self._status = (10, "Target did not connected. get_tag_list will not be executed.") 741 | logger.warning(self._status) 742 | raise DataError("Target did not connected. get_tag_list will not be executed.") 743 | 744 | self._byte_offset = 0 745 | self._buffer = "" 746 | self._get_template_in_progress = True 747 | 748 | try: 749 | while self._get_template_in_progress: 750 | 751 | # Creating the Message Request Packet 752 | 753 | message_request = [ 754 | pack_uint(self._get_sequence()), 755 | chr(TAG_SERVICES_REQUEST['Read Template']), 756 | chr(3), # Request Path ( 20 6B 25 00 Instance ) 757 | CLASS_ID["8-bit"], # Class id = 20 from spec 0x20 758 | CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C 759 | INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25 760 | '\x00', 761 | pack_uint(instance_id), 762 | pack_dint(self._byte_offset), # Offset 763 | pack_uint(((object_definition_size * 4)-23) - self._byte_offset) 764 | ] 765 | 766 | if not self.send_unit_data( 767 | build_common_packet_format(DATA_ITEM['Connected'], ''.join(message_request), 768 | ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)): 769 | raise DataError("send_unit_data returned not valid data") 770 | 771 | self._get_template_in_progress = False 772 | return self._buffer 773 | 774 | except Exception as e: 775 | raise DataError(e) 776 | 777 | def _isolating_user_tag(self): 778 | try: 779 | lst = self._tag_list 780 | self._tag_list = [] 781 | for tag in lst: 782 | if tag['tag_name'].find(':') != -1 or tag['tag_name'].find('__') != -1: 783 | continue 784 | if tag['symbol_type'] & 0b0001000000000000: 785 | continue 786 | dimension = (tag['symbol_type'] & 0b0110000000000000) >> 13 787 | 788 | if tag['symbol_type'] & 0b1000000000000000 : 789 | template_instance_id = tag['symbol_type'] & 0b0000111111111111 790 | tag_type = 'struct' 791 | data_type = 'user-created' 792 | self._tag_list.append({'instance_id': tag['instance_id'], 793 | 'template_instance_id': template_instance_id, 794 | 'tag_name': tag['tag_name'], 795 | 'dim': dimension, 796 | 'tag_type': tag_type, 797 | 'data_type': data_type, 798 | 'template': {}, 799 | 'udt': {}}) 800 | else: 801 | tag_type = 'atomic' 802 | datatype = tag['symbol_type'] & 0b0000000011111111 803 | data_type = I_DATA_TYPE[datatype] 804 | if datatype == 0xc1: 805 | bit_position = (tag['symbol_type'] & 0b0000011100000000) >> 8 806 | self._tag_list.append({'instance_id': tag['instance_id'], 807 | 'tag_name': tag['tag_name'], 808 | 'dim': dimension, 809 | 'tag_type': tag_type, 810 | 'data_type': data_type, 811 | 'bit_position' : bit_position}) 812 | else: 813 | self._tag_list.append({'instance_id': tag['instance_id'], 814 | 'tag_name': tag['tag_name'], 815 | 'dim': dimension, 816 | 'tag_type': tag_type, 817 | 'data_type': data_type}) 818 | except Exception as e: 819 | raise DataError(e) 820 | 821 | def _parse_udt_raw(self, tag): 822 | try: 823 | buff = self._read_template(tag['template_instance_id'], tag['template']['object_definition_size']) 824 | member_count = tag['template']['member_count'] 825 | names = buff.split('\00') 826 | lst = [] 827 | 828 | tag['udt']['name'] = 'Not an user defined structure' 829 | for name in names: 830 | if len(name) > 1: 831 | 832 | if name.find(';') != -1: 833 | tag['udt']['name'] = name[:name.find(';')] 834 | elif name.find('ZZZZZZZZZZ') != -1: 835 | continue 836 | elif name.isalpha(): 837 | lst.append(name) 838 | else: 839 | continue 840 | tag['udt']['internal_tags'] = lst 841 | 842 | type_list = [] 843 | 844 | for i in xrange(member_count): 845 | # skip member 1 846 | 847 | if i != 0: 848 | array_size = unpack_uint(buff[:2]) 849 | try: 850 | data_type = I_DATA_TYPE[unpack_uint(buff[2:4])] 851 | except Exception: 852 | data_type = "None" 853 | 854 | offset = unpack_dint(buff[4:8]) 855 | type_list.append((array_size, data_type, offset)) 856 | 857 | buff = buff[8:] 858 | 859 | tag['udt']['data_type'] = type_list 860 | except Exception as e: 861 | raise DataError(e) 862 | 863 | def get_tag_list(self): 864 | self._tag_list = [] 865 | # Step 1 866 | self._get_instance_attribute_list_service() 867 | 868 | # Step 2 869 | self._isolating_user_tag() 870 | 871 | # Step 3 872 | for tag in self._tag_list: 873 | if tag['tag_type'] == 'struct': 874 | tag['template'] = self._get_structure_makeup(tag['template_instance_id']) 875 | 876 | for idx, tag in enumerate(self._tag_list): 877 | # print (tag) 878 | if tag['tag_type'] == 'struct': 879 | self._parse_udt_raw(tag) 880 | 881 | # Step 4 882 | 883 | return self._tag_list 884 | 885 | def write_string(self, tag, value, size=82): 886 | """ 887 | Rockwell define different string size: 888 | STRING STRING_12 STRING_16 STRING_20 STRING_40 STRING_8 889 | by default we assume size 82 (STRING) 890 | """ 891 | if size not in string_sizes: 892 | raise DataError("String size is incorrect") 893 | 894 | data_tag = ".".join((tag, "DATA")) 895 | len_tag = ".".join((tag, "LEN")) 896 | 897 | # create an empty array 898 | data_to_send = [0] * size 899 | for idx, val in enumerate(value): 900 | data_to_send[idx] = ord(val) 901 | 902 | self.write_tag(len_tag, len(value), 'DINT') 903 | self.write_array(data_tag, data_to_send, 'SINT') 904 | 905 | def read_string(self, tag): 906 | data_tag = ".".join((tag, "DATA")) 907 | len_tag = ".".join((tag, "LEN")) 908 | length = self.read_tag(len_tag) 909 | values = self.read_array(data_tag, length[0]) 910 | values = [val[1] for val in values] 911 | char_array = [chr(ch) for ch in values] 912 | return ''.join(char_array) 913 | -------------------------------------------------------------------------------- /pycomm/ab_comm/slc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # clx.py - Ethernet/IP Client for Rockwell PLCs 4 | # 5 | # 6 | # Copyright (c) 2014 Agostino Ruscito 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | from pycomm.cip.cip_base import * 27 | import re 28 | import math 29 | #import binascii 30 | 31 | import logging 32 | try: # Python 2.7+ 33 | from logging import NullHandler 34 | except ImportError: 35 | class NullHandler(logging.Handler): 36 | def emit(self, record): 37 | pass 38 | 39 | logger = logging.getLogger(__name__) 40 | logger.addHandler(NullHandler()) 41 | 42 | 43 | def parse_tag(tag): 44 | t = re.search(r"(?P[CT])(?P\d{1,3})" 45 | r"(:)(?P\d{1,3})" 46 | r"(.)(?PACC|PRE|EN|DN|TT|CU|CD|DN|OV|UN|UA)", tag, flags=re.IGNORECASE) 47 | if t: 48 | if (1 <= int(t.group('file_number')) <= 255) \ 49 | and (0 <= int(t.group('element_number')) <= 255): 50 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 51 | 'file_number': t.group('file_number'), 52 | 'element_number': t.group('element_number'), 53 | 'sub_element': PCCC_CT[t.group('sub_element').upper()], 54 | 'read_func': '\xa2', 55 | 'write_func': '\xab', 56 | 'address_field': 3} 57 | 58 | t = re.search(r"(?P[LFBN])(?P\d{1,3})" 59 | r"(:)(?P\d{1,3})" 60 | r"(/(?P\d{1,2}))?", 61 | tag, flags=re.IGNORECASE) 62 | if t: 63 | if t.group('sub_element') is not None: 64 | if (1 <= int(t.group('file_number')) <= 255) \ 65 | and (0 <= int(t.group('element_number')) <= 255) \ 66 | and (0 <= int(t.group('sub_element')) <= 15): 67 | 68 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 69 | 'file_number': t.group('file_number'), 70 | 'element_number': t.group('element_number'), 71 | 'sub_element': t.group('sub_element'), 72 | 'read_func': '\xa2', 73 | 'write_func': '\xab', 74 | 'address_field': 3} 75 | else: 76 | if (1 <= int(t.group('file_number')) <= 255) \ 77 | and (0 <= int(t.group('element_number')) <= 255): 78 | 79 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 80 | 'file_number': t.group('file_number'), 81 | 'element_number': t.group('element_number'), 82 | 'sub_element': t.group('sub_element'), 83 | 'read_func': '\xa2', 84 | 'write_func': '\xab', 85 | 'address_field': 2} 86 | 87 | t = re.search(r"(?P[IO])(:)(?P\d{1,3})" 88 | r"(.)(?P\d{1,3})" 89 | r"(/(?P\d{1,2}))?", tag, flags=re.IGNORECASE) 90 | if t: 91 | if t.group('sub_element') is not None: 92 | if (0 <= int(t.group('file_number')) <= 255) \ 93 | and (0 <= int(t.group('element_number')) <= 255) \ 94 | and (0 <= int(t.group('sub_element')) <= 15): 95 | 96 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 97 | 'file_number': t.group('file_number'), 98 | 'element_number': t.group('element_number'), 99 | 'sub_element': t.group('sub_element'), 100 | 'read_func': '\xa2', 101 | 'write_func': '\xab', 102 | 'address_field': 3} 103 | else: 104 | if (0 <= int(t.group('file_number')) <= 255) \ 105 | and (0 <= int(t.group('element_number')) <= 255): 106 | 107 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 108 | 'file_number': t.group('file_number'), 109 | 'element_number': t.group('element_number'), 110 | 'read_func': '\xa2', 111 | 'write_func': '\xab', 112 | 'address_field': 2} 113 | 114 | t = re.search(r"(?PS)" 115 | r"(:)(?P\d{1,3})" 116 | r"(/(?P\d{1,2}))?", tag, flags=re.IGNORECASE) 117 | if t: 118 | if t.group('sub_element') is not None: 119 | if (0 <= int(t.group('element_number')) <= 255) \ 120 | and (0 <= int(t.group('sub_element')) <= 15): 121 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 122 | 'file_number': '2', 123 | 'element_number': t.group('element_number'), 124 | 'sub_element': t.group('sub_element'), 125 | 'read_func': '\xa2', 126 | 'write_func': '\xab', 127 | 'address_field': 3} 128 | else: 129 | if 0 <= int(t.group('element_number')) <= 255: 130 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 131 | 'file_number': '2', 132 | 'element_number': t.group('element_number'), 133 | 'read_func': '\xa2', 134 | 'write_func': '\xab', 135 | 'address_field': 2} 136 | 137 | t = re.search(r"(?PB)(?P\d{1,3})" 138 | r"(/)(?P\d{1,4})", 139 | tag, flags=re.IGNORECASE) 140 | if t: 141 | if (1 <= int(t.group('file_number')) <= 255) \ 142 | and (0 <= int(t.group('element_number')) <= 4095): 143 | bit_position = int(t.group('element_number')) 144 | element_number = bit_position / 16 145 | sub_element = bit_position - (element_number * 16) 146 | return True, t.group(0), {'file_type': t.group('file_type').upper(), 147 | 'file_number': t.group('file_number'), 148 | 'element_number': element_number, 149 | 'sub_element': sub_element, 150 | 'read_func': '\xa2', 151 | 'write_func': '\xab', 152 | 'address_field': 3} 153 | 154 | return False, tag 155 | 156 | 157 | class Driver(Base): 158 | """ 159 | SLC/PLC_5 Implementation 160 | """ 161 | def __init__(self): 162 | super(Driver, self).__init__() 163 | 164 | self.__version__ = '0.1' 165 | self._last_sequence = 0 166 | 167 | def _check_reply(self): 168 | """ 169 | check the replayed message for error 170 | """ 171 | self._more_packets_available = False 172 | try: 173 | if self._reply is None: 174 | self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])]) 175 | return False 176 | # Get the type of command 177 | typ = unpack_uint(self._reply[:2]) 178 | 179 | # Encapsulation status check 180 | if unpack_dint(self._reply[8:12]) != SUCCESS: 181 | self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ], 182 | SERVICE_STATUS[unpack_dint(self._reply[8:12])])) 183 | return False 184 | 185 | # Command Specific Status check 186 | if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]): 187 | status = unpack_usint(self._reply[42:43]) 188 | if status != SUCCESS: 189 | self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format( 190 | SERVICE_STATUS[status], get_extended_status(self._reply, 42))) 191 | return False 192 | else: 193 | return True 194 | 195 | elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]): 196 | status = unpack_usint(self._reply[48:49]) 197 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]: 198 | self._parse_fragment(50, status) 199 | return True 200 | if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]: 201 | self._parse_tag_list(50, status) 202 | return True 203 | if status == 0x06: 204 | self._status = (3, "Insufficient Packet Space") 205 | self._more_packets_available = True 206 | elif status != SUCCESS: 207 | self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format( 208 | SERVICE_STATUS[status], get_extended_status(self._reply, 48))) 209 | return False 210 | else: 211 | return True 212 | 213 | return True 214 | except Exception as e: 215 | raise DataError(e) 216 | 217 | def __queue_data_available(self, queue_number): 218 | """ read the queue 219 | 220 | Possible combination can be passed to this method: 221 | print c.read_tag('F8:0', 3) return a list of 3 registers starting from F8:0 222 | print c.read_tag('F8:0') return one value 223 | 224 | It is possible to read status bit 225 | 226 | :return: None is returned in case of error 227 | """ 228 | 229 | # Creating the Message Request Packet 230 | self._last_sequence = pack_uint(Base._get_sequence()) 231 | 232 | # PCCC_Cmd_Rd_w3_Q2 = [0x0f, 0x00, 0x30, 0x00, 0xa2, 0x6d, 0x00, 0xa5, 0x02, 0x00] 233 | message_request = [ 234 | self._last_sequence, 235 | '\x4b', 236 | '\x02', 237 | CLASS_ID["8-bit"], 238 | PATH["PCCC"], 239 | '\x07', 240 | self.attribs['vid'], 241 | self.attribs['vsn'], 242 | '\x0f', 243 | '\x00', 244 | self._last_sequence[1], 245 | self._last_sequence[0], 246 | '\xa2', # protected typed logical read with three address fields FNC 247 | '\x6d', # Byte size to read = 109 248 | '\x00', # File Number 249 | '\xa5', # File Type 250 | pack_uint(queue_number) 251 | ] 252 | 253 | if self.send_unit_data( 254 | build_common_packet_format( 255 | DATA_ITEM['Connected'], 256 | ''.join(message_request), 257 | ADDRESS_ITEM['Connection Based'], 258 | addr_data=self._target_cid,)): 259 | 260 | sts = int(unpack_uint(self._reply[2:4])) 261 | if sts == 146: 262 | return True 263 | else: 264 | return False 265 | else: 266 | raise DataError("read_queue [send_unit_data] returned not valid data") 267 | 268 | def __save_record(self, filename): 269 | with open(filename, "a") as csv_file: 270 | logger.debug("SLC __save_record read:{0}".format(self._reply[61:])) 271 | csv_file.write(self._reply[61:]+'\n') 272 | csv_file.close() 273 | 274 | def __get_queue_size(self, queue_number): 275 | """ get queue size 276 | """ 277 | # Creating the Message Request Packet 278 | self._last_sequence = pack_uint(Base._get_sequence()) 279 | 280 | message_request = [ 281 | self._last_sequence, 282 | '\x4b', 283 | '\x02', 284 | CLASS_ID["8-bit"], 285 | PATH["PCCC"], 286 | '\x07', 287 | self.attribs['vid'], 288 | self.attribs['vsn'], 289 | '\x0f', 290 | '\x00', 291 | self._last_sequence[1], 292 | self._last_sequence[0], 293 | # '\x30', 294 | # '\x00', 295 | '\xa1', # FNC to get the queue size 296 | '\x06', # Byte size to read = 06 297 | '\x00', # File Number 298 | '\xea', # File Type ???? 299 | '\xff', # File Type ???? 300 | pack_uint(queue_number) 301 | ] 302 | 303 | if self.send_unit_data( 304 | build_common_packet_format( 305 | DATA_ITEM['Connected'], 306 | ''.join(message_request), 307 | ADDRESS_ITEM['Connection Based'], 308 | addr_data=self._target_cid,)): 309 | sts = int(unpack_uint(self._reply[65:67])) 310 | logger.debug("SLC __get_queue_size({0}) returned {1}".format(queue_number, sts)) 311 | return sts 312 | else: 313 | raise DataError("read_queue [send_unit_data] returned not valid data") 314 | 315 | def read_queue(self, queue_number, file_name): 316 | """ read the queue 317 | 318 | """ 319 | if not self._target_is_connected: 320 | if not self.forward_open(): 321 | self._status = (5, "Target did not connected. is_queue_available will not be executed.") 322 | logger.warning(self._status) 323 | raise DataError("Target did not connected. is_queue_available will not be executed.") 324 | 325 | if self.__queue_data_available(queue_number): 326 | logger.debug("SLC read_queue: Queue {0} has data".format(queue_number)) 327 | self.__save_record(file_name) 328 | size = self.__get_queue_size(queue_number) 329 | if size > 0: 330 | for i in range(0, size): 331 | if self.__queue_data_available(queue_number): 332 | self.__save_record(file_name) 333 | 334 | logger.debug("SLC read_queue: {0} record extract from queue {1}".format(size, queue_number)) 335 | else: 336 | logger.debug("SLC read_queue: Queue {0} has no data".format(queue_number)) 337 | 338 | def read_tag(self, tag, n=1): 339 | """ read tag from a connected plc 340 | 341 | Possible combination can be passed to this method: 342 | print c.read_tag('F8:0', 3) return a list of 3 registers starting from F8:0 343 | print c.read_tag('F8:0') return one value 344 | 345 | It is possible to read status bit 346 | 347 | :return: None is returned in case of error 348 | """ 349 | res = parse_tag(tag) 350 | if not res[0]: 351 | self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, n)) 352 | logger.warning(self._status) 353 | raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, n)) 354 | 355 | bit_read = False 356 | bit_position = 0 357 | sub_element = 0 358 | if int(res[2]['address_field'] == 3): 359 | bit_read = True 360 | bit_position = int(res[2]['sub_element']) 361 | 362 | if not self._target_is_connected: 363 | if not self.forward_open(): 364 | self._status = (5, "Target did not connected. read_tag will not be executed.") 365 | logger.warning(self._status) 366 | raise DataError("Target did not connected. read_tag will not be executed.") 367 | 368 | data_size = PCCC_DATA_SIZE[res[2]['file_type']] 369 | 370 | # Creating the Message Request Packet 371 | self._last_sequence = pack_uint(Base._get_sequence()) 372 | 373 | message_request = [ 374 | self._last_sequence, 375 | '\x4b', 376 | '\x02', 377 | CLASS_ID["8-bit"], 378 | PATH["PCCC"], 379 | '\x07', 380 | self.attribs['vid'], 381 | self.attribs['vsn'], 382 | '\x0f', 383 | '\x00', 384 | self._last_sequence[1], 385 | self._last_sequence[0], 386 | res[2]['read_func'], 387 | pack_usint(data_size * n), 388 | pack_usint(int(res[2]['file_number'])), 389 | PCCC_DATA_TYPE[res[2]['file_type']], 390 | pack_usint(int(res[2]['element_number'])), 391 | pack_usint(sub_element) 392 | ] 393 | 394 | logger.debug("SLC read_tag({0},{1})".format(tag, n)) 395 | if self.send_unit_data( 396 | build_common_packet_format( 397 | DATA_ITEM['Connected'], 398 | ''.join(message_request), 399 | ADDRESS_ITEM['Connection Based'], 400 | addr_data=self._target_cid,)): 401 | sts = int(unpack_usint(self._reply[58])) 402 | try: 403 | if sts != 0: 404 | sts_txt = PCCC_ERROR_CODE[sts] 405 | self._status = (1000, "Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n)) 406 | logger.warning(self._status) 407 | raise DataError("Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n)) 408 | 409 | new_value = 61 410 | if bit_read: 411 | if res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C': 412 | if bit_position == PCCC_CT['PRE']: 413 | return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( 414 | self._reply[new_value+2:new_value+2+data_size]) 415 | elif bit_position == PCCC_CT['ACC']: 416 | return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( 417 | self._reply[new_value+4:new_value+4+data_size]) 418 | 419 | tag_value = UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']]( 420 | self._reply[new_value:new_value+data_size]) 421 | return get_bit(tag_value, bit_position) 422 | 423 | else: 424 | values_list = [] 425 | while len(self._reply[new_value:]) >= data_size: 426 | values_list.append( 427 | UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](self._reply[new_value:new_value+data_size]) 428 | ) 429 | new_value = new_value+data_size 430 | 431 | if len(values_list) > 1: 432 | return values_list 433 | else: 434 | return values_list[0] 435 | 436 | except Exception as e: 437 | self._status = (1000, "Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n)) 438 | logger.warning(self._status) 439 | raise DataError("Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n)) 440 | else: 441 | raise DataError("send_unit_data returned not valid data") 442 | 443 | def write_tag(self, tag, value): 444 | """ write tag from a connected plc 445 | 446 | Possible combination can be passed to this method: 447 | c.write_tag('N7:0', [-30, 32767, -32767]) 448 | c.write_tag('N7:0', 21) 449 | c.read_tag('N7:0', 10) 450 | 451 | It is not possible to write status bit 452 | 453 | :return: None is returned in case of error 454 | """ 455 | res = parse_tag(tag) 456 | if not res[0]: 457 | self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, value)) 458 | logger.warning(self._status) 459 | raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, value)) 460 | 461 | if isinstance(value, list) and int(res[2]['address_field'] == 3): 462 | self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value)) 463 | logger.warning(self._status) 464 | raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value)) 465 | 466 | if isinstance(value, list) and int(res[2]['address_field'] == 3): 467 | self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value)) 468 | logger.warning(self._status) 469 | raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value)) 470 | 471 | bit_field = False 472 | bit_position = 0 473 | sub_element = 0 474 | if int(res[2]['address_field'] == 3): 475 | bit_field = True 476 | bit_position = int(res[2]['sub_element']) 477 | values_list = '' 478 | else: 479 | values_list = '\xff\xff' 480 | 481 | multi_requests = False 482 | if isinstance(value, list): 483 | multi_requests = True 484 | 485 | if not self._target_is_connected: 486 | if not self.forward_open(): 487 | self._status = (1000, "Target did not connected. write_tag will not be executed.") 488 | logger.warning(self._status) 489 | raise DataError("Target did not connected. write_tag will not be executed.") 490 | 491 | try: 492 | n = 0 493 | if multi_requests: 494 | data_size = PCCC_DATA_SIZE[res[2]['file_type']] 495 | for v in value: 496 | values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](v) 497 | n += 1 498 | else: 499 | n = 1 500 | if bit_field: 501 | data_size = 2 502 | 503 | if (res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C') \ 504 | and (bit_position == PCCC_CT['PRE'] or bit_position == PCCC_CT['ACC']): 505 | sub_element = bit_position 506 | values_list = '\xff\xff' + PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value) 507 | else: 508 | sub_element = 0 509 | if value > 0: 510 | values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(math.pow(2, bit_position)) 511 | else: 512 | values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(0) 513 | 514 | else: 515 | values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value) 516 | data_size = PCCC_DATA_SIZE[res[2]['file_type']] 517 | 518 | except Exception as e: 519 | self._status = (1000, "Error({0}) packing the values to write to the" 520 | "SLC write_tag({1},{2})".format(e, tag, value)) 521 | logger.warning(self._status) 522 | raise DataError("Error({0}) packing the values to write to the " 523 | "SLC write_tag({1},{2})".format(e, tag, value)) 524 | 525 | data_to_write = values_list 526 | 527 | # Creating the Message Request Packet 528 | self._last_sequence = pack_uint(Base._get_sequence()) 529 | 530 | message_request = [ 531 | self._last_sequence, 532 | '\x4b', 533 | '\x02', 534 | CLASS_ID["8-bit"], 535 | PATH["PCCC"], 536 | '\x07', 537 | self.attribs['vid'], 538 | self.attribs['vsn'], 539 | '\x0f', 540 | '\x00', 541 | self._last_sequence[1], 542 | self._last_sequence[0], 543 | res[2]['write_func'], 544 | pack_usint(data_size * n), 545 | pack_usint(int(res[2]['file_number'])), 546 | PCCC_DATA_TYPE[res[2]['file_type']], 547 | pack_usint(int(res[2]['element_number'])), 548 | pack_usint(sub_element) 549 | ] 550 | 551 | logger.debug("SLC write_tag({0},{1})".format(tag, value)) 552 | if self.send_unit_data( 553 | build_common_packet_format( 554 | DATA_ITEM['Connected'], 555 | ''.join(message_request) + data_to_write, 556 | ADDRESS_ITEM['Connection Based'], 557 | addr_data=self._target_cid,)): 558 | sts = int(unpack_usint(self._reply[58])) 559 | try: 560 | if sts != 0: 561 | sts_txt = PCCC_ERROR_CODE[sts] 562 | self._status = (1000, "Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value)) 563 | logger.warning(self._status) 564 | raise DataError("Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value)) 565 | 566 | return True 567 | except Exception as e: 568 | self._status = (1000, "Error({0}) parsing the data returned from " 569 | "SLC write_tag({1},{2})".format(e, tag, value)) 570 | logger.warning(self._status) 571 | raise DataError("Error({0}) parsing the data returned from " 572 | "SLC write_tag({1},{2})".format(e, tag, value)) 573 | else: 574 | raise DataError("send_unit_data returned not valid data") 575 | -------------------------------------------------------------------------------- /pycomm/cip/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'agostino' 2 | -------------------------------------------------------------------------------- /pycomm/cip/cip_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # cip_base.py - A set of classes methods and structures used to implement Ethernet/IP 4 | # 5 | # 6 | # Copyright (c) 2014 Agostino Ruscito 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | 27 | import struct 28 | import socket 29 | import random 30 | 31 | from os import getpid 32 | from pycomm.cip.cip_const import * 33 | from pycomm.common import PycommError 34 | 35 | 36 | import logging 37 | try: # Python 2.7+ 38 | from logging import NullHandler 39 | except ImportError: 40 | class NullHandler(logging.Handler): 41 | def emit(self, record): 42 | pass 43 | logger = logging.getLogger(__name__) 44 | logger.addHandler(NullHandler()) 45 | 46 | 47 | class CommError(PycommError): 48 | pass 49 | 50 | 51 | class DataError(PycommError): 52 | pass 53 | 54 | 55 | def pack_sint(n): 56 | return struct.pack('b', n) 57 | 58 | 59 | def pack_usint(n): 60 | return struct.pack('B', n) 61 | 62 | 63 | def pack_int(n): 64 | """pack 16 bit into 2 bytes little endian""" 65 | return struct.pack(' 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | 27 | ELEMENT_ID = { 28 | "8-bit": '\x28', 29 | "16-bit": '\x29', 30 | "32-bit": '\x2a' 31 | } 32 | 33 | CLASS_ID = { 34 | "8-bit": '\x20', 35 | "16-bit": '\x21', 36 | } 37 | 38 | INSTANCE_ID = { 39 | "8-bit": '\x24', 40 | "16-bit": '\x25' 41 | } 42 | 43 | ATTRIBUTE_ID = { 44 | "8-bit": '\x30', 45 | "16-bit": '\x31' 46 | } 47 | 48 | # Path are combined as: 49 | # CLASS_ID + PATHS 50 | # For example PCCC path is CLASS_ID["8-bit"]+PATH["PCCC"] -> 0x20, 0x67, 0x24, 0x01. 51 | PATH = { 52 | 'Connection Manager': '\x06\x24\x01', 53 | 'Router': '\x02\x24\x01', 54 | 'Backplane Data Type': '\x66\x24\x01', 55 | 'PCCC': '\x67\x24\x01', 56 | 'DHCP Channel A': '\xa6\x24\x01\x01\x2c\x01', 57 | 'DHCP Channel B': '\xa6\x24\x01\x02\x2c\x01' 58 | } 59 | 60 | ENCAPSULATION_COMMAND = { # Volume 2: 2-3.2 Command Field UINT 2 byte 61 | "nop": '\x00\x00', 62 | "list_targets": '\x01\x00', 63 | "list_services": '\x04\x00', 64 | "list_identity": '\x63\x00', 65 | "list_interfaces": '\x64\x00', 66 | "register_session": '\x65\x00', 67 | "unregister_session": '\x66\x00', 68 | "send_rr_data": '\x6F\x00', 69 | "send_unit_data": '\x70\x00' 70 | } 71 | 72 | """ 73 | When a tag is created, an instance of the Symbol Object (Class ID 0x6B) is created 74 | inside the controller. 75 | 76 | When a UDT is created, an instance of the Template object (Class ID 0x6C) is 77 | created to hold information about the structure makeup. 78 | """ 79 | CLASS_CODE = { 80 | "Message Router": '\x02', # Volume 1: 5-1 81 | "Symbol Object": '\x6b', 82 | "Template Object": '\x6c', 83 | "Connection Manager": '\x06' # Volume 1: 3-5 84 | } 85 | 86 | CONNECTION_MANAGER_INSTANCE = { 87 | 'Open Request': '\x01', 88 | 'Open Format Rejected': '\x02', 89 | 'Open Resource Rejected': '\x03', 90 | 'Open Other Rejected': '\x04', 91 | 'Close Request': '\x05', 92 | 'Close Format Request': '\x06', 93 | 'Close Other Request': '\x07', 94 | 'Connection Timeout': '\x08' 95 | } 96 | 97 | TAG_SERVICES_REQUEST = { 98 | "Read Tag": 0x4c, 99 | "Read Tag Fragmented": 0x52, 100 | "Write Tag": 0x4d, 101 | "Write Tag Fragmented": 0x53, 102 | "Read Modify Write Tag": 0x4e, 103 | "Multiple Service Packet": 0x0a, 104 | "Get Instance Attributes List": 0x55, 105 | "Get Attributes": 0x03, 106 | "Read Template": 0x4c, 107 | } 108 | 109 | TAG_SERVICES_REPLY = { 110 | 0xcc: "Read Tag", 111 | 0xd2: "Read Tag Fragmented", 112 | 0xcd: "Write Tag", 113 | 0xd3: "Write Tag Fragmented", 114 | 0xce: "Read Modify Write Tag", 115 | 0x8a: "Multiple Service Packet", 116 | 0xd5: "Get Instance Attributes List", 117 | 0x83: "Get Attributes", 118 | 0xcc: "Read Template" 119 | } 120 | 121 | 122 | I_TAG_SERVICES_REPLY = { 123 | "Read Tag": 0xcc, 124 | "Read Tag Fragmented": 0xd2, 125 | "Write Tag": 0xcd, 126 | "Write Tag Fragmented": 0xd3, 127 | "Read Modify Write Tag": 0xce, 128 | "Multiple Service Packet": 0x8a, 129 | "Get Instance Attributes List": 0xd5, 130 | "Get Attributes": 0x83, 131 | "Read Template": 0xcc 132 | } 133 | 134 | 135 | """ 136 | EtherNet/IP Encapsulation Error Codes 137 | 138 | Standard CIP Encapsulation Error returned in the cip message header 139 | """ 140 | STATUS = { 141 | 0x0000: "Success", 142 | 0x0001: "The sender issued an invalid or unsupported encapsulation command", 143 | 0x0002: "Insufficient memory", 144 | 0x0003: "Poorly formed or incorrect data in the data portion", 145 | 0x0064: "An originator used an invalid session handle when sending an encapsulation message to the target", 146 | 0x0065: "The target received a message of invalid length", 147 | 0x0069: "Unsupported Protocol Version" 148 | } 149 | 150 | """ 151 | MSG Error Codes: 152 | 153 | The following error codes have been taken from: 154 | 155 | Rockwell Automation Publication 156 | 1756-RM003P-EN-P - December 2014 157 | """ 158 | SERVICE_STATUS = { 159 | 0x01: "Connection failure (see extended status)", 160 | 0x02: "Insufficient resource", 161 | 0x03: "Invalid value", 162 | 0x04: "IOI syntax error. A syntax error was detected decoding the Request Path (see extended status)", 163 | 0x05: "Destination unknown, class unsupported, instance \nundefined or structure element undefined (see extended status)", 164 | 0x06: "Insufficient Packet Space", 165 | 0x07: "Connection lost", 166 | 0x08: "Service not supported", 167 | 0x09: "Error in data segment or invalid attribute value", 168 | 0x0A: "Attribute list error", 169 | 0x0B: "State already exist", 170 | 0x0C: "Object state conflict", 171 | 0x0D: "Object already exist", 172 | 0x0E: "Attribute not settable", 173 | 0x0F: "Permission denied", 174 | 0x10: "Device state conflict", 175 | 0x11: "Reply data too large", 176 | 0x12: "Fragmentation of a primitive value", 177 | 0x13: "Insufficient command data", 178 | 0x14: "Attribute not supported", 179 | 0x15: "Too much data", 180 | 0x1A: "Bridge request too large", 181 | 0x1B: "Bridge response too large", 182 | 0x1C: "Attribute list shortage", 183 | 0x1D: "Invalid attribute list", 184 | 0x1E: "Request service error", 185 | 0x1F: "Connection related failure (see extended status)", 186 | 0x22: "Invalid reply received", 187 | 0x25: "Key segment error", 188 | 0x26: "Invalid IOI error", 189 | 0x27: "Unexpected attribute in list", 190 | 0x28: "DeviceNet error - invalid member ID", 191 | 0x29: "DeviceNet error - member not settable", 192 | 0xD1: "Module not in run state", 193 | 0xFB: "Message port not supported", 194 | 0xFC: "Message unsupported data type", 195 | 0xFD: "Message uninitialized", 196 | 0xFE: "Message timeout", 197 | 0xff: "General Error (see extended status)" 198 | } 199 | 200 | EXTEND_CODES = { 201 | 0x01: { 202 | 0x0100: "Connection in use", 203 | 0x0103: "Transport not supported", 204 | 0x0106: "Ownership conflict", 205 | 0x0107: "Connection not found", 206 | 0x0108: "Invalid connection type", 207 | 0x0109: "Invalid connection size", 208 | 0x0110: "Module not configured", 209 | 0x0111: "EPR not supported", 210 | 0x0114: "Wrong module", 211 | 0x0115: "Wrong device type", 212 | 0x0116: "Wrong revision", 213 | 0x0118: "Invalid configuration format", 214 | 0x011A: "Application out of connections", 215 | 0x0203: "Connection timeout", 216 | 0x0204: "Unconnected message timeout", 217 | 0x0205: "Unconnected send parameter error", 218 | 0x0206: "Message too large", 219 | 0x0301: "No buffer memory", 220 | 0x0302: "Bandwidth not available", 221 | 0x0303: "No screeners available", 222 | 0x0305: "Signature match", 223 | 0x0311: "Port not available", 224 | 0x0312: "Link address not available", 225 | 0x0315: "Invalid segment type", 226 | 0x0317: "Connection not scheduled" 227 | }, 228 | 0x04: { 229 | 0x0000: "Extended status out of memory", 230 | 0x0001: "Extended status out of instances" 231 | }, 232 | 0x05: { 233 | 0x0000: "Extended status out of memory", 234 | 0x0001: "Extended status out of instances" 235 | }, 236 | 0x1F: { 237 | 0x0203: "Connection timeout" 238 | }, 239 | 0xff: { 240 | 0x7: "Wrong data type", 241 | 0x2001: "Excessive IOI", 242 | 0x2002: "Bad parameter value", 243 | 0x2018: "Semaphore reject", 244 | 0x201B: "Size too small", 245 | 0x201C: "Invalid size", 246 | 0x2100: "Privilege failure", 247 | 0x2101: "Invalid keyswitch position", 248 | 0x2102: "Password invalid", 249 | 0x2103: "No password issued", 250 | 0x2104: "Address out of range", 251 | 0x2105: "Access beyond end of the object", 252 | 0x2106: "Data in use", 253 | 0x2107: "Tag type used n request dose not match the target tag's data type", 254 | 0x2108: "Controller in upload or download mode", 255 | 0x2109: "Attempt to change number of array dimensions", 256 | 0x210A: "Invalid symbol name", 257 | 0x210B: "Symbol does not exist", 258 | 0x210E: "Search failed", 259 | 0x210F: "Task cannot start", 260 | 0x2110: "Unable to write", 261 | 0x2111: "Unable to read", 262 | 0x2112: "Shared routine not editable", 263 | 0x2113: "Controller in faulted mode", 264 | 0x2114: "Run mode inhibited" 265 | 266 | } 267 | } 268 | DATA_ITEM = { 269 | 'Connected': '\xb1\x00', 270 | 'Unconnected': '\xb2\x00' 271 | } 272 | 273 | ADDRESS_ITEM = { 274 | 'Connection Based': '\xa1\x00', 275 | 'Null': '\x00\x00', 276 | 'UCMM': '\x00\x00' 277 | } 278 | 279 | UCMM = { 280 | 'Interface Handle': 0, 281 | 'Item Count': 2, 282 | 'Address Type ID': 0, 283 | 'Address Length': 0, 284 | 'Data Type ID': 0x00b2 285 | } 286 | 287 | CONNECTION_SIZE = { 288 | 'Backplane': '\x03', # CLX 289 | 'Direct Network': '\x02' 290 | } 291 | 292 | HEADER_SIZE = 24 293 | EXTENDED_SYMBOL = '\x91' 294 | BOOL_ONE = 0xff 295 | REQUEST_SERVICE = 0 296 | REQUEST_PATH_SIZE = 1 297 | REQUEST_PATH = 2 298 | SUCCESS = 0 299 | INSUFFICIENT_PACKETS = 6 300 | OFFSET_MESSAGE_REQUEST = 40 301 | 302 | 303 | FORWARD_CLOSE = '\x4e' 304 | UNCONNECTED_SEND = '\x52' 305 | FORWARD_OPEN = '\x54' 306 | LARGE_FORWARD_OPEN = '\x5b' 307 | GET_CONNECTION_DATA = '\x56' 308 | SEARCH_CONNECTION_DATA = '\x57' 309 | GET_CONNECTION_OWNER = '\x5a' 310 | MR_SERVICE_SIZE = 2 311 | 312 | PADDING_BYTE = '\x00' 313 | PRIORITY = '\x0a' 314 | TIMEOUT_TICKS = '\x05' 315 | TIMEOUT_MULTIPLIER = '\x01' 316 | TRANSPORT_CLASS = '\xa3' 317 | 318 | CONNECTION_PARAMETER = { 319 | 'PLC5': 0x4302, 320 | 'SLC500': 0x4302, 321 | 'CNET': 0x4320, 322 | 'DHP': 0x4302, 323 | 'Default': 0x43f8, 324 | } 325 | 326 | """ 327 | Atomic Data Type: 328 | 329 | Bit = Bool 330 | Bit array = DWORD (32-bit boolean aray) 331 | 8-bit integer = SINT 332 | 16-bit integer = UINT 333 | 32-bit integer = DINT 334 | 32-bit float = REAL 335 | 64-bit integer = LINT 336 | 337 | From Rockwell Automation Publication 1756-PM020C-EN-P November 2012: 338 | When reading a BOOL tag, the values returned for 0 and 1 are 0 and 0xff, respectively. 339 | """ 340 | 341 | S_DATA_TYPE = { 342 | 'BOOL': 0xc1, 343 | 'SINT': 0xc2, # Signed 8-bit integer 344 | 'INT': 0xc3, # Signed 16-bit integer 345 | 'DINT': 0xc4, # Signed 32-bit integer 346 | 'LINT': 0xc5, # Signed 64-bit integer 347 | 'USINT': 0xc6, # Unsigned 8-bit integer 348 | 'UINT': 0xc7, # Unsigned 16-bit integer 349 | 'UDINT': 0xc8, # Unsigned 32-bit integer 350 | 'ULINT': 0xc9, # Unsigned 64-bit integer 351 | 'REAL': 0xca, # 32-bit floating point 352 | 'LREAL': 0xcb, # 64-bit floating point 353 | 'STIME': 0xcc, # Synchronous time 354 | 'DATE': 0xcd, 355 | 'TIME_OF_DAY': 0xce, 356 | 'DATE_AND_TIME': 0xcf, 357 | 'STRING': 0xd0, # character string (1 byte per character) 358 | 'BYTE': 0xd1, # byte string 8-bits 359 | 'WORD': 0xd2, # byte string 16-bits 360 | 'DWORD': 0xd3, # byte string 32-bits 361 | 'LWORD': 0xd4, # byte string 64-bits 362 | 'STRING2': 0xd5, # character string (2 byte per character) 363 | 'FTIME': 0xd6, # Duration high resolution 364 | 'LTIME': 0xd7, # Duration long 365 | 'ITIME': 0xd8, # Duration short 366 | 'STRINGN': 0xd9, # character string (n byte per character) 367 | 'SHORT_STRING': 0xda, # character string (1 byte per character, 1 byte length indicator) 368 | 'TIME': 0xdb, # Duration in milliseconds 369 | 'EPATH': 0xdc, # CIP Path segment 370 | 'ENGUNIT': 0xdd, # Engineering Units 371 | 'STRINGI': 0xde # International character string 372 | } 373 | 374 | I_DATA_TYPE = { 375 | 0xc1: 'BOOL', 376 | 0xc2: 'SINT', # Signed 8-bit integer 377 | 0xc3: 'INT', # Signed 16-bit integer 378 | 0xc4: 'DINT', # Signed 32-bit integer 379 | 0xc5: 'LINT', # Signed 64-bit integer 380 | 0xc6: 'USINT', # Unsigned 8-bit integer 381 | 0xc7: 'UINT', # Unsigned 16-bit integer 382 | 0xc8: 'UDINT', # Unsigned 32-bit integer 383 | 0xc9: 'ULINT', # Unsigned 64-bit integer 384 | 0xca: 'REAL', # 32-bit floating point 385 | 0xcb: 'LREAL', # 64-bit floating point 386 | 0xcc: 'STIME', # Synchronous time 387 | 0xcd: 'DATE', 388 | 0xce: 'TIME_OF_DAY', 389 | 0xcf: 'DATE_AND_TIME', 390 | 0xd0: 'STRING', # character string (1 byte per character) 391 | 0xd1: 'BYTE', # byte string 8-bits 392 | 0xd2: 'WORD', # byte string 16-bits 393 | 0xd3: 'DWORD', # byte string 32-bits 394 | 0xd4: 'LWORD', # byte string 64-bits 395 | 0xd5: 'STRING2', # character string (2 byte per character) 396 | 0xd6: 'FTIME', # Duration high resolution 397 | 0xd7: 'LTIME', # Duration long 398 | 0xd8: 'ITIME', # Duration short 399 | 0xd9: 'STRINGN', # character string (n byte per character) 400 | 0xda: 'SHORT_STRING', # character string (1 byte per character, 1 byte length indicator) 401 | 0xdb: 'TIME', # Duration in milliseconds 402 | 0xdc: 'EPATH', # CIP Path segment 403 | 0xdd: 'ENGUNIT', # Engineering Units 404 | 0xde: 'STRINGI' # International character string 405 | } 406 | 407 | REPLAY_INFO = { 408 | 0x4e: 'FORWARD_CLOSE (4E,00)', 409 | 0x52: 'UNCONNECTED_SEND (52,00)', 410 | 0x54: 'FORWARD_OPEN (54,00)', 411 | 0x6f: 'send_rr_data (6F,00)', 412 | 0x70: 'send_unit_data (70,00)', 413 | 0x00: 'nop', 414 | 0x01: 'list_targets', 415 | 0x04: 'list_services', 416 | 0x63: 'list_identity', 417 | 0x64: 'list_interfaces', 418 | 0x65: 'register_session', 419 | 0x66: 'unregister_session', 420 | } 421 | 422 | PCCC_DATA_TYPE = { 423 | 'N': '\x89', 424 | 'B': '\x85', 425 | 'T': '\x86', 426 | 'C': '\x87', 427 | 'S': '\x84', 428 | 'F': '\x8a', 429 | 'ST': '\x8d', 430 | 'A': '\x8e', 431 | 'R': '\x88', 432 | 'O': '\x8b', 433 | 'I': '\x8c' 434 | } 435 | 436 | PCCC_DATA_SIZE = { 437 | 'N': 2, 438 | # 'L': 4, 439 | 'B': 2, 440 | 'T': 6, 441 | 'C': 6, 442 | 'S': 2, 443 | 'F': 4, 444 | 'ST': 84, 445 | 'A': 2, 446 | 'R': 6, 447 | 'O': 2, 448 | 'I': 2 449 | } 450 | 451 | PCCC_CT = { 452 | 'PRE': 1, 453 | 'ACC': 2, 454 | 'EN': 15, 455 | 'TT': 14, 456 | 'DN': 13, 457 | 'CU': 15, 458 | 'CD': 14, 459 | 'OV': 12, 460 | 'UN': 11, 461 | 'UA': 10 462 | } 463 | 464 | PCCC_ERROR_CODE = { 465 | -2: "Not Acknowledged (NAK)", 466 | -3: "No Reponse, Check COM Settings", 467 | -4: "Unknown Message from DataLink Layer", 468 | -5: "Invalid Address", 469 | -6: "Could Not Open Com Port", 470 | -7: "No data specified to data link layer", 471 | -8: "No data returned from PLC", 472 | -20: "No Data Returned", 473 | 16: "Illegal Command or Format, Address may not exist or not enough elements in data file", 474 | 32: "PLC Has a Problem and Will Not Communicate", 475 | 48: "Remote Node Host is Missing, Disconnected, or Shut Down", 476 | 64: "Host Could Not Complete Function Due To Hardware Fault", 477 | 80: "Addressing problem or Memory Protect Rungs", 478 | 96: "Function not allows due to command protection selection", 479 | 112: "Processor is in Program mode", 480 | 128: "Compatibility mode file missing or communication zone problem", 481 | 144: "Remote node cannot buffer command", 482 | 240: "Error code in EXT STS Byte" 483 | } -------------------------------------------------------------------------------- /pycomm/common.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Agostino Ruscito' 2 | __version__ = "1.0.8" 3 | __date__ = "08 03 2015" 4 | 5 | 6 | class PycommError(Exception): 7 | pass 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from pycomm import common 3 | import os 4 | 5 | 6 | def read(file_name): 7 | return open(os.path.join(os.path.dirname(__file__), file_name)).read() 8 | 9 | setup( 10 | name="pycomm", 11 | version=common.__version__, 12 | author="Agostino Ruscito", 13 | author_email="ruscito@gmail.com", 14 | url="https://github.com/ruscito/pycomm", 15 | download_url="", 16 | description="A PLC communication library for Python", 17 | long_description=read('README.rst'), 18 | license="MIT", 19 | packages=[ 20 | "pycomm", 21 | "pycomm.ab_comm", 22 | "pycomm.cip" 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 5 - Production/Stable', 26 | 'Intended Audience :: Developers', 27 | 'Natural Language :: English', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.6', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Topic :: Software Development :: Libraries :: Python Modules', 35 | ], 36 | ) --------------------------------------------------------------------------------