uphy-upgen 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. uphy_upgen-1.0.0/LICENSE.txt +5 -0
  2. uphy_upgen-1.0.0/PKG-INFO +54 -0
  3. uphy_upgen-1.0.0/README.md +19 -0
  4. uphy_upgen-1.0.0/pyproject.toml +47 -0
  5. uphy_upgen-1.0.0/upgen/__init__.py +16 -0
  6. uphy_upgen-1.0.0/upgen/cli.py +150 -0
  7. uphy_upgen-1.0.0/upgen/generators/__init__.py +11 -0
  8. uphy_upgen-1.0.0/upgen/generators/c_code.py +247 -0
  9. uphy_upgen-1.0.0/upgen/generators/ethercat.py +1091 -0
  10. uphy_upgen-1.0.0/upgen/generators/ethernetip.py +375 -0
  11. uphy_upgen-1.0.0/upgen/generators/profinet.py +380 -0
  12. uphy_upgen-1.0.0/upgen/generators/schema/ESI/EtherCATBase.xsd +648 -0
  13. uphy_upgen-1.0.0/upgen/generators/schema/ESI/EtherCATInfo.xsd +945 -0
  14. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/GSDML-DeviceProfile-v2.44.xsd +2535 -0
  15. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/GSDML-Primitives-v2.44.xsd +495 -0
  16. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/XAdES01903v132-201601.xsd +534 -0
  17. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/XAdES01903v132-201601_org.xsd +532 -0
  18. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xml.xsd +287 -0
  19. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xmldsig-core-schema.xsd +320 -0
  20. uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xmldsig-core-schema_org.xsd +318 -0
  21. uphy_upgen-1.0.0/upgen/generators/sii.py +292 -0
  22. uphy_upgen-1.0.0/upgen/model/__init__.py +6 -0
  23. uphy_upgen-1.0.0/upgen/model/ethercat.py +188 -0
  24. uphy_upgen-1.0.0/upgen/model/ethernetip.py +38 -0
  25. uphy_upgen-1.0.0/upgen/model/modbus.py +23 -0
  26. uphy_upgen-1.0.0/upgen/model/profinet.py +135 -0
  27. uphy_upgen-1.0.0/upgen/model/runtime.py +41 -0
  28. uphy_upgen-1.0.0/upgen/model/uphy.py +315 -0
  29. uphy_upgen-1.0.0/upgen/templates/GSDML-V2.44-RT-Labs-U-Phy-Template.xml +105 -0
  30. uphy_upgen-1.0.0/upgen/templates/__init__.py +16 -0
  31. uphy_upgen-1.0.0/upgen/templates/model_template.c +372 -0
  32. uphy_upgen-1.0.0/upgen/templates/model_template.h +109 -0
  33. uphy_upgen-1.0.0/upgen/templates/template.eds +149 -0
