├── 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 |
--------------------------------------------------------------------------------
/readme-images/py27-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |  
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 | 
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 | 
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 |
--------------------------------------------------------------------------------