├── readme-images ├── image-demo1.png ├── image-debug-mode.png ├── py3-badge.svg └── py27-badge.svg ├── edslib_schema.json ├── LICENSE ├── demo1.py ├── demo.eds ├── demo2.py ├── .gitignore ├── common_object_class.json ├── README.md ├── meta_eds_lib.json ├── cip_eds_types.py ├── ethernetip_lib.json └── eds_pie.py /readme-images/image-demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omidbimo/eds_pie/HEAD/readme-images/image-demo1.png -------------------------------------------------------------------------------- /readme-images/image-debug-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omidbimo/eds_pie/HEAD/readme-images/image-debug-mode.png -------------------------------------------------------------------------------- /edslib_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_verison": 1, 3 | "schema_file": "edslib_schema.json", 4 | "project": "eds_pie", 5 | "lib_name": "CIP family EDS dictionary name", 6 | "protocol": "CIP protocol name", 7 | "comment": "This is the template json file for eds_pie libraries.", 8 | "sections": { 9 | "Section Name": { 10 | "description": "Section Description", 11 | "class_id": 0, 12 | "required": true, 13 | "entries": { 14 | "Entry keyword": { 15 | "name": "Entry Description Text", 16 | "required": true, 17 | "enumerated_fields": { "first_enum_field": 1, "enum_member_count": 2 }, 18 | "fields": [ 19 | { "name": "Field Description Text", 20 | "required": true, 21 | "data_types": { 22 | "EDS_DATA_TYPE1": "type_info", 23 | "EDS_DATA_TYPE2": ["val1", "val2"], 24 | "EDS_DATA_TYPE3": [0, 1] 25 | } 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Omid Kompani 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 | -------------------------------------------------------------------------------- /readme-images/py3-badge.svg: -------------------------------------------------------------------------------- 1 | Badge TitleLeft TitleRight Titlepythonpython33 -------------------------------------------------------------------------------- /readme-images/py27-badge.svg: -------------------------------------------------------------------------------- 1 | Badge TitleLeft TitleRight Titlepythonpython2.72.7 -------------------------------------------------------------------------------- /demo1.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Omid Kompani 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | """ 26 | 27 | import logging 28 | 29 | logging.basicConfig(level=logging.ERROR, 30 | format='%(asctime)s - %(name)s.%(levelname)-8s %(message)s') 31 | logger = logging.getLogger(__name__) 32 | 33 | from eds_pie import eds_pie 34 | 35 | with open('demo.eds', 'r') as srcfile: 36 | eds_content = srcfile.read() 37 | eds = eds_pie.parse(eds_content, showprogress = False) 38 | 39 | print('EDS protocol: {}'.format(eds.protocol)) 40 | 41 | for section in eds.sections: 42 | print(section) 43 | for entry in section.entries: 44 | print(' {}'.format(entry)) 45 | for field in entry.fields: 46 | print(' {}'.format(field)) 47 | ## Alternate API: Using the list method of the eds object 48 | # eds.list() 49 | # eds.list('file') 50 | # eds.list('file', 'DescText') -------------------------------------------------------------------------------- /demo.eds: -------------------------------------------------------------------------------- 1 | $ **************************************************************************** 2 | $ MIT License 3 | $ 4 | $ Copyright (c) 2021 Omid Kompani 5 | $ 6 | $ Permission is hereby granted, free of charge, to any person obtaining a copy 7 | $ of this software and associated documentation files (the "Software"), to deal 8 | $ in the Software without restriction, including without limitation the rights 9 | $ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | $ copies of the Software, and to permit persons to whom the Software is 11 | $ furnished to do so, subject to the following conditions: 12 | $ 13 | $ The above copyright notice and this permission notice shall be included in all 14 | $ copies or substantial portions of the Software. 15 | $ 16 | $ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | $ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | $ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | $ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | $ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | $ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | $ SOFTWARE. 23 | $ **************************************************************************** 24 | $ 25 | $ ATTENTION: 26 | $ 27 | $ Changes in this file can cause configuration or communication problems. 28 | $ 29 | $ **************************************************************************** 30 | 31 | [File] 32 | DescText = "A Demo EDS file"; 33 | CreateDate = 01-01-2021; 34 | CreateTime = 10:50:30; 35 | ModDate = 01-01-2021; 36 | ModTime = 10:50:30; 37 | Revision = 1.1; 38 | HomeURL = "https://github.com/omidbimo/eds_pie"; 39 | 40 | [Device] 41 | VendCode = 65535; 42 | VendName = "omidbimo"; 43 | ProdType = 12; 44 | ProdTypeStr = "Communications Adapter"; 45 | ProdCode = 1; 46 | MajRev = 1; 47 | MinRev = 1; 48 | ProdName = "EDS pie"; 49 | 50 | [Device Classification] 51 | Class1 = EtherNetIP; 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo2.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Omid Kompani 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | """ 26 | 27 | import logging 28 | 29 | logging.basicConfig(level=logging.ERROR, 30 | format='%(asctime)s - %(name)s.%(levelname)-8s %(message)s') 31 | logger = logging.getLogger(__name__) 32 | 33 | from eds_pie import eds_pie 34 | 35 | with open('demo.eds', 'r') as srcfile: 36 | eds_content = srcfile.read() 37 | eds = eds_pie.parse(eds_content, showprogress = True) 38 | 39 | if eds.protocol == 'EtherNetIP': 40 | entry = eds.getentry('device', 'ProdType') 41 | field = entry.fields[0] 42 | if field.value == 12: 43 | print('This is an EtherNet/IP Communication adapter device.') 44 | # Alternate way: The value attribute of an entry always returns its first field value. 45 | #if entry.value == 12: 46 | # print('This is an EtherNet/IP Communication adapter device.') 47 | 48 | if eds.hassection(0x5D): 49 | eds.list(eds.get_cip_section_name(0x5D)) 50 | ''' 51 | The device is capable of CIP security. 52 | Do some stuff with security objects. 53 | ''' 54 | else: 55 | print('Device doesn\'t support CIP security') 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /common_object_class.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_verison": 1, 3 | "schema_file": "edslib_schema.json", 4 | "project": "eds_pie", 5 | "lib_name": "Common Object Class", 6 | "protocol": "CIP", 7 | "comment": "Common Entries for all CIP and EtherNet/IP classes.", 8 | "sections": { 9 | "Common Object Class": { 10 | "description": "Common Object Class", 11 | "class_id": null, 12 | "required": false, 13 | "entries": { 14 | "Revision": { 15 | "name": "Revision", 16 | "required": true, 17 | "enumerated_fields": null, 18 | "fields": [ 19 | { 20 | "name": "Revision", 21 | "required": true, 22 | "data_types": { 23 | "UINT": [] 24 | } 25 | } 26 | ] 27 | }, 28 | "MaxInst": { 29 | "name": "Maximum Instance Number", 30 | "required": true, 31 | "enumerated_fields": null, 32 | "fields": [ 33 | { "name": "MaxInst", 34 | "required": true, 35 | "data_types": { 36 | "UINT": [] 37 | } 38 | } 39 | ] 40 | }, 41 | "Number_Of_Static_Instances": { 42 | "name": "Number of Static Instances", 43 | "required": true, 44 | "enumerated_fields": null, 45 | "fields": [ 46 | { "name": "Number of Static Instances", 47 | "required": true, 48 | "data_types": { 49 | "UINT": [] 50 | } 51 | } 52 | ] 53 | }, 54 | "Max_Number_Of_Dynamic_Instances": { 55 | "name": "Maximum Number of Dynamic Instances", 56 | "required": true, 57 | "enumerated_fields": null, 58 | "fields": [ 59 | { "name": "Maximum Number of Dynamic Instances", 60 | "required": true, 61 | "data_types": { 62 | "UINT": [] 63 | } 64 | } 65 | ] 66 | }, 67 | "Class_Attributes": { 68 | "name": "Class attribute identification", 69 | "required": false, 70 | "enumerated_fields": { "first_enum_field": 1, "enum_member_count": 1 }, 71 | "fields": [ 72 | { "name": "Attribute ID 1", 73 | "required": true, 74 | "data_types": { 75 | "UINT": [] 76 | } 77 | } 78 | ] 79 | }, 80 | "Instance_Attributes": { 81 | "name": "Instance attribute identification", 82 | "required": false, 83 | "enumerated_fields": { "first_enum_field": 1, "enum_member_count": 1 }, 84 | "fields": [ 85 | { "name": "Attribute ID 1", 86 | "required": true, 87 | "data_types": { 88 | "UINT": [] 89 | } 90 | } 91 | ] 92 | }, 93 | "Class_Services": { 94 | "name": "Class service support", 95 | "required": false, 96 | "enumerated_fields": { "first_enum_field": 1, "enum_member_count": 1 }, 97 | "fields": [ 98 | { "name": "Service 1", 99 | "required": true, 100 | "data_types": { 101 | "USINT": [] 102 | } 103 | } 104 | ] 105 | }, 106 | "Instance_Services": { 107 | "name": "Instance service support", 108 | "required": false, 109 | "enumerated_fields": { "first_enum_field": 1, "enum_member_count": 1 }, 110 | "fields": [ 111 | { "name": "Service 1", 112 | "required": true, 113 | "data_types": { 114 | "USINT": [] 115 | } 116 | } 117 | ] 118 | }, 119 | "Object_Name": { 120 | "name": "Object Name", 121 | "required": true, 122 | "enumerated_fields": null, 123 | "fields": [ 124 | { "name": "Name", 125 | "required": true, 126 | "data_types": { 127 | "STRING": [] 128 | } 129 | } 130 | ] 131 | }, 132 | "Object_Class_Code": { 133 | "name": "Object Class Code", 134 | "required": true, 135 | "enumerated_fields": null, 136 | "fields": [ 137 | { "name": "Code", 138 | "required": true, 139 | "data_types": { 140 | "UDINT": [] 141 | } 142 | } 143 | ] 144 | } 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EDS pie 2 | 3 | **EDS parser library for ODVA's CIP® protocol family(EtherNet/IP®, DeviceNet®,...)** 4 | 5 | ![python version3](readme-images/py3-badge.svg "python version 3") ![python version27](readme-images/py27-badge.svg "python version 2.7") 6 | 7 | 8 | ### The following are trademarks of ODVA: 9 | CIP, CIP Energy, CIP Motion, CIP Security, CIP Safety, CIP Sync, CompoNet, ControlNet, DeviceNet, 10 | EtherNet/IP, QuickConnect. 11 | 12 | Visit http://www.odva.org for product information and publications 13 | 14 | ### What is an EDS 15 | EDS stands for Electronic Data Sheet and is a set of data that provides all of the information necessary to access and alter the configurable 16 | parameters of a device (CIP, CANopen, ...). 17 | 18 | ### Who may need an EDS parser library 19 | 20 | Use cases: 21 | 22 | - ​ Batch processing of EDS files 23 | - To find out which devices are supporting a specific CIP object or feature (i.e. CIP Security) 24 | - To change contents of multiple of EDS files (i.e. Add a new section to all EDSs with a specific ProductCode) 25 | - Automation 26 | - To implement automated scripts/tests 27 | - Automated establishment of IO connections or other types of connections 28 | - To implement QA scripts to make sure the EDS file matches a device configuration 29 | - To Create new EDS files 30 | - Format conversion 31 | - To convert EDS data into other formats such as XML / JSON and feed the applications that do not understand the EDS notations 32 | 33 | 34 | 35 | ## Usage 36 | 37 | ```python 38 | # Demo1.py 39 | from eds_pie import eds_pie 40 | 41 | with open('demo.eds', 'r') as srcfile: 42 | eds_content = srcfile.read() 43 | eds = eds_pie.parse(eds_content, showprogress = True) 44 | 45 | print eds.protocol 46 | 47 | for section in eds.sections: 48 | print section 49 | for entry in section.entries: 50 | print ' ', entry 51 | for field in entry.fields: 52 | print ' ', field 53 | ## or use the list method of the eds object 54 | eds.list() 55 | eds.list('file') 56 | eds.list('file', 'DescText') 57 | ``` 58 | 59 | ![image-demo1](readme-images/image-demo1.png) 60 | 61 | 62 | 63 | ```python 64 | # Demo2.py 65 | 66 | from eds_pie import eds_pie 67 | 68 | with open('demo.eds', 'r') as srcfile: 69 | eds_content = srcfile.read() 70 | eds = eds_pie.parse(eds_content, showprogress = True) 71 | 72 | if eds.protocol == 'EtherNetIP': 73 | entry = eds.getentry('device', 'ProdType') 74 | field = entry.fields[0] 75 | if field.value == 12: 76 | print 'This is an EtherNet/IP Communication adapter device.' 77 | # Alternate way: The value attribute of an entry always returns its first field value. 78 | if entry.value == 12: 79 | print 'This is an EtherNet/IP Communication adapter device.' 80 | 81 | if eds.hassection(0x5D): 82 | eds.list(eds.get_cip_section_name(0x5D)) 83 | ''' 84 | The device is capable of CIP security. 85 | Do some stuff with security objects. 86 | ''' 87 | else: 88 | print 'Device doesn\'t support CIP security' 89 | 90 | 91 | ``` 92 | 93 | 94 | 95 | ## API Reference 96 | 97 | ### EDS object methods 98 | 99 | - EDS.addsection( sectionname ) 100 | - EDS.addentry( sectionname, entryname ) 101 | - EDS.addfield( sectionname, entryname, fieldvalue, *[fielddatatype]* ) 102 | - EDS.getsection( sectionname/cip_class_id ) To get a specific section element 103 | - EDS.getentry( sectionname, entryname ) To get a sepecific entry element 104 | - EDS.getfield( sectionname, entryname, fieldindex / fieldname ) To get a sepecific field element 105 | - EDS.getvalue( sectionname, entryname, fieldindex / fieldname ) To get the value of an addressed field 106 | - EDS.get_cip_section_name(classid, protocol=None) To get the section_kay for a CIP object specified by its CIP Class ID 107 | - EDS.hassection( sectionname/cip_class_id ) 108 | - EDS.hasenry( sectionname, entryname ) 109 | - EDS.hasfield( sectionname, entryname, fieldindex ) 110 | - EDS.list( *[sectionname],* *[entryname]*) To print out a list of EDS elements (sections, entries, fields) 111 | - EDS.removesection( sectionname ) 112 | - EDS.removeentry( sectionname, entryname ) 113 | - EDS.removefield( sectionname, entryname, fieldindex ) 114 | - EDS.resolve_epath( epath ) To resolve an epath containing reference to params 115 | - EDS.save( *[filename], [overwrite]* ) To save the EDS contents into a file 116 | - EDS.setvalue( sectionname, entryname, fieldindex, value ) To set value of an addressed field 117 | 118 | ### EDS object attributes 119 | 120 | - EDS.protocol To get the string protocol name of the eds file (generic, EtherNetIP, ...) 121 | - EDS.sections An iterable list of EDS sections 122 | 123 | ### Section object methods 124 | 125 | - section.getentry( entryname ) 126 | 127 | - section.addentry( entryname ) 128 | - section.getfield( entryname, fieldindex / fieldname ) 129 | 130 | ### Section object attributes 131 | 132 | - section.name to get string name of the section 133 | - section.entrycount to get the number of entries for this section 134 | - section.entries An iterable list of Section Entries 135 | 136 | ### Entry object methods 137 | 138 | - entry.getfield( fieldindex / fieldname ) 139 | - entry.addfield( fieldvalue, *[datatype]* ) 140 | 141 | ### Entry object attributes 142 | 143 | - entry.name to get string name of the entry 144 | - entry.fieldcount to get the number of fields for this entry 145 | - entry.fields An iterable list of Entry Fields 146 | 147 | ### Field object attributes 148 | 149 | - field.name to get string name of the field 150 | - field.index to get the index of this field in the of parent entry fields 151 | - field.value to get / set the value of the field 152 | - field.datatype to get the data-type object of this field 153 | 154 | 155 | 156 | **All object are printable using the print instruction** 157 | 158 | 159 | 160 | 161 | 162 | ## Debug mode 163 | 164 | To retrieve the maximum information about the parsing process, set the logging level of eds_pie to DEBUG. In the debug mode, a list of parsed tokens will be displayed. 165 | 166 | ```python 167 | import logging 168 | 169 | logging.basicConfig(level=logging.DEBUG, 170 | format='%(asctime)s - %(name)s.%(levelname)-8s %(message)s') 171 | logger = logging.getLogger(__name__) 172 | 173 | from eds_pie import eds_pie 174 | 175 | with open('demo.eds', 'r') as srcfile: 176 | eds_content = srcfile.read() 177 | eds = eds_pie.parse(eds_content, showprogress = True) 178 | ``` 179 | 180 | ![image-debugmode](readme-images/image-debug-mode.png) 181 | 182 | -------------------------------------------------------------------------------- /meta_eds_lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_verison": 1, 3 | "schema_file": "edslib_schema.json", 4 | "project": "eds_pie", 5 | "lib_name": "MetaEDS", 6 | "protocol": "", 7 | "comment": "EDS about EDS. Essential administrative Meta data to understand the EDS structure.", 8 | "sections": { 9 | "File": { 10 | "name": "File Description", 11 | "required": true, 12 | "class_id": null, 13 | "entries": { 14 | "DescText": { 15 | "name": "File Description Text", 16 | "required": true, 17 | "enumerated_fields": null, 18 | "fields": [ 19 | { 20 | "name": "File Description Text", 21 | "required": false, 22 | "data_types": { 23 | "STRING": null 24 | } 25 | } 26 | ] 27 | }, 28 | "CreateDate": { 29 | "name": "File Creation Date", 30 | "required": true, 31 | "enumerated_fields": null, 32 | "fields": [ 33 | { 34 | "name": "File Creation Date", 35 | "required": false, 36 | "data_types": { 37 | "DATE": [] 38 | } 39 | } 40 | ] 41 | }, 42 | "CreateTime": { 43 | "name": "File Creation Time", 44 | "required": true, 45 | "enumerated_fields": null, 46 | "fields": [ 47 | { 48 | "name": "File Creation Time", 49 | "required": false, 50 | "data_types": { 51 | "TIME": [] 52 | } 53 | } 54 | ] 55 | }, 56 | "ModDate": { 57 | "name": "Last Modification Date", 58 | "required": false, 59 | "enumerated_fields": null, 60 | "fields": [ 61 | { 62 | "name": "Last Modification Date", 63 | "required": false, 64 | "data_types": { 65 | "DATE": [] 66 | } 67 | } 68 | ] 69 | }, 70 | "ModTime": { 71 | "name": "Last Modification Time", 72 | "required": false, 73 | "enumerated_fields": null, 74 | "fields": [ 75 | { 76 | "name": "Last Modification Time", 77 | "required": false, 78 | "data_types": { 79 | "TIME": [] 80 | } 81 | } 82 | ] 83 | }, 84 | "Revision": { 85 | "name": "EDS Revision", 86 | "required": true, 87 | "enumerated_fields": null, 88 | "fields": [ 89 | { 90 | "name": "EDS Revision", 91 | "required": false, 92 | "data_types": { 93 | "REVISION": [] 94 | } 95 | } 96 | ] 97 | }, 98 | "HomeURL": { 99 | "name": "Home URL", 100 | "required": false, 101 | "enumerated_fields": null, 102 | "fields": [ 103 | { 104 | "name": "Home URL", 105 | "required": false, 106 | "data_types": { 107 | "STRING": [] 108 | } 109 | } 110 | ] 111 | }, 112 | "Exclude": { 113 | "name": "Exclude", 114 | "required": false, 115 | "enumerated_fields": null, 116 | "fields": [ 117 | { 118 | "name": "Exclude", 119 | "required": false, 120 | "data_types": { 121 | "KEYWORD": [ 122 | "NONE", 123 | "WRITE", 124 | "READ_WRITE" 125 | ] 126 | } 127 | } 128 | ] 129 | }, 130 | "EDSFileCRC": { 131 | "name": "EDS File CRC", 132 | "required": false, 133 | "enumerated_fields": null, 134 | "fields": [ 135 | { 136 | "name": "EDS File CRC", 137 | "required": false, 138 | "data_types": { 139 | "UDINT": [] 140 | } 141 | } 142 | ] 143 | } 144 | } 145 | }, 146 | "Device": { 147 | "name": "Device Description", 148 | "required": true, 149 | "class_id": null, 150 | "entries": { 151 | "VendCode": { 152 | "name": "Vendor ID", 153 | "required": true, 154 | "enumerated_fields": null, 155 | "fields": [ 156 | { 157 | "name": "Vendor ID", 158 | "required": false, 159 | "data_types": { 160 | "UINT": [] 161 | } 162 | } 163 | ] 164 | }, 165 | "VendName": { 166 | "name": "Vendor Name", 167 | "required": true, 168 | "enumerated_fields": null, 169 | "fields": [ 170 | { 171 | "name": "Vendor Name", 172 | "required": false, 173 | "data_types": { 174 | "STRING": [] 175 | } 176 | } 177 | ] 178 | }, 179 | "ProdType": { 180 | "name": "Device Type", 181 | "required": true, 182 | "enumerated_fields": null, 183 | "fields": [ 184 | { 185 | "name": "Device Type", 186 | "required": false, 187 | "data_types": { 188 | "UINT": [] 189 | } 190 | } 191 | ] 192 | }, 193 | "ProdTypeStr": { 194 | "name": "Device Type String", 195 | "required": true, 196 | "enumerated_fields": null, 197 | "fields": [ 198 | { 199 | "name": "Device Type String", 200 | "required": false, 201 | "data_types": { 202 | "STRING": [] 203 | } 204 | } 205 | ] 206 | }, 207 | "ProdCode": { 208 | "name": "Product Code", 209 | "required": true, 210 | "enumerated_fields": null, 211 | "fields": [ 212 | { 213 | "name": "Product Code", 214 | "required": false, 215 | "data_types": { 216 | "UDINT": [] 217 | } 218 | } 219 | ] 220 | }, 221 | "MajRev": { 222 | "name": "Major Revision", 223 | "required": true, 224 | "enumerated_fields": null, 225 | "fields": [ 226 | { 227 | "name": "Major Revision", 228 | "required": false, 229 | "data_types": { 230 | "USINT": [] 231 | } 232 | } 233 | ] 234 | }, 235 | "MinRev": { 236 | "name": "Minor Revision", 237 | "required": true, 238 | "enumerated_fields": null, 239 | "fields": [ 240 | { 241 | "name": "Minor Revision", 242 | "required": false, 243 | "data_types": { 244 | "USINT": [] 245 | } 246 | } 247 | ] 248 | }, 249 | "ProdName": { 250 | "name": "Product Name", 251 | "required": true, 252 | "enumerated_fields": null, 253 | "fields": [ 254 | { 255 | "name": "Product Name", 256 | "required": false, 257 | "data_types": { 258 | "STRING": [] 259 | } 260 | } 261 | ] 262 | }, 263 | "Catalog": { 264 | "name": "Catalog Number", 265 | "required": false, 266 | "enumerated_fields": null, 267 | "fields": [ 268 | { 269 | "name": "Catalog Number", 270 | "required": false, 271 | "data_types": { 272 | "STRING": [] 273 | } 274 | } 275 | ] 276 | }, 277 | "Icon": { 278 | "name": "Icon File Name", 279 | "required": true, 280 | "enumerated_fields": null, 281 | "fields": [ 282 | { 283 | "name": "Icon File Name", 284 | "required": false, 285 | "data_types": { 286 | "STRING": [] 287 | } 288 | } 289 | ] 290 | }, 291 | "IconContents": { 292 | "name": "Icon Contents", 293 | "required": false, 294 | "enumerated_fields": null, 295 | "fields": [ 296 | { 297 | "name": "Icon Contents", 298 | "required": false, 299 | "data_types": { 300 | "STRING": [] 301 | } 302 | } 303 | ] 304 | } 305 | } 306 | }, 307 | "Device Classification": { 308 | "name": "Device Classification", 309 | "required": true, 310 | "class_id": null, 311 | "entries": { 312 | "ClassN": { 313 | "name": "Class", 314 | "required": true, 315 | "enumerated_fields": { 316 | "first_enum_field": 1, 317 | "enum_member_count": 1 318 | }, 319 | "fields": [ 320 | { 321 | "name": "Classification N", 322 | "required": false, 323 | "data_types": { 324 | "VENDOR_SPECIFIC": [], 325 | "KEYWORD": [ 326 | "CompoNet", 327 | "ControlNet", 328 | "DeviceNet", 329 | "EtherNetIP", 330 | "EtherNetIP_In_Cabinet", 331 | "EtherNetIP_UDP_Only", 332 | "ModbusSL", 333 | "ModbusTCP", 334 | "Safety", 335 | "HART", 336 | "IOLink" 337 | ] 338 | } 339 | } 340 | ] 341 | } 342 | } 343 | }, 344 | "Params": { 345 | "name": "Parameters", 346 | "required": false, 347 | "class_id": null, 348 | "entries": { 349 | "ParamN": { 350 | "name": "Parameter", 351 | "required": false, 352 | "enumerated_fields": null, 353 | "fields": [ 354 | { 355 | "name": "Reserved", 356 | "required": false, 357 | "data_types": { 358 | "USINT": [] 359 | } 360 | }, 361 | { 362 | "name": "Link Path Size", 363 | "required": false, 364 | "data_types": { 365 | "USINT": [], 366 | "EMPTY": [] 367 | } 368 | }, 369 | { 370 | "name": "Link Path", 371 | "required": false, 372 | "data_types": { 373 | "EPATH": [], 374 | "KEYWORD": [ 375 | "SYMBOL_ANSI" 376 | ], 377 | "EMPTY": [] 378 | } 379 | }, 380 | { 381 | "name": "Descriptor", 382 | "required": false, 383 | "data_types": { 384 | "WORD": [] 385 | } 386 | }, 387 | { 388 | "name": "Data Type", 389 | "required": false, 390 | "data_types": { 391 | "USINT": [] 392 | } 393 | }, 394 | { 395 | "name": "Data Size", 396 | "required": false, 397 | "data_types": { 398 | "USINT": [], 399 | "EMPTY": [] 400 | } 401 | }, 402 | { 403 | "name": "Parameter Name", 404 | "required": false, 405 | "data_types": { 406 | "STRING": [] 407 | } 408 | }, 409 | { 410 | "name": "Units String", 411 | "required": false, 412 | "data_types": { 413 | "STRING": [] 414 | } 415 | }, 416 | { 417 | "name": "Help String", 418 | "required": false, 419 | "data_types": { 420 | "STRING": [] 421 | } 422 | }, 423 | { 424 | "name": "Minimum Value", 425 | "required": false, 426 | "data_types": { 427 | "DATATYPE_REF": [ 428 | "Data Type" 429 | ], 430 | "EMPTY": [] 431 | } 432 | }, 433 | { 434 | "name": "Maximum Value", 435 | "required": false, 436 | "data_types": { 437 | "DATATYPE_REF": [ 438 | "Data Type" 439 | ], 440 | "EMPTY": [] 441 | } 442 | }, 443 | { 444 | "name": "Default Value", 445 | "required": false, 446 | "data_types": { 447 | "DATATYPE_REF": [ 448 | "Data Type" 449 | ], 450 | "EMPTY": [] 451 | } 452 | }, 453 | { 454 | "name": "Scaling Multiplier", 455 | "required": false, 456 | "data_types": { 457 | "UINT": [] 458 | } 459 | }, 460 | { 461 | "name": "Scaling Divider", 462 | "required": false, 463 | "data_types": { 464 | "UINT": [] 465 | } 466 | }, 467 | { 468 | "name": "Scaling Base", 469 | "required": false, 470 | "data_types": { 471 | "UINT": [] 472 | } 473 | }, 474 | { 475 | "name": "Scaling Offset", 476 | "required": false, 477 | "data_types": { 478 | "DINT": [] 479 | } 480 | }, 481 | { 482 | "name": "Multiplier Link", 483 | "required": false, 484 | "data_types": { 485 | "UINT": [] 486 | } 487 | }, 488 | { 489 | "name": "Divisor Link", 490 | "required": false, 491 | "data_types": { 492 | "UINT": [] 493 | } 494 | }, 495 | { 496 | "name": "Base Link", 497 | "required": false, 498 | "data_types": { 499 | "UINT": [] 500 | } 501 | }, 502 | { 503 | "name": "Offset Link", 504 | "required": false, 505 | "data_types": { 506 | "UINT": [] 507 | } 508 | }, 509 | { 510 | "name": "Decimal Precision", 511 | "required": false, 512 | "data_types": { 513 | "USINT": [] 514 | } 515 | }, 516 | { 517 | "name": "International Parameter Name", 518 | "required": false, 519 | "data_types": { 520 | "STRINGI": [] 521 | } 522 | }, 523 | { 524 | "name": "International Engineering Units", 525 | "required": false, 526 | "data_types": { 527 | "STRINGI": [] 528 | } 529 | }, 530 | { 531 | "name": "International Help String", 532 | "required": false, 533 | "data_types": { 534 | "STRINGI": [] 535 | } 536 | } 537 | ] 538 | }, 539 | "EnumN": { 540 | "name": "Enumeration", 541 | "required": false, 542 | "enumerated_fields": { 543 | "first_enum_field": 2, 544 | "enum_member_count": 2 545 | }, 546 | "fields": [ 547 | { 548 | "name": "Enum", 549 | "required": false, 550 | "data_types": { 551 | "USINT": [], 552 | "REF": [ 553 | "ParamN" 554 | ] 555 | } 556 | }, 557 | { 558 | "name": "Enum String", 559 | "required": false, 560 | "data_types": { 561 | "STRING": [] 562 | } 563 | } 564 | ] 565 | } 566 | } 567 | }, 568 | "Capacity": { 569 | "name": "Capacity", 570 | "required": false, 571 | "class_id": null, 572 | "entries": { 573 | "TSpecN": { 574 | "name": "Traffic Spec", 575 | "required": false, 576 | "enumerated_fields": null, 577 | "fields": [ 578 | { 579 | "name": "TxRx", 580 | "required": false, 581 | "data_types": { 582 | "KEYWORD": [ 583 | "Tx", 584 | "Rx", 585 | "TxRx" 586 | ] 587 | } 588 | }, 589 | { 590 | "name": "ConnSize", 591 | "required": false, 592 | "data_types": { 593 | "UINT": [] 594 | } 595 | }, 596 | { 597 | "name": "PacketsPerSecond", 598 | "required": false, 599 | "data_types": { 600 | "UDINT": [] 601 | } 602 | } 603 | ] 604 | }, 605 | "ConnOverhead": { 606 | "name": "Connection overhead", 607 | "required": false, 608 | "enumerated_fields": null, 609 | "fields": [ 610 | { 611 | "name": "Connection overhead", 612 | "required": false, 613 | "data_types": { 614 | "REAL": [] 615 | } 616 | } 617 | ] 618 | }, 619 | "MaxCIPConnections": { 620 | "name": "Maximum CIP connections", 621 | "required": false, 622 | "enumerated_fields": null, 623 | "fields": [ 624 | { 625 | "name": "Maximum CIP connections", 626 | "required": false, 627 | "data_types": { 628 | "UINT": [] 629 | } 630 | } 631 | ] 632 | }, 633 | "MaxIOConnections": { 634 | "name": "Maximum I/O connections", 635 | "required": false, 636 | "enumerated_fields": null, 637 | "fields": [ 638 | { 639 | "name": "Maximum I/O connections", 640 | "required": false, 641 | "data_types": { 642 | "UINT": [] 643 | } 644 | } 645 | ] 646 | }, 647 | "MaxMsgConnections": { 648 | "name": "Maximum explicit connections", 649 | "required": false, 650 | "enumerated_fields": null, 651 | "fields": [ 652 | { 653 | "name": "Maximum explicit connections", 654 | "required": false, 655 | "data_types": { 656 | "UINT": [] 657 | } 658 | } 659 | ] 660 | }, 661 | "MaxIOProducers": { 662 | "name": "Maximum I/O producers", 663 | "required": false, 664 | "enumerated_fields": null, 665 | "fields": [ 666 | { 667 | "name": "Maximum I/O producers", 668 | "required": false, 669 | "data_types": { 670 | "UINT": [] 671 | } 672 | } 673 | ] 674 | }, 675 | "MaxIOConsumers": { 676 | "name": "Maximum I/O consumers", 677 | "required": false, 678 | "enumerated_fields": null, 679 | "fields": [ 680 | { 681 | "name": "Maximum I/O consumers", 682 | "required": false, 683 | "data_types": { 684 | "UINT": [] 685 | } 686 | } 687 | ] 688 | }, 689 | "MaxIOProduceConsume": { 690 | "name": "Maximum I/O producers plus consumers", 691 | "required": false, 692 | "enumerated_fields": null, 693 | "fields": [ 694 | { 695 | "name": "Maximum I/O producers plus consumers", 696 | "required": false, 697 | "data_types": { 698 | "UINT": [] 699 | } 700 | } 701 | ] 702 | }, 703 | "MaxIOMcastProducers": { 704 | "name": "Maximum I/O multicast producers", 705 | "required": false, 706 | "enumerated_fields": null, 707 | "fields": [ 708 | { 709 | "name": "Maximum I/O multicast producers", 710 | "required": false, 711 | "data_types": { 712 | "UINT": [] 713 | } 714 | } 715 | ] 716 | }, 717 | "MaxIOMcastConsumers": { 718 | "name": "Maximum I/O multicast consumers", 719 | "required": false, 720 | "enumerated_fields": null, 721 | "fields": [ 722 | { 723 | "name": "Maximum I/O multicast consumers", 724 | "required": false, 725 | "data_types": { 726 | "UINT": [] 727 | } 728 | } 729 | ] 730 | }, 731 | "MaxConsumersPerMcast": { 732 | "name": "Maximum consumers per multicast connection", 733 | "required": false, 734 | "enumerated_fields": null, 735 | "fields": [ 736 | { 737 | "name": "Maximum consumers per multicast connection", 738 | "required": false, 739 | "data_types": { 740 | "UINT": [] 741 | } 742 | } 743 | ] 744 | } 745 | } 746 | }, 747 | "CommonObjectClass": { 748 | "name": "Common Object Class", 749 | "required": false, 750 | "class_id": null, 751 | "entries": { 752 | "Revision": { 753 | "name": "Revision", 754 | "required": false, 755 | "enumerated_fields": null, 756 | "fields": [ 757 | { 758 | "name": "Revision", 759 | "required": false, 760 | "data_types": { 761 | "UINT": [] 762 | } 763 | } 764 | ] 765 | }, 766 | "MaxInst": { 767 | "name": "Maximum Instance Number", 768 | "required": false, 769 | "enumerated_fields": null, 770 | "fields": [ 771 | { 772 | "name": "MaxInst", 773 | "required": false, 774 | "data_types": { 775 | "UINT": [] 776 | } 777 | } 778 | ] 779 | }, 780 | "Number_Of_Static_Instances": { 781 | "name": "Number of Static Instances", 782 | "required": false, 783 | "enumerated_fields": null, 784 | "fields": [ 785 | { 786 | "name": "Maximum Instance Number", 787 | "required": false, 788 | "data_types": { 789 | "UINT": [] 790 | } 791 | } 792 | ] 793 | }, 794 | "Max_Number_Of_Dynamic_Instances": { 795 | "name": "Maximum Number of Dynamic Instances", 796 | "required": false, 797 | "enumerated_fields": null, 798 | "fields": [ 799 | { 800 | "name": "Maximum Number of Dynamic Instances", 801 | "required": false, 802 | "data_types": { 803 | "UINT": [] 804 | } 805 | } 806 | ] 807 | }, 808 | "Class_Attributes": { 809 | "name": "Class attribute identification", 810 | "required": false, 811 | "enumerated_fields": { 812 | "first_enum_field": 1, 813 | "enum_member_count": 1 814 | }, 815 | "fields": [ 816 | { 817 | "name": "Attribute ID", 818 | "required": false, 819 | "data_types": { 820 | "UINT": [] 821 | } 822 | } 823 | ] 824 | }, 825 | "Instance_Attributes": { 826 | "name": "Instance attribute identification", 827 | "required": false, 828 | "enumerated_fields": { 829 | "first_enum_field": 1, 830 | "enum_member_count": 1 831 | }, 832 | "fields": [ 833 | { 834 | "name": "Attribute ID", 835 | "required": false, 836 | "data_types": { 837 | "UINT": [] 838 | } 839 | } 840 | ] 841 | }, 842 | "Class_Services": { 843 | "name": "Class service support", 844 | "required": false, 845 | "enumerated_fields": { 846 | "first_enum_field": 1, 847 | "enum_member_count": 1 848 | }, 849 | "fields": [ 850 | { 851 | "name": "Service", 852 | "required": false, 853 | "data_types": { 854 | "EDS_SERVICE": [] 855 | } 856 | } 857 | ] 858 | }, 859 | "Instance_Services": { 860 | "name": "Instance service support", 861 | "required": false, 862 | "enumerated_fields": { 863 | "first_enum_field": 1, 864 | "enum_member_count": 1 865 | }, 866 | "fields": [ 867 | { 868 | "name": "Service", 869 | "required": false, 870 | "data_types": { 871 | "EDS_SERVICE": [] 872 | } 873 | } 874 | ] 875 | }, 876 | "Object_Name": { 877 | "name": "Object Name", 878 | "required": false, 879 | "enumerated_fields": null, 880 | "fields": [ 881 | { 882 | "name": "Name", 883 | "required": false, 884 | "data_types": { 885 | "STRING": [] 886 | } 887 | } 888 | ] 889 | }, 890 | "Object_Class_Code": { 891 | "name": "Object Class Code", 892 | "required": false, 893 | "enumerated_fields": null, 894 | "fields": [ 895 | { 896 | "name": "Object Class Code", 897 | "required": false, 898 | "data_types": { 899 | "UDINT": [] 900 | } 901 | } 902 | ] 903 | }, 904 | "Service_DescriptionN": { 905 | "name": "Service Description", 906 | "required": false, 907 | "enumerated_fields": { 908 | "first_enum_field": 1, 909 | "enum_member_count": 1 910 | }, 911 | "fields": [ 912 | { 913 | "name": "Service Code", 914 | "required": false, 915 | "data_types": { 916 | "USINT": [] 917 | } 918 | }, 919 | { 920 | "name": "Name", 921 | "required": false, 922 | "data_types": { 923 | "STRING": [] 924 | } 925 | }, 926 | { 927 | "name": "Service Application Path", 928 | "required": false, 929 | "data_types": { 930 | "EPATH": [], 931 | "KEYWORD": [ 932 | "SYMBOL_ANSI" 933 | ] 934 | } 935 | }, 936 | { 937 | "name": "Service Request Data", 938 | "required": false, 939 | "data_types": { 940 | "REF": [ 941 | "AssemExaN", 942 | "ParamN", 943 | "ConstructedParamN" 944 | ], 945 | "EMPTY": [] 946 | } 947 | }, 948 | { 949 | "name": "Service Response Data", 950 | "required": false, 951 | "data_types": { 952 | "REF": [ 953 | "AssemExaN", 954 | "ParamN", 955 | "ConstructedParamN" 956 | ], 957 | "EMPTY": [] 958 | } 959 | } 960 | ] 961 | } 962 | } 963 | } 964 | } 965 | } -------------------------------------------------------------------------------- /cip_eds_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Omid Kompani 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | """ 26 | 27 | from collections import namedtuple 28 | from calendar import monthrange 29 | from string import digits 30 | from datetime import datetime, timedelta 31 | import inspect 32 | 33 | from collections import namedtuple 34 | RANGE = namedtuple('RANGE', 'min max') 35 | 36 | import logging 37 | logging.basicConfig(level=logging.WARNING, 38 | format='%(asctime)s - %(name)s.%(levelname)-8s %(message)s') 39 | logger = logging.getLogger(__name__) 40 | 41 | class ENUMS(object): 42 | 43 | def stringify(self, enum): 44 | for attr in vars(self.__class__): 45 | if isinstance(self.__class__.__dict__[attr], int) and self.__class__.__dict__[attr] == enum: return '{}'.format(attr) 46 | for base in self.__class__.__bases__: 47 | for attr in vars(base): 48 | if isinstance(base.__dict__[attr], int) and base.__dict__[attr] == enum: return '{}'.format(attr) 49 | return '' 50 | 51 | @classmethod 52 | def stringify(cls, enum): 53 | for attr in vars(cls): 54 | if isinstance(cls.__dict__[attr], int) and cls.__dict__[attr] == enum: return '{}'.format(attr) 55 | for base in cls.__bases__: 56 | for attr in vars(base): 57 | if isinstance(base.__dict__[attr], int) and base.__dict__[attr] == enum: return '{}'.format(attr) 58 | return '' 59 | 60 | class CIP_STD_TYPES(ENUMS): 61 | CIP_EDS_UTIME = 0xC0 62 | CIP_EDS_BOOL = 0xC1 63 | CIP_EDS_SINT = 0xC2 64 | CIP_EDS_INT = 0xC3 65 | CIP_EDS_DINT = 0xC4 66 | CIP_EDS_LINT = 0xC5 67 | CIP_EDS_USINT = 0xC6 68 | CIP_EDS_UINT = 0xC7 69 | CIP_EDS_UDINT = 0xC8 70 | CIP_EDS_ULINT = 0xC9 71 | CIP_EDS_REAL = 0xCA 72 | CIP_EDS_LREAL = 0xCB 73 | CIP_EDS_STIME = 0xCC 74 | CIP_EDS_DATE = 0xCD 75 | CIP_EDS_TIME_OF_DAY = 0xCE 76 | CIP_EDS_DATE_AND_TIME = 0xCF 77 | CIP_EDS_STRING = 0xD0 78 | CIP_EDS_BYTE = 0xD1 79 | CIP_EDS_WORD = 0xD2 80 | CIP_EDS_DWORD = 0xD3 81 | CIP_EDS_LWORD = 0xD4 82 | CIP_EDS_STRING2 = 0xD5 83 | CIP_EDS_FTIME = 0xD6 84 | CIP_EDS_LTIME = 0xD7 85 | CIP_EDS_ITIME = 0xD8 86 | CIP_EDS_STRINGN = 0xD9 87 | CIP_EDS_SHORT_STRING = 0xDA 88 | CIP_EDS_TIME = 0xDB 89 | CIP_EDS_EPATH = 0xDC 90 | CIP_EDS_ENGUNIT = 0xDD 91 | CIP_EDS_STRINGI = 0xDE 92 | CIP_EDS_NTIME = 0xDF 93 | 94 | def getnumber(data): 95 | ''' 96 | Converts an input of string type into its numeric representaion. 97 | ''' 98 | if data is None: return None 99 | if data == '': return None 100 | if isint(data): return int(data) 101 | if isfloat(data): return float(data) 102 | if ishex(data): return int(data, 16) 103 | if isbin(data): return int(data, 2) 104 | return None 105 | 106 | 107 | def isnumber(data): 108 | ''' 109 | Checks if a string represents a numeric value. 110 | ''' 111 | if data is None: return False 112 | if data == '': return False 113 | if isint(data): return True 114 | if isfloat(data): return True 115 | if ishex(data): return True 116 | if isbin(data): return True 117 | return False 118 | 119 | 120 | def isint(data): 121 | ''' 122 | Checks if a string represents a decimal coded numeric value. 123 | ''' 124 | if data is None: return False 125 | try: 126 | int(data) 127 | except ValueError: 128 | return False 129 | return True 130 | 131 | 132 | def isfloat(data): 133 | ''' 134 | Checks if a string represents a floating point numeric value. 135 | ''' 136 | if data is None: return False 137 | try: 138 | float(data) 139 | except ValueError: 140 | return False 141 | return True 142 | 143 | 144 | def ishex(data): 145 | ''' 146 | Checks if a string represents a hexadecimal coded numeric value. 147 | ''' 148 | if data is None: return False 149 | try: 150 | int(data, 16) 151 | except ValueError: 152 | return False 153 | return True 154 | 155 | 156 | def isbin(data): 157 | ''' 158 | Checks if a string represents a binary coded numeric value. 159 | ''' 160 | if data is None: return False 161 | try: 162 | int(data, 2) 163 | except ValueError: 164 | return False 165 | return True 166 | 167 | 168 | def isdate(data): 169 | if data is None: 170 | return False 171 | 172 | try: 173 | m, d, y = data.split('-') 174 | 175 | if len(m) != 2 or len(d) != 2 or int(m) < 1 or int(m) > 12: 176 | logger.error('Invalid EDS_DATE month length or month value!') 177 | return False 178 | 179 | if len(y) == 4: 180 | if int(y) < 1994: 181 | logger.error('Invalid EDS_DATE yyyy value!') 182 | return False 183 | elif len(y) == 2: 184 | if int(y) < 94: 185 | logger.error('Invalid EDS_DATE yy value!') 186 | return False 187 | else: 188 | logger.error('Invalid EDS_DATE year format!') 189 | return False 190 | 191 | if int(d) < 1 or (int(d) > (monthrange(int(y), int(m))[1]) ): 192 | logger.error('Invalid EDS_DATE day value!') 193 | return False 194 | except: 195 | return False 196 | return True 197 | 198 | 199 | def getdate(): 200 | return datetime.strftime(datetime.now(), "%m-%d-%Y") 201 | 202 | def cast2date(val): 203 | ''' 204 | Converts a 16-bit value to a valid DATE string between 01.01.1972 and 06.06.2151 205 | ''' 206 | return datetime.strftime(datetime.strptime('01-01-1972', "%m-%d-%Y") + timedelta(days=val), "%m-%d-%Y") 207 | 208 | def istime(data): 209 | if data is None: return False 210 | data = data.split(':') 211 | if len(data) != 3: 212 | return False 213 | hh = data[0] 214 | mm = data[1] 215 | ss = data[2] 216 | 217 | if len(mm) != 2 or len(hh) != 2 or len(ss) != 2: 218 | return False 219 | 220 | if int(hh) > 24 or int(hh) < 0: 221 | return False 222 | if int(mm) > 60 or int(mm) < 0: 223 | return False 224 | if int(ss) > 60 or int(ss) < 0: 225 | return False 226 | return True 227 | 228 | 229 | def gettime(): 230 | hh = format(datetime.now().hour, '02') 231 | mm = format(datetime.now().minute, '02') 232 | ss = format(datetime.now().second, '02') 233 | return "%s:%s:%s" %(hh, mm, ss) 234 | 235 | 236 | 237 | class CIP_EDS_BASE_TYPE(object): 238 | _typeid = None 239 | _range = [] 240 | 241 | def __init__(self, value, *args): 242 | self._value = value 243 | #sele._range = *args 244 | 245 | @property 246 | def range(self): 247 | return self._range 248 | 249 | @property 250 | def value(self): 251 | return self._value 252 | 253 | @classmethod 254 | def validate(cls, value, *args): 255 | raise(NotImplementedError) 256 | 257 | def __repr__(self): 258 | return "{}({})".format(self.__class__.__name__, self.__value) 259 | 260 | def __str__(self): 261 | return '{}'.format(self._value) 262 | 263 | class CIP_EDS_BASE_INT(CIP_EDS_BASE_TYPE): 264 | 265 | def __init__(self, value, *args): 266 | self._value = value 267 | #sele._range = *args 268 | 269 | @classmethod 270 | def validate(cls, value, *args): 271 | value = getnumber(value) 272 | return value is not None and value >= cls._range.min and value <= cls._range.max 273 | 274 | def __format__(self, format_spec): 275 | return format(self._value, format_spec) 276 | 277 | def __hex__(self): 278 | return '0x{:X}'.format(self._value) 279 | 280 | def __eq__( self , other): 281 | return self._value == other 282 | 283 | def __ne__( self , other): 284 | return self._value != other 285 | 286 | def __lt__( self , other): 287 | return self._value < other 288 | 289 | def __gt__( self , other): 290 | return self._value > other 291 | 292 | def __le__( self , other): 293 | return self._value <= other 294 | 295 | def __ge__( self , other): 296 | return self._value <= other 297 | 298 | def __add__(self, other): 299 | return self._value + other 300 | 301 | def __sub__(self, other): 302 | return self._value - other 303 | 304 | def __mul__(self, other): 305 | return self._value * other 306 | 307 | def __truediv__(self, other): 308 | return self._value / other 309 | 310 | def __floordiv__(self, other): 311 | return self._value // other 312 | 313 | def __int__(self): 314 | return self._value 315 | 316 | def __index__(self): 317 | return self.__int__() 318 | 319 | def __len__(self): 320 | return self._size 321 | 322 | 323 | class BOOL(CIP_EDS_BASE_TYPE): 324 | _typeid = CIP_STD_TYPES.CIP_EDS_BOOL 325 | _range = RANGE(0, 1) 326 | 327 | def __new__(cls, value, *args): 328 | if cls.validate(value): 329 | return super(BOOL, cls).__new__(cls) 330 | else: 331 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 332 | + "<{} (CIP typeID: 0x{:X})> data type." 333 | .format(cls.__name__, cls._typeid)) 334 | 335 | def __init__(self, value, *args): 336 | super(BOOL, self).__init__(value) 337 | 338 | def __str__(self): 339 | return str(self._value != 0) 340 | 341 | 342 | class USINT(CIP_EDS_BASE_INT): 343 | _typeid = CIP_STD_TYPES.CIP_EDS_USINT 344 | _range = RANGE(0, 255) 345 | 346 | def __new__(cls, value, *args): 347 | if cls.validate(value): 348 | return super(USINT, cls).__new__(cls) 349 | else: 350 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 351 | + "<{} (CIP typeID: 0x{:X})> data type." 352 | .format(cls.__name__, cls._typeid)) 353 | 354 | def __init__(self, value, *args): 355 | super(USINT, self).__init__(value) 356 | 357 | 358 | class SINT(CIP_EDS_BASE_INT): 359 | _typeid = CIP_STD_TYPES.CIP_EDS_USINT 360 | _range = RANGE(0, 255) 361 | 362 | def __new__(cls, value, *args): 363 | if cls.validate(value): 364 | return super(SINT, cls).__new__(cls) 365 | else: 366 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 367 | + "<{} (CIP typeID: 0x{:X})> data type." 368 | .format(cls.__name__, cls._typeid)) 369 | 370 | def __init__(self, value, *args): 371 | super(SINT, self).__init__(value) 372 | 373 | 374 | class UINT(CIP_EDS_BASE_INT): 375 | _typeid = CIP_STD_TYPES.CIP_EDS_UINT 376 | _range = RANGE(0, 65535) 377 | 378 | def __new__(cls, value, *args): 379 | if cls.validate(value): 380 | return super(UINT, cls).__new__(cls) 381 | else: 382 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 383 | + "<{} (CIP typeID: 0x{:X})> data type." 384 | .format(cls.__name__, cls._typeid)) 385 | 386 | def __init__(self, value, *args): 387 | super(UINT, self).__init__(value) 388 | 389 | 390 | class INT(CIP_EDS_BASE_INT): 391 | _typeid = CIP_STD_TYPES.CIP_EDS_INT 392 | _range = RANGE(-32768, 32767) 393 | 394 | def __new__(cls, value, *args): 395 | if cls.validate(value): 396 | return super(INT, cls).__new__(cls) 397 | else: 398 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 399 | + "<{} (CIP typeID: 0x{:X})> data type." 400 | .format(cls.__name__, cls._typeid)) 401 | 402 | def __init__(self, value, *args): 403 | super(INT, self).__init__(value) 404 | 405 | 406 | class UDINT(CIP_EDS_BASE_INT): 407 | _typeid = CIP_STD_TYPES.CIP_EDS_UDINT 408 | _range = RANGE(0, 4294967295) 409 | 410 | def __new__(cls, value, *args): 411 | if cls.validate(value): 412 | return super(UDINT, cls).__new__(cls) 413 | else: 414 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 415 | + "<{} (CIP typeID: 0x{:X})> data type." 416 | .format(cls.__name__, cls._typeid)) 417 | 418 | def __init__(self, value, *args): 419 | super(UDINT, self).__init__(value) 420 | 421 | 422 | class DINT(CIP_EDS_BASE_INT): 423 | _typeid = CIP_STD_TYPES.CIP_EDS_DINT 424 | _range = RANGE(-2147483648, 2147483647) 425 | 426 | def __new__(cls, value, *args): 427 | if cls.validate(value): 428 | return super(DINT, cls).__new__(cls) 429 | else: 430 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 431 | + "<{} (CIP typeID: 0x{:X})> data type." 432 | .format(cls.__name__, cls._typeid)) 433 | 434 | def __init__(self, value, *args): 435 | super(DINT, self).__init__(value) 436 | 437 | 438 | 439 | class ULINT(CIP_EDS_BASE_INT): 440 | _typeid = CIP_STD_TYPES.CIP_EDS_ULINT 441 | _range = RANGE(0, 18446744073709551615) 442 | 443 | def __new__(cls, value, *args): 444 | if cls.validate(value): 445 | return super(ULINT, cls).__new__(cls) 446 | else: 447 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 448 | + "<{} (CIP typeID: 0x{:X})> data type." 449 | .format(cls.__name__, cls._typeid)) 450 | 451 | def __init__(self, value, *args): 452 | super(ULINT, self).__init__(value) 453 | 454 | 455 | class LINT(CIP_EDS_BASE_INT): 456 | _typeid = CIP_STD_TYPES.CIP_EDS_LINT 457 | _range = RANGE(-9223372036854775808, 9223372036854775807) 458 | def __new__(cls, value, *args): 459 | if cls.validate(value): 460 | return super(LINT, cls).__new__(cls) 461 | else: 462 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 463 | + "<{} (CIP typeID: 0x{:X})> data type." 464 | .format(cls.__name__, cls._typeid)) 465 | 466 | def __init__(self, value, *args): 467 | super(LINT, self).__init__(value) 468 | 469 | 470 | class BYTE(CIP_EDS_BASE_INT): 471 | _typeid = CIP_STD_TYPES.CIP_EDS_BYTE 472 | _range = RANGE(0, 255) 473 | 474 | def __new__(cls, value, *args): 475 | if cls.validate(value): 476 | return super(BYTE, cls).__new__(cls) 477 | else: 478 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 479 | + "<{} (CIP typeID: 0x{:X})> data type." 480 | .format(cls.__name__, cls._typeid)) 481 | 482 | def __init__(self, value, *args): 483 | super(BYTE, self).__init__(value) 484 | 485 | 486 | class WORD(CIP_EDS_BASE_INT): 487 | _typeid = CIP_STD_TYPES.CIP_EDS_WORD 488 | _range = RANGE(0, 65535) 489 | 490 | def __new__(cls, value, *args): 491 | if cls.validate(value): 492 | return super(WORD, cls).__new__(cls) 493 | else: 494 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 495 | + "<{} (CIP typeID: 0x{:X})> data type." 496 | .format(cls.__name__, cls._typeid)) 497 | 498 | def __init__(self, value, *args): 499 | super(WORD, self).__init__(value) 500 | 501 | 502 | class DWORD(CIP_EDS_BASE_INT): 503 | _typeid = CIP_STD_TYPES.CIP_EDS_DWORD 504 | _range = RANGE(0, 4294967295) 505 | 506 | def __new__(cls, value, *args): 507 | if cls.validate(value): 508 | return super(DWORD, cls).__new__(cls) 509 | else: 510 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 511 | + "<{} (CIP typeID: 0x{:X})> data type." 512 | .format(cls.__name__, cls._typeid)) 513 | 514 | def __init__(self, value, *args): 515 | super(DWORD, self).__init__(value) 516 | 517 | 518 | class LWORD(CIP_EDS_BASE_INT): 519 | _typeid = CIP_STD_TYPES.CIP_EDS_LWORD 520 | _range = RANGE(0, 18446744073709551615) 521 | 522 | def __new__(cls, value, *args): 523 | if cls.validate(value): 524 | return super(LWORD, cls).__new__(cls) 525 | else: 526 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 527 | + "<{} (CIP typeID: 0x{:X})> data type." 528 | .format(cls.__name__, cls._typeid)) 529 | 530 | def __init__(self, value, *args): 531 | super(LWORD, self).__init__(value) 532 | 533 | class REAL(CIP_EDS_BASE_INT): # TODO: improve validate 534 | _typeid = CIP_STD_TYPES.CIP_EDS_REAL 535 | _range = RANGE(-16777216.0, 16777216.0) 536 | 537 | def __new__(cls, value, *args): 538 | if cls.validate(value): 539 | return super(REAL, cls).__new__(cls) 540 | else: 541 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 542 | + "<{} (CIP typeID: 0x{:X})> data type." 543 | .format(cls.__name__, cls._typeid)) 544 | 545 | def __init__(self, value, *args): 546 | super(REAL, self).__init__(value) 547 | 548 | class LREAL(CIP_EDS_BASE_INT): # TODO: improve validate 549 | _typeid = CIP_STD_TYPES.CIP_EDS_LREAL 550 | _range = RANGE(-9007199254740992.0, 9007199254740992.0) 551 | 552 | def __new__(cls, value, *args): 553 | if cls.validate(value): 554 | return super(LREAL, cls).__new__(cls) 555 | else: 556 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 557 | + "<{} (CIP typeID: 0x{:X})> data type." 558 | .format(cls.__name__, cls._typeid)) 559 | 560 | def __init__(self, value, *args): 561 | super(LREAL, self).__init__(value) 562 | 563 | 564 | class STIME(CIP_EDS_BASE_TYPE): # dummy type! TODO 565 | _typeid = CIP_STD_TYPES.CIP_EDS_STIME 566 | 567 | def __new__(cls, value, *args): 568 | if cls.validate(value): 569 | return super(STIME, cls).__new__(cls) 570 | else: 571 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 572 | + "<{} (CIP typeID: 0x{:X})> data type." 573 | .format(cls.__name__, cls._typeid)) 574 | 575 | def __init__(self, value, *args): 576 | super(STIME, self).__init__(value) 577 | 578 | class STRING(CIP_EDS_BASE_TYPE): 579 | _typeid = CIP_STD_TYPES.CIP_EDS_STRING 580 | 581 | def __new__(cls, value, *args): 582 | if cls.validate(value): 583 | return super(STRING, cls).__new__(cls) 584 | else: 585 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 586 | + "<{} (CIP typeID: 0x{:X})> data type." 587 | .format(cls.__name__, cls._typeid)) 588 | 589 | def __init__(self, value, *args): 590 | super(STRING, self).__init__(value) 591 | 592 | @classmethod 593 | def validate(cls, value, *args): 594 | return isinstance(value, str) 595 | 596 | def __str__(self): 597 | return '\n'.join('\"{}\"'.format(self.value[offset : offset + 60]) 598 | for offset in range(0, len(self.value), 60)) 599 | 600 | 601 | class STRINGI(CIP_EDS_BASE_TYPE): 602 | _typeid = CIP_STD_TYPES.CIP_EDS_STRINGI 603 | 604 | def __new__(cls, value, *args): 605 | if cls.validate(value): 606 | return super(STRINGI, cls).__new__(cls) 607 | else: 608 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 609 | + "<{} (CIP typeID: 0x{:X})> data type." 610 | .format(cls.__name__, cls._typeid)) 611 | 612 | def __init__(self, value, *args): 613 | self._range = ['mm-dd-yyyy'] 614 | super(STRINGI, self).__init__(value) 615 | 616 | @classmethod 617 | def validate(cls, value, *args): 618 | # TODO 619 | pass 620 | 621 | def __str__(self): 622 | return 'STRINGI...' # TODO 623 | 624 | 625 | class DATE(CIP_EDS_BASE_TYPE): 626 | # EDS_DATE mm.dd.yyyy from 1994 to 9999 627 | _range = ['mm.dd.yyyy', 'mm.dd.yy'] 628 | 629 | def __new__(cls, value, *args): 630 | if cls.validate(value): 631 | return super(DATE, cls).__new__(cls) 632 | else: 633 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 634 | + "<{} (CIP typeID: 0x{:X})> data type." 635 | .format(cls.__name__, cls._typeid)) 636 | 637 | def __init__(self, value, *args): 638 | super(DATE, self).__init__(value) 639 | 640 | @staticmethod 641 | def validate(value, *args): 642 | return isdate(value) 643 | 644 | 645 | class TIME(CIP_EDS_BASE_TYPE): 646 | _typeid = CIP_STD_TYPES.CIP_EDS_TIME 647 | 648 | def __new__(cls, value, *args): 649 | if cls.validate(value): 650 | return super(TIME, cls).__new__(cls) 651 | else: 652 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 653 | + "<{} (CIP typeID: 0x{:X})> data type." 654 | .format(cls.__name__, cls._typeid)) 655 | 656 | def __init__(self, value, *args): 657 | self.__value = value 658 | super(TIME, self).__init__(value) 659 | 660 | @property 661 | def range(self): 662 | return ['HH:MM:SS'] # TODO 663 | 664 | @staticmethod 665 | def validate(value, *args): 666 | try: 667 | data = value.split(':') 668 | except: 669 | return False 670 | 671 | if len(data) != 3: 672 | return False 673 | hh = data[0] 674 | mm = data[1] 675 | ss = data[2] 676 | 677 | # Tolerate no leading zeros 678 | #if len(mm) < 2 or len(hh) < 2 or len(ss) < 2: 679 | # return False 680 | 681 | if ((len(hh) < 1 or len(hh) > 2) or 682 | (len(mm) < 1 or len(mm) > 2) or 683 | (len(ss) < 1 or len(ss) > 2)): 684 | return False 685 | 686 | if int(hh) > 24 or int(hh) < 0: 687 | return False 688 | if int(mm) > 60 or int(mm) < 0: 689 | return False 690 | if int(ss) > 60 or int(ss) < 0: 691 | return False 692 | return True 693 | 694 | 695 | class EPATH(CIP_EDS_BASE_TYPE): 696 | _typeid = CIP_STD_TYPES.CIP_EDS_EPATH 697 | 698 | def __new__(cls, value, *args): 699 | if cls.validate(value): 700 | return super(EPATH, cls).__new__(cls) 701 | else: 702 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 703 | + "<{} (CIP typeID: 0x{:X})> data type." 704 | .format(cls.__name__, cls._typeid)) 705 | 706 | def __init__(self, value, *args): 707 | super(EPATH, self).__init__(value) 708 | 709 | @staticmethod 710 | def validate(value, *args): 711 | try: 712 | elements = value.split() 713 | except: 714 | return False 715 | for element in elements: 716 | if len(element) < 2: 717 | return False 718 | if not isnumber(element): 719 | if (element[0] == '[' and element[-1] == ']'): #TODO: accept references without brackets 720 | continue 721 | return False 722 | elif not ishex(element): 723 | return False 724 | return True 725 | 726 | def __str__(self): 727 | return "\"{}\"".format(self.value) 728 | 729 | 730 | class REVISION(CIP_EDS_BASE_TYPE): 731 | 732 | def __new__(cls, value, *args): 733 | if cls.validate(value): 734 | return super(REVISION, cls).__new__(cls) 735 | else: 736 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 737 | + "<{}> data type." 738 | .format(cls.__name__, cls._typeid)) 739 | 740 | def __init__(self, value, *args): 741 | super(REVISION, self).__init__(value) 742 | 743 | @staticmethod 744 | def validate(value, *args): 745 | try: 746 | elements = value.split('.') 747 | except: 748 | return False 749 | 750 | if len(elements) != 2: 751 | return False 752 | for element in elements: 753 | if not isnumber(element): 754 | return False 755 | return True 756 | 757 | 758 | class ETH_MAC_ADDR(CIP_EDS_BASE_TYPE): 759 | 760 | def __new__(cls, value, *args): 761 | if cls.validate(value): 762 | return super(ETH_MAC_ADDR, cls).__new__(cls) 763 | else: 764 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 765 | + "<{}> data type." 766 | .format(cls.__name__, cls._typeid)) 767 | 768 | def __init__(self, value, *args): 769 | super(ETH_MAC_ADDR, self).__init__(value) 770 | 771 | @staticmethod 772 | def validate(value, *args): 773 | macaddr = value.rstrip('}').lstrip('{').strip().replace(':', '-').replace('.', '-').split('-') 774 | if len(macaddr) != 6: 775 | return False 776 | for field in macaddr: 777 | if USINT.validate(field) == False: 778 | return False 779 | return True 780 | 781 | 782 | class REF(CIP_EDS_BASE_TYPE): 783 | def __new__(cls, value, *args): 784 | if cls.validate(value, *args): 785 | return super(REF, cls).__new__(cls) 786 | else: 787 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 788 | + "<{}> data type." 789 | .format(cls.__name__)) 790 | 791 | def __init__(self, value, *args): 792 | self._range = args[0] # TODO 793 | super(REF, self).__init__(value) 794 | 795 | @staticmethod 796 | def validate(value, *args): 797 | if not isinstance(value, str): 798 | return False 799 | for keyword in args[0]: 800 | keyword = keyword.rstrip('N').lower() 801 | if value[:len(keyword)].lower() == keyword: 802 | return True 803 | return False 804 | 805 | 806 | class KEYWORD(CIP_EDS_BASE_TYPE): 807 | 808 | def __new__(cls, value, *args): 809 | if cls.validate(value, *args): 810 | return super(KEYWORD, cls).__new__(cls) 811 | else: 812 | raise Exception(__name__ + ":> Invalid value: {} ".format(arg[0]) 813 | + "for <{}> data type." 814 | .format(cls.__name__)) 815 | 816 | def __init__(self, value, *args): 817 | self._range = args[0] 818 | super(KEYWORD, self).__init__(value) 819 | 820 | @staticmethod 821 | def validate(value, *args): 822 | for keyword in args[0]: 823 | if value.lower() == keyword.lower(): 824 | return True 825 | return False 826 | 827 | 828 | class DATATYPE_REF(CIP_EDS_BASE_TYPE): 829 | 830 | def __new__(cls, value, *args): 831 | if cls.validate(value): 832 | return super(DATATYPE_REF, cls).__new__(cls) 833 | else: 834 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 835 | + "<{}> data type." 836 | .format(cls.__name__)) 837 | 838 | def __init__(self, value, *args): 839 | super(DATATYPE_REF, self).__init__(value) 840 | 841 | @staticmethod 842 | def validate(value, *args): 843 | return True # TODO 844 | 845 | 846 | class EDS_SERVICE(CIP_EDS_BASE_TYPE): 847 | 848 | def __new__(cls, value, *args): 849 | if cls.validate(value): 850 | return super(EDS_SERVICE, cls).__new__(cls) 851 | else: 852 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 853 | + "<{}> data type." 854 | .format(cls.__name__)) 855 | 856 | def __init__(self, value, *args): 857 | super(EDS_SERVICE, self).__init__(value) 858 | 859 | @staticmethod 860 | def validate(value, *args): 861 | return True # TODO 862 | 863 | 864 | class EMPTY(CIP_EDS_BASE_TYPE): 865 | 866 | def __new__(cls, value, *args): 867 | if cls.validate(value): 868 | return super(EMPTY, cls).__new__(cls) 869 | else: 870 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 871 | + "<{}> data type." 872 | .format(cls.__name__)) 873 | 874 | def __init__(self, value, *args): 875 | super(EMPTY, self).__init__(value) 876 | 877 | @staticmethod 878 | def validate(value, *args): 879 | if args is None or value == '': 880 | return True 881 | return False 882 | 883 | def __str__(self): 884 | return '' 885 | 886 | 887 | class VENDOR_SPECIFIC(CIP_EDS_BASE_TYPE): 888 | 889 | def __new__(cls, value, *args): 890 | if cls.validate(value): 891 | return super(VENDOR_SPECIFIC, cls).__new__(cls) 892 | else: 893 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 894 | + "<{}> data type." 895 | .format(cls.__name__)) 896 | 897 | def __init__(self, value, *args): 898 | super(VENDOR_SPECIFIC, self).__init__(value) 899 | 900 | @staticmethod 901 | def validate(value, *args): 902 | if isinstance(value, str) and value != '': #TODO 903 | if value[0].isdigit(): 904 | return True 905 | return False 906 | 907 | 908 | class UNDEFINED(CIP_EDS_BASE_TYPE): 909 | def __new__(cls, value, *args): 910 | if cls.validate(value): 911 | return super(UNDEFINED, cls).__new__(cls) 912 | else: 913 | raise Exception(__name__ + ":> Invalid value: {} for ".format(value) 914 | + "<{}> data type." 915 | .format(cls.__name__)) 916 | 917 | def __init__(self, value, *args): 918 | self.__value = value 919 | super(UNDEFINED, self).__init__(value) 920 | 921 | @staticmethod 922 | def validate(value, *args): 923 | return True 924 | -------------------------------------------------------------------------------- /ethernetip_lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_verison": 1, 3 | "schema_file": "edslib_schema.json", 4 | "project": "eds_pie", 5 | "lib_name": "EtherNet/IP", 6 | "protocol": "EtherNetIP", 7 | "comment": "All CIP and EtherNet/IP related classes.", 8 | "sections": { 9 | "Identity Class": { 10 | "name": "Identity Class", 11 | "required": false, 12 | "class_id": 1, 13 | "entries": {} 14 | }, 15 | "Message Router Class": { 16 | "name": "Message Router Class", 17 | "required": false, 18 | "class_id": 2, 19 | "entries": {} 20 | }, 21 | "DeviceNet Class": { 22 | "name": "DeviceNet Class", 23 | "required": false, 24 | "class_id": 3, 25 | "entries": {} 26 | }, 27 | "Assembly": { 28 | "name": "Assembly", 29 | "required": false, 30 | "class_id": 4, 31 | "entries": { 32 | "AssemN": { 33 | "name": "Assem", 34 | "required": false, 35 | "enumerated_fields": { 36 | "first_enum_field": 7, 37 | "enum_member_count": 2 38 | }, 39 | "fields": [ 40 | { 41 | "name": "Name", 42 | "required": false, 43 | "data_types": { 44 | "STRING": [] 45 | } 46 | }, 47 | { 48 | "name": "Path", 49 | "required": false, 50 | "data_types": { 51 | "EPATH": [], 52 | "KEYWORD": [ 53 | "SYMBOL_ANSI" 54 | ] 55 | } 56 | }, 57 | { 58 | "name": "Size", 59 | "required": false, 60 | "data_types": { 61 | "UINT": [] 62 | } 63 | }, 64 | { 65 | "name": "Descriptor", 66 | "required": false, 67 | "data_types": { 68 | "WORD": [] 69 | } 70 | }, 71 | { 72 | "name": "Reserved", 73 | "required": false, 74 | "data_types": { 75 | "EMPTY": [] 76 | } 77 | }, 78 | { 79 | "name": "Reserved", 80 | "required": false, 81 | "data_types": { 82 | "EMPTY": [] 83 | } 84 | }, 85 | { 86 | "name": "Member Size", 87 | "required": false, 88 | "data_types": { 89 | "UINT": [] 90 | } 91 | }, 92 | { 93 | "name": "Member Reference", 94 | "required": false, 95 | "data_types": { 96 | "UDINT": [], 97 | "EPATH": [], 98 | "REF": [ 99 | "AssemN", 100 | "ParamN", 101 | "ProxyAssemN", 102 | "ProxyParamN" 103 | ], 104 | "EMPTY": [] 105 | } 106 | } 107 | ] 108 | }, 109 | "ProxyAssemN": { 110 | "name": "Assem", 111 | "required": false, 112 | "enumerated_fields": { 113 | "first_enum_field": 7, 114 | "enum_member_count": 2 115 | }, 116 | "fields": [ 117 | { 118 | "name": "Name", 119 | "required": false, 120 | "data_types": { 121 | "STRING": [] 122 | } 123 | }, 124 | { 125 | "name": "Path", 126 | "required": false, 127 | "data_types": { 128 | "EPATH": [], 129 | "KEYWORD": [ 130 | "SYMBOL_ANSI" 131 | ] 132 | } 133 | }, 134 | { 135 | "name": "Size", 136 | "required": false, 137 | "data_types": { 138 | "UINT": [] 139 | } 140 | }, 141 | { 142 | "name": "Descriptor", 143 | "required": false, 144 | "data_types": { 145 | "WORD": [] 146 | } 147 | }, 148 | { 149 | "name": "Reserved", 150 | "required": false, 151 | "data_types": { 152 | "EMPTY": [] 153 | } 154 | }, 155 | { 156 | "name": "Reserved", 157 | "required": false, 158 | "data_types": { 159 | "EMPTY": [] 160 | } 161 | }, 162 | { 163 | "name": "Member Size", 164 | "required": false, 165 | "data_types": { 166 | "UINT": [] 167 | } 168 | }, 169 | { 170 | "name": "Member Reference", 171 | "required": false, 172 | "data_types": { 173 | "UDINT": [], 174 | "EPATH": [], 175 | "REF": [ 176 | "AssemN", 177 | "ParamN" 178 | ], 179 | "EMPTY": [] 180 | } 181 | } 182 | ] 183 | }, 184 | "ProxiedAssemN": { 185 | "name": "Assem", 186 | "required": false, 187 | "enumerated_fields": { 188 | "first_enum_field": 7, 189 | "enum_member_count": 2 190 | }, 191 | "fields": [ 192 | { 193 | "name": "Name", 194 | "required": false, 195 | "data_types": { 196 | "STRING": [] 197 | } 198 | }, 199 | { 200 | "name": "Path", 201 | "required": false, 202 | "data_types": { 203 | "EPATH": [], 204 | "KEYWORD": [ 205 | "SYMBOL_ANSI" 206 | ] 207 | } 208 | }, 209 | { 210 | "name": "Size", 211 | "required": false, 212 | "data_types": { 213 | "UINT": [] 214 | } 215 | }, 216 | { 217 | "name": "Descriptor", 218 | "required": false, 219 | "data_types": { 220 | "WORD": [] 221 | } 222 | }, 223 | { 224 | "name": "Reserved", 225 | "required": false, 226 | "data_types": { 227 | "EMPTY": [] 228 | } 229 | }, 230 | { 231 | "name": "Reserved", 232 | "required": false, 233 | "data_types": { 234 | "EMPTY": [] 235 | } 236 | }, 237 | { 238 | "name": "Member Size", 239 | "required": false, 240 | "data_types": { 241 | "UINT": [] 242 | } 243 | }, 244 | { 245 | "name": "Member Reference", 246 | "required": false, 247 | "data_types": { 248 | "UDINT": [], 249 | "EPATH": [], 250 | "REF": [ 251 | "AssemN", 252 | "ParamN" 253 | ], 254 | "EMPTY": [] 255 | } 256 | } 257 | ] 258 | }, 259 | "AssemExaN": { 260 | "name": "Assem", 261 | "required": false, 262 | "enumerated_fields": { 263 | "first_enum_field": 7, 264 | "enum_member_count": 2 265 | }, 266 | "fields": [ 267 | { 268 | "name": "Name", 269 | "required": false, 270 | "data_types": { 271 | "STRING": [] 272 | } 273 | }, 274 | { 275 | "name": "Path", 276 | "required": false, 277 | "data_types": { 278 | "EPATH": [], 279 | "KEYWORD": [ 280 | "SYMBOL_ANSI" 281 | ] 282 | } 283 | }, 284 | { 285 | "name": "Size", 286 | "required": false, 287 | "data_types": { 288 | "UINT": [], 289 | "REF": [ 290 | "ParamN" 291 | ] 292 | } 293 | }, 294 | { 295 | "name": "Descriptor", 296 | "required": false, 297 | "data_types": { 298 | "WORD": [] 299 | } 300 | }, 301 | { 302 | "name": "Reserved", 303 | "required": false, 304 | "data_types": { 305 | "EMPTY": [] 306 | } 307 | }, 308 | { 309 | "name": "Reserved", 310 | "required": false, 311 | "data_types": { 312 | "EMPTY": [] 313 | } 314 | }, 315 | { 316 | "name": "Member Size", 317 | "required": false, 318 | "data_types": { 319 | "UINT": [] 320 | } 321 | }, 322 | { 323 | "name": "Member Reference", 324 | "required": false, 325 | "data_types": { 326 | "UDINT": [], 327 | "EPATH": [], 328 | "REF": [ 329 | "AssemN", 330 | "ParamN", 331 | "AssemExaN", 332 | "VariantN", 333 | "BitStringVariantN", 334 | "VariantExaN", 335 | "ArrayN", 336 | "ConstructedParamN" 337 | ], 338 | "EMPTY": [] 339 | } 340 | } 341 | ] 342 | }, 343 | "ProxyAssemExaN": { 344 | "name": "Assem", 345 | "required": false, 346 | "enumerated_fields": { 347 | "first_enum_field": 7, 348 | "enum_member_count": 2 349 | }, 350 | "fields": [ 351 | { 352 | "name": "Name", 353 | "required": false, 354 | "data_types": { 355 | "STRING": [] 356 | } 357 | }, 358 | { 359 | "name": "Path", 360 | "required": false, 361 | "data_types": { 362 | "EPATH": [], 363 | "KEYWORD": [ 364 | "SYMBOL_ANSI" 365 | ] 366 | } 367 | }, 368 | { 369 | "name": "Size", 370 | "required": false, 371 | "data_types": { 372 | "UINT": [], 373 | "REF": [ 374 | "ParamN" 375 | ] 376 | } 377 | }, 378 | { 379 | "name": "Descriptor", 380 | "required": false, 381 | "data_types": { 382 | "WORD": [] 383 | } 384 | }, 385 | { 386 | "name": "Reserved", 387 | "required": false, 388 | "data_types": { 389 | "EMPTY": [] 390 | } 391 | }, 392 | { 393 | "name": "Reserved", 394 | "required": false, 395 | "data_types": { 396 | "EMPTY": [] 397 | } 398 | }, 399 | { 400 | "name": "Member Size", 401 | "required": false, 402 | "data_types": { 403 | "UINT": [] 404 | } 405 | }, 406 | { 407 | "name": "Member Reference", 408 | "required": false, 409 | "data_types": { 410 | "UDINT": [], 411 | "EPATH": [], 412 | "REF": [ 413 | "AssemN", 414 | "ParamN", 415 | "AssemExaN", 416 | "VariantN", 417 | "BitStringVariantN", 418 | "VariantExaN", 419 | "ArrayN", 420 | "ConstructedParamN" 421 | ], 422 | "EMPTY": [] 423 | } 424 | } 425 | ] 426 | }, 427 | "ProxiedAssemExaN": { 428 | "name": "Assem", 429 | "required": false, 430 | "enumerated_fields": { 431 | "first_enum_field": 7, 432 | "enum_member_count": 2 433 | }, 434 | "fields": [ 435 | { 436 | "name": "Name", 437 | "required": false, 438 | "data_types": { 439 | "STRING": [] 440 | } 441 | }, 442 | { 443 | "name": "Path", 444 | "required": false, 445 | "data_types": { 446 | "EPATH": [], 447 | "KEYWORD": [ 448 | "SYMBOL_ANSI" 449 | ] 450 | } 451 | }, 452 | { 453 | "name": "Size", 454 | "required": false, 455 | "data_types": { 456 | "UINT": [], 457 | "REF": [ 458 | "ParamN" 459 | ] 460 | } 461 | }, 462 | { 463 | "name": "Descriptor", 464 | "required": false, 465 | "data_types": { 466 | "WORD": [] 467 | } 468 | }, 469 | { 470 | "name": "Reserved", 471 | "required": false, 472 | "data_types": { 473 | "EMPTY": [] 474 | } 475 | }, 476 | { 477 | "name": "Reserved", 478 | "required": false, 479 | "data_types": { 480 | "EMPTY": [] 481 | } 482 | }, 483 | { 484 | "name": "Member Size", 485 | "required": false, 486 | "data_types": { 487 | "UINT": [] 488 | } 489 | }, 490 | { 491 | "name": "Member Reference", 492 | "required": false, 493 | "data_types": { 494 | "UDINT": [], 495 | "EPATH": [], 496 | "REF": [ 497 | "AssemN", 498 | "ParamN", 499 | "AssemExaN", 500 | "VariantN", 501 | "BitStringVariantN", 502 | "VariantExaN", 503 | "ArrayN", 504 | "ConstructedParamN" 505 | ], 506 | "EMPTY": [] 507 | } 508 | } 509 | ] 510 | }, 511 | "VariantN": { 512 | "name": "Variant", 513 | "required": false, 514 | "enumerated_fields": { 515 | "first_enum_field": 11, 516 | "enum_member_count": 2 517 | }, 518 | "fields": [ 519 | { 520 | "name": "Name", 521 | "required": false, 522 | "data_types": { 523 | "STRING": [] 524 | } 525 | }, 526 | { 527 | "name": "Help String", 528 | "required": false, 529 | "data_types": { 530 | "STRING": [] 531 | } 532 | }, 533 | { 534 | "name": "Reserved", 535 | "required": false, 536 | "data_types": { 537 | "EMPTY": [] 538 | } 539 | }, 540 | { 541 | "name": "Reserved", 542 | "required": false, 543 | "data_types": { 544 | "EMPTY": [] 545 | } 546 | }, 547 | { 548 | "name": "Reserved", 549 | "required": false, 550 | "data_types": { 551 | "EMPTY": [] 552 | } 553 | }, 554 | { 555 | "name": "switch selector", 556 | "required": false, 557 | "data_types": { 558 | "REF": [ 559 | "ParamN", 560 | "AssemN" 561 | ] 562 | } 563 | }, 564 | { 565 | "name": "First selection value", 566 | "required": false, 567 | "data_types": { 568 | "UINT": [] 569 | } 570 | }, 571 | { 572 | "name": "First selection entry", 573 | "required": false, 574 | "data_types": { 575 | "REF": [ 576 | "ParamN" 577 | ] 578 | } 579 | }, 580 | { 581 | "name": "Second Selection value", 582 | "required": false, 583 | "data_types": { 584 | "UINT": [] 585 | } 586 | }, 587 | { 588 | "name": "Second Selection entry", 589 | "required": false, 590 | "data_types": { 591 | "REF": [ 592 | "ParamN" 593 | ] 594 | } 595 | }, 596 | { 597 | "name": "Subsequent Selection values", 598 | "required": false, 599 | "data_types": { 600 | "UINT": [] 601 | } 602 | }, 603 | { 604 | "name": "Subsequent Selection entries", 605 | "required": false, 606 | "data_types": { 607 | "REF": [ 608 | "ParamN" 609 | ] 610 | } 611 | } 612 | ] 613 | }, 614 | "VariantExaN": { 615 | "name": "Variant", 616 | "required": false, 617 | "enumerated_fields": { 618 | "first_enum_field": 11, 619 | "enum_member_count": 2 620 | }, 621 | "fields": [ 622 | { 623 | "name": "Name", 624 | "required": false, 625 | "data_types": { 626 | "STRING": [] 627 | } 628 | }, 629 | { 630 | "name": "Help String", 631 | "required": false, 632 | "data_types": { 633 | "STRING": [] 634 | } 635 | }, 636 | { 637 | "name": "Reserved", 638 | "required": false, 639 | "data_types": { 640 | "EMPTY": [] 641 | } 642 | }, 643 | { 644 | "name": "Reserved", 645 | "required": false, 646 | "data_types": { 647 | "EMPTY": [] 648 | } 649 | }, 650 | { 651 | "name": "Reserved", 652 | "required": false, 653 | "data_types": { 654 | "EMPTY": [] 655 | } 656 | }, 657 | { 658 | "name": "switch selector", 659 | "required": false, 660 | "data_types": { 661 | "REF": [ 662 | "ParamN", 663 | "AssemN", 664 | "AssemExaN" 665 | ] 666 | } 667 | }, 668 | { 669 | "name": "First selection value", 670 | "required": false, 671 | "data_types": { 672 | "UINT": [] 673 | } 674 | }, 675 | { 676 | "name": "First selection entry", 677 | "required": false, 678 | "data_types": { 679 | "REF": [ 680 | "ParamN", 681 | "AssemN", 682 | "AssemExaN", 683 | "ArrayN", 684 | "ConstructedParamN" 685 | ] 686 | } 687 | }, 688 | { 689 | "name": "Second Selection value", 690 | "required": false, 691 | "data_types": { 692 | "UINT": [] 693 | } 694 | }, 695 | { 696 | "name": "Second Selection entry", 697 | "required": false, 698 | "data_types": { 699 | "REF": [ 700 | "ParamN", 701 | "AssemN", 702 | "AssemExaN", 703 | "ArrayN", 704 | "ConstructedParamN" 705 | ] 706 | } 707 | }, 708 | { 709 | "name": "Subsequent Selection values", 710 | "required": false, 711 | "data_types": { 712 | "UINT": [] 713 | } 714 | }, 715 | { 716 | "name": "Subsequent Selection entries", 717 | "required": false, 718 | "data_types": { 719 | "REF": [ 720 | "ParamN", 721 | "AssemN", 722 | "AssemExaN", 723 | "ArrayN", 724 | "ConstructedParamN" 725 | ] 726 | } 727 | } 728 | ] 729 | }, 730 | "BitStringVariantN": { 731 | "name": "Variant", 732 | "required": false, 733 | "enumerated_fields": { 734 | "first_enum_field": 10, 735 | "enum_member_count": 3 736 | }, 737 | "fields": [ 738 | { 739 | "name": "Name", 740 | "required": false, 741 | "data_types": { 742 | "STRING": [] 743 | } 744 | }, 745 | { 746 | "name": "Help String", 747 | "required": false, 748 | "data_types": { 749 | "STRING": [] 750 | } 751 | }, 752 | { 753 | "name": "Reserved", 754 | "required": false, 755 | "data_types": { 756 | "EMPTY": [] 757 | } 758 | }, 759 | { 760 | "name": "Reserved", 761 | "required": false, 762 | "data_types": { 763 | "EMPTY": [] 764 | } 765 | }, 766 | { 767 | "name": "Reserved", 768 | "required": false, 769 | "data_types": { 770 | "EMPTY": [] 771 | } 772 | }, 773 | { 774 | "name": "Bit switch selector", 775 | "required": false, 776 | "data_types": { 777 | "REF": [ 778 | "AssemN", 779 | "ParamN", 780 | "AssemExaN" 781 | ] 782 | } 783 | }, 784 | { 785 | "name": "First bit selection value", 786 | "required": false, 787 | "data_types": { 788 | "UINT": [] 789 | } 790 | }, 791 | { 792 | "name": "First bit set selection entry", 793 | "required": false, 794 | "data_types": { 795 | "REF": [ 796 | "AssemN", 797 | "ParamN", 798 | "AssemExaN", 799 | "ArrayN", 800 | "ConstructedParamN" 801 | ], 802 | "EMPTY": [] 803 | } 804 | }, 805 | { 806 | "name": "First bit reset selection entry", 807 | "required": false, 808 | "data_types": { 809 | "REF": [ 810 | "AssemN", 811 | "ParamN", 812 | "AssemExaN", 813 | "ArrayN", 814 | "ConstructedParamN" 815 | ], 816 | "EMPTY": [] 817 | } 818 | }, 819 | { 820 | "name": "Subsequent bit selection value", 821 | "required": false, 822 | "data_types": { 823 | "UINT": [] 824 | } 825 | }, 826 | { 827 | "name": "Subsequent bit set selection entry", 828 | "required": false, 829 | "data_types": { 830 | "REF": [ 831 | "AssemN", 832 | "ParamN", 833 | "AssemExaN", 834 | "ArrayN", 835 | "ConstructedParamN" 836 | ], 837 | "EMPTY": [] 838 | } 839 | }, 840 | { 841 | "name": "Subsequent bit reset selection entry", 842 | "required": false, 843 | "data_types": { 844 | "REF": [ 845 | "AssemN", 846 | "ParamN", 847 | "AssemExaN", 848 | "ArrayN", 849 | "ConstructedParamN" 850 | ], 851 | "EMPTY": [] 852 | } 853 | } 854 | ] 855 | }, 856 | "ArrayN": { 857 | "name": "Array", 858 | "required": false, 859 | "enumerated_fields": { 860 | "first_enum_field": 11, 861 | "enum_member_count": 1 862 | }, 863 | "fields": [ 864 | { 865 | "name": "Name", 866 | "required": false, 867 | "data_types": { 868 | "STRING": [] 869 | } 870 | }, 871 | { 872 | "name": "Path", 873 | "required": false, 874 | "data_types": { 875 | "EPATH": [], 876 | "KEYWORD": [ 877 | "SYMBOL_ANSI" 878 | ] 879 | } 880 | }, 881 | { 882 | "name": "Descriptor", 883 | "required": false, 884 | "data_types": { 885 | "WORD": [] 886 | } 887 | }, 888 | { 889 | "name": "Help String", 890 | "required": false, 891 | "data_types": { 892 | "STRING": [] 893 | } 894 | }, 895 | { 896 | "name": "Reserved", 897 | "required": false, 898 | "data_types": { 899 | "EMPTY": [] 900 | } 901 | }, 902 | { 903 | "name": "Reserved", 904 | "required": false, 905 | "data_types": { 906 | "EMPTY": [] 907 | } 908 | }, 909 | { 910 | "name": "Reserved", 911 | "required": false, 912 | "data_types": { 913 | "EMPTY": [] 914 | } 915 | }, 916 | { 917 | "name": "Array Element Size", 918 | "required": false, 919 | "data_types": { 920 | "UINT": [] 921 | } 922 | }, 923 | { 924 | "name": "Array Element Type", 925 | "required": false, 926 | "data_types": { 927 | "REF": [ 928 | "AssemN", 929 | "AssemExaN", 930 | "ParamN", 931 | "VariantN", 932 | "BitStringVariantN", 933 | "VariantExaN", 934 | "ConstructedParamN" 935 | ], 936 | "EMPTY": [] 937 | } 938 | }, 939 | { 940 | "name": "Number of Dimensions", 941 | "required": false, 942 | "data_types": { 943 | "USINT": [] 944 | } 945 | }, 946 | { 947 | "name": "Number of Dimension Elements", 948 | "required": false, 949 | "data_types": { 950 | "UDINT": [] 951 | } 952 | } 953 | ] 954 | } 955 | } 956 | }, 957 | "Connection Class": { 958 | "name": "Connection Class", 959 | "required": false, 960 | "class_id": 5, 961 | "entries": {} 962 | }, 963 | "Connection Manager": { 964 | "name": "Connection Manager", 965 | "required": false, 966 | "class_id": 6, 967 | "entries": { 968 | "ConnectionN": { 969 | "name": "Connection", 970 | "required": false, 971 | "enumerated_fields": null, 972 | "fields": [ 973 | { 974 | "name": "Trigger and transport", 975 | "required": false, 976 | "data_types": { 977 | "DWORD": [] 978 | } 979 | }, 980 | { 981 | "name": "Connection parameters", 982 | "required": false, 983 | "data_types": { 984 | "DWORD": [] 985 | } 986 | }, 987 | { 988 | "name": "O2T RPI", 989 | "required": false, 990 | "data_types": { 991 | "UDINT": [], 992 | "REF": [ 993 | "ParamN" 994 | ] 995 | } 996 | }, 997 | { 998 | "name": "O2T size", 999 | "required": false, 1000 | "data_types": { 1001 | "UINT": [], 1002 | "REF": [ 1003 | "ParamN" 1004 | ] 1005 | } 1006 | }, 1007 | { 1008 | "name": "O2T format", 1009 | "required": false, 1010 | "data_types": { 1011 | "REF": [ 1012 | "ParamN", 1013 | "AssemN", 1014 | "AssemExaN", 1015 | "AssemExaN", 1016 | "ArrayN", 1017 | "ConstructedParamN" 1018 | ] 1019 | } 1020 | }, 1021 | { 1022 | "name": "T2O RPI", 1023 | "required": false, 1024 | "data_types": { 1025 | "REF": [ 1026 | "ParamN" 1027 | ] 1028 | } 1029 | }, 1030 | { 1031 | "name": "T2O size", 1032 | "required": false, 1033 | "data_types": { 1034 | "UINT": [], 1035 | "REF": [ 1036 | "ParamN" 1037 | ] 1038 | } 1039 | }, 1040 | { 1041 | "name": "T2O format", 1042 | "required": false, 1043 | "data_types": { 1044 | "REF": [ 1045 | "ParamN", 1046 | "AssemN", 1047 | "AssemExaN", 1048 | "AssemExaN", 1049 | "ArrayN", 1050 | "ConstructedParamN" 1051 | ] 1052 | } 1053 | }, 1054 | { 1055 | "name": "Proxy Config size", 1056 | "required": false, 1057 | "data_types": { 1058 | "UINT": [], 1059 | "REF": [ 1060 | "ParamN" 1061 | ] 1062 | } 1063 | }, 1064 | { 1065 | "name": "Proxy Config format", 1066 | "required": false, 1067 | "data_types": { 1068 | "REF": [ 1069 | "ParamN", 1070 | "AssemN", 1071 | "AssemExaN", 1072 | "AssemExaN", 1073 | "ArrayN", 1074 | "ConstructedParamN" 1075 | ] 1076 | } 1077 | }, 1078 | { 1079 | "name": "Target Config size", 1080 | "required": false, 1081 | "data_types": { 1082 | "UINT": [], 1083 | "REF": [ 1084 | "ParamN" 1085 | ] 1086 | } 1087 | }, 1088 | { 1089 | "name": "Target Config format", 1090 | "required": false, 1091 | "data_types": { 1092 | "REF": [ 1093 | "ParamN", 1094 | "AssemN", 1095 | "AssemExaN", 1096 | "AssemExaN", 1097 | "ArrayN", 1098 | "ConstructedParamN" 1099 | ] 1100 | } 1101 | }, 1102 | { 1103 | "name": "Connection name string", 1104 | "required": false, 1105 | "data_types": { 1106 | "STRING": [] 1107 | } 1108 | }, 1109 | { 1110 | "name": "Help string", 1111 | "required": false, 1112 | "data_types": { 1113 | "STRING": [] 1114 | } 1115 | }, 1116 | { 1117 | "name": "Path", 1118 | "required": false, 1119 | "data_types": { 1120 | "EPATH": [], 1121 | "KEYWORD": [ 1122 | "SYMBOL_ANSI" 1123 | ] 1124 | } 1125 | }, 1126 | { 1127 | "name": "Safety ASYNC", 1128 | "required": false, 1129 | "data_types": { 1130 | "EMPTY": [] 1131 | } 1132 | }, 1133 | { 1134 | "name": "Safety Max Consumer Number", 1135 | "required": false, 1136 | "data_types": { 1137 | "EMPTY": [] 1138 | } 1139 | } 1140 | ] 1141 | }, 1142 | "PITNS": { 1143 | "name": "Production Inhibit Time in Milliseconds Network Segment", 1144 | "required": false, 1145 | "enumerated_fields": null, 1146 | "fields": [ 1147 | { 1148 | "name": "PITNS", 1149 | "required": false, 1150 | "data_types": { 1151 | "KEYWORD": [ 1152 | "Yes", 1153 | "No" 1154 | ] 1155 | } 1156 | } 1157 | ] 1158 | }, 1159 | "PITNS_usec": { 1160 | "name": "Production Inhibit Time in Microseconds Network Segment", 1161 | "required": false, 1162 | "enumerated_fields": null, 1163 | "fields": [ 1164 | { 1165 | "name": "PITNS_usec", 1166 | "required": false, 1167 | "data_types": { 1168 | "KEYWORD": [ 1169 | "Yes", 1170 | "No" 1171 | ] 1172 | } 1173 | } 1174 | ] 1175 | } 1176 | } 1177 | }, 1178 | "Register Class": { 1179 | "name": "Register Class", 1180 | "required": false, 1181 | "class_id": 7, 1182 | "entries": {} 1183 | }, 1184 | "Discrete Input Class": { 1185 | "name": "Discrete Input Class", 1186 | "required": false, 1187 | "class_id": 8, 1188 | "entries": {} 1189 | }, 1190 | "Discrete Output Class": { 1191 | "name": "Discrete Output Class", 1192 | "required": false, 1193 | "class_id": 9, 1194 | "entries": {} 1195 | }, 1196 | "Analog Input Class": { 1197 | "name": "Analog Input Class", 1198 | "required": false, 1199 | "class_id": 10, 1200 | "entries": {} 1201 | }, 1202 | "Analog Output Class": { 1203 | "name": "Analog Output Class", 1204 | "required": false, 1205 | "class_id": 11, 1206 | "entries": {} 1207 | }, 1208 | "Presence Sensing Class": { 1209 | "name": "Presence Sensing Class", 1210 | "required": false, 1211 | "class_id": 14, 1212 | "entries": {} 1213 | }, 1214 | "ParamClass": { 1215 | "name": "Parameter Class", 1216 | "required": false, 1217 | "class_id": 15, 1218 | "entries": {} 1219 | }, 1220 | "Groups": { 1221 | "name": "Parameter Group", 1222 | "required": false, 1223 | "class_id": 16, 1224 | "entries": { 1225 | "GroupN": { 1226 | "name": "Group", 1227 | "required": false, 1228 | "enumerated_fields": { 1229 | "first_enum_field": 3, 1230 | "enum_member_count": 1 1231 | }, 1232 | "fields": [ 1233 | { 1234 | "name": "Group Name String", 1235 | "required": false, 1236 | "data_types": { 1237 | "STRING": [] 1238 | } 1239 | }, 1240 | { 1241 | "name": "Number of Members", 1242 | "required": false, 1243 | "data_types": { 1244 | "UINT": [] 1245 | } 1246 | }, 1247 | { 1248 | "name": "Parameter, Proxy Parameter or Variant", 1249 | "required": false, 1250 | "data_types": { 1251 | "UINT": [] 1252 | } 1253 | } 1254 | ] 1255 | } 1256 | } 1257 | }, 1258 | "File Class": { 1259 | "name": "File Class", 1260 | "required": false, 1261 | "class_id": 55, 1262 | "entries": {} 1263 | }, 1264 | "Port": { 1265 | "name": "Port", 1266 | "required": false, 1267 | "class_id": 244, 1268 | "entries": {} 1269 | }, 1270 | "DLR Class": { 1271 | "name": "Device Level Ring Class", 1272 | "required": false, 1273 | "class_id": 71, 1274 | "entries": {} 1275 | }, 1276 | "TCP/IP Interface Class": { 1277 | "name": "TCP/IP Interface Class", 1278 | "required": false, 1279 | "class_id": 245, 1280 | "entries": { 1281 | "ENetQCTN": { 1282 | "name": "EtherNet/IP QuickConnect Target", 1283 | "required": false, 1284 | "enumerated_fields": { 1285 | "first_enum_field": 1, 1286 | "enum_member_count": 1 1287 | }, 1288 | "fields": [ 1289 | { 1290 | "name": "Ready for Connection Time", 1291 | "required": false, 1292 | "data_types": { 1293 | "UINT": [] 1294 | } 1295 | } 1296 | ] 1297 | }, 1298 | "ENetQCON": { 1299 | "name": "EtherNet/IP QuickConnect Originator", 1300 | "required": false, 1301 | "enumerated_fields": { 1302 | "first_enum_field": 1, 1303 | "enum_member_count": 1 1304 | }, 1305 | "fields": [ 1306 | { 1307 | "name": "Connection Origination Time", 1308 | "required": false, 1309 | "data_types": { 1310 | "UINT": [] 1311 | } 1312 | } 1313 | ] 1314 | } 1315 | } 1316 | }, 1317 | "Ethernet Link Class": { 1318 | "name": "Ethernet Link Class", 1319 | "required": false, 1320 | "class_id": 246, 1321 | "entries": { 1322 | "InterfaceSpeedN": { 1323 | "name": "Interface Speed", 1324 | "required": false, 1325 | "enumerated_fields": null, 1326 | "fields": [ 1327 | { 1328 | "name": "Interface Speed", 1329 | "required": false, 1330 | "data_types": { 1331 | "UDINT": [] 1332 | } 1333 | } 1334 | ] 1335 | }, 1336 | "InterfaceLabelN": { 1337 | "name": "Interface Label", 1338 | "required": false, 1339 | "enumerated_fields": null, 1340 | "fields": [ 1341 | { 1342 | "name": "Interface Label", 1343 | "required": false, 1344 | "data_types": { 1345 | "STRING": [] 1346 | } 1347 | } 1348 | ] 1349 | } 1350 | } 1351 | }, 1352 | "QoS Class": { 1353 | "name": "Quality of Service Class", 1354 | "required": false, 1355 | "class_id": 72, 1356 | "entries": {} 1357 | }, 1358 | "CIP Security Class": { 1359 | "name": "CIP Security Class", 1360 | "required": false, 1361 | "class_id": 93, 1362 | "entries": {} 1363 | }, 1364 | "EtherNet/IP Security Class": { 1365 | "name": "EtherNet/IP Security Class", 1366 | "required": false, 1367 | "class_id": 94, 1368 | "entries": {} 1369 | }, 1370 | "Certificate Management Class": { 1371 | "name": "Certificate Management Class", 1372 | "required": false, 1373 | "class_id": 95, 1374 | "entries": {} 1375 | }, 1376 | "Authority Class": { 1377 | "name": "Authority Class", 1378 | "required": false, 1379 | "class_id": 96, 1380 | "entries": {} 1381 | }, 1382 | "Password Authenticator Class": { 1383 | "name": "Password Authenticator Class", 1384 | "required": false, 1385 | "class_id": 97, 1386 | "entries": {} 1387 | }, 1388 | "Certificate Authenticator Class": { 1389 | "name": "Certificate Authenticator Class", 1390 | "required": false, 1391 | "class_id": 98, 1392 | "entries": {} 1393 | }, 1394 | "Ingress Egress Class": { 1395 | "name": "Ingress Egress Class", 1396 | "required": false, 1397 | "class_id": 99, 1398 | "entries": {} 1399 | }, 1400 | "Connection Configuration": { 1401 | "name": "Connection Configuration Class", 1402 | "required": false, 1403 | "class_id": 243, 1404 | "entries": {} 1405 | }, 1406 | "LLDP Management Class": { 1407 | "name": "LLDP Management Class", 1408 | "required": false, 1409 | "class_id": 265, 1410 | "entries": {} 1411 | }, 1412 | "LLDP Data Table Class": { 1413 | "name": "LLDP Data Table Class", 1414 | "required": false, 1415 | "class_id": 266, 1416 | "entries": {} 1417 | } 1418 | } 1419 | } -------------------------------------------------------------------------------- /eds_pie.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Omid Kompani 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the 'Software'), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ''' 26 | 27 | ''' 28 | EDS grammatics: 29 | --------------- 30 | 31 | OPERATOR 32 | {=} 33 | 34 | SEPARATOR 35 | {,;-:} 36 | 37 | EOL 38 | \n 39 | 40 | STRING 41 | {ASCII symbols} 42 | 43 | NUMBER 44 | {.0-9} 45 | 46 | HEXNUMBER 47 | 0x{0-9a-fA-F} 48 | 49 | CIP_DATE 50 | mm'-'dd'-'yyyy 51 | [m,d,y] = NUMBER/HEXNUMBER 52 | 53 | CIP_TIME 54 | hh':'mm':'ss 55 | [h,m,s] = NUMBER/HEXNUMBER 56 | 57 | COMMENT 58 | $ {ASCII symbols} EOL 59 | 60 | HEADER_COMMENT 61 | COMMENT 62 | 63 | FOOTER_COMMENT 64 | {ASCII symbols} COMMENT 65 | 66 | IDENTIFIER 67 | {a-zA-Z0-9_} 68 | 69 | DATASET 70 | '{'...,...,...'}' 71 | 72 | KEYWORD 73 | IDENTIFIER 74 | 75 | SECTION_IDENTIFIER 76 | '[' {a-zA-Z0-9_/- } ']' 77 | ***Note: the SYMBOLS '/' , '-' and ' ' should be used non-consecutive 78 | ***Note: A public section identifier shall never begin with a number 79 | ***Note: A vendor specific section identifier shall always begin with 80 | the vendor Id of the company making the addition followed by an 81 | underscore. VendorID_VendorSpecificKeyword 82 | 83 | KEYWORDVALUE (or keyword data field) 84 | NUMBER | STRING | IDENTIFIER | VSIDENTIFIER| CIP_DATE | CIP_TIME 85 | | DATASET 86 | 87 | ENTRY 88 | KEYWORD '=' KEYWORDVALUE {',' KEYWORDVALUE} ';' 89 | 90 | SECTION 91 | HEADRCOMMENT 92 | SECTION_IDENTIFIER FOOTERCOMENT 93 | { HEADRCOMMENT 94 | ENTRY = { HEADRCOMMENT 95 | KEYWORDVALUE } FOOTERCOMENT } 96 | ''' 97 | 98 | import os 99 | import sys 100 | import inspect 101 | import struct 102 | import numbers 103 | import json 104 | 105 | from datetime import datetime, date, time 106 | from string import digits 107 | 108 | import cip_eds_types as EDS_Types 109 | 110 | import logging 111 | logging.basicConfig(level=logging.WARNING, 112 | format='%(asctime)s - %(name)s.%(levelname)-8s %(message)s') 113 | logger = logging.getLogger(__name__) 114 | #------------------------------------------------------------------------------- 115 | EDS_PIE_VERSION = '0.1' 116 | EDS_PIE_RELASE_DATE = '3 Nov. 2020' 117 | SECTION_NAME_VALID_SYMBOLES = '-.\\_/' 118 | #------------------------------------------------------------------------------- 119 | END_COMMENT_TEMPLATE = ( ' '.ljust(79, '-') + '\n' + ' EOF \n' 120 | + ' '.ljust(79, '-') + '\n' ) 121 | 122 | HEADING_COMMENT_TEMPLATE = ( ' Electronic Data Sheet Generated with EDS-pie Version ' 123 | + '{} - {}\n'.format(EDS_PIE_VERSION, EDS_PIE_RELASE_DATE) 124 | + ' '.ljust(79, '-') + '\n' 125 | + ' Created on: {} - {}:{}\n'.format(str(date.today()), 126 | str(datetime.now().hour), str(datetime.now().minute)) 127 | + ' '.ljust(79, '-') + '\n\n ATTENTION: \n' 128 | + ' Changes in this file may cause configuration or ' 129 | + 'communication problems.\n\n' + ' '.ljust(79, '-') 130 | + '\n' ) 131 | # ------------------------------------------------------------------------------ 132 | 133 | class TOKEN_TYPES(EDS_Types.ENUMS): 134 | DATE = 1 135 | TIME = 2 136 | NUMBER = 3 137 | STRING = 4 138 | COMMENT = 5 139 | SECTION = 6 140 | OPERATOR = 7 141 | SEPARATOR = 8 142 | IDENTIFIER = 9 143 | DATASET = 10 144 | 145 | class SYMBOLS(EDS_Types.ENUMS): 146 | ASSIGNMENT = '=' 147 | COMMA = ',' 148 | SEMICOLON = ';' 149 | COLON = ':' 150 | MINUS = '-' 151 | UNDERLINE = '_' 152 | PLUS = '+' 153 | POINT = '.' 154 | BACKSLASH = '\\' 155 | QUOTATION = '\"' 156 | TAB = '\t' 157 | DOLLAR = '$' 158 | OPENINGBRACKET = '[' 159 | CLOSINGBRACKET = ']' 160 | OPENINGBRACE = '{' 161 | CLOSINGBRACE = '}' 162 | AMPERSAND = '&' 163 | SPACE = ' ' 164 | EOL = '\n' 165 | EOF = None 166 | OPERATORS = [ASSIGNMENT] 167 | SEPARATORS = [COMMA, SEMICOLON] 168 | 169 | CIP_BOOL = EDS_Types.BOOL 170 | CIP_USINT = EDS_Types.USINT 171 | CIP_UINT = EDS_Types.UINT 172 | CIP_UDINT = EDS_Types.UDINT 173 | CIP_ULINT = EDS_Types.ULINT 174 | CIP_SINT = EDS_Types.SINT 175 | CIP_INT = EDS_Types.INT 176 | CIP_DINT = EDS_Types.DINT 177 | CIP_LINT = EDS_Types.LINT 178 | CIP_WORD = EDS_Types.WORD 179 | CIP_DWORD = EDS_Types.DWORD 180 | CIP_REAL = EDS_Types.REAL 181 | CIP_LREAL = EDS_Types.LREAL 182 | CIP_BYTE = EDS_Types.BYTE 183 | CIP_STRING = EDS_Types.STRING 184 | CIP_STRINGI = EDS_Types.STRINGI 185 | EDS_DATE = EDS_Types.DATE 186 | CIP_TIME = EDS_Types.TIME 187 | CIP_EPATH = EDS_Types.EPATH 188 | 189 | EDS_REVISION = EDS_Types.REVISION 190 | EDS_KEYWORD = EDS_Types.KEYWORD 191 | EDS_DATAREF = EDS_Types.REF 192 | EDS_VENDORSPEC = EDS_Types.VENDOR_SPECIFIC 193 | EDS_TYPEREF = EDS_Types.DATATYPE_REF # Reference to another field which contains a cip_dtypeid 194 | EDS_MAC_ADDR = EDS_Types.ETH_MAC_ADDR 195 | EDS_EMPTY = EDS_Types.EMPTY 196 | EDS_UNDEFINED = EDS_Types.UNDEFINED 197 | EDS_SERVICE = EDS_Types.EDS_SERVICE 198 | 199 | class PSTATE(EDS_Types.ENUMS): 200 | EXPECT_SECTION = 0 201 | EXPECT_ENTRY = 1 202 | EXPECT_SECTION_OR_ENTRY = 2 203 | EXPECT_FIELD = 3 204 | 205 | class EDS_Section(object): 206 | _instancecount = -1 207 | def __init__(self, eds, name, id = 0): 208 | type(self)._instancecount += 1 209 | self._eds = eds 210 | self._index = type(self)._instancecount 211 | self._id = id 212 | self._name = name 213 | self._entries = {} 214 | self.hcomment = '' 215 | self.fcomment = '' 216 | 217 | @property 218 | def name(self): 219 | return self._name 220 | 221 | @property 222 | def entrycount(self): 223 | return len(self._entries) 224 | 225 | @property 226 | def entries(self): 227 | return tuple(self._entries.values()) 228 | 229 | def add_entry(self, entry_name, serialize = False): 230 | return self._eds.add_entry(self._name, entry_name) 231 | 232 | def has_entry(self, entry_name = None, entryindex = None): 233 | if entry_name.replace(' ', '').lower() in self._entries.keys(): 234 | return True 235 | return False 236 | 237 | def get_entry(self, entry_name): 238 | return self._entries.get(entry_name) 239 | 240 | def get_field(self, entry_name, field): 241 | ''' 242 | To get a section.entry.field using the entry name + (ield name or field index. 243 | ''' 244 | entry = self._entries.get(entry_name) 245 | if entry: 246 | return entry.get_field(field) 247 | return None 248 | 249 | def __str__(self): 250 | return 'SECTION({})'.format(self._name) 251 | 252 | class EDS_Entry(object): 253 | 254 | def __init__(self, section, name, index): 255 | self._index = index 256 | self._section = section 257 | self._name = name 258 | self._fields = [] # Unlike the _sections and _entries, _fields are implemented as a list. 259 | # One reason is entry fields with the same name which doesn't easily fit to a dictionary. 260 | self.hcomment = '' 261 | # Entries don't have fcomment attribute. The fcomments belongs to fields 262 | 263 | def add_field(self, field_value, datatype = None): 264 | return self._section._eds.add_field(self._section.name, self._name, field_value, datatype) 265 | 266 | def has_field(self, field): 267 | if isinstance(field, str): # field name 268 | fieldname = field.replace(' ', '').lower() 269 | for field in self._fields: 270 | if fieldname == field.name.replace(' ', '').lower(): 271 | return True 272 | elif isinstance(field, numbers.Number): # field index 273 | return field < entry.fieldcount 274 | else: 275 | raise TypeError('Inappropriate data type: {}'.format(type(field))) 276 | 277 | def get_field(self, field): 278 | if isinstance(field, str): # field name 279 | fieldname = field.replace(' ', '').lower() 280 | for field in self._fields: 281 | if fieldname == field: 282 | return field 283 | elif isinstance(field, numbers.Number): # field index 284 | if field < self.fieldcount: 285 | return self.fields[field] 286 | else: 287 | raise TypeError('Inappropriate data type: {}'.format(type(field))) 288 | return None 289 | 290 | @property 291 | def name(self): 292 | return self._name 293 | 294 | @property 295 | def fieldcount(self): 296 | return len(self._fields) 297 | 298 | @property 299 | def fields(self): 300 | return tuple(self._fields) 301 | 302 | @property 303 | def value(self): 304 | if len(self._fields) > 1: 305 | logger.warning('Entry has multiple fields. Only the first field is returned.') 306 | return self._fields[0].value 307 | 308 | def __str__(self): 309 | return 'ENTRY({})'.format(self._name) 310 | 311 | class EDS_Field(object): 312 | def __init__(self, entry, name, data, index): 313 | self._index = index 314 | self._entry = entry 315 | self._name = name 316 | self._data = data # datatype object. Actually is the Field value containing also its type information 317 | self._data_types = [] # Valid datatypes a field supports 318 | # Fields don't have hcomment attribute. The hcomments belongs to entries 319 | self.fcomment = '' 320 | 321 | @property 322 | def index(self): 323 | return self._index 324 | 325 | @property 326 | def name(self): 327 | return self._name 328 | 329 | @property 330 | def value(self): 331 | return self._data.value 332 | 333 | @value.setter 334 | def value(self, value): 335 | if type(self._data) != EMPTY or type(self._data) != EDS_UNDEFINED: 336 | if type(self._data).validate(value, self._data.range): 337 | self._data._value = value 338 | return 339 | # Setting with the actual datatype is failed. Try other supported types. 340 | if self._data_types: 341 | for datatype, valid_data in self._data_types: 342 | if datatype.validate(value, valid_data): 343 | del self._data 344 | self._data = datatype(value, valid_data) 345 | return 346 | types_str = ', '.join('<{}>{}'.format(datatype.__name__, valid_data) 347 | for datatype, valid_data in self._data_types) 348 | raise Exception('Unable to set Field value! Data_type mismatch!' 349 | ' [{}].{}.{} = ({}), should be a type of: {}' 350 | .format(self._entry._section.name, self._entry.name, self.name, value, types_str)) 351 | 352 | @property 353 | def datatype(self): 354 | return (type(self._data), self._data.range) 355 | 356 | def __str__(self): 357 | if self._data is None: 358 | return '\"\"' 359 | # TODO: If a field of STRING contains multi lines of string, print each line as a seperate string. 360 | return 'FIELD(index: {}, name: \"{}\", value: ({}), type: <{}>{})'.format( 361 | self._index, self._name, str(self._data), type(self._data).__name__, self._data.range) 362 | 363 | class EDS_RefLib(object): 364 | type_mapping = { 365 | "BOOL" : EDS_Types.BOOL, 366 | "USINT" : EDS_Types.USINT, 367 | "UINT" : EDS_Types.UINT, 368 | "UDINT" : EDS_Types.UDINT, 369 | "ULINT" : EDS_Types.ULINT, 370 | "SINT" : EDS_Types.SINT, 371 | "INT" : EDS_Types.INT, 372 | "DINT" : EDS_Types.DINT, 373 | "LINT" : EDS_Types.LINT, 374 | "WORD" : EDS_Types.WORD, 375 | "DWORD" : EDS_Types.DWORD, 376 | "REAL" : EDS_Types.REAL, 377 | "LREAL" : EDS_Types.LREAL, 378 | "BYTE" : EDS_Types.BYTE, 379 | "STRING" : EDS_Types.STRING, 380 | "STRINGI" : EDS_Types.STRINGI, 381 | "DATE" : EDS_Types.DATE, 382 | "TIME" : EDS_Types.TIME, 383 | "EPATH" : EDS_Types.EPATH, 384 | 385 | "REVISION" : EDS_Types.REVISION, 386 | "KEYWORD" : EDS_Types.KEYWORD, 387 | "REF" : EDS_Types.REF, 388 | "VENDOR_SPECIFIC" : EDS_Types.VENDOR_SPECIFIC, 389 | "DATATYPE_REF" : EDS_Types.DATATYPE_REF, # Reference to another field which contains a cip_dtypeid 390 | "MAC_ADDR" : EDS_Types.ETH_MAC_ADDR, 391 | "EMPTY" : EDS_Types.EMPTY, 392 | "UNDEFINED" : EDS_Types.UNDEFINED, 393 | "SERVICE" : EDS_Types.EDS_SERVICE, 394 | } 395 | 396 | supported_data_types = { 397 | 0xC1: CIP_BOOL, 398 | 0xC2: CIP_SINT, 399 | 0xC3: CIP_INT, 400 | 0xC4: CIP_DINT, 401 | 0xC5: CIP_LINT, 402 | 0xC6: CIP_USINT, 403 | 0xC7: CIP_UINT, 404 | 0xC8: CIP_UDINT, 405 | 0xC9: CIP_ULINT, 406 | 0xCA: CIP_REAL, 407 | 0xCB: CIP_LREAL, 408 | 0xCC: EDS_Types.STIME, 409 | 0xCD: EDS_DATE, 410 | #, 0xCE: EDS_Types.TIME_OF_DAY 411 | #, 0xCF: EDS_Types.DATE_AND_TIME 412 | 0xD0: CIP_STRING, 413 | 0xD1: CIP_BYTE, 414 | 0xD2: CIP_WORD, 415 | 0xD3: CIP_DWORD, 416 | 0xD4: EDS_Types.LWORD, 417 | #, 0xD5: EDS_Types.STRING2 418 | #, 0xD6: EDS_Types.FTIME 419 | #, 0xD7: EDS_Types.LTIME 420 | #, 0xD8: EDS_Types.ITIME 421 | #, 0xD9: EDS_Types.STRINGN 422 | #, 0xDA: EDS_Types.SHORT_STRING 423 | 0xDB: CIP_TIME, 424 | 0xDC: CIP_EPATH, 425 | #, 0xDD: EDS_Types.ENGUNIT 426 | 0xDE: CIP_STRINGI, 427 | } 428 | 429 | def __init__(self): 430 | self.libs = {} 431 | 432 | for file in os.listdir(): 433 | if file.endswith(".json"): 434 | with open(file, "r") as src: 435 | jlib = json.loads(src.read()) 436 | if jlib["project"] == "eds_pie" and file != "edslib_schema.json": 437 | self.libs[jlib["protocol"].lower()] = jlib 438 | 439 | 440 | 441 | def get_lib_name(self, section_keyword): 442 | for _, lib in self.libs.items(): 443 | if section_keyword in lib["sections"]: 444 | return lib["protocol"] 445 | return None 446 | 447 | def get_section_name(self, class_id): 448 | ''' 449 | To get a protocol specific EDS section_name by its CIP class ID 450 | ''' 451 | for _, lib in self.libs.items(): 452 | for section in lib["sections"]: 453 | if section["class_id"] == class_id: 454 | return section 455 | return '' 456 | 457 | def get_section_id(self, section_keyword): 458 | section = self.get_section(section_keyword) 459 | if section: 460 | return section["class_id"] 461 | return None 462 | 463 | def has_section(self, section_keyword): 464 | ''' 465 | Checks the existence of a section by its name 466 | ''' 467 | for _, lib in self.libs.items(): 468 | if section_keyword in lib["sections"]: 469 | return True 470 | return False 471 | 472 | def get_section(self, section_keyword, protocol=None): 473 | section = None 474 | for _, lib in self.libs.items(): 475 | section = lib["sections"].get(section_keyword, None) 476 | if section: 477 | break 478 | else: 479 | continue 480 | return section 481 | 482 | def has_entry(self, section_keyword, entry_name): 483 | ''' 484 | To get an entry dictionary by its section name and entry name 485 | ''' 486 | return self.get_entry(section_keyword, entry_name) is not None 487 | 488 | def get_entry(self, section_keyword, entry_name): 489 | ''' 490 | To get an entry dictionary by its section name and entry name 491 | ''' 492 | entry = None 493 | 494 | if entry_name[-1].isdigit(): # Incremental entry_name 495 | entry_name = entry_name.rstrip(digits) + 'N' 496 | 497 | section = self.get_section(section_keyword) 498 | if section: 499 | # First check if the entry is in common class object 500 | if section["class_id"] and section["class_id"] != 0: 501 | common_section = self.get_section("Common Object Class") 502 | entry = common_section["entries"].get(entry_name, None) 503 | #print(entry_name, entry) 504 | if entry is None: 505 | entry = section["entries"].get(entry_name, None) 506 | return entry 507 | 508 | def get_field_byindex(self, section_keyword, entry_name, field_index): 509 | ''' 510 | To get a field dictionary by its section name and entry name and field index 511 | ''' 512 | field = None 513 | if entry_name[-1].isdigit(): # Incremental entry_name 514 | entry_name = entry_name.rstrip(digits) + 'N' 515 | 516 | entry = self.get_entry(section_keyword, entry_name) 517 | if entry: 518 | ''' 519 | The requested index is greater than listed fields in the lib, 520 | Consider the field as Nth field filed and re-calculate the index. 521 | ''' 522 | if field_index >= len(entry["fields"]) and entry.get("enumerated_fields", None): 523 | # Calculating reference field index 524 | field_index = (field_index % entry["enumerated_fields"]["enum_member_count"]) + entry["enumerated_fields"]["first_enum_field"] - 1 525 | try: 526 | field = entry["fields"][field_index] 527 | except: 528 | field = None 529 | return field 530 | 531 | def get_field_byname(self, section_keyword, entry_name, field_name): 532 | ''' 533 | To get a field dictionary by its section name and entry name and field name 534 | ''' 535 | field = None 536 | 537 | if entry_name[-1].isdigit(): # Incremental entry_name 538 | entry_name = entry_name.rstrip(digits) + 'N' 539 | 540 | entry = self.get_entry(section_keyword, entry_name) 541 | if entry: 542 | if field_name[-1].isdigit(): # Incremental field_name 543 | field_name = field_name.rstrip(digits) + 'N' 544 | 545 | for fld in entry["fields"]: 546 | if fld["name"] == field_name: 547 | field = fld 548 | return field 549 | 550 | def get_type(self, cip_typeid): 551 | return self.supported_data_types[cip_typeid] 552 | 553 | def get_required_sections(self): 554 | 555 | required_sections = {} 556 | 557 | for _, lib in self.libs.items(): 558 | for section_name, section in lib["sections"].items(): 559 | if section["required"] == True: 560 | required_sections.update({section_name: section}) 561 | 562 | return required_sections 563 | 564 | def get_type(self, type_name): 565 | return self.type_mapping.get(type_name, None) 566 | 567 | def validate(self, type_name, type_info, value): 568 | dt = self.get_type(type_name) 569 | if dt: 570 | return dt.validate(value, type_info) 571 | return False 572 | 573 | def is_required_field(self, section_keyword, entry_name, field_name): 574 | field = self.get_field_byname(section_keyword, entry_name, field_name) 575 | if field: 576 | return field["required"] 577 | return False 578 | 579 | class EDS(object): 580 | 581 | def __init__(self): 582 | self.heading_comment = '' 583 | self.end_comment = '' 584 | self._protocol = None 585 | self._sections = {} 586 | self.ref_libs = EDS_RefLib() 587 | 588 | def list(self, section_name='', entry_name=''): 589 | if section_name: 590 | self.list_section(self.get_section(section_name), entry_name) 591 | else: 592 | for section in sorted(self.sections, key = lambda section: section._index): 593 | self.list_section(section, entry_name) 594 | 595 | def list_section(self, section, entry_name=''): 596 | if entry_name: 597 | self.list_entry(section.get_entry(entry_name)) 598 | else: 599 | for entry in sorted(section.entries, key = lambda entry: entry._index): 600 | self.list_entry(entry) 601 | 602 | def list_entry(self, entry): 603 | print (' {}'.format(entry)) 604 | for field in entry.fields: 605 | print (' {}'.format(field)) 606 | 607 | @property 608 | def protocol(self): 609 | return self._protocol 610 | 611 | @property 612 | def sections(self): 613 | return tuple(self._sections.values()) 614 | 615 | def get_section(self, section): 616 | ''' 617 | To get a section object by its EDS keyword or by its CIP classID. 618 | ''' 619 | if isinstance(section, str): 620 | return self._sections.get(section) 621 | if isinstance(section, numbers.Number): 622 | return self._sections.get(self.ref_libs.get_section_name(section)) 623 | raise TypeError('Inappropriate data type: {}'.format(type(section))) 624 | 625 | def get_entry(self, section, entry_name): 626 | ''' 627 | To get an entry by its section name/section id and its entry name. 628 | ''' 629 | sec = self.get_section(section) 630 | if sec: 631 | return sec.get_entry(entry_name) 632 | return None 633 | 634 | def get_field(self, section, entry_name, field): 635 | ''' 636 | To get an field by its section name/section id, its entry name and its field anme/field index 637 | ''' 638 | entry = self.get_entry(section, entry_name) 639 | if entry: 640 | return entry.get_field(field) 641 | return None 642 | 643 | def get_value(self, section, entry_name, field): 644 | field = self.get_field(section, entry_name, field) 645 | if field: 646 | return field.value 647 | return None 648 | 649 | def set_value(self, section, entry_name, field, value): 650 | field = self.get_field(section, entry_name, field) 651 | if field is None: 652 | raise Exception('Not a valid field! Unable to set the field value.') 653 | field.value = value 654 | 655 | 656 | def has_section(self, section): 657 | ''' 658 | To check if the EDS contains a section by its EDS keyword or by its CIP classID. 659 | ''' 660 | if isinstance(section, str): 661 | return section in self._sections.keys() 662 | if isinstance(section, numbers.Number): 663 | return self.ref_libs.get_section_name(section, self.protocol) in self._sections.keys() 664 | raise TypeError('Inappropriate data type: {}'.format(type(section))) 665 | 666 | def has_entry(self, section, entry_name): 667 | section = self.get_section(section) 668 | if section: 669 | return entry_name in section._entries.keys() 670 | return False 671 | 672 | def has_field(self, section, entry_name, field): 673 | entry = self.get_entry(section, entry_name) 674 | if entry: 675 | return fieldindex < entry.fieldcount 676 | return False 677 | 678 | def add_section(self, section_name): 679 | if section_name == '': 680 | raise Exception("Invalid section name! [{}]".format(section_name)) 681 | 682 | if section_name in self._sections.keys(): 683 | raise Exception('Duplicate section! [{}}'.format(section_name)) 684 | 685 | if self.ref_libs.has_section(section_name) == False: 686 | logger.warning('Unknown Section [{}]'.format(section_name)) 687 | 688 | section = EDS_Section(self, section_name, self.ref_libs.get_section_id(section_name)) 689 | self._sections.update({section_name: section}) 690 | 691 | return section 692 | 693 | def add_entry(self, section_name, entry_name): 694 | section = self._sections[section_name] 695 | 696 | if entry_name == '': 697 | raise Exception("Invalid Entry name! [{}]\"{}\"".format(section_name, entry_name)) 698 | 699 | if entry_name in section._entries.keys(): 700 | raise Exception("Duplicate Entry! [{}]\"{}\"".format(section_name, entry_name)) 701 | 702 | # Search for the same section:entry inside the reference lib 703 | if self.ref_libs.has_entry(section_name, entry_name) == False: 704 | logger.warning('Unknown Entry [{}].{}'.format(section_name, entry_name)) 705 | 706 | entry = EDS_Entry(section, entry_name, section.entrycount) 707 | section._entries[entry_name] = entry 708 | 709 | return entry 710 | 711 | def add_field(self, section_name, entry_name, field_value, field_datatype = None): 712 | ''' 713 | Fields must be added in order and no random access is allowed. 714 | ''' 715 | section = self.get_section(section_name) 716 | 717 | if section is None: 718 | raise Exception('Section not found! [{}]'.format(section_name)) 719 | 720 | entry = section.get_entry(entry_name) 721 | if entry is None: 722 | raise Exception('Entry not found! [{}]'.format(entry_name)) 723 | 724 | # Getting field's info from eds reference libraries 725 | field_data = None 726 | ref_data_types = [] 727 | ref_field = self.ref_libs.get_field_byindex(section._name, entry.name, entry.fieldcount) 728 | 729 | if ref_field: 730 | # Reference field is now known. Use the ref information to create the field 731 | ref_data_types = ref_field.get("data_types", None) 732 | field_name = ref_field["name"] or entry.name 733 | # Serialize the field name if the entry can have enumerated fields like AssemN and ParamN. 734 | if self.ref_libs.get_entry(section_name, entry_name).get("enumerated_fields", None): 735 | field_name = field_name.rstrip('N') + str(entry.fieldcount + 1) 736 | else: 737 | # No reference field was found. Use a general naming scheme 738 | field_name = 'field{}'.format(entry.fieldcount) 739 | 740 | # Validate field's value and assign a data type to the field 741 | if field_value == '': 742 | logger.info("Field [{}].{}.{} has no value. Switched to EDS_EMPTY field.".format(section._name, entry.name, field_name)) 743 | field_data = EDS_EMPTY(field_value) 744 | 745 | elif not ref_data_types: 746 | # The filed is unknown and no ref_types are in hand. Try some default data types. 747 | if EDS_VENDORSPEC.validate(field_value): 748 | logger.info('Unknown Field [{}].{}.{} = {}. Switched to VENDOR_SPECIFIC field.'.format(section._name, entry.name, field_name, field_value)) 749 | field_data = EDS_VENDORSPEC(field_value) 750 | elif EDS_UNDEFINED.validate(field_value): 751 | logger.info('Unknown Field [{}].{}.{} = {}. Switched to EDS_UNDEFINED field.'.format(section._name, entry.name, field_name, field_value)) 752 | field_data = EDS_UNDEFINED(field_value) 753 | else: 754 | raise Exception('Unknown Field [{}].{}.{} = {} with no matching data types.'.format(section._name, entry.name, field_name, field_value)) 755 | 756 | else: 757 | for type_name, type_info in ref_data_types.items(): # Getting the listed data types and their acceptable ranges 758 | if self.ref_libs.validate(type_name, type_info, field_value): 759 | # creating type instance with field value 760 | field_data = self.ref_libs.get_type(type_name)(field_value, type_info) 761 | 762 | if field_data is None: # No proper type was found 763 | # Providing info on potential acceptable data types 764 | type_list = [] 765 | for type_name, type_info in ref_data_types.items(): 766 | if type_info: 767 | type_list.append((type_name, type_info)) 768 | else: 769 | try: 770 | type_list.append((type_name, self.ref_libs.get_type(type_name)._range)) 771 | except: 772 | continue 773 | types_str = ', '.join('<{}({})>'.format(self.ref_libs.get_type(type_name).__name__, type_info) for type_name, type_info in type_list) 774 | 775 | if self.ref_libs.get_field_byname(section._name, entry.name, field_name)["required"]: 776 | raise Exception('Data_type mismatch! [{}].{}.{} = ({}), should be a type of: {}' 777 | .format(section._name, entry.name, field_name, field_value, types_str)) 778 | else: 779 | logger.error('Data_type mismatch! [{}].{}.{} = ({}), should be a type of: {}' 780 | .format(section._name, entry.name, field_name, field_value, types_str)) 781 | if EDS_VENDORSPEC.validate(field_value): 782 | field_data = EDS_VENDORSPEC(field_value) 783 | else: 784 | field_data = EDS_UNDEFINED(field_value) 785 | 786 | field = EDS_Field(entry, field_name, field_data, entry.fieldcount) 787 | 788 | field._data_types = ref_data_types 789 | entry._fields.append(field) 790 | 791 | return field 792 | 793 | def remove_section(self, section_name, removetree = False): 794 | section = self.get_section(section_name) 795 | 796 | if section is None: return 797 | if not section.entries: 798 | del self._sections[section_name] 799 | elif removetree: 800 | for entry in section.entries: 801 | self.remove_entry(section_name, entry.name, removetree) 802 | del self._sections[section_name] 803 | else: 804 | logger.error('Unable to remove section! [{}] contains one or more entries.' 805 | 'Remove the entries first or use removetree = True'.format(section._name)) 806 | 807 | def remove_entry(self, section_name, entry_name, removetree = False): 808 | entry = self.get_entry(section_name, entry_name) 809 | if entry is None: return 810 | if not entry.fields: 811 | section = self.get_section(section_name) 812 | del section._entries[entry_name] 813 | elif removetree: 814 | entry._fields = [] 815 | else: 816 | logger.error('Unable to remove entry! [{}].{} contains one or more fields.' 817 | 'Remove the fields first or use removetree = True'.format(section._name, entry.name)) 818 | 819 | def remove_field(self, section_name, sentryname, fieldindex): 820 | # TODO 821 | pass 822 | 823 | def semantic_check(self): 824 | required_sections = self.ref_libs.get_required_sections() 825 | 826 | for section_name, section in required_sections.items(): 827 | if self.has_section(section_name): 828 | continue 829 | raise Exception('Missing required section! [{}]'.format(section_name)) 830 | ''' 831 | #TODO: re-enable this part 832 | for section in self.sections: 833 | requiredentries = self.ref.get_required_entries(section.name) 834 | for entry in requiredentries: 835 | if self.has_entry(section.name, entry.keyword) == False: 836 | logger.error('Missing required entry! [{}].\"{}\"{}' 837 | .format(section.name, entry.keyword, entry.name)) 838 | 839 | for entry in section.entries: 840 | requiredfields = self.ref.get_required_fields(section.name, entry.name) 841 | for field in requiredfields: 842 | if self.has_field(section.name, entry.name, field.placement) == False: 843 | logger.error('Missing required field! [{}].{}.{} #{}' 844 | .format(section.name, entry.name, field.name, field.placement)) 845 | ''' 846 | """ 847 | if type_name == "EDS_TYPEREF": 848 | ''' 849 | Type of a field is determined by value of another field. A referenced-type has to be validated. 850 | The name of the ref field that contains the a data_type, is listed in the primary field's 851 | datatype.valid_ranges(typeinfo) which itself is a list of names 852 | Example: The datatype of Params.Param1.MinimumValue is determined by Params.Param1.DataType 853 | ''' 854 | # TODO: here we read only the first item of the reference field list. Iterating the list might be a better way 855 | typeid = self.get_field(section_name, entry_name, typeinfo[0]).value 856 | try: 857 | dtype = self.ref_libs.get_type(typeid) 858 | if dtype.validate(field_value, []): 859 | field_data = dtype(field_value, []) 860 | break 861 | except: 862 | field_data = EDS_UNDEFINED(field_value) 863 | """ 864 | def save(self, filename, overwrite = False): 865 | if os.path.isfile(filename) and overwrite == False: 866 | raise Exception('Failed to write to file! \"{}\" already exists and overrwite is not enabled.'.format(filename)) 867 | 868 | if self.heading_comment == '': 869 | self.heading_comment = HEADING_COMMENT_TEMPLATE 870 | eds_content = ''.join('$ {}\n'.format(line.strip()) for line in self.heading_comment.splitlines()) 871 | 872 | tabsize = 4 873 | # sections 874 | # Creating a list of standard sections. 875 | std_sections = [self.get_section('File')] 876 | std_sections.append(self.get_section('Device')) 877 | std_sections.append(self.get_section('Device Classification')) 878 | for section in self.sections: 879 | if section._id is None and section not in std_sections: 880 | std_sections.append(section) 881 | # Creating a list of protocol specific sections oredred by their ids. 882 | protocol_sections = [section for section in self.sections if section._id is not None] 883 | protocol_sections = sorted(protocol_sections, key = lambda section: section._id) 884 | sections = std_sections + protocol_sections 885 | 886 | for section in sections: 887 | if section.hcomment != '': 888 | eds_content += ''.join('$ {}\n'.format(line.strip()) for line in section.hcomment.splitlines()) 889 | eds_content += '\n[{}]'.format(section.name) 890 | 891 | if section.fcomment != '': 892 | eds_content += ''.join('$ {}\n'.format(line.strip()) for line in section.fcomment.splitlines()) 893 | 894 | eds_content += '\n' 895 | 896 | # Entries 897 | entries = sorted(section.entries, key = lambda entry: entry._index) 898 | for entry in entries: 899 | 900 | if entry.hcomment != '': 901 | eds_content += ''.join(''.ljust(tabsize, ' ') + '$ {}\n'.format(line.strip()) for line in entry.hcomment.splitlines()) 902 | eds_content += ''.ljust(tabsize, ' ') + '{} ='.format(entry.name) 903 | 904 | # fields 905 | if entry.fieldcount == 1: 906 | if '\n' in str(entry.fields[0]._data): 907 | eds_content += '\n' 908 | eds_content += '\n'.join(''.ljust(2 * tabsize, ' ') + line 909 | for line in str(entry.fields[0]._data).splitlines()) 910 | eds_content += ';' 911 | else: 912 | eds_content += '{};'.format(entry.fields[0]._data) 913 | if entry.fields[0].fcomment != '': 914 | eds_content += ''.join(''.ljust(tabsize, ' ') + 915 | '$ {}\n'.format(line.strip()) for line in entry.fields[0].fcomment.splitlines()) 916 | eds_content += '\n' 917 | else: # entry has multiple fields 918 | eds_content += '\n' 919 | 920 | for fieldindex, field in enumerate(entry.fields): 921 | singleline_field_str = ''.ljust(2 * tabsize, ' ') + '{}'.format(field._data) 922 | 923 | # separator 924 | if (fieldindex + 1) == entry.fieldcount: 925 | singleline_field_str += ';' 926 | else: 927 | singleline_field_str += ',' 928 | 929 | # footer comment 930 | if field.fcomment != '': 931 | singleline_field_str = singleline_field_str.ljust(30, ' ') 932 | singleline_field_str += ''.join('$ {}'.format(line.strip()) for line in field.fcomment.splitlines()) 933 | eds_content += singleline_field_str + '\n' 934 | 935 | # end comment 936 | eds_content += '\n' 937 | if self.end_comment == '': 938 | self.end_comment = END_COMMENT_TEMPLATE 939 | eds_content += ''.join('$ {}\n'.format(line.strip()) for line in self.end_comment.splitlines()) 940 | 941 | hfile = open(filename, 'w') 942 | hfile.write(eds_content) 943 | hfile.close() 944 | 945 | def get_cip_section_name(self, class_id, protocol=None): 946 | if protocol is None: 947 | protocol = self.protocol 948 | return self.ref_libs.get_section_name(class_id, protocol) 949 | 950 | def resolve_epath(self, epath): 951 | ''' 952 | EPATH data types can contain references to param entries in params section. 953 | This method validates the path and resolves the referenced items inside the epath. 954 | input EPATH in string format. example \'20 04 24 [Param1] 30 03\' 955 | return: EPATH in string format 956 | ''' 957 | items = epath.split() 958 | for i in range(len(items)): 959 | item = items[i] 960 | if len(item) < 2: 961 | raise Exception('Invalid EPATH format! item[{}]:\"{}\" in [{}]'.format(index, item, path)) 962 | 963 | if not isnumber(item): 964 | item = item.strip('[]') 965 | if 'Param' == item.rstrip(digits) or 'ProxyParam' == item.rstrip(digits): 966 | entry_name = item 967 | field = self.get_field('Params', entry_name, 'Default Value') 968 | if field: 969 | items[i] = '{:02X}'.format(field.value) 970 | continue 971 | raise Exception('Entry not found! tem[\'{}\'] in [{}]'.format(item, path)) 972 | raise Exception('Invalid path format! tem[\'{}\'] in [{}]'.format(item, path)) 973 | elif not ishex(item): 974 | raise Exception('Invalid EPATH format! item[\'{}\'] in [{}]'.format(item, path)) 975 | 976 | return ' '.join(item for item in items) 977 | 978 | def __str__(self): 979 | Msg = '' 980 | for section in self.__sections: 981 | Msg += '[%s]\n'%(section._name) 982 | for entry in section.entries: 983 | Msg += ' %s = '%(entry.name) 984 | for entryvalue in entry.fields: 985 | Msg += '%s,'%(entryvalue.data) 986 | Msg += '\n' 987 | return Msg 988 | 989 | class Token(object): 990 | 991 | def __init__(self, type=None, value=None, offset=None, line=None, col=None): 992 | self.type = type 993 | self.value = value 994 | self.offset = offset 995 | self.line = line 996 | self.col = col 997 | 998 | def __str__(self): 999 | return '[Ln: {}, Col: {}, Pos: {}] {} \"{}\"'.format( 1000 | str(self.line).rjust(4), 1001 | str(self.col).rjust(3), 1002 | str(self.offset).rjust(5), 1003 | TOKEN_TYPES.stringify(self.type).ljust(11), 1004 | self.value) 1005 | 1006 | class parser(object): 1007 | def __init__(self, eds_content, showprogress = False): 1008 | self.src_text = eds_content 1009 | self.src_len = len(eds_content) 1010 | self.offset = -1 1011 | self.line = 1 1012 | self.col = 0 1013 | 1014 | self.eds = EDS() 1015 | 1016 | # these two are only to keep track of element comments. A comment on the 1017 | # same line of a field is the field's footer comment. Otherwise it's the 1018 | # entry's header comment. 1019 | self.token = None 1020 | self.prevtoken = None 1021 | self.comment = '' 1022 | 1023 | self.active_section = None 1024 | self.active_entry = None 1025 | self.last_created_element = None 1026 | self.state = PSTATE.EXPECT_SECTION 1027 | 1028 | self.showprogress = showprogress 1029 | self.progress = 0.0 1030 | self.progress_step = float(self.src_len) / 100.0 1031 | 1032 | def get_char(self): 1033 | if self.showprogress: 1034 | self.progress += 1.0 1035 | if self.progress % self.progress_step < 1.0: 1036 | sys.stdout.write('Parsing... [%0.0f%%] \r' %(self.progress / self.progress_step) ) 1037 | sys.stdout.flush() 1038 | sys.stdout.write('') 1039 | 1040 | assert self.offset <= self.src_len 1041 | self.offset += 1 1042 | 1043 | # EOF 1044 | if self.offset == self.src_len: 1045 | return SYMBOLS.EOF 1046 | 1047 | char = self.src_text[self.offset] 1048 | self.col += 1 1049 | if char == SYMBOLS.EOL: 1050 | self.line += 1 1051 | self.col = 0 1052 | 1053 | return char 1054 | 1055 | def lookahead(self, offset = 1): 1056 | if self.offset + offset >= self.src_len: 1057 | return None 1058 | return self.src_text[self.offset + offset] 1059 | 1060 | def lookbehind(self, offset = 1): 1061 | if self.offset - offset < 0: 1062 | return None 1063 | return self.src_text[self.offset - offset] 1064 | 1065 | def get_token(self): 1066 | 1067 | token = None 1068 | 1069 | while True: 1070 | ch = self.get_char() 1071 | 1072 | if token is None: 1073 | 1074 | if ch is SYMBOLS.EOF: 1075 | return SYMBOLS.EOF 1076 | 1077 | if ch.isspace(): 1078 | # Ignoring space characters including: space, tab, carriage return 1079 | continue 1080 | 1081 | if ch == SYMBOLS.DOLLAR: 1082 | token = Token(type=TOKEN_TYPES.COMMENT, value='', 1083 | offset=self.offset, line=self.line, col=self.col) 1084 | continue 1085 | 1086 | if ch == SYMBOLS.OPENINGBRACKET: 1087 | token = Token(type=TOKEN_TYPES.SECTION, value='', 1088 | offset=self.offset, line=self.line, col=self.col) 1089 | continue 1090 | 1091 | if ch == SYMBOLS.OPENINGBRACE: 1092 | token = Token(type=TOKEN_TYPES.DATASET, value=ch, 1093 | offset=self.offset, line=self.line, col=self.col) 1094 | continue 1095 | 1096 | if ch == SYMBOLS.POINT or ch == SYMBOLS.MINUS or ch == SYMBOLS.PLUS or ch.isdigit(): 1097 | token = Token(type=TOKEN_TYPES.NUMBER, value=ch, 1098 | offset=self.offset, line=self.line, col=self.col) 1099 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1100 | return token 1101 | continue 1102 | 1103 | if ch.isalpha(): 1104 | token = Token(type=TOKEN_TYPES.IDENTIFIER, value=ch, 1105 | offset=self.offset, line=self.line, col=self.col) 1106 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1107 | return token 1108 | continue 1109 | 1110 | if ch == SYMBOLS.QUOTATION: 1111 | token = Token(type=TOKEN_TYPES.STRING, value='', 1112 | offset=self.offset, line=self.line, col=self.col) 1113 | continue 1114 | 1115 | if ch in SYMBOLS.OPERATORS: 1116 | return Token(type=TOKEN_TYPES.OPERATOR, value=ch, 1117 | offset=self.offset, line=self.line, col=self.col) 1118 | 1119 | if ch in SYMBOLS.SEPARATORS: 1120 | return Token(type=TOKEN_TYPES.SEPARATOR, value=ch, 1121 | offset=self.offset, line=self.line, col=self.col) 1122 | 1123 | if token.type is TOKEN_TYPES.COMMENT: 1124 | if ch == SYMBOLS.EOL or self.lookahead() == SYMBOLS.EOF: 1125 | return token 1126 | token.value += ch 1127 | continue 1128 | 1129 | if token.type is TOKEN_TYPES.SECTION: 1130 | if ch == SYMBOLS.CLOSINGBRACKET: 1131 | return token 1132 | 1133 | # filtering invalid symbols in section name 1134 | if (not ch.isspace() and not ch.isalpha() and not ch.isdigit() 1135 | and (ch not in SECTION_NAME_VALID_SYMBOLES)): 1136 | 1137 | raise Exception( __name__ + '.lexer:> Invalid section identifier!' 1138 | + ' Unexpected char sequence ' 1139 | + '@[idx: {}] [ln: {}] [col: {}]' 1140 | .format(self.offset, self.line, self.col)) 1141 | 1142 | # unexpected symbols at the beginning or at the end of the section identifier 1143 | if ((token.value == '' or self.lookahead() == ']') and 1144 | (not ch.isalpha() and not ch.isdigit())): 1145 | raise Exception( __name__ + '.lexer:> Invalid section identifier!' 1146 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1147 | 1148 | # Sequential spaces 1149 | if ch == ' ' and self.lookahead().isspace(): 1150 | raise Exception( __name__ + '.lexer:> Invalid section identifier! Sequential spaces are not allowed.' 1151 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1152 | 1153 | if ch == SYMBOLS.EOF or ch == SYMBOLS.EOL: 1154 | raise Exception( __name__ + '.lexer:> Invalid section identifier!' 1155 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1156 | 1157 | token.value += ch 1158 | continue 1159 | 1160 | if token.type is TOKEN_TYPES.NUMBER: 1161 | if ch.isspace(): 1162 | return token 1163 | # Switching the token type to other types 1164 | if ch is SYMBOLS.COLON: token.type = TOKEN_TYPES.TIME 1165 | elif ch is SYMBOLS.MINUS: token.type = TOKEN_TYPES.DATE 1166 | elif ch is SYMBOLS.UNDERLINE: token.type = TOKEN_TYPES.IDENTIFIER 1167 | 1168 | token.value += ch 1169 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1170 | return token 1171 | continue 1172 | 1173 | if token.type is TOKEN_TYPES.IDENTIFIER: 1174 | if ch.isspace(): 1175 | return token 1176 | 1177 | token.value += ch 1178 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1179 | return token 1180 | continue 1181 | 1182 | if token.type is TOKEN_TYPES.STRING: 1183 | if ch == SYMBOLS.QUOTATION and self.lookbehind() != SYMBOLS.BACKSLASH: 1184 | return token 1185 | 1186 | if ch == SYMBOLS.EOF or ch == SYMBOLS.EOL: 1187 | raise Exception( __name__ + '.lexer:> Invalid string value!' 1188 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1189 | 1190 | token.value += ch 1191 | continue 1192 | 1193 | if token.type is TOKEN_TYPES.DATASET: 1194 | if self.lookahead() == SYMBOLS.SEMICOLON: 1195 | return token 1196 | 1197 | token.value += ch 1198 | if ch == SYMBOLS.CLOSINGBRACE: 1199 | return token 1200 | continue 1201 | 1202 | if token.type is TOKEN_TYPES.TIME: 1203 | if ch.isspace(): 1204 | return token 1205 | 1206 | if not ch.isdigit() and ch is not SYMBOLS.COLON: 1207 | raise Exception( __name__ + '.lexer:> Invalid TIME value!' 1208 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1209 | token.value += ch 1210 | 1211 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1212 | return token 1213 | continue 1214 | 1215 | if token.type is TOKEN_TYPES.DATE: 1216 | if ch.isspace(): 1217 | return token 1218 | 1219 | if not ch.isdigit() and ch is not SYMBOLS.MINUS: 1220 | raise Exception( __name__ + '.lexer:> Invalid DATE value!' 1221 | + ' Unexpected char sequence @[idx: {}] [ln: {}] [col: {}]'.format(self.offset, self.line, self.col)) 1222 | token.value += ch 1223 | 1224 | if self.lookahead() in SYMBOLS.OPERATORS or self.lookahead() in SYMBOLS.SEPARATORS: 1225 | return token 1226 | continue 1227 | 1228 | def next_token(self): 1229 | token = self.get_token() 1230 | self.prevtoken = self.token 1231 | self.token = token 1232 | logger.debug('token: {}'.format(token or 'EOF')) 1233 | return token 1234 | 1235 | def parse(self): 1236 | while True: 1237 | token = self.next_token() 1238 | 1239 | if token is SYMBOLS.EOF: 1240 | self.on_EOF() 1241 | return self.eds 1242 | 1243 | if self.match(token, TOKEN_TYPES.COMMENT): 1244 | self.add_comment(token) 1245 | continue 1246 | 1247 | if self.state is PSTATE.EXPECT_SECTION: 1248 | if self.match(token, TOKEN_TYPES.SECTION): 1249 | self.add_section(token) 1250 | continue 1251 | else: 1252 | raise Exception("Invalid token! Expected a Section token but got: {}".format(token)) 1253 | 1254 | if self.state is PSTATE.EXPECT_ENTRY: 1255 | if self.match(token, TOKEN_TYPES.IDENTIFIER): 1256 | self.add_entry(token) 1257 | continue 1258 | else: 1259 | raise Exception("Invalid token! Expected an Entry token but got: {}".format(token)) 1260 | 1261 | if self.state is PSTATE.EXPECT_FIELD: 1262 | self.add_field(token) 1263 | continue 1264 | 1265 | if self.state is PSTATE.EXPECT_SECTION_OR_ENTRY: 1266 | if self.match(token, TOKEN_TYPES.SECTION): 1267 | self.add_section(token) 1268 | elif self.match(token, TOKEN_TYPES.IDENTIFIER): 1269 | self.add_entry(token) 1270 | else: 1271 | raise Exception("Invalid token! Expected a Section or an Entry token but got: {}".format(token)) 1272 | continue 1273 | 1274 | raise Exception(__name__ + ':> Invalid Parser state! {}'.format(self.state)) 1275 | 1276 | def add_section(self, token): 1277 | self.active_section = self.eds.add_section(token.value) 1278 | 1279 | if self.active_section is None: 1280 | raise Exception("Unable to create section: {}".format(token.value)) 1281 | 1282 | # If there are cached comments then they are header comments of the new element 1283 | self.active_section.hcomment = self.comment 1284 | self.last_created_element = self.active_section 1285 | self.comment = '' 1286 | 1287 | # This is a new section. Expecting at least one entry. 1288 | self.state = PSTATE.EXPECT_ENTRY 1289 | 1290 | def add_entry(self, token): 1291 | self.active_entry = self.eds.add_entry(self.active_section.name, token.value) 1292 | 1293 | if self.active_entry is None: 1294 | raise Exception("Unable to create entry: {}".format(token.value)) 1295 | 1296 | # If there are cached comments then they are header comments of the new element 1297 | self.active_entry.hcomment = self.comment 1298 | self.last_created_element = self.active_entry 1299 | self.comment = '' 1300 | 1301 | # This is a new entry. Expecting at least one field. 1302 | self.expect(self.next_token(), TOKEN_TYPES.OPERATOR, SYMBOLS.ASSIGNMENT) 1303 | self.state = PSTATE.EXPECT_FIELD 1304 | 1305 | def add_field(self, token): 1306 | field_value = '' 1307 | field_type = None 1308 | 1309 | # It's possible that a field value contains multiple tokens. Fetch tokens 1310 | # in a loop until reaching the end of the field. Concatenate the values 1311 | # if possible(to support multi-line strings) 1312 | while True: 1313 | 1314 | if token is SYMBOLS.EOF: 1315 | raise Exception("Unexpected token. Expected a field token but got EOF.") 1316 | 1317 | if (self.match(token, TOKEN_TYPES.SEPARATOR, SYMBOLS.COMMA) or 1318 | self.match(token, TOKEN_TYPES.SEPARATOR, SYMBOLS.SEMICOLON)): 1319 | field = self.eds.add_field(self.active_section.name, self.active_entry.name, field_value, field_type) 1320 | 1321 | if field is None: 1322 | raise Exception("Unable to create field: {} of type: {}".format(token.value, token.type)) 1323 | 1324 | # If there are cached comments then they are header comments of the new element 1325 | field.hcomment = self.comment 1326 | self.last_created_element = field 1327 | self.comment = '' 1328 | field_value = '' 1329 | field_type = None 1330 | 1331 | if self.match(token, TOKEN_TYPES.SEPARATOR, SYMBOLS.SEMICOLON): 1332 | # The next token might be an entry or a new section 1333 | self.state = PSTATE.EXPECT_SECTION_OR_ENTRY 1334 | break 1335 | 1336 | elif (self.match(token, TOKEN_TYPES.IDENTIFIER) or 1337 | self.match(token, TOKEN_TYPES.STRING) or 1338 | self.match(token, TOKEN_TYPES.NUMBER) or 1339 | self.match(token, TOKEN_TYPES.DATE) or 1340 | self.match(token, TOKEN_TYPES.TIME) or 1341 | self.match(token, TOKEN_TYPES.DATASET)): 1342 | 1343 | if field_value == '' and field_type is None: 1344 | field_value += token.value 1345 | field_type = token.type 1346 | elif field_type == TOKEN_TYPES.STRING and self.match(token, TOKEN_TYPES.STRING): 1347 | # There are two strings literals in one field that must be Concatenated 1348 | field_value += token.value 1349 | else: 1350 | # There are different types of tokens to be concatenated. 1351 | raise Exception("Concatenating these literals is not allowed." 1352 | + '({})<{}> + ({})<{}> @({})'.format(field_value, TOKEN_TYPES.stringify(field_type), token.value, TOKEN_TYPES.stringify(token.type), token)) 1353 | else: 1354 | raise Exception("Unexpected token type. Expected a field value token but got: {}".format(token)) 1355 | 1356 | token = self.next_token() 1357 | 1358 | def add_comment(self, token): 1359 | if self.state is PSTATE.EXPECT_SECTION: 1360 | self.eds.heading_comment += token.value.strip() + '\n' 1361 | return 1362 | # The footer comment only appears on the same line after the eds item 1363 | # otherwise the comment is a header comment 1364 | if self.prevtoken and self.prevtoken.line == token.line: 1365 | # Footer comment 1366 | self.last_created_element.fcomment = token.value.strip() 1367 | else: 1368 | # Caching the header comment for the next element. 1369 | self.comment += token.value.strip() + '\n' 1370 | 1371 | def on_EOF(self): 1372 | # The rest of cached comments belong to no elements 1373 | self.eds.end_comment = self.comment 1374 | self.comment = '' 1375 | 1376 | def expect(self, token, expected_type, expected_value=None): 1377 | if token.type == expected_type: 1378 | if expected_value is None or token.value == expected_value: 1379 | return 1380 | 1381 | raise Exception("Unexpected token! Expected: (\"{}\": {}) but received: {}".format( 1382 | TOKEN_TYPES.stringify(exptokentype), exptokenval, self.token)) 1383 | 1384 | def match(self, token, expected_type, expected_value=None): 1385 | if token.type == expected_type and expected_value is not None: 1386 | if token.value == expected_value: 1387 | return True 1388 | elif token.type == expected_type : 1389 | return True 1390 | return False 1391 | 1392 | class eds_pie(object): 1393 | 1394 | @staticmethod 1395 | def parse(eds_content = '', showprogress = True): 1396 | 1397 | eds = parser(eds_content, showprogress).parse() 1398 | eds.semantic_check() 1399 | # setting the protocol 1400 | eds._protocol = 'Generic' 1401 | 1402 | if eds.get_section('Device Classification').entries: 1403 | field = eds.get_section('Device Classification').entries[0].get_field(0) 1404 | if field: 1405 | eds._protocol = field.value 1406 | 1407 | eds.semantic_check() 1408 | if showprogress: print('') 1409 | return eds 1410 | 1411 | 1412 | --------------------------------------------------------------------------------