@@ -0,0 +1,5 @@
1
+ Copyright rt-labs AB, Sweden.
2
+ All rights reserved.
3
+
4
+ You may not use this software in a commercial product without
5
+ purchasing a license. Contact sales@rt-labs.com for more information.
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.3
2
+ Name: uphy-upgen
3
+ Version: 1.0.0
4
+ Summary: U-Phy Generator
5
+ License: Copyright rt-labs AB, Sweden.
6
+ All rights reserved.
7
+
8
+ You may not use this software in a commercial product without
9
+ purchasing a license. Contact sales@rt-labs.com for more information.
10
+ Author: RT-Labs
11
+ Author-email: support@rt-labs.com
12
+ Requires-Python: >=3.8
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Provides-Extra: compile
22
+ Provides-Extra: dev
23
+ Requires-Dist: crc8 (>=0.2.0,<1.0.0)
24
+ Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
25
+ Requires-Dist: nuitka (>=2.4.11,<3.0.0) ; extra == "compile"
26
+ Requires-Dist: pydantic (>=1.10.7,<2.0.0)
27
+ Requires-Dist: pytest (>=7.4.4,<8.0.0) ; extra == "dev"
28
+ Requires-Dist: pytest-cov (>=5.0.0,<6.0.0) ; extra == "dev"
29
+ Requires-Dist: xmlschema (>=3.0.1,<4.0.0)
30
+ Project-URL: Documentation, https://docs.rt-labs.com/u-phy
31
+ Project-URL: RT-Labs, https://rt-labs.com/
32
+ Project-URL: U-Phy, https://rt-labs.com/u-phy
33
+ Description-Content-Type: text/markdown
34
+
35
+ # U-Phy Device Generator
36
+
37
+ The U-Phy Device Generator is a tool designed to create fieldbus
38
+ device description files and U-Phy source code directly from device
39
+ models generated by the U-Phy Device Builder.
40
+
41
+ Maintaining synchronization between source code and description files
42
+ can be challenging. Therefore, having a tool that generates both from
43
+ a single source of truth is extremely beneficial.
44
+
45
+ ## Developing
46
+
47
+ Python Poetry is recommended. See
48
+ https://python-poetry.org/docs/#installation for installation
49
+ instructions.
50
+
51
+ $ cd /path/to/upgen
52
+ $ poetry install
53
+ $ poetry shell
54
+
@@ -0,0 +1,19 @@
1
+ # U-Phy Device Generator
2
+
3
+ The U-Phy Device Generator is a tool designed to create fieldbus
4
+ device description files and U-Phy source code directly from device
5
+ models generated by the U-Phy Device Builder.
6
+
7
+ Maintaining synchronization between source code and description files
8
+ can be challenging. Therefore, having a tool that generates both from
9
+ a single source of truth is extremely beneficial.
10
+
11
+ ## Developing
12
+
13
+ Python Poetry is recommended. See
14
+ https://python-poetry.org/docs/#installation for installation
15
+ instructions.
16
+
17
+ $ cd /path/to/upgen
18
+ $ poetry install
19
+ $ poetry shell
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "uphy-upgen"
3
+ version = "1.0.0"
4
+ description = "U-Phy Generator"
5
+ readme = "README.md"
6
+ license = {file = "LICENSE.txt"}
7
+ authors = [
8
+ {name="RT-Labs", email="support@rt-labs.com"}
9
+ ]
10
+ requires-python = ">=3.8"
11
+
12
+ dependencies = [
13
+ "pydantic (>=1.10.7,<2.0.0)",
14
+ "jinja2 (>=3.1.2,<4.0.0)",
15
+ "crc8 (>=0.2.0,<1.0.0)",
16
+ "xmlschema (>=3.0.1,<4.0.0)",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest (>=7.4.4,<8.0.0)",
22
+ "pytest-cov (>=5.0.0,<6.0.0)"
23
+ ]
24
+ compile = [
25
+ "nuitka (>=2.4.11,<3.0.0)"
26
+ ]
27
+
28
+ [project.scripts]
29
+ upgen = 'upgen.cli:run'
30
+
31
+ [project.urls]
32
+ "RT-Labs" = "https://rt-labs.com/"
33
+ "U-Phy" = "https://rt-labs.com/u-phy"
34
+ "Documentation" = "https://docs.rt-labs.com/u-phy"
35
+
36
+ [tool.poetry]
37
+ packages = [
38
+ { include = "upgen" },
39
+ ]
40
+ requires-poetry = ">=2.0"
41
+
42
+ [tool.pytest.ini_options]
43
+ addopts = "--cov=upgen --cov-report term-missing"
44
+
45
+ [build-system]
46
+ requires = ["poetry-core>=2.0"]
47
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,16 @@
1
+ import sys
2
+
3
+ if sys.version_info[:2] >= (3, 8):
4
+ # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
5
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
6
+ else:
7
+ from importlib_metadata import PackageNotFoundError, version # pragma: no cover
8
+
9
+ try:
10
+ # Change here if project is renamed and does not equal the package name
11
+ dist_name = "uphy-upgen"
12
+ __version__ = version(dist_name)
13
+ except PackageNotFoundError: # pragma: no cover
14
+ __version__ = "unknown"
15
+ finally:
16
+ del version, PackageNotFoundError
@@ -0,0 +1,150 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ import os
5
+ import traceback
6
+ from argparse import RawTextHelpFormatter
7
+
8
+ from pathlib import Path
9
+ from json.decoder import JSONDecodeError
10
+ from pydantic import ValidationError
11
+
12
+ from upgen import __version__
13
+ from upgen.model import *
14
+ from upgen.generators import EtherCATGenerator
15
+ from upgen.generators import ProfinetGenerator
16
+ from upgen.generators import EthernetipGenerator
17
+ from upgen.generators import CCodeGenerator
18
+ from upgen.generators import SIIGenerator
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+ GENERATORS = {
23
+ 'EtherCAT': EtherCATGenerator,
24
+ 'Profinet': ProfinetGenerator,
25
+ "EtherNetIP":EthernetipGenerator,
26
+ 'Code': CCodeGenerator,
27
+ 'SII': SIIGenerator,
28
+
29
+ }
30
+
31
+
32
+ def get_generator_classes(name):
33
+ if name == 'All':
34
+ return [GENERATORS[g] for g in GENERATORS]
35
+ try:
36
+ return [GENERATORS[name]]
37
+ except KeyError:
38
+ raise ValueError(f"--generator name must be in {tuple(GENERATORS)} or \'All\')")
39
+
40
+
41
+ def parse_args(args):
42
+ parser = argparse.ArgumentParser(description=
43
+ """
44
+ Tool for generation of device artifacts from a U-Phy device model
45
+ file, including:
46
+ - C-code with the device specific data model
47
+ - EtherCAT ESI file
48
+ - EtherCAT SII EEPROM file
49
+ - Profinet GSDML file
50
+
51
+ To evaluate the device functionality, copy generated model.c/h and
52
+ eeprom.bin to the \'generated\' folder of the U-Phy sample application
53
+ and rebuild.
54
+
55
+ Example usage:
56
+ upgen export my_device_model.json
57
+ """,
58
+ formatter_class=RawTextHelpFormatter)
59
+ parser.add_argument(
60
+ "--version",
61
+ action="version",
62
+ version=f"upgen {__version__}",
63
+ )
64
+ parser.add_argument(
65
+ "-v",
66
+ "--verbose",
67
+ dest="loglevel",
68
+ help="set loglevel to INFO",
69
+ action="store_const",
70
+ const=logging.INFO,
71
+ )
72
+ parser.add_argument(
73
+ "-vv",
74
+ "--very-verbose",
75
+ dest="loglevel",
76
+ help="set loglevel to DEBUG",
77
+ action="store_const",
78
+ const=logging.DEBUG,
79
+ )
80
+ parser.add_argument("-d", "--destination", help="destination folder for generated model", default=".")
81
+
82
+ parser.add_argument('--generator', help="the generator to use. Option value "
83
+ "can be one of {} or \'All\'. Default value is \'All\'".format(tuple(GENERATORS)), default="All")
84
+
85
+ parser.add_argument('-n', '--device_name', help="the name of the device to generate. "
86
+ "Only used when generating C code or SII eeprom for multi device models.")
87
+
88
+ # Import device description file or
89
+ # Export source code and device description files from json model
90
+ # TODO - for export the generators for fieldbuses defined by the
91
+ # model should be run. All outputs placed in the destination folder.
92
+ parser.add_argument('action', choices=('import', 'export', 'schema'))
93
+ parser.add_argument("file", help="Device model in JSON format")
94
+
95
+ return parser.parse_args(args)
96
+
97
+
98
+ def setup_logging(loglevel):
99
+ logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s"
100
+ if loglevel is None:
101
+ loglevel = logging.WARNING
102
+ logging.basicConfig(
103
+ level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S"
104
+ )
105
+
106
+
107
+ def main(args):
108
+ args = parse_args(args)
109
+ setup_logging(args.loglevel)
110
+
111
+ if args.action == 'import':
112
+ raise Exception( "import not supported")
113
+ generator_class = get_generator_class(args.generator)
114
+ generator = generator_class(None)
115
+ generator.import_file(args.file)
116
+ print(generator.model)
117
+
118
+ elif args.action == 'export':
119
+ try:
120
+ model = Root.parse_file(Path(args.file))
121
+ except (JSONDecodeError, ValidationError) as e:
122
+ print(e)
123
+ sys.exit(1)
124
+
125
+ os.makedirs(args.destination, exist_ok=True)
126
+ os.chdir(args.destination)
127
+ generator_classes = get_generator_classes(args.generator)
128
+ for gen_cls in generator_classes:
129
+ try:
130
+ generator = gen_cls(model)
131
+ if (gen_cls in [CCodeGenerator, SIIGenerator, EthernetipGenerator]):
132
+ generator.select_device(args.device_name)
133
+ generator.export_file()
134
+ except Exception as e:
135
+ print(
136
+ f"Error while running {gen_cls.__name__}: \"{str(e)}\" Continue generating rest of artifacts.")
137
+ _logger.warning(''.join(traceback.TracebackException.from_exception(e).format()))
138
+
139
+ elif args.action == 'schema':
140
+ print(Root.schema_json(indent=2))
141
+
142
+ _logger.info("Script ends here")
143
+
144
+
145
+ def run():
146
+ main(sys.argv[1:])
147
+
148
+
149
+ if __name__ == "__main__":
150
+ run()
@@ -0,0 +1,11 @@
1
+
2
+ # import os
3
+ from pathlib import Path, PurePath
4
+
5
+ SCHEMA_DIR = Path(PurePath(__file__).parent, "schema")
6
+
7
+ from .ethercat import EtherCATGenerator
8
+ from .sii import SIIGenerator
9
+ from .profinet import ProfinetGenerator
10
+ from .ethernetip import EthernetipGenerator
11
+ from .c_code import CCodeGenerator
@@ -0,0 +1,247 @@
1
+ import logging
2
+ import re
3
+ import json
4
+ import importlib.resources
5
+ from .. import templates
6
+ from jinja2 import Environment, PackageLoader, select_autoescape
7
+ from ..model.uphy import *
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+
12
+ def c_type(value):
13
+ map = {
14
+ DataType.UINT32: 'uint32_t',
15
+ DataType.UINT16: 'uint16_t',
16
+ DataType.UINT8: 'uint8_t',
17
+ DataType.INT32: 'int32_t',
18
+ DataType.INT16: 'int16_t',
19
+ DataType.INT8: 'int8_t',
20
+ DataType.REAL32: 'float',
21
+ }
22
+ return map[value.datatype]
23
+
24
+ class CNameContext():
25
+ """
26
+ Defines scopes for identifiers:
27
+ - Slots
28
+ - Signals/Parameters (per slot)
29
+ - Modules
30
+ - Alarms
31
+ """
32
+ class Scope(Enum):
33
+ NONE = 0,
34
+ SLOT = 1,
35
+ SIGNAL = 2,
36
+ MODULE = 3,
37
+ ALARM = 4,
38
+ TXPDO = 5,
39
+ RXPDO = 6
40
+
41
+ def __init__(self, slot=""):
42
+ self.slot = slot
43
+ self.scope = self.Scope.NONE
44
+ self.ctx = {
45
+ self.Scope.SLOT: {}, # (name, index)
46
+ self.Scope.SIGNAL: {}, # {slot_index: (name, index)}
47
+ self.Scope.MODULE: {}, # (name, index)
48
+ self.Scope.ALARM: {}, # (name, index)
49
+ self.Scope.TXPDO: {}, # (name, index)
50
+ self.Scope.RXPDO: {}, # (name, index)
51
+ }
52
+
53
+ def _get_ctx(self):
54
+ assert self.ctx != self.Scope.NONE
55
+
56
+ if self.Scope == self.Scope.SIGNAL:
57
+ assert self.slot != ""
58
+ if self.slot not in self.ctx[self.scope]:
59
+ self.ctx[self.scope][self.slot] = {}
60
+
61
+ ctx = self.ctx[self.scope][self.slot]
62
+ else:
63
+ ctx = self.ctx[self.scope]
64
+
65
+ return ctx
66
+
67
+ def name_available(self, name, index):
68
+ ctx = self._get_ctx()
69
+
70
+ for other_index, other_name in ctx.items():
71
+ if name == other_name and index != other_index:
72
+ _logger.debug(f"[{self.scope}] name collision: {name}, '{index}' '{other_index}'")
73
+ return False
74
+
75
+ return True
76
+
77
+ def set_name(self, name, index):
78
+ ctx = self._get_ctx()
79
+ ctx[index] = name
80
+
81
+ def get_name(self, index):
82
+ ctx = self._get_ctx()
83
+ return ctx.get(index, None)
84
+
85
+ def index_exists(self, index):
86
+ ctx = self._get_ctx()
87
+ return index in ctx
88
+
89
+ def new_unique_name(self, name, index):
90
+ if self.index_exists(index):
91
+ name = self.get_name(index)
92
+ else:
93
+ while not self.name_available(name, index):
94
+ name += "_" + str(index)
95
+
96
+ # Store name for future calls
97
+ self.set_name(name, index)
98
+
99
+ _logger.debug(f"c_name: {name} allocated to index '{index}'")
100
+
101
+ return name
102
+
103
+ def c_name(value, index, ctx, scope):
104
+ """
105
+ Filter for C names. An index should be included to avoid duplicate
106
+ names and ensure consistency across repeated calls. For example:
107
+ {{slot | c_name(loop.index0, ctx, ctx.Scope.SLOT)}}
108
+ {{signal | c_name("in_" ~ loop.index0, ctx, ctx.Scope.SIGNAL)}}
109
+
110
+ Valid scopes are defined by the enum ctx.Scope (except NONE):
111
+ - ctx.Scope.SLOT
112
+ - ctx.Scope.SIGNAL (NOTE: requires previous call with scope SLOT)
113
+ - ctx.Scope.MODULE
114
+ - ctx.Scope.ALARM
115
+ - ctx.Scope.TXPDO
116
+ - ctx.Scope.RXPDO
117
+
118
+ Note that the SIGNAL scope is shared by inputs, outputs and parameters.
119
+ """
120
+ name = value.name
121
+ if re.search("^[0-9].*", name): # Escape leading numeral
122
+ name = f"_{name}"
123
+
124
+ name = re.sub("[^0-9A-Za-z_]", "_", name) # Replace special chars
125
+ name = re.sub("_+", "_", name) # Contract repeating underscore
126
+ if name != "_":
127
+ name = re.sub("_$", "", name) # Strip trailing underscore
128
+
129
+ # Set scope
130
+ ctx.scope = scope
131
+ if scope == ctx.Scope.SLOT:
132
+ ctx.slot = index # Define signal scope
133
+
134
+ name = ctx.new_unique_name(name, index) # Ensure name is unique
135
+
136
+ return name
137
+
138
+
139
+ def c_name_upper(value, index, ctx, scope):
140
+ """Filter for uppercase C names. For example defines."""
141
+ return c_name(value, index, ctx, scope).upper()
142
+
143
+
144
+ def c_hex(value):
145
+ return value.replace('#', '0')
146
+
147
+ def c_array(signal):
148
+ if not signal.is_array:
149
+ return ""
150
+ else:
151
+ return f"[{signal.array_length}]"
152
+
153
+ def c_flags(signal):
154
+ if not signal.is_array:
155
+ return "0"
156
+ else:
157
+ return "UP_SIG_FLAG_IS_ARRAY"
158
+
159
+ def c_bool(value):
160
+ if value:
161
+ return "true"
162
+ else:
163
+ return "false"
164
+
165
+ # str.__repr__ never escapes double quotes:
166
+ # - print(("\"").__repr__()) => '"'
167
+ # - print(("\"" + '\'').__repr__()) => '\'"'
168
+ # Backslashes also need to appear escaped when output.
169
+ def c_string(s):
170
+ if s:
171
+ s = s.replace("\\", "\\\\")
172
+ s = s.replace('"', r'\"')
173
+ return s
174
+
175
+ # str.__repr__ only escapes single quotes if there is also a
176
+ # double quote in the string:
177
+ # - print(('\'').__repr__()) => "'"
178
+ # - print(("\"" + '\'').__repr__()) => '\'"'
179
+ # For C chars an extra backslash therefore needs to be added.
180
+ def c_char(s):
181
+ if s:
182
+ s = s.replace("'", r"\'")
183
+ return s
184
+
185
+ def enip_major_rev(rev_str):
186
+ return rev_str.split('.')[0]
187
+
188
+ def enip_minor_rev(rev_str):
189
+ return rev_str.split('.')[1]
190
+
191
+ class CCodeGenerator:
192
+ def __init__(self, model):
193
+ self.model = model
194
+ self.device = None
195
+ self.name_ctx = CNameContext()
196
+
197
+ self.env = Environment(
198
+ loader=PackageLoader("upgen"),
199
+ autoescape=select_autoescape(),
200
+ trim_blocks=True,
201
+ lstrip_blocks=True
202
+ )
203
+ self.env.filters['c_type'] = c_type
204
+ self.env.filters['c_name'] = c_name
205
+ self.env.filters['c_hex'] = c_hex
206
+ self.env.filters['c_name_upper'] = c_name_upper
207
+ self.env.filters['c_array'] = c_array
208
+ self.env.filters['c_flags'] = c_flags
209
+ self.env.filters['c_bool'] = c_bool
210
+ self.env.filters['c_string'] = c_string
211
+ self.env.filters['c_char'] = c_char
212
+ self.env.filters['enip_major_rev'] = enip_major_rev
213
+ self.env.filters['enip_minor_rev'] = enip_minor_rev
214
+ self.env.globals['ctx'] = self.name_ctx
215
+
216
+ def select_device(self, device_name):
217
+ if device_name == None:
218
+ self.device = self.model.devices[0]
219
+ else:
220
+ self.device = self.model.get_device(device_name)
221
+
222
+ if self.device == None:
223
+ raise Exception(f'{device_name} not found in model')
224
+
225
+ def export_file(self):
226
+ _logger.info('Generate model.h')
227
+ header = self.generate_header_file()
228
+ _logger.info('Generate model.c')
229
+ source = self.generate_source_file()
230
+
231
+ with open('model.h', 'w') as file:
232
+ file.write(header)
233
+
234
+ with open('model.c', 'w') as file:
235
+ file.write(source)
236
+
237
+ def generate_header_file(self):
238
+ template = self.env.get_template("model_template.h")
239
+ code = template.render(model=self.model, device=self.device)
240
+ _logger.debug(code)
241
+ return code
242
+
243
+ def generate_source_file(self):
244
+ template = self.env.get_template("model_template.c")
245
+ code = template.render(model=self.model, device=self.device)
246
+ _logger.debug(code)
247
+ return code