@h2loop/autosar 0.1.2 → 1.1.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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/agents/autosar-integrator.md +34 -2
  3. package/package.json +4 -1
  4. package/plugins/index.js +53 -23
  5. package/python/ecuc_engine/__init__.py +1 -1
  6. package/python/ecuc_engine/add_signal.py +351 -0
  7. package/python/ecuc_engine/arxml_edit.py +387 -0
  8. package/python/ecuc_engine/audit.py +372 -69
  9. package/python/ecuc_engine/cli.py +119 -10
  10. package/python/ecuc_engine/codegen/__init__.py +185 -0
  11. package/python/ecuc_engine/codegen/binding.py +210 -0
  12. package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
  13. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  14. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  15. package/python/ecuc_engine/codegen/emit.py +395 -0
  16. package/python/ecuc_engine/codegen/handles.py +89 -0
  17. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  18. package/python/ecuc_engine/codegen/model.py +259 -0
  19. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  20. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  21. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  22. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  23. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  24. package/python/ecuc_engine/codegen/registry.py +133 -0
  25. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  26. package/python/ecuc_engine/context.py +481 -0
  27. package/python/ecuc_engine/csrc.py +189 -0
  28. package/python/ecuc_engine/defs.py +30 -0
  29. package/python/ecuc_engine/diff.py +13 -5
  30. package/python/ecuc_engine/find.py +162 -0
  31. package/python/ecuc_engine/gen.py +105 -0
  32. package/python/ecuc_engine/history.py +146 -0
  33. package/python/ecuc_engine/model.py +33 -29
  34. package/python/ecuc_engine/paths.py +221 -0
  35. package/python/ecuc_engine/probe.py +229 -0
  36. package/python/ecuc_engine/rte_api.py +935 -0
  37. package/python/ecuc_engine/rules/__init__.py +5 -3
  38. package/python/ecuc_engine/rules/mem.py +84 -0
  39. package/python/ecuc_engine/rules/nvm.py +131 -0
  40. package/python/ecuc_engine/rules/pdur.py +87 -0
  41. package/python/ecuc_engine/rules/system.py +162 -0
  42. package/python/ecuc_engine/sysmodel.py +152 -0
  43. package/python/ecuc_engine/validate.py +52 -3
  44. package/python/ecuc_engine/verify_rte.py +445 -0
  45. package/python/pyproject.toml +1 -1
  46. package/skills/autosar/SKILL.md +57 -1
  47. package/skills/autosar/references/codegen.md +30 -0
  48. package/skills/autosar/references/project-layout.md +11 -2
  49. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,351 @@
