@h2loop/autosar 1.0.1 → 1.2.0
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.
- package/README.md +11 -1
- package/agents/autosar-integrator.md +45 -2
- package/commands/autosar-trace.md +2 -2
- package/package.json +4 -1
- package/plugins/index.js +55 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +352 -50
- package/python/ecuc_engine/cli.py +125 -5
- package/python/ecuc_engine/codegen/__init__.py +233 -0
- package/python/ecuc_engine/codegen/binding.py +305 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +463 -0
- package/python/ecuc_engine/codegen/handles.py +176 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +264 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/com.py +145 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +208 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +164 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +4 -1
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/specids.py +163 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/values.py +26 -0
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +78 -6
- package/skills/autosar/references/codegen.md +58 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +10 -1
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""The shared BSW emitter: one ModuleOutput per module, rendered into the standard file set.
|
|
2
|
+
|
|
3
|
+
File placement follows each parameter's effective config class for the module's
|
|
4
|
+
IMPLEMENTATION-CONFIG-VARIANT (from the param defs' VALUE-CONFIG-CLASSES):
|
|
5
|
+
PRE-COMPILE / PUBLISHED-INFORMATION scalars -> #defines in <Mip>_Cfg.h
|
|
6
|
+
tables whose fields are all pre-compile -> <Mip>_Cfg.c (SWS_BSW_00103 Table 5.1)
|
|
7
|
+
tables with a link-time field -> <Mip>_Lcfg.c (SWS_BSW_00013)
|
|
8
|
+
tables with a post-build field -> <Mip>_PBcfg.c (SWS_BSW_00015)
|
|
9
|
+
Everything generic is driven by the defs: scalars of singleton containers become macros, every
|
|
10
|
+
symbolicNameValue parameter a TPS_ECUC_02108 symbol, every multiplicity-n container a const table.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .. import ENGINE_VERSION
|
|
19
|
+
from .. import defs as D
|
|
20
|
+
from . import cwriter as W
|
|
21
|
+
from .model import CLASS_FILE, CLASS_RANK, CgModel, Container, ModuleCfg, snake
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Ctx:
|
|
26
|
+
cg: CgModel
|
|
27
|
+
headers: dict # project header basename -> path (static code, excluding the output dir)
|
|
28
|
+
bindings: dict
|
|
29
|
+
binding_name: Optional[str]
|
|
30
|
+
release: tuple
|
|
31
|
+
sources: list = field(default_factory=list) # project .c/.h files (static + in-house), for SchM scans
|
|
32
|
+
generating: set = field(default_factory=set) # module names generated in this run
|
|
33
|
+
available: set = field(default_factory=set) # header basenames reachable by the build (incl. the output dir)
|
|
34
|
+
out_headers: dict = field(default_factory=dict) # headers of other generators (e.g. the RTE) in the project's output dir
|
|
35
|
+
|
|
36
|
+
def module_release(self, mip: str) -> tuple:
|
|
37
|
+
"""AUTOSAR release the module's generated configuration is built for: the published information of its
|
|
38
|
+
static code (<MIP>_AR_RELEASE_*_VERSION in <Mip>.h, SWS_BSW_00059), which SWS_BSW_00036-style checks compare
|
|
39
|
+
against; the ECUC schema release only when the project has no such header."""
|
|
40
|
+
f = self.headers.get(f"{mip}.h")
|
|
41
|
+
rel = published_release(f, mip.upper()) if f else None
|
|
42
|
+
return rel or self.release
|
|
43
|
+
|
|
44
|
+
def header_text(self, name: str) -> Optional[str]:
|
|
45
|
+
"""Text of a project header or of another generator's header in the output dir (None if absent)."""
|
|
46
|
+
f = self.headers.get(name) or self.out_headers.get(name)
|
|
47
|
+
if not f:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
with open(f, errors="replace") as fh:
|
|
51
|
+
return fh.read()
|
|
52
|
+
except OSError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def xsym(self, c: Container, here: ModuleCfg, used: set) -> str:
|
|
56
|
+
"""Symbol of a container for use in module `here`: the TPS_ECUC_02108 name when its provider header
|
|
57
|
+
(<Mip>_Cfg.h) is generated now or already exists, else the configured value (named in a comment)."""
|
|
58
|
+
sym = self.cg.symbol(c)
|
|
59
|
+
if c.module is here:
|
|
60
|
+
return sym
|
|
61
|
+
if c.module.name in self.generating or f"{c.module.mip}_Cfg.h" in self.available:
|
|
62
|
+
used.add(c.module.mip)
|
|
63
|
+
return sym
|
|
64
|
+
from ..values import parse_int
|
|
65
|
+
v = parse_int(self.cg.symbol_value(c) or "")
|
|
66
|
+
return f"{W.c_uint(v)} /* {sym} */" if v is not None else sym
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def published_release(path: str, MIP: str) -> Optional[tuple]:
|
|
70
|
+
"""(major, minor, revision) from <MIP>_AR_RELEASE_{MAJOR,MINOR,REVISION}_VERSION in a header, if defined."""
|
|
71
|
+
import re
|
|
72
|
+
try:
|
|
73
|
+
with open(path, errors="replace") as fh:
|
|
74
|
+
t = fh.read()
|
|
75
|
+
except OSError:
|
|
76
|
+
return None
|
|
77
|
+
v = []
|
|
78
|
+
for k in ("MAJOR", "MINOR", "REVISION"):
|
|
79
|
+
m = re.search(rf"#\s*define\s+{re.escape(MIP)}_AR_RELEASE_{k}_VERSION\s+\(?(\d+)", t)
|
|
80
|
+
v.append(int(m.group(1)) if m else None)
|
|
81
|
+
if v[0] is None or v[1] is None:
|
|
82
|
+
return None
|
|
83
|
+
return (v[0], v[1], v[2] or 0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ModuleOutput:
|
|
87
|
+
def __init__(self, ctx: Ctx, mc: ModuleCfg):
|
|
88
|
+
self.ctx = ctx
|
|
89
|
+
self.mc = mc
|
|
90
|
+
self.mip = mc.mip
|
|
91
|
+
self.MIP = mc.MIP
|
|
92
|
+
self.release = ctx.module_release(mc.mip)
|
|
93
|
+
self.vars: dict = {} # rank -> [(section, text)]: RAM objects (e.g. I-PDU buffers) next to the tables
|
|
94
|
+
self.errors: list = [] # configuration the static-code binding cannot represent (generation fails)
|
|
95
|
+
self.groups: list = [] # (title, [define lines])
|
|
96
|
+
self.types: list = []
|
|
97
|
+
self.hdecls: list = []
|
|
98
|
+
self.h_includes: list = []
|
|
99
|
+
self.tables: dict = {} # rank -> [text]
|
|
100
|
+
self.c_externs: dict = {} # rank -> [text]
|
|
101
|
+
self.c_includes: list = []
|
|
102
|
+
self.used_mips: set = set()
|
|
103
|
+
self.defined: set = set()
|
|
104
|
+
self.notes: list = []
|
|
105
|
+
self.extra_files: dict = {} # name -> text (module-specific additional files)
|
|
106
|
+
self.symbols: list = [] # (name, value, container path)
|
|
107
|
+
|
|
108
|
+
# ---------- collection ----------
|
|
109
|
+
def group(self, title: str, defs: list):
|
|
110
|
+
lines = []
|
|
111
|
+
for d in defs:
|
|
112
|
+
name = d[0]
|
|
113
|
+
if name in self.defined:
|
|
114
|
+
self.notes.append(f"duplicate #define {name} suppressed ({title})")
|
|
115
|
+
continue
|
|
116
|
+
self.defined.add(name)
|
|
117
|
+
lines.append(W.define(*d))
|
|
118
|
+
if lines:
|
|
119
|
+
self.groups.append((title, lines))
|
|
120
|
+
|
|
121
|
+
def table(self, rank: int, text: str, externs: Optional[list] = None):
|
|
122
|
+
rank = max(rank, 1)
|
|
123
|
+
self.tables.setdefault(rank, []).append(text)
|
|
124
|
+
for e in externs or []:
|
|
125
|
+
lst = self.c_externs.setdefault(rank, [])
|
|
126
|
+
if e not in lst:
|
|
127
|
+
lst.append(e)
|
|
128
|
+
|
|
129
|
+
def var(self, rank: int, section: str, text: str):
|
|
130
|
+
"""A RAM object owned by the configuration (e.g. a buffer the const tables point to)."""
|
|
131
|
+
rank = max(rank, 1)
|
|
132
|
+
self.vars.setdefault(rank, []).append((section, text))
|
|
133
|
+
self.tables.setdefault(rank, [])
|
|
134
|
+
|
|
135
|
+
def include(self, h: str):
|
|
136
|
+
if h not in self.c_includes:
|
|
137
|
+
self.c_includes.append(h)
|
|
138
|
+
|
|
139
|
+
# ---------- rendering ----------
|
|
140
|
+
def digest(self) -> str:
|
|
141
|
+
h = hashlib.sha256(ENGINE_VERSION.encode())
|
|
142
|
+
for f in sorted({f.file for c in self.mc.containers for f in c.frags} | {self.mc.elem.file}):
|
|
143
|
+
h.update(self.ctx.cg.p.files[f].data)
|
|
144
|
+
for f in sorted(self.ctx.cg.defs.files):
|
|
145
|
+
h.update(f.rsplit("/", 1)[-1].encode())
|
|
146
|
+
return "sha256:" + h.hexdigest()[:16]
|
|
147
|
+
|
|
148
|
+
def _banner(self, name: str) -> str:
|
|
149
|
+
return W.banner(name, self.mc.name, self.mc.variant, self.release, self.ctx.cg.rel_files(self.mc),
|
|
150
|
+
self._digest)
|
|
151
|
+
|
|
152
|
+
def render(self) -> dict:
|
|
153
|
+
self._digest = self.digest()
|
|
154
|
+
out = {}
|
|
155
|
+
hname = f"{self.mip}_Cfg.h"
|
|
156
|
+
g = W.guard(hname)
|
|
157
|
+
parts = [self._banner(hname), "", f"#ifndef {g}", f"#define {g}", ""]
|
|
158
|
+
for inc in self.h_includes:
|
|
159
|
+
parts.append(f'#include "{inc}"')
|
|
160
|
+
maj, mnr, rev = self.release
|
|
161
|
+
sw = [int(x) if x.isdigit() else 0 for x in ENGINE_VERSION.split("-")[0].split(".")][:3]
|
|
162
|
+
ver = [(f"{self.MIP}_CFG_AR_RELEASE_MAJOR_VERSION", f"{maj}u"),
|
|
163
|
+
(f"{self.MIP}_CFG_AR_RELEASE_MINOR_VERSION", f"{mnr}u"),
|
|
164
|
+
(f"{self.MIP}_CFG_AR_RELEASE_REVISION_VERSION", f"{rev}u"),
|
|
165
|
+
(f"{self.MIP}_CFG_SW_MAJOR_VERSION", f"{sw[0]}u"),
|
|
166
|
+
(f"{self.MIP}_CFG_SW_MINOR_VERSION", f"{sw[1]}u"),
|
|
167
|
+
(f"{self.MIP}_CFG_SW_PATCH_VERSION", f"{sw[2]}u")]
|
|
168
|
+
parts += ["", "/* Published information of this generated configuration (SWS_BSW_00059) */"]
|
|
169
|
+
parts += [W.define(n, v) for n, v in ver]
|
|
170
|
+
for title, lines in self.groups:
|
|
171
|
+
parts += ["", f"/* {title} */", *lines]
|
|
172
|
+
if self.types:
|
|
173
|
+
parts += ["", "/* Generated configuration types (one struct per multiplicity-n container def) */"]
|
|
174
|
+
parts += self.types
|
|
175
|
+
if self.hdecls:
|
|
176
|
+
parts += ["", *self.hdecls]
|
|
177
|
+
parts += ["", f"#endif /* {g} */", ""]
|
|
178
|
+
out[hname] = "\n".join(parts)
|
|
179
|
+
for rank in sorted(self.tables):
|
|
180
|
+
fname = f"{self.mip}_{CLASS_FILE[rank]}"
|
|
181
|
+
if fname in out: # PUBLISHED-INFORMATION and PRE-COMPILE share <Mip>_Cfg.c
|
|
182
|
+
continue
|
|
183
|
+
ranks = [r for r in self.tables if CLASS_FILE[r] == CLASS_FILE[rank]]
|
|
184
|
+
body = []
|
|
185
|
+
externs = []
|
|
186
|
+
for r in sorted(ranks):
|
|
187
|
+
body += self.tables[r]
|
|
188
|
+
externs += [e for e in self.c_externs.get(r, []) if e not in externs]
|
|
189
|
+
incs = list(self.c_includes) or [f"{self.mip}_Cfg.h"]
|
|
190
|
+
for m in sorted(self.used_mips - {self.mip}):
|
|
191
|
+
h = f"{m}_Cfg.h"
|
|
192
|
+
if h not in incs:
|
|
193
|
+
incs.append(h)
|
|
194
|
+
text = [self._banner(fname), ""]
|
|
195
|
+
text += [f'#include "{i}"' for i in incs]
|
|
196
|
+
text += ["", self._version_check(), ""]
|
|
197
|
+
if externs:
|
|
198
|
+
text += ["/* Objects owned by other modules / the application, referenced by this configuration */",
|
|
199
|
+
*externs, ""]
|
|
200
|
+
sections: dict = {}
|
|
201
|
+
for r in sorted(ranks):
|
|
202
|
+
for sec, v in self.vars.get(r, []):
|
|
203
|
+
sections.setdefault(sec, []).append(v)
|
|
204
|
+
for sec in sorted(sections):
|
|
205
|
+
text += [W.memmap(self.mip, "\n".join(sections[sec]), sec), ""]
|
|
206
|
+
text += [W.memmap(self.mip, "\n\n".join(body)), ""]
|
|
207
|
+
out[fname] = "\n".join(text)
|
|
208
|
+
out.update(self.extra_files)
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
def _version_check(self) -> str:
|
|
212
|
+
M = self.MIP
|
|
213
|
+
return "\n".join([
|
|
214
|
+
f"/* SWS_BSW_00036: the static code ({self.mip}.h) and this configuration must use the same AUTOSAR release */",
|
|
215
|
+
f"#if defined({M}_AR_RELEASE_MAJOR_VERSION) && defined({M}_AR_RELEASE_MINOR_VERSION)",
|
|
216
|
+
f"#if ({M}_AR_RELEASE_MAJOR_VERSION != {M}_CFG_AR_RELEASE_MAJOR_VERSION) || "
|
|
217
|
+
f"({M}_AR_RELEASE_MINOR_VERSION != {M}_CFG_AR_RELEASE_MINOR_VERSION)",
|
|
218
|
+
f'#error "{self.mip}: AUTOSAR release of the generated configuration does not match {self.mip}.h"',
|
|
219
|
+
"#endif",
|
|
220
|
+
"#endif",
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ======================================================================================
|
|
225
|
+
# Generic emission, driven only by the param defs
|
|
226
|
+
# ======================================================================================
|
|
227
|
+
|
|
228
|
+
def strip_prefix(mip: str, name: str) -> str:
|
|
229
|
+
return name[len(mip):] if name.lower().startswith(mip.lower()) and len(name) > len(mip) else name
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def scalar_macros(cg: CgModel, mo: ModuleOutput):
|
|
233
|
+
"""PRE-COMPILE / PUBLISHED-INFORMATION params of singleton containers -> #define <MIP>_<PARAM>."""
|
|
234
|
+
mc = mo.mc
|
|
235
|
+
names: dict = {}
|
|
236
|
+
for c in mc.containers:
|
|
237
|
+
if cg.is_multi(c) or c.defn is None:
|
|
238
|
+
continue
|
|
239
|
+
lines = []
|
|
240
|
+
for pd_path in c.defn.params:
|
|
241
|
+
d = cg.defs.get(pd_path)
|
|
242
|
+
if d is None or d.upper != 1 or cg.eff_class(mc, d) > CLASS_RANK["PRE-COMPILE"]:
|
|
243
|
+
continue
|
|
244
|
+
raw = c.text(d.name)
|
|
245
|
+
if raw is None:
|
|
246
|
+
raw = d.default
|
|
247
|
+
if raw is None:
|
|
248
|
+
continue
|
|
249
|
+
base = f"{mo.MIP}_{snake(strip_prefix(mo.mip, d.name))}"
|
|
250
|
+
if base in names and names[base] != c.path:
|
|
251
|
+
base = f"{mo.MIP}_{snake(strip_prefix(mo.mip, c.def_name))}_{snake(strip_prefix(mo.mip, d.name))}"
|
|
252
|
+
names[base] = c.path
|
|
253
|
+
lines.append((base, W.macro_value(d, raw)))
|
|
254
|
+
if d.kind == "ENUMERATION":
|
|
255
|
+
enum_literals(mo, d)
|
|
256
|
+
mo.group(f"Pre-compile parameters of {c.def_name} ({c.path})", lines)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def enum_literal_values(literals: list) -> list:
|
|
260
|
+
"""C values of enumeration literals used in #if comparisons.
|
|
261
|
+
|
|
262
|
+
When every literal is <common stem><decimal number> with distinct numbers (NVM_API_CONFIG_CLASS_1..3), the
|
|
263
|
+
number in the name is the value: the name states the ordinal (SWS_NvM_00560..00562 API configuration class
|
|
264
|
+
1/2/3), so #if (X >= X_2) keeps its meaning and no literal is 0. Otherwise the def literal order is used (the
|
|
265
|
+
ECUC def lists literals alphabetically, so that order carries no meaning beyond being stable).
|
|
266
|
+
"""
|
|
267
|
+
import re
|
|
268
|
+
m = [re.fullmatch(r"(.*?)(\d+)", x) for x in literals]
|
|
269
|
+
if all(m) and len({x.group(1) for x in m}) == 1 and len({int(x.group(2)) for x in m}) == len(literals):
|
|
270
|
+
return [int(x.group(2)) for x in m]
|
|
271
|
+
return list(range(len(literals)))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def enum_literals(mo: ModuleOutput, d: D.DefNode):
|
|
275
|
+
"""Guarded values for module-prefixed enum literals used in #if comparisons (see enum_literal_values)."""
|
|
276
|
+
lits = [x for x in d.literals if x.upper().startswith(mo.MIP + "_")]
|
|
277
|
+
if not lits or len(lits) != len(d.literals):
|
|
278
|
+
return
|
|
279
|
+
key = f"__enum__{d.path}"
|
|
280
|
+
if key in mo.defined:
|
|
281
|
+
return
|
|
282
|
+
mo.defined.add(key)
|
|
283
|
+
vals = enum_literal_values(d.literals)
|
|
284
|
+
lines = [f"#ifndef {lit}\n" + W.define(lit, W.c_uint(v)) + "\n#endif" for v, lit in zip(vals, d.literals)]
|
|
285
|
+
mo.groups.append((f"Literals of {d.name} (the static code may predefine them)", lines))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def symbolic_names(cg: CgModel, mo: ModuleOutput):
|
|
289
|
+
"""TPS_ECUC_02108 / SWS_BSW_00200 symbols for every symbolicNameValue parameter of the module."""
|
|
290
|
+
by_def: dict = {}
|
|
291
|
+
for c in mo.mc.containers:
|
|
292
|
+
d = cg.symbolic_param(c)
|
|
293
|
+
if d is None:
|
|
294
|
+
continue
|
|
295
|
+
v = c.text(d.name)
|
|
296
|
+
if v is None:
|
|
297
|
+
continue
|
|
298
|
+
by_def.setdefault(c.def_name, []).append((c, d, v))
|
|
299
|
+
for dname, items in by_def.items():
|
|
300
|
+
lines = []
|
|
301
|
+
items = sorted(items, key=lambda t: (cg.sorted_instances(mo.mc, dname).index(t[0])))
|
|
302
|
+
for c, d, v in items:
|
|
303
|
+
sym = cg.symbol(c)
|
|
304
|
+
val = W.macro_value(d, v)
|
|
305
|
+
lines.append((sym, val))
|
|
306
|
+
mo.symbols.append((sym, val, c.path))
|
|
307
|
+
mo.group(f"Symbolic names of {dname} (TPS_ECUC_02108, {items[0][1].name})", lines)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------- generic tables ----------
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class Field:
|
|
314
|
+
name: str
|
|
315
|
+
ctype: str
|
|
316
|
+
steps: list # sub-container def names to the owner of the param
|
|
317
|
+
d: Optional[D.DefNode] = None # param/ref def
|
|
318
|
+
choice: Optional[D.DefNode] = None # for the choice selector field
|
|
319
|
+
parent: bool = False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _fields(cg: CgModel, cdef: D.DefNode, prefix: str = "", steps: Optional[list] = None, depth: int = 0) -> list:
|
|
323
|
+
steps = steps or []
|
|
324
|
+
out: list = []
|
|
325
|
+
skipped: list = []
|
|
326
|
+
for p in cdef.params + cdef.refs:
|
|
327
|
+
d = cg.defs.get(p)
|
|
328
|
+
if d is None:
|
|
329
|
+
continue
|
|
330
|
+
if d.upper != 1:
|
|
331
|
+
if d.kind == "ENUMERATION" and 0 < len(d.literals) <= 32:
|
|
332
|
+
out.append(Field(prefix + d.name + "Mask", "uint32", steps, d)) # set of literals as a bitmask
|
|
333
|
+
else:
|
|
334
|
+
skipped.append(f"{d.name} (multi-valued)")
|
|
335
|
+
continue
|
|
336
|
+
if D.is_ref_kind(d.kind or ""):
|
|
337
|
+
if d.kind not in ("REFERENCE", "SYMBOLIC-NAME-REFERENCE", "CHOICE-REFERENCE"):
|
|
338
|
+
skipped.append(f"{d.name} ({d.kind.lower()})")
|
|
339
|
+
continue
|
|
340
|
+
out.append(Field(prefix + d.name, "uint16", steps, d))
|
|
341
|
+
continue
|
|
342
|
+
ct = W.param_ctype(d)
|
|
343
|
+
if ct is None:
|
|
344
|
+
skipped.append(f"{d.name} ({d.kind.lower()})")
|
|
345
|
+
continue
|
|
346
|
+
out.append(Field(prefix + d.name, ct, steps, d))
|
|
347
|
+
if depth < 4:
|
|
348
|
+
for sp in cdef.containers:
|
|
349
|
+
sd = cg.defs.get(sp)
|
|
350
|
+
if sd is None or sd.upper != 1:
|
|
351
|
+
continue # multiplicity-n sub-containers get their own table
|
|
352
|
+
if sd.tag == D.CHOICE_TAG:
|
|
353
|
+
out.append(Field(prefix + sd.name, "uint8", steps + [sd.name], choice=sd))
|
|
354
|
+
for bp in sd.containers:
|
|
355
|
+
bd = cg.defs.get(bp)
|
|
356
|
+
if bd is not None:
|
|
357
|
+
f2 = _fields(cg, bd, f"{prefix}{sd.name}_{bd.name}_", steps + [sd.name, bd.name], depth + 1)
|
|
358
|
+
out += f2
|
|
359
|
+
continue
|
|
360
|
+
out += _fields(cg, sd, f"{prefix}{sd.name}_", steps + [sd.name], depth + 1)
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _owner(c: Optional[Container], steps: list) -> Optional[Container]:
|
|
365
|
+
for s in steps:
|
|
366
|
+
if c is None:
|
|
367
|
+
return None
|
|
368
|
+
subs = c.sub(s)
|
|
369
|
+
c = subs[0] if subs else None
|
|
370
|
+
return c
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _value(cg: CgModel, mo: ModuleOutput, row: Container, f: Field) -> tuple:
|
|
374
|
+
if f.parent:
|
|
375
|
+
p = row.parent
|
|
376
|
+
while p is not None and not cg.is_multi(p):
|
|
377
|
+
p = p.parent
|
|
378
|
+
return (W.c_uint(cg.index_of(p)) if p is not None else "0u", p.name if p is not None else "")
|
|
379
|
+
owner = _owner(row, f.steps)
|
|
380
|
+
if f.choice is not None:
|
|
381
|
+
if owner is None or not owner.children:
|
|
382
|
+
return ("0xFFu", "no choice")
|
|
383
|
+
br = owner.children[0]
|
|
384
|
+
names = [x.rsplit("/", 1)[-1] for x in f.choice.containers]
|
|
385
|
+
return (W.c_uint(names.index(br.def_name)) if br.def_name in names else "0xFFu", br.def_name)
|
|
386
|
+
d = f.d
|
|
387
|
+
if D.is_ref_kind(d.kind or ""):
|
|
388
|
+
t = cg.target(owner.value(d.name)) if owner is not None else None
|
|
389
|
+
if t is None:
|
|
390
|
+
return ("0xFFFFu", "no reference")
|
|
391
|
+
sym = cg.symbol(t)
|
|
392
|
+
if sym is not None:
|
|
393
|
+
return (mo.ctx.xsym(t, mo.mc, mo.used_mips), "")
|
|
394
|
+
if cg.is_multi(t):
|
|
395
|
+
return (W.c_uint(cg.index_of(t)), t.name)
|
|
396
|
+
return ("0u", t.name)
|
|
397
|
+
if d.upper != 1: # multi-valued enumeration -> bitmask of def literal indices
|
|
398
|
+
lits = [pv.value for pv in owner.values(d.name)] if owner is not None else []
|
|
399
|
+
mask = 0
|
|
400
|
+
for x in lits:
|
|
401
|
+
if x in d.literals:
|
|
402
|
+
mask |= 1 << d.literals.index(x)
|
|
403
|
+
return (W.c_uint(mask, hexa=True), "|".join(lits))
|
|
404
|
+
raw = owner.text(d.name) if owner is not None else None
|
|
405
|
+
return W.table_value(d, raw)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def generic_tables(cg: CgModel, mo: ModuleOutput, skip: set):
|
|
409
|
+
"""One const struct array per multiplicity-n container def (plus singleton structs for non-PC scalars)."""
|
|
410
|
+
mc = mo.mc
|
|
411
|
+
seen: list = []
|
|
412
|
+
for c in mc.containers:
|
|
413
|
+
if c.def_name in skip or c.defn is None or c.def_name in seen:
|
|
414
|
+
continue
|
|
415
|
+
if c.defn.upper == 1 and not _has_dynamic(cg, mc, c):
|
|
416
|
+
continue
|
|
417
|
+
if c.defn.upper == 1 and cg.is_multi(c):
|
|
418
|
+
continue # flattened into its multi ancestor's row
|
|
419
|
+
seen.append(c.def_name)
|
|
420
|
+
for dname in seen:
|
|
421
|
+
rows = cg.sorted_instances(mc, dname)
|
|
422
|
+
cdef = rows[0].defn
|
|
423
|
+
fields = _fields(cg, cdef)
|
|
424
|
+
if rows[0].parent is not None and cg.is_multi(rows[0].parent):
|
|
425
|
+
fields = [Field("Parent", "uint16", [], parent=True)] + fields
|
|
426
|
+
if not fields:
|
|
427
|
+
continue
|
|
428
|
+
tname = f"{mo.mip}_Cfg{dname}Type"
|
|
429
|
+
oname = f"{mo.mip}_Cfg{dname}"
|
|
430
|
+
rank = max([cg.eff_class(mc, f.d) for f in fields if f.d is not None] + [cg.eff_class(mc, cdef), 1])
|
|
431
|
+
mo.types.append("typedef struct {\n" + "\n".join(f" {f.ctype} {f.name};" for f in fields) +
|
|
432
|
+
f"\n}} {tname};")
|
|
433
|
+
lines = []
|
|
434
|
+
for r in rows:
|
|
435
|
+
vals = []
|
|
436
|
+
notes = []
|
|
437
|
+
for f in fields:
|
|
438
|
+
v, n = _value(cg, mo, r, f)
|
|
439
|
+
vals.append(v)
|
|
440
|
+
sym = cg.symbol(r)
|
|
441
|
+
lines.append(W.initializer(vals, sym or r.name))
|
|
442
|
+
multi = cdef.upper != 1 or len(rows) > 1
|
|
443
|
+
cnt = f"{mo.MIP}_CFG_NUM_{snake(strip_prefix(mo.mip, dname))}"
|
|
444
|
+
if multi:
|
|
445
|
+
mo.group(f"Size of {oname}[]", [(cnt, W.c_uint(len(rows)))])
|
|
446
|
+
decl = f"CONST({tname}, {mo.MIP}_CONST) {oname}[{cnt}]"
|
|
447
|
+
body = f"{decl} = {{\n" + ",\n".join(lines) + "\n};"
|
|
448
|
+
else:
|
|
449
|
+
decl = f"CONST({tname}, {mo.MIP}_CONST) {oname}"
|
|
450
|
+
body = f"{decl} =\n" + lines[0].strip() + ";"
|
|
451
|
+
mo.hdecls.append(f"extern {decl};")
|
|
452
|
+
names = ", ".join(f.name for f in fields)
|
|
453
|
+
mo.table(rank, f"/* {dname}: {len(rows)} row(s), fields {names} */\n{body}")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _has_dynamic(cg: CgModel, mc: ModuleCfg, c: Container) -> bool:
|
|
457
|
+
"""A singleton container holding link-time/post-build scalars needs a const object, not macros."""
|
|
458
|
+
for p in c.defn.params:
|
|
459
|
+
d = cg.defs.get(p)
|
|
460
|
+
if d is not None and d.upper == 1 and W.param_ctype(d) and cg.eff_class(mc, d) > CLASS_RANK["PRE-COMPILE"]:
|
|
461
|
+
if c.text(d.name) is not None:
|
|
462
|
+
return True
|
|
463
|
+
return False
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Deterministic handle-ID assignment for symbolicNameValue parameters, written back in place.
|
|
2
|
+
|
|
3
|
+
TPS_ECUC_02107: values of symbolicNameValue parameters that the configuration editor or module
|
|
4
|
+
generator assigns are stored in the ECUC XML. TPS_ECUC_02108 turns them into the
|
|
5
|
+
`<Mip>Conf_<ContainerDef>_<ContainerValue>` #defines.
|
|
6
|
+
|
|
7
|
+
Algorithm (stable by construction):
|
|
8
|
+
* values already in the ECUC are kept as they are (so regeneration never renumbers a handle that
|
|
9
|
+
code or another ECU already uses), duplicates among them are reported, not "fixed";
|
|
10
|
+
* containers without a value get the lowest free integers >= max(def MIN, 0), in document order;
|
|
11
|
+
* the scope is one container def within one module (the symbol namespace of TPS_ECUC_02108).
|
|
12
|
+
Only INTEGER parameters are assigned. A missing STRING symbolic value is reported.
|
|
13
|
+
|
|
14
|
+
Static-code layout (layout()): a static code that indexes its tables by handle (e.g. a Com whose
|
|
15
|
+
signal table holds the Tx signals at 0..nTx-1 and the Rx signals after them) declares that order in
|
|
16
|
+
its binding ("handleLayout"). When the configured values break it (a Tx signal added after the Rx
|
|
17
|
+
ones), the generator renumbers that handle param, keeping the relative order inside each row set,
|
|
18
|
+
but only for withAuto params (the def says the configuration tool computes the value); otherwise the
|
|
19
|
+
mismatch is an error for the integrator.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from ..values import parse_int
|
|
26
|
+
from .model import CgModel, ModuleCfg
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def plan(cg: CgModel, modules: list) -> dict:
|
|
30
|
+
"""-> {"assign": [{path, param, value, def}], "errors": [...], "notes": [...]}"""
|
|
31
|
+
out: dict = {"assign": [], "errors": [], "notes": []}
|
|
32
|
+
for name in modules:
|
|
33
|
+
mc = cg.module(name)
|
|
34
|
+
if mc is None:
|
|
35
|
+
continue
|
|
36
|
+
_plan_module(cg, mc, out)
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _plan_module(cg: CgModel, mc: ModuleCfg, out: dict):
|
|
41
|
+
seen_defs: list = []
|
|
42
|
+
for c in mc.containers:
|
|
43
|
+
if c.def_name not in seen_defs:
|
|
44
|
+
seen_defs.append(c.def_name)
|
|
45
|
+
for dname in seen_defs:
|
|
46
|
+
insts = cg.instances(mc, dname)
|
|
47
|
+
d = cg.symbolic_param(insts[0])
|
|
48
|
+
if d is None:
|
|
49
|
+
continue
|
|
50
|
+
if d.kind != "INTEGER":
|
|
51
|
+
for c in insts:
|
|
52
|
+
if c.text(d.name) in (None, ""):
|
|
53
|
+
out["notes"].append(f"{c.path}: {d.name} (symbolic {d.kind}) has no value and cannot be "
|
|
54
|
+
"auto-assigned; its symbol is not generated")
|
|
55
|
+
continue
|
|
56
|
+
lo = max(parse_int(d.min) if d.min else 0, 0)
|
|
57
|
+
hi: Optional[int] = parse_int(d.max) if d.max else None
|
|
58
|
+
used: dict = {}
|
|
59
|
+
missing = []
|
|
60
|
+
for c in insts:
|
|
61
|
+
v = parse_int(c.text(d.name) or "")
|
|
62
|
+
if v is None:
|
|
63
|
+
missing.append(c)
|
|
64
|
+
continue
|
|
65
|
+
if v in used:
|
|
66
|
+
out["errors"].append(f"{d.name}={v} is used by both {used[v]} and {c.path} "
|
|
67
|
+
f"(handle IDs must be unique per module, TPS_ECUC_02108)")
|
|
68
|
+
used.setdefault(v, c.path)
|
|
69
|
+
nxt = lo
|
|
70
|
+
for c in missing:
|
|
71
|
+
while nxt in used:
|
|
72
|
+
nxt += 1
|
|
73
|
+
if hi is not None and nxt > hi:
|
|
74
|
+
out["errors"].append(f"{c.path}: no free {d.name} left in [{lo}..{hi}]")
|
|
75
|
+
break
|
|
76
|
+
used[nxt] = c.path
|
|
77
|
+
out["assign"].append({"path": c.path, "param": d.name, "value": nxt, "def": d.path})
|
|
78
|
+
if not d.with_auto:
|
|
79
|
+
out["notes"].append(f"{c.path}: assigned {d.name}={nxt}; the def has no withAuto, so check that "
|
|
80
|
+
"the number carries no hardware meaning")
|
|
81
|
+
nxt += 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def layout(cg: CgModel, modules: list, selected: dict) -> dict:
|
|
85
|
+
"""-> {"assign": [...], "errors": [...], "notes": [...]} renumbering handles to each binding's handleLayout."""
|
|
86
|
+
from .binding import Eval
|
|
87
|
+
out: dict = {"assign": [], "errors": [], "notes": []}
|
|
88
|
+
for name in modules:
|
|
89
|
+
mb, mc = selected.get(name), cg.module(name)
|
|
90
|
+
if not mb or mc is None:
|
|
91
|
+
continue
|
|
92
|
+
ev = Eval(cg, mc)
|
|
93
|
+
for hl in mb.get("handleLayout", []):
|
|
94
|
+
rows: list = []
|
|
95
|
+
for st in hl["sets"]:
|
|
96
|
+
rows += [r for r in ev.rows("@" + st, mb.get("sets", {})) if r not in rows]
|
|
97
|
+
insts = cg.instances(mc, mb["sets"][hl["sets"][0]]["rows"])
|
|
98
|
+
rows += [r for r in cg.sorted_instances(mc, insts[0].def_name) if r not in rows] if insts else []
|
|
99
|
+
if not rows:
|
|
100
|
+
continue
|
|
101
|
+
d = cg.symbolic_param(rows[0])
|
|
102
|
+
changes = [(r, i) for i, r in enumerate(rows) if parse_int(r.text(d.name) or "") != i]
|
|
103
|
+
if not changes:
|
|
104
|
+
continue
|
|
105
|
+
what = f"{d.name}: the {mb.get('_binding')} static code indexes its tables by handle in the order " \
|
|
106
|
+
f"{' then '.join(hl['sets'])}"
|
|
107
|
+
if not d.with_auto:
|
|
108
|
+
out["errors"].append(f"{what}; {len(changes)} value(s) break that order and the def has no withAuto, "
|
|
109
|
+
f"so they are not renumbered (first: {changes[0][0].path}={changes[0][0].text(d.name)})")
|
|
110
|
+
continue
|
|
111
|
+
for r, i in changes:
|
|
112
|
+
out["assign"].append({"path": r.path, "param": d.name, "value": i, "def": d.path})
|
|
113
|
+
out["notes"].append(f"{what}; renumbered {len(changes)} {d.name} value(s) (withAuto, TPS_ECUC_02107), "
|
|
114
|
+
f"e.g. {changes[0][0].name}: {changes[0][0].text(d.name)} -> {changes[0][1]}")
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def apply(project, assignments: list, write: bool) -> dict:
|
|
119
|
+
"""Write the assigned values into the ECUC ARXML in place (TPS_ECUC_02107).
|
|
120
|
+
|
|
121
|
+
Values that replace an existing VALUE are patched in one pass per file (only the VALUE text bytes change,
|
|
122
|
+
so a renumbering of thousands of handles stays linear); new values go through the engine's editor, which
|
|
123
|
+
places the element per the schema. write=False patches only the in-memory model (dry run), so the
|
|
124
|
+
generated code still uses them.
|
|
125
|
+
"""
|
|
126
|
+
if not assignments:
|
|
127
|
+
return {"written": False, "changes": []}
|
|
128
|
+
import difflib
|
|
129
|
+
import os
|
|
130
|
+
from ..xmltree import escape
|
|
131
|
+
by_path = {}
|
|
132
|
+
for e in _elems(project):
|
|
133
|
+
by_path.setdefault(e.path, []).append(e)
|
|
134
|
+
patches: dict = {}
|
|
135
|
+
rest = []
|
|
136
|
+
for a in assignments:
|
|
137
|
+
hits = [(e, pv) for e in by_path.get(a["path"], []) for pv in e.params if pv.name == a["param"]]
|
|
138
|
+
vn = hits[0][1].value_node if len(hits) == 1 else None
|
|
139
|
+
if vn is None or vn.cstart is None or vn.tag != "VALUE":
|
|
140
|
+
rest.append(a)
|
|
141
|
+
continue
|
|
142
|
+
patches.setdefault(hits[0][0].file, []).append((vn.cstart, vn.cend, escape(str(a["value"])).encode()))
|
|
143
|
+
changes = []
|
|
144
|
+
for f in sorted(patches):
|
|
145
|
+
before = project.files[f].data
|
|
146
|
+
after = bytearray(before)
|
|
147
|
+
for start, end, repl in sorted(patches[f], reverse=True):
|
|
148
|
+
after[start:end] = repl
|
|
149
|
+
after = bytes(after)
|
|
150
|
+
project.reload(f, after)
|
|
151
|
+
rel = project.rel(f)
|
|
152
|
+
a_, b_ = before.decode("utf-8", "replace").splitlines(True), after.decode("utf-8", "replace").splitlines(True)
|
|
153
|
+
changes.append({"file": rel, "diff": "".join(difflib.unified_diff(a_, b_, f"a/{rel}", f"b/{rel}", n=1))})
|
|
154
|
+
if write:
|
|
155
|
+
tmp = f + ".ecuc-tmp"
|
|
156
|
+
with open(tmp, "wb") as fh:
|
|
157
|
+
fh.write(after)
|
|
158
|
+
os.replace(tmp, f)
|
|
159
|
+
written = bool(patches) and write
|
|
160
|
+
if rest:
|
|
161
|
+
from ..edit import edit
|
|
162
|
+
ops = [{"op": "set", "path": a["path"], "param": a["param"], "value": a["value"]} for a in rest]
|
|
163
|
+
res = edit(project, ops, dry_run=not write, run_validate=False)
|
|
164
|
+
if not res.get("ok"):
|
|
165
|
+
raise RuntimeError(f"handle-ID write-back failed: {res.get('error')}")
|
|
166
|
+
written = written or bool(res.get("written"))
|
|
167
|
+
changes += res.get("changes", [])
|
|
168
|
+
return {"written": written, "changes": changes}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _elems(project):
|
|
172
|
+
stack = list(project.modules)
|
|
173
|
+
while stack:
|
|
174
|
+
e = stack.pop()
|
|
175
|
+
yield e
|
|
176
|
+
stack.extend(e.children)
|