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.
- uphy_upgen-1.0.0/LICENSE.txt +5 -0
- uphy_upgen-1.0.0/PKG-INFO +54 -0
- uphy_upgen-1.0.0/README.md +19 -0
- uphy_upgen-1.0.0/pyproject.toml +47 -0
- uphy_upgen-1.0.0/upgen/__init__.py +16 -0
- uphy_upgen-1.0.0/upgen/cli.py +150 -0
- uphy_upgen-1.0.0/upgen/generators/__init__.py +11 -0
- uphy_upgen-1.0.0/upgen/generators/c_code.py +247 -0
- uphy_upgen-1.0.0/upgen/generators/ethercat.py +1091 -0
- uphy_upgen-1.0.0/upgen/generators/ethernetip.py +375 -0
- uphy_upgen-1.0.0/upgen/generators/profinet.py +380 -0
- uphy_upgen-1.0.0/upgen/generators/schema/ESI/EtherCATBase.xsd +648 -0
- uphy_upgen-1.0.0/upgen/generators/schema/ESI/EtherCATInfo.xsd +945 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/GSDML-DeviceProfile-v2.44.xsd +2535 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/GSDML-Primitives-v2.44.xsd +495 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/XAdES01903v132-201601.xsd +534 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/XAdES01903v132-201601_org.xsd +532 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xml.xsd +287 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xmldsig-core-schema.xsd +320 -0
- uphy_upgen-1.0.0/upgen/generators/schema/GSDML/xmldsig-core-schema_org.xsd +318 -0
- uphy_upgen-1.0.0/upgen/generators/sii.py +292 -0
- uphy_upgen-1.0.0/upgen/model/__init__.py +6 -0
- uphy_upgen-1.0.0/upgen/model/ethercat.py +188 -0
- uphy_upgen-1.0.0/upgen/model/ethernetip.py +38 -0
- uphy_upgen-1.0.0/upgen/model/modbus.py +23 -0
- uphy_upgen-1.0.0/upgen/model/profinet.py +135 -0
- uphy_upgen-1.0.0/upgen/model/runtime.py +41 -0
- uphy_upgen-1.0.0/upgen/model/uphy.py +315 -0
- uphy_upgen-1.0.0/upgen/templates/GSDML-V2.44-RT-Labs-U-Phy-Template.xml +105 -0
- uphy_upgen-1.0.0/upgen/templates/__init__.py +16 -0
- uphy_upgen-1.0.0/upgen/templates/model_template.c +372 -0
- uphy_upgen-1.0.0/upgen/templates/model_template.h +109 -0
- uphy_upgen-1.0.0/upgen/templates/template.eds +149 -0
|
@@ -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
|