1
+ """autosar_add_signal: add one Com signal end to end, consistently, or refuse.
2
+
3
+ Chain (all derived by following references from a sibling signal already in the target I-PDU, so it works
4
+ for any project that follows its own conventions):
5
+ ECUC ComSignal (parameter elements templated from the sibling: DEFINITION-REF/DEST/value-element types),
6
+ ComIPduSignalRef on the I-PDU, ComHandleId = lowest unused (existing handles never move)
7
+ System SYSTEM-SIGNAL, I-SIGNAL, I-SIGNAL-TO-I-PDU-MAPPING and every element in the sibling's chain that
8
+ carries the sibling's name (I-SIGNAL-TRIGGERING, I-SIGNAL-PORT, ...), plus the reference entries that
9
+ shared elements (PDU-TRIGGERING, ECU extract FIBEX-ELEMENTS, ...) hold for the sibling
10
+ DBC an SG_ line in the frame with the I-PDU's CAN ID, if a .dbc in the project has it
11
+ Layout is checked first (inside the I-PDU, no overlap); ecuc_validate runs after the write and any new
12
+ finding rolls everything back.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import re
18
+ from typing import Optional
19
+
20
+ from . import paths
21
+ from .arxml_edit import ArxmlEditError, ArxmlEditor
22
+ from .model import Project
23
+ from .rules import RuleContext
24
+ from .rules.com import _layout, bits
25
+ from .values import parse_int
26
+ from .xmltree import escape, line_indent
27
+
28
+ TYPES = {"BOOLEAN": 1, "UINT8": 8, "SINT8": 8, "UINT16": 16, "SINT16": 16, "UINT32": 32, "SINT32": 32,
29
+ "UINT64": 64, "SINT64": 64, "FLOAT32": 32, "FLOAT64": 64}
30
+ ORDER = {"LITTLE_ENDIAN": "MOST-SIGNIFICANT-BYTE-LAST", "BIG_ENDIAN": "MOST-SIGNIFICANT-BYTE-FIRST", "OPAQUE": "OPAQUE"}
31
+
32
+
33
+ class AddSignalError(Exception):
34
+ pass
35
+
36
+
37
+ def _resolve_ipdu(project: Project, ipdu: str):
38
+ from .ecucjson import resolve
39
+ r = resolve(project, ipdu)
40
+ cands = [r] if isinstance(r, str) else r.get("didYouMean", [])
41
+ cands = [c for c in cands if project.find(c) and (project.find(c)[0].def_ref or "").endswith("/ComIPdu")]
42
+ if len(cands) != 1:
43
+ raise AddSignalError(f"I-PDU {ipdu!r} does not name exactly one ComIPdu ({cands or 'none found'})")
44
+ return project.find(cands[0])[0]
45
+
46
+
47
+ def plan_layout(ctx: RuleContext, ip, pos: Optional[int], size: int, endian: str):
48
+ pdu = ctx.target(ctx.value(ip, "ComPduIdRef"))
49
+ length = parse_int(ctx.text(pdu, "PduLength") or "") if pdu is not None else None
50
+ if length is None:
51
+ raise AddSignalError(f"{ip.name}: PduLength unknown (ComPduIdRef -> EcuC Pdu)")
52
+ used = set()
53
+ members = [ctx.target(pv) for pv in ctx.values(ip, "ComIPduSignalRef")]
54
+ for s in (m for m in members if m is not None):
55
+ for _label, bs, *_rest in _layout(ctx, s):
56
+ used |= set(bs)
57
+ limit = length * 8
58
+ if pos is None: # first free byte-aligned position
59
+ for start in range(0, limit, 8):
60
+ b = bits(start, size, endian)
61
+ if min(b) >= 0 and max(b) < limit and not used & set(b):
62
+ pos = start
63
+ break
64
+ if pos is None:
65
+ raise AddSignalError(f"{ip.name}: no free byte-aligned position for {size} bits (PduLength {length})")
66
+ b = bits(pos, size, endian)
67
+ if min(b) < 0 or max(b) >= limit:
68
+ raise AddSignalError(f"bits {min(b)}..{max(b)} lie outside {ip.name} (PduLength {length} bytes)")
69
+ if used & set(b):
70
+ raise AddSignalError(f"bits {sorted(used & set(b))[:8]} of the new signal overlap existing signals in {ip.name}")
71
+ return pos, members, pdu
72
+
73
+
74
+ def _container_text(data: bytes, node) -> bytes:
75
+ return data[node.start:node.end]
76
+
77
+
78
+ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] = None, signal_type: str = "UINT8",
79
+ bit_position: Optional[int] = None, endianness: str = "LITTLE_ENDIAN", init=0,
80
+ transfer_property: Optional[str] = None, dry_run: bool = False) -> dict:
81
+ from .edit import Editor
82
+ from .validate import validate
83
+ signal_type = signal_type.upper()
84
+ endianness = endianness.upper()
85
+ size = bit_size or TYPES.get(signal_type)
86
+ if not size:
87
+ raise AddSignalError(f"bit_size needed for type {signal_type}")
88
+ if not re.fullmatch(r"[A-Za-z_]\w*", name):
89
+ raise AddSignalError(f"{name!r} is not a valid SHORT-NAME / C identifier")
90
+ ctx = RuleContext(project)
91
+ ip = _resolve_ipdu(project, ipdu)
92
+ if project.find(f"{ip.parent.path}/{name}"):
93
+ raise AddSignalError(f"{ip.parent.path}/{name} already exists")
94
+ pos, members, pdu = plan_layout(ctx, ip, bit_position, size, endianness)
95
+ sib = next((m for m in members if m is not None and (m.def_ref or "").endswith("/ComSignal")), None)
96
+ if sib is None:
97
+ raise AddSignalError(f"{ip.name} has no ComSignal to use as template")
98
+ before = {(d["rule"], d["path"], d["message"]) for d in validate(project, check_generated=False)["diagnostics"]
99
+ if d["severity"] in ("error", "warning")}
100
+ notes: list = []
101
+
102
+ # ---------- system extract ----------
103
+ sysi = project.system
104
+ ref_pv = next((pv for g in project.find(sib.path) for pv in g.params if pv.is_ref and sysi.get(pv.value or "")), None)
105
+ sys_writes, new_mapping = {}, None
106
+ if ref_pv is not None and sysi.by_path:
107
+ ed = ArxmlEditor(project)
108
+ try:
109
+ new_mapping = _clone_system_chain(project, ed, sysi.get(ref_pv.value), sib.name, name)
110
+ m_path = new_mapping
111
+ ed.op_set({"path": m_path, "element": "START-POSITION", "value": pos})
112
+ ed.op_set({"path": m_path, "element": "PACKING-BYTE-ORDER", "value": ORDER.get(endianness, endianness)})
113
+ if transfer_property:
114
+ ed.op_set({"path": m_path, "element": "TRANSFER-PROPERTY", "value": transfer_property.upper().replace("_", "-")})
115
+ isig = _renamed(sysi.get(ref_pv.value).one("I-SIGNAL-REF"), sib.name, name)
116
+ if isig:
117
+ ed.op_set({"path": isig, "element": "LENGTH", "value": size})
118
+ ft, node = ed.tree_for(isig)
119
+ if ed.own_child(node, "VALUE") is not None:
120
+ ed.op_set({"path": isig, "element": "VALUE", "value": init})
121
+ else:
122
+ ed.op_add({"parent_path": isig, "element": "INIT-VALUE",
123
+ "values": {"NUMERICAL-VALUE-SPECIFICATION": {"VALUE": init}}})
124
+ except ArxmlEditError as e:
125
+ raise AddSignalError(f"system extract: {e}") from None
126
+ probs = ed.check()
127
+ if probs:
128
+ raise AddSignalError("system extract edit would leave: " + "; ".join(probs[:5]))
129
+ sys_writes = {f: ft.data for f, ft in ed.files.items() if ft.data != ed.original[f]}
130
+ else:
131
+ notes.append("no system-extract link on the template signal; extract not changed")
132
+
133
+ # ---------- ECUC ----------
134
+ ced = Editor(project)
135
+ data = project.files[sib.file].data
136
+ unit = ced.unit(sib.file, sib.node)
137
+ ind = line_indent(data, sib.node.start) or b""
138
+ tmpl = {pv.name: pv for g in project.find(sib.path) for pv in g.params}
139
+ used_ids = {parse_int(pv.value or "") for c in ctx.containers(ctx.rel(sib.def_ref)) for pv in ctx.values(c, "ComHandleId")}
140
+ hid = next(i for i in range(0, 1 << 16) if i not in used_ids)
141
+ want = {"ComBitPosition": pos, "ComBitSize": size, "ComSignalType": signal_type, "ComSignalEndianness": endianness,
142
+ "ComSignalInitValue": init, "ComHandleId": hid}
143
+ if transfer_property:
144
+ want["ComTransferProperty"] = transfer_property.upper()
145
+ elif "ComTransferProperty" in tmpl:
146
+ want["ComTransferProperty"] = tmpl["ComTransferProperty"].value
147
+ missing = [k for k in ("ComBitPosition", "ComBitSize", "ComHandleId") if k not in tmpl]
148
+ if missing:
149
+ raise AddSignalError(f"template signal {sib.name} lacks {missing}; add the signal with ecuc_edit instead")
150
+ skipped = [k for k in want if k not in tmpl]
151
+ if skipped:
152
+ notes.append(f"not set (template {sib.name} has no such parameter element): {', '.join(skipped)} — "
153
+ "set them with ecuc_edit if the module requires them")
154
+ nl = project.files[sib.file].newline
155
+ params, refs = [], []
156
+ for k, pv in tmpl.items():
157
+ if pv.is_ref:
158
+ if new_mapping and sysi.get(pv.value or ""):
159
+ refs.append(_retarget(data, pv, new_mapping))
160
+ continue
161
+ if k in want:
162
+ params.append(_revalue(data, pv, want[k]))
163
+ head = re.sub(r'\s+UUID="[^"]*"', "", data[sib.node.start:sib.node.stag_end].decode())
164
+ dr = sib.node.child("DEFINITION-REF")
165
+ u = unit.decode()
166
+ lines = [(0, head), (1, f"<SHORT-NAME>{escape(name)}</SHORT-NAME>"), (1, data[dr.start:dr.end].decode()),
167
+ (1, "<PARAMETER-VALUES>")]
168
+ for p in params:
169
+ lines += [(2, p[0])] + [(2, ln) for ln in p[1:]]
170
+ lines.append((1, "</PARAMETER-VALUES>"))
171
+ if refs:
172
+ lines.append((1, "<REFERENCE-VALUES>"))
173
+ for r in refs:
174
+ lines += [(2, r[0])] + [(2, ln) for ln in r[1:]]
175
+ lines.append((1, "</REFERENCE-VALUES>"))
176
+ lines.append((0, f"</{sib.node.tag}>"))
177
+ block = nl.join((ind if i else b"") + (u * d + t).encode() for i, (d, t) in enumerate(lines))
178
+ last = max((c for c in sib.parent.children if (c.def_ref or "").endswith("/ComSignal") and c.file == sib.file),
179
+ key=lambda c: c.node.end)
180
+ ced.patch(sib.file, last.node.end, last.node.end, nl + ind + block)
181
+ ip = project.find(ip.path)[0]
182
+ sref = [pv for pv in ip.params if pv.name == "ComIPduSignalRef"]
183
+ tpl = sref[-1].node
184
+ d2 = project.files[ip.file].data
185
+ rtext = d2[tpl.start:tpl.end].decode().replace(f">{sref[-1].value}<", f">{ip.parent.path}/{name}<")
186
+ rind = line_indent(d2, tpl.start) or b""
187
+ ced.patch(ip.file, tpl.end, tpl.end, project.files[ip.file].newline + rind + rtext.encode())
188
+ ecuc_writes = {f: project.files[f].data for f in ced.original if project.files[f].data != ced.original[f]}
189
+
190
+ # ---------- DBC ----------
191
+ dbc_writes = _dbc_line(project, pdu, name, pos, size, endianness, signal_type, notes)
192
+
193
+ writes = {**sys_writes, **ecuc_writes, **dbc_writes}
194
+ originals = {f: open(f, "rb").read() for f in writes}
195
+ res = {"signal": f"{ip.parent.path}/{name}", "ipdu": ip.path, "ComBitPosition": pos, "ComBitSize": size,
196
+ "ComHandleId": hid, "systemMapping": new_mapping, "files": sorted(project.rel(f) for f in writes),
197
+ "notes": notes}
198
+ if dry_run:
199
+ return {**res, "ok": True, "written": False}
200
+ for f, b in writes.items():
201
+ with open(f, "wb") as fh:
202
+ fh.write(b)
203
+ after = validate(Project(project.root), check_generated=False)
204
+ new = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
205
+ and (d["rule"], d["path"], d["message"]) not in before]
206
+ if new:
207
+ for f, b in originals.items():
208
+ with open(f, "wb") as fh:
209
+ fh.write(b)
210
+ return {**res, "ok": False, "written": False, "error": "validation found new problems; all changes rolled back",
211
+ "diagnostics": new[:20]}
212
+ return {**res, "ok": True, "written": True, "validation": after["summary"],
213
+ "next": "regenerate (autosar_generate), use the generated symbol "
214
+ f"ComConf_ComSignal_{name} in the C code, build (autosar_build)"}
215
+
216
+
217
+ def _dedent(text: str) -> list:
218
+ lines = text.split("\n")
219
+ rest = [ln for ln in lines[1:] if ln.strip()]
220
+ cut = min((len(ln) - len(ln.lstrip()) for ln in rest), default=0)
221
+ first_ind = 0
222
+ if rest:
223
+ # the first line has no leading whitespace (it starts at the element); later lines are deeper by one unit
224
+ first_ind = cut - min((len(ln) - len(ln.lstrip()) for ln in rest[-1:]), default=cut)
225
+ return [lines[0].strip()] + [ln[cut + first_ind:] if len(ln) >= cut else ln.strip() for ln in lines[1:]]
226
+
227
+
228
+ def _revalue(data: bytes, pv, value) -> list:
229
+ vn = pv.value_node
230
+ if vn is None or vn.cstart is None:
231
+ raise AddSignalError(f"template parameter {pv.name} has no VALUE")
232
+ v = "true" if value is True else "false" if value is False else str(value)
233
+ text = (data[pv.node.start:vn.cstart] + escape(v).encode() + data[vn.cend:pv.node.end]).decode()
234
+ return _dedent(text)
235
+
236
+
237
+ def _retarget(data: bytes, pv, target: str) -> list:
238
+ vn = pv.value_node
239
+ text = (data[pv.node.start:vn.cstart] + escape(target).encode() + data[vn.cend:pv.node.end]).decode()
240
+ return _dedent(text)
241
+
242
+
243
+ def _renamed(path, old: str, new: str) -> Optional[str]:
244
+ if not path or not isinstance(path, str):
245
+ return None
246
+ head, _, last = path.rpartition("/")
247
+ return f"{head}/{last.replace(old, new)}" if old in last else None
248
+
249
+
250
+ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, new: str) -> str:
251
+ """Clone the sibling's extract chain under the new name; return the new mapping path."""
252
+ sysi = project.system
253
+ if mapping is None or mapping.tag != "I-SIGNAL-TO-I-PDU-MAPPING":
254
+ raise ArxmlEditError("template signal's system reference is not an I-SIGNAL-TO-I-PDU-MAPPING")
255
+ seed = {mapping.path}
256
+ isig = sysi.get(mapping.one("I-SIGNAL-REF"))
257
+ if isig is not None:
258
+ seed.add(isig.path)
259
+ ss = sysi.get(isig.one("SYSTEM-SIGNAL-REF"))
260
+ if ss is not None:
261
+ seed.add(ss.path)
262
+ chain = set(seed)
263
+ changed = True
264
+ while changed: # referencing idents named after the sibling, and named idents they reference
265
+ changed = False
266
+ for p in list(chain):
267
+ for r in sysi.refs_to(p):
268
+ if r.path not in chain and old in r.path.rsplit("/", 1)[-1]:
269
+ chain.add(r.path)
270
+ changed = True
271
+ i = sysi.get(p)
272
+ for t, vals in (i.values.items() if i else []):
273
+ for v in vals if (t.endswith("-REF") or t.endswith("-TREF")) else []:
274
+ vi = sysi.get(v)
275
+ if vi is not None and v not in chain and old in v.rsplit("/", 1)[-1]:
276
+ chain.add(v)
277
+ changed = True
278
+ mapping_new = None
279
+ subst = {p: _renamed(p, old, new) for p in chain}
280
+ if any(v is None for v in subst.values()):
281
+ subst = {k: v for k, v in subst.items() if v}
282
+ for p in sorted(chain, key=lambda x: (x != mapping.path, x)):
283
+ ft, node = ed.tree_for(p)
284
+ text = ft.data[node.start:node.end].decode()
285
+ for a, b in subst.items():
286
+ text = text.replace(f">{a}<", f">{b}<")
287
+ text = text.replace(f"<SHORT-NAME>{p.rsplit('/', 1)[-1]}</SHORT-NAME>",
288
+ f"<SHORT-NAME>{subst[p].rsplit('/', 1)[-1]}</SHORT-NAME>", 1)
289
+ if subst[p] in ft.by_path or project.identifiables.get(subst[p]):
290
+ raise ArxmlEditError(f"{subst[p]} already exists")
291
+ ind = line_indent(ft.data, node.start) or b""
292
+ ed.added_tags[subst[p]] = node.tag
293
+ ed.patch(ft, node.end, node.end, ft.nl + ind + text.encode())
294
+ if p == mapping.path:
295
+ mapping_new = subst[p]
296
+ # reference entries held by shared elements (not renamed): clone the wrapper next to the original
297
+ for p in chain:
298
+ for r in sysi.refs_to(p):
299
+ if r.path in chain:
300
+ continue
301
+ ft, node = ed.tree_for(r.path)
302
+ for leaf in node.iter():
303
+ if leaf.text.strip() != p or not (leaf.tag.endswith("-REF") or leaf.tag.endswith("-TREF")):
304
+ continue
305
+ wrap = leaf
306
+ while wrap.parent is not None and wrap.parent is not node and len(wrap.parent.children) == 1 \
307
+ and not wrap.parent.tag.endswith("S"):
308
+ wrap = wrap.parent
309
+ text = ft.data[wrap.start:wrap.end].decode().replace(f">{p}<", f">{subst[p]}<")
310
+ ind = line_indent(ft.data, wrap.start) or b""
311
+ ed.patch(ft, wrap.end, wrap.end, ft.nl + ind + text.encode())
312
+ ft, node = ed.tree_for(r.path)
313
+ break
314
+ return mapping_new
315
+
316
+
317
+ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes) -> dict:
318
+ from .audit import can_info
319
+ infos = can_info(project, pdu.path if pdu is not None else None)
320
+ dbcs = paths.walk(project.root, lambda n: n.lower().endswith(".dbc")).files
321
+ if not dbcs or not infos:
322
+ return {}
323
+ out = {}
324
+ for info in infos:
325
+ cid = info.get("canId")
326
+ if cid is None:
327
+ continue
328
+ for f in dbcs:
329
+ text = open(f, encoding="utf-8", errors="replace").read()
330
+ m = re.search(rf"^BO_ ({cid}|{cid | 0x80000000}) (\w+)\s*:.*$", text, re.M)
331
+ if not m:
332
+ continue
333
+ block_end = m.end()
334
+ sg = list(re.finditer(r"^\s*SG_ .*$", text[m.end():].split("\n\n", 1)[0], re.M))
335
+ recv = "Vector__XXX"
336
+ if sg:
337
+ block_end = m.end() + sg[-1].end()
338
+ rm = re.search(r'"[^"]*"\s*(.*)$', sg[-1].group(0))
339
+ recv = (rm.group(1).strip() if rm else "") or recv
340
+ start = pos
341
+ if endian == "BIG_ENDIAN":
342
+ from .rules.system import _motorola_msb
343
+ start = _motorola_msb(pos, size)
344
+ sign = "-" if typ.startswith("S") else "+"
345
+ mx = (1 << size) - 1 if sign == "+" else (1 << (size - 1)) - 1
346
+ line = f'\n SG_ {name} : {start}|{size}@{0 if endian == "BIG_ENDIAN" else 1}{sign} (1,0) [0|{mx}] "" {recv}'
347
+ if re.search(rf"^\s*SG_ {re.escape(name)}\b", text, re.M):
348
+ continue
349
+ out[f] = (text[:block_end] + line + text[block_end:]).encode()
350
+ notes.append(f"added SG_ {name} to BO_ {m.group(1)} {m.group(2)} in {project.rel(f)}")
351
+ return out