@h2loop/autosar 1.2.1 → 1.4.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 +3 -1
- package/agents/autosar-integrator.md +12 -0
- package/package.json +1 -1
- package/plugins/index.js +37 -30
- package/python/ecuc_engine/arxml_edit.py +163 -7
- package/python/ecuc_engine/arxml_new.py +113 -0
- package/python/ecuc_engine/cli.py +20 -2
- package/python/ecuc_engine/codegen/binding.py +41 -1
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +118 -0
- package/python/ecuc_engine/codegen/modules/linif.py +53 -0
- package/python/ecuc_engine/codegen/registry.py +67 -37
- package/skills/autosar/SKILL.md +52 -0
- package/skills/autosar/references/codegen.md +18 -0
|
@@ -7,6 +7,10 @@ Ops (paths are AUTOSAR paths of identifiables; tags are XML element names):
|
|
|
7
7
|
{"op": "set", "path": "/Communication/Pdus/P/X", "element": "START-POSITION", "value": 40}
|
|
8
8
|
{"op": "remove", "path": "/Communication/ISignals/X"}
|
|
9
9
|
|
|
10
|
+
- A parent_path that does not exist yet is created as AR-PACKAGEs below its deepest existing package (or at the
|
|
11
|
+
top level of ``file``, or of the only non-ECUC ARXML file), so a new file from arxml_new can be filled
|
|
12
|
+
directly. Only packages are created: a missing path below a non-package (a typo in a SWC path) is an error.
|
|
13
|
+
- The ecuc_edit shape is accepted for add ({"path": parent, "container": "TAG:Name"}) and rewritten, with a note.
|
|
10
14
|
- The aggregating collection (ELEMENTS, I-SIGNAL-TO-PDU-MAPPINGS, ...) is inferred from the AUTOSAR schema
|
|
11
15
|
(autosar-data) when not given; children are written in schema order.
|
|
12
16
|
- Every *-REF / *-TREF gets its DEST from the referenced element's actual tag ("->/path"), looked up in all
|
|
@@ -95,6 +99,7 @@ class ArxmlEditor:
|
|
|
95
99
|
self.files: dict = {}
|
|
96
100
|
self.original: dict = {}
|
|
97
101
|
self.added_tags: dict = {} # path -> tag of elements added in this call
|
|
102
|
+
self.created_packages: list = []
|
|
98
103
|
|
|
99
104
|
# ---------- files / lookup ----------
|
|
100
105
|
def tree_for(self, path: str) -> tuple:
|
|
@@ -115,6 +120,53 @@ class ArxmlEditor:
|
|
|
115
120
|
raise ArxmlEditError(f"{path} not found in {self.p.rel(f)}")
|
|
116
121
|
return ft, ft.by_path[path]
|
|
117
122
|
|
|
123
|
+
def root_of(self, file: Optional[str]) -> tuple:
|
|
124
|
+
"""(FileTree, AUTOSAR root node) for a top-level package: ``file`` or the only non-ECUC ARXML."""
|
|
125
|
+
if file:
|
|
126
|
+
f = os.path.realpath(os.path.join(self.p.root, file))
|
|
127
|
+
known = [os.path.realpath(x) for x in self.p.sources.input + self.p.sources.config]
|
|
128
|
+
if f not in known:
|
|
129
|
+
raise ArxmlEditError(f"file {file!r} is not an ARXML file of this project (create it with arxml_new)")
|
|
130
|
+
ft = self.load(next(x for x in self.p.sources.input + self.p.sources.config if os.path.realpath(x) == f))
|
|
131
|
+
return ft, ft.root
|
|
132
|
+
cands = list(self.p.sources.input)
|
|
133
|
+
if len(cands) == 1:
|
|
134
|
+
ft = self.load(cands[0])
|
|
135
|
+
return ft, ft.root
|
|
136
|
+
if not cands:
|
|
137
|
+
raise ArxmlEditError("no non-ECUC ARXML file in this project to put a new top-level package in; create "
|
|
138
|
+
"one with arxml_new first")
|
|
139
|
+
raise ArxmlEditError("a new top-level package needs \"file\" (which ARXML file to put it in); one of: "
|
|
140
|
+
+ ", ".join(self.p.rel(c) for c in cands[:20]))
|
|
141
|
+
|
|
142
|
+
def parent_for(self, parent_path: str, file: Optional[str]) -> tuple:
|
|
143
|
+
"""(FileTree, node) of parent_path, creating missing AR-PACKAGEs below the deepest existing package."""
|
|
144
|
+
pp = parent_path.rstrip("/")
|
|
145
|
+
if pp == "":
|
|
146
|
+
return self.root_of(file)
|
|
147
|
+
try:
|
|
148
|
+
return self.tree_for(pp)
|
|
149
|
+
except ArxmlEditError:
|
|
150
|
+
pass
|
|
151
|
+
segs = [s for s in pp.split("/") if s]
|
|
152
|
+
anc, k = "", 0
|
|
153
|
+
for i in range(len(segs) - 1, 0, -1):
|
|
154
|
+
cand = "/" + "/".join(segs[:i])
|
|
155
|
+
try:
|
|
156
|
+
_, node = self.tree_for(cand)
|
|
157
|
+
except ArxmlEditError:
|
|
158
|
+
continue
|
|
159
|
+
if node.tag != "AR-PACKAGE":
|
|
160
|
+
raise ArxmlEditError(f"{pp} does not exist, and {cand} is a {node.tag}, not a package (only missing "
|
|
161
|
+
"packages are created; check the path)")
|
|
162
|
+
anc, k = cand, i
|
|
163
|
+
break
|
|
164
|
+
for s in segs[k:]:
|
|
165
|
+
self.op_add({"parent_path": anc or "/", "element": "AR-PACKAGE", "short_name": s, "file": file})
|
|
166
|
+
anc = f"{anc}/{s}"
|
|
167
|
+
self.created_packages.append(anc)
|
|
168
|
+
return self.tree_for(pp)
|
|
169
|
+
|
|
118
170
|
def load(self, f: str) -> FileTree:
|
|
119
171
|
if f not in self.files:
|
|
120
172
|
with open(f, "rb") as fh:
|
|
@@ -208,8 +260,9 @@ class ArxmlEditor:
|
|
|
208
260
|
def op_add(self, op: dict):
|
|
209
261
|
parent_path, tag = op.get("parent_path"), op.get("element")
|
|
210
262
|
if not parent_path or not tag:
|
|
211
|
-
raise ArxmlEditError("add needs parent_path and element"
|
|
212
|
-
|
|
263
|
+
raise ArxmlEditError("add needs parent_path (the package or element to add under) and element (the XML "
|
|
264
|
+
"tag), e.g. " + ADD_EXAMPLE)
|
|
265
|
+
ft, parent = self.parent_for(parent_path, op.get("file"))
|
|
213
266
|
version = _version(ft.data)
|
|
214
267
|
ptype = _etype(ft.chain(parent), version)
|
|
215
268
|
coll = op.get("collection")
|
|
@@ -221,7 +274,8 @@ class ArxmlEditor:
|
|
|
221
274
|
coll = c
|
|
222
275
|
break
|
|
223
276
|
if coll is None:
|
|
224
|
-
raise ArxmlEditError(f"{tag} cannot be placed under {parent.tag} {parent_path} in this schema"
|
|
277
|
+
raise ArxmlEditError(f"{tag} cannot be placed under {parent.tag} {parent_path} in this schema; "
|
|
278
|
+
f"{parent.tag} takes: {', '.join(_placeable(ptype, version))[:600]}")
|
|
225
279
|
elif coll is None:
|
|
226
280
|
coll = "ELEMENTS" if parent.tag == "AR-PACKAGE" else None
|
|
227
281
|
sn = op.get("short_name")
|
|
@@ -342,13 +396,63 @@ class ArxmlEditor:
|
|
|
342
396
|
return problems
|
|
343
397
|
|
|
344
398
|
|
|
399
|
+
ADD_EXAMPLE = ('{"op": "add", "parent_path": "/Interfaces", "element": "SENDER-RECEIVER-INTERFACE", '
|
|
400
|
+
'"short_name": "If_X", "values": {"DATA-ELEMENTS": {"VARIABLE-DATA-PROTOTYPE": {"SHORT-NAME": "Val", '
|
|
401
|
+
'"TYPE-TREF": "->/DataTypes/uint8"}}}}')
|
|
402
|
+
_TAG = re.compile(r"[A-Z][A-Z0-9-]*\Z")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _placeable(ptype, version) -> list:
|
|
406
|
+
"""Tags that can be added under an element: direct children and the children of its collections."""
|
|
407
|
+
out = []
|
|
408
|
+
for c in _order(ptype):
|
|
409
|
+
if c == "SHORT-NAME":
|
|
410
|
+
continue
|
|
411
|
+
ct = ptype.find_sub_element(c, version)
|
|
412
|
+
subs = [s for s in _order(ct)] if ct is not None else []
|
|
413
|
+
out.append(f"{c} ({'/'.join(subs[:6])}{'/...' if len(subs) > 6 else ''})" if subs else c)
|
|
414
|
+
return out
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def normalize(op: dict, notes: list, i: int) -> dict:
|
|
418
|
+
"""Accept the shapes agents mix up (ecuc_edit's path/container, name/tag) for add; note each rewrite."""
|
|
419
|
+
if op.get("op") != "add":
|
|
420
|
+
return op
|
|
421
|
+
op = dict(op)
|
|
422
|
+
if not op.get("parent_path") and op.get("path"):
|
|
423
|
+
op["parent_path"] = op.pop("path")
|
|
424
|
+
notes.append(f"op {i}: 'path' read as parent_path")
|
|
425
|
+
cont = op.get("container")
|
|
426
|
+
if not op.get("element") and isinstance(cont, str):
|
|
427
|
+
tag, _, sn = cont.partition(":")
|
|
428
|
+
if _TAG.match(tag):
|
|
429
|
+
op.pop("container")
|
|
430
|
+
op["element"] = tag
|
|
431
|
+
if sn and not op.get("short_name"):
|
|
432
|
+
op["short_name"] = sn
|
|
433
|
+
notes.append(f"op {i}: container {cont!r} read as element {tag!r}" + (f", short_name {sn!r}" if sn else ""))
|
|
434
|
+
for alias, key in (("tag", "element"), ("name", "short_name"), ("shortName", "short_name")):
|
|
435
|
+
if alias in op and not op.get(key):
|
|
436
|
+
op[key] = op.pop(alias)
|
|
437
|
+
notes.append(f"op {i}: {alias!r} read as {key!r}")
|
|
438
|
+
vals = op.get("values")
|
|
439
|
+
if isinstance(vals, dict) and "SHORT-NAME" in vals and not op.get("short_name"):
|
|
440
|
+
vals = dict(vals)
|
|
441
|
+
op["short_name"] = vals.pop("SHORT-NAME")
|
|
442
|
+
op["values"] = vals
|
|
443
|
+
notes.append(f"op {i}: values.SHORT-NAME read as short_name")
|
|
444
|
+
return op
|
|
445
|
+
|
|
446
|
+
|
|
345
447
|
def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
346
448
|
if isinstance(ops, dict):
|
|
347
449
|
ops = [ops]
|
|
348
450
|
if not isinstance(ops, list) or not ops or not all(isinstance(o, dict) for o in ops):
|
|
349
|
-
return {"ok": False, "error": "ops must be a non-empty list of op objects"}
|
|
451
|
+
return {"ok": False, "error": "ops must be a non-empty list of op objects, e.g. [" + ADD_EXAMPLE + "]"}
|
|
350
452
|
ed = ArxmlEditor(project)
|
|
351
453
|
created = []
|
|
454
|
+
notes: list = []
|
|
455
|
+
ops = [normalize(o, notes, i) for i, o in enumerate(ops)]
|
|
352
456
|
try:
|
|
353
457
|
for i, op in enumerate(ops):
|
|
354
458
|
kind = op.get("op")
|
|
@@ -364,7 +468,7 @@ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
|
364
468
|
except ArxmlEditError as e:
|
|
365
469
|
raise ArxmlEditError(f"op {i} ({kind} {op.get('path') or op.get('parent_path')}): {e}") from None
|
|
366
470
|
except ArxmlEditError as e:
|
|
367
|
-
return {"ok": False, "error": str(e), "written": False}
|
|
471
|
+
return {"ok": False, "error": str(e), "written": False, "notes": notes}
|
|
368
472
|
problems = ed.check()
|
|
369
473
|
changes = []
|
|
370
474
|
for f, ft in ed.files.items():
|
|
@@ -375,7 +479,7 @@ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
|
375
479
|
changes.append({"file": rel, "diff": "".join(difflib.unified_diff(a, b, f"a/{rel}", f"b/{rel}", n=1))})
|
|
376
480
|
if problems:
|
|
377
481
|
return {"ok": False, "error": "edit rejected; nothing written", "problems": problems, "changes": changes,
|
|
378
|
-
"written": False}
|
|
482
|
+
"written": False, "notes": notes}
|
|
379
483
|
if not dry_run:
|
|
380
484
|
for f, ft in ed.files.items():
|
|
381
485
|
if ft.data != ed.original[f]:
|
|
@@ -383,5 +487,57 @@ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
|
383
487
|
with open(tmp, "wb") as fh:
|
|
384
488
|
fh.write(ft.data)
|
|
385
489
|
os.replace(tmp, f)
|
|
386
|
-
return {"ok": True, "written": not dry_run, "created": [c for c in created if c],
|
|
490
|
+
return {"ok": True, "written": not dry_run, "created": [c for c in created if c],
|
|
491
|
+
"createdPackages": ed.created_packages, "notes": notes, "changes": changes,
|
|
387
492
|
"schemaChecked": _ad() is not None}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def arxml_check(project: Project, files: Optional[list] = None) -> dict:
|
|
496
|
+
"""Verify non-ECUC ARXML on demand: strict schema load, every *-REF resolves, DEST = the target's real tag.
|
|
497
|
+
|
|
498
|
+
The verify step of the loop for System-extract / SWC / interface files (ecuc_validate covers ECUC values).
|
|
499
|
+
``files``: project-relative paths or substrings to limit the check; default all non-ECUC files.
|
|
500
|
+
"""
|
|
501
|
+
from .edit import schema_check
|
|
502
|
+
srcs = list(project.sources.input)
|
|
503
|
+
if files:
|
|
504
|
+
want = [str(x).strip().lstrip("./") for x in files if str(x).strip()]
|
|
505
|
+
srcs = [f for f in srcs if any(w in project.rel(f) for w in want)]
|
|
506
|
+
if not srcs:
|
|
507
|
+
return {"ok": False, "error": f"no non-ECUC ARXML file matches {files}; files: "
|
|
508
|
+
+ ", ".join(project.rel(f) for f in project.sources.input[:30])}
|
|
509
|
+
trees = {}
|
|
510
|
+
for f in project.sources.input:
|
|
511
|
+
try:
|
|
512
|
+
with open(f, "rb") as fh:
|
|
513
|
+
trees[f] = FileTree(f, fh.read())
|
|
514
|
+
except Exception as e: # noqa: BLE001
|
|
515
|
+
trees[f] = e
|
|
516
|
+
known = {p: v[0] for p, v in project.identifiables.items()}
|
|
517
|
+
for ft in trees.values():
|
|
518
|
+
if isinstance(ft, FileTree):
|
|
519
|
+
for p, n in ft.by_path.items():
|
|
520
|
+
known.setdefault(p, n.tag)
|
|
521
|
+
problems, refs = [], 0
|
|
522
|
+
for f in srcs:
|
|
523
|
+
ft = trees[f]
|
|
524
|
+
rel = project.rel(f)
|
|
525
|
+
if not isinstance(ft, FileTree):
|
|
526
|
+
problems.append(f"{rel}: not well-formed XML: {ft}")
|
|
527
|
+
continue
|
|
528
|
+
err = schema_check(f, ft.data)
|
|
529
|
+
if err:
|
|
530
|
+
problems.append(f"{rel}: schema: {err}")
|
|
531
|
+
for n in ft.root.iter():
|
|
532
|
+
if not (n.tag.endswith("-REF") or n.tag.endswith("-TREF")) or n.children:
|
|
533
|
+
continue
|
|
534
|
+
refs += 1
|
|
535
|
+
tgt, dest = n.text.strip(), n.attrs.get("DEST")
|
|
536
|
+
if not dest:
|
|
537
|
+
problems.append(f"{rel}:{n.line} {n.tag} -> {tgt}: missing DEST attribute")
|
|
538
|
+
if tgt not in known:
|
|
539
|
+
problems.append(f"{rel}:{n.line} {n.tag} -> {tgt}: target does not exist in the project")
|
|
540
|
+
elif dest and known[tgt] != dest:
|
|
541
|
+
problems.append(f"{rel}:{n.line} {n.tag} DEST={dest} but {tgt} is a {known[tgt]}")
|
|
542
|
+
return {"ok": True, "clean": not problems, "files": [project.rel(f) for f in srcs], "references": refs,
|
|
543
|
+
"problems": problems, "schemaChecked": _ad() is not None}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Create a new, empty ARXML file that arxml_edit can then fill.
|
|
2
|
+
|
|
3
|
+
The .arxml guard blocks hand-written ARXML, and arxml_edit only adds under an existing package, so without
|
|
4
|
+
this an agent on a project with no ARXML yet (or one that needs a new SWC / extract file) has no way in.
|
|
5
|
+
|
|
6
|
+
arxml_new(root, "Input/SWC_DoorMonitor.arxml", ["/Swcs/DoorMonitor", "/Interfaces"])
|
|
7
|
+
|
|
8
|
+
writes the R25-11 (AUTOSAR_00054) header and the given AR-PACKAGE tree, nothing else, strictly re-loads it
|
|
9
|
+
with autosar-data, and refuses to overwrite an existing file or to write outside the project.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .edit import schema_check
|
|
18
|
+
from .xmltree import escape
|
|
19
|
+
|
|
20
|
+
NS = "http://autosar.org/schema/r4.0"
|
|
21
|
+
# The release this pack targets (R25-11 = AUTOSAR_00054); older ones only on request.
|
|
22
|
+
DEFAULT_SCHEMA = "AUTOSAR_00054"
|
|
23
|
+
_NAME = re.compile(r"[A-Za-z][A-Za-z0-9_]{0,127}\Z") # SHORT-NAME (TPS_GST: identifier, starts with a letter)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _schema(version: Optional[str]) -> str:
|
|
27
|
+
v = (version or DEFAULT_SCHEMA).strip()
|
|
28
|
+
if re.fullmatch(r"\d{5}", v):
|
|
29
|
+
v = "AUTOSAR_" + v
|
|
30
|
+
if not re.fullmatch(r"AUTOSAR_\d{5}", v):
|
|
31
|
+
raise ValueError(f"schema {version!r}: expected AUTOSAR_000NN (default {DEFAULT_SCHEMA} = R25-11)")
|
|
32
|
+
return v
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _tree(packages: list) -> dict:
|
|
36
|
+
"""{"Swcs": {"DoorMonitor": {}}, ...} from AUTOSAR package paths."""
|
|
37
|
+
tree: dict = {}
|
|
38
|
+
for p in packages:
|
|
39
|
+
segs = [s for s in str(p).strip().strip("/").split("/") if s]
|
|
40
|
+
if not segs:
|
|
41
|
+
raise ValueError(f"package path {p!r} is empty")
|
|
42
|
+
for s in segs:
|
|
43
|
+
if not _NAME.match(s):
|
|
44
|
+
raise ValueError(f"package {p!r}: {s!r} is not a valid SHORT-NAME (letters, digits, _; starts with "
|
|
45
|
+
"a letter)")
|
|
46
|
+
node = tree
|
|
47
|
+
for s in segs:
|
|
48
|
+
node = node.setdefault(s, {})
|
|
49
|
+
return tree
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render(packages: list, schema: str = DEFAULT_SCHEMA) -> str:
|
|
53
|
+
lines = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
54
|
+
f'<AUTOSAR xmlns="{NS}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
|
55
|
+
f'xsi:schemaLocation="{NS} {schema}.xsd">']
|
|
56
|
+
|
|
57
|
+
def pkgs(tree: dict, depth: int):
|
|
58
|
+
lines.append(" " * depth + "<AR-PACKAGES>")
|
|
59
|
+
for name, sub in tree.items():
|
|
60
|
+
lines.append(" " * (depth + 1) + "<AR-PACKAGE>")
|
|
61
|
+
lines.append(" " * (depth + 2) + f"<SHORT-NAME>{escape(name)}</SHORT-NAME>")
|
|
62
|
+
if sub:
|
|
63
|
+
pkgs(sub, depth + 2)
|
|
64
|
+
lines.append(" " * (depth + 1) + "</AR-PACKAGE>")
|
|
65
|
+
lines.append(" " * depth + "</AR-PACKAGES>")
|
|
66
|
+
|
|
67
|
+
pkgs(_tree(packages), 1)
|
|
68
|
+
lines.append("</AUTOSAR>")
|
|
69
|
+
return "\n".join(lines) + "\n"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def arxml_new(root: str, file: str, packages: list, schema: Optional[str] = None, dry_run: bool = False) -> dict:
|
|
73
|
+
if not file:
|
|
74
|
+
return {"ok": False, "error": "file is required (project-relative path ending in .arxml)"}
|
|
75
|
+
if not str(file).lower().endswith(".arxml"):
|
|
76
|
+
return {"ok": False, "error": f"{file}: the file name must end in .arxml"}
|
|
77
|
+
if isinstance(packages, str):
|
|
78
|
+
packages = [packages]
|
|
79
|
+
if not packages:
|
|
80
|
+
return {"ok": False, "error": "give at least one package path, e.g. [\"/Swcs\", \"/Interfaces\"] -- an ARXML "
|
|
81
|
+
"file needs an AR-PACKAGE to hold elements"}
|
|
82
|
+
root_abs = os.path.realpath(root)
|
|
83
|
+
path = os.path.realpath(os.path.join(root_abs, file))
|
|
84
|
+
if os.path.commonpath([root_abs, path]) != root_abs:
|
|
85
|
+
return {"ok": False, "error": f"{file} is outside the project directory {root}"}
|
|
86
|
+
rel = os.path.relpath(path, root_abs)
|
|
87
|
+
if os.path.exists(path):
|
|
88
|
+
return {"ok": False, "error": f"{rel} already exists; add to it with arxml_edit instead"}
|
|
89
|
+
try:
|
|
90
|
+
text = render(packages, _schema(schema))
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
return {"ok": False, "error": str(e)}
|
|
93
|
+
err = schema_check(path, text.encode("utf-8"))
|
|
94
|
+
if err: # a bug here, not the caller's: never write a file that does not load
|
|
95
|
+
return {"ok": False, "error": f"internal: generated file does not load: {err}"}
|
|
96
|
+
if not dry_run:
|
|
97
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
98
|
+
tmp = path + ".arxml-tmp"
|
|
99
|
+
with open(tmp, "w", encoding="utf-8", newline="\n") as fh:
|
|
100
|
+
fh.write(text)
|
|
101
|
+
os.replace(tmp, path)
|
|
102
|
+
pk = sorted({"/" + "/".join(s.strip("/").split("/")[:i + 1])
|
|
103
|
+
for s in packages for i in range(len([x for x in str(s).strip("/").split("/") if x]))})
|
|
104
|
+
return {"ok": True, "written": not dry_run, "file": rel, "schema": _schema(schema), "packages": pk,
|
|
105
|
+
"schemaChecked": err is None and _has_ad(), "content": text}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _has_ad() -> bool:
|
|
109
|
+
try:
|
|
110
|
+
import autosar_data # type: ignore # noqa: F401
|
|
111
|
+
return True
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
return False
|
|
@@ -71,6 +71,13 @@ def main(argv=None) -> int:
|
|
|
71
71
|
ae = sub.add_parser("arxml-edit")
|
|
72
72
|
ae.add_argument("--ops", required=True)
|
|
73
73
|
ae.add_argument("--dry-run", action="store_true")
|
|
74
|
+
an = sub.add_parser("arxml-new", help="create an empty ARXML file (header + AR-PACKAGE tree) for arxml-edit")
|
|
75
|
+
an.add_argument("--file", required=True, help="project-relative path, ending in .arxml")
|
|
76
|
+
an.add_argument("--package", action="append", default=[], help="AUTOSAR package path, e.g. /Swcs/DoorMonitor")
|
|
77
|
+
an.add_argument("--schema", help="AUTOSAR_000NN (default AUTOSAR_00054 = R25-11)")
|
|
78
|
+
an.add_argument("--dry-run", action="store_true")
|
|
79
|
+
ac = sub.add_parser("arxml-check", help="verify non-ECUC ARXML: strict schema + every reference and its DEST")
|
|
80
|
+
ac.add_argument("--file", action="append", default=[], help="limit to files whose path contains this")
|
|
74
81
|
fd = sub.add_parser("find")
|
|
75
82
|
fd.add_argument("--module")
|
|
76
83
|
fd.add_argument("--container-def", dest="container_def")
|
|
@@ -116,7 +123,7 @@ def main(argv=None) -> int:
|
|
|
116
123
|
bc.add_argument("--task", required=True, help="task text, @file, or - for stdin")
|
|
117
124
|
bc.add_argument("--budget-tokens", dest="budget_tokens", type=int, default=24000)
|
|
118
125
|
bc.add_argument("--no-history", dest="history", action="store_false")
|
|
119
|
-
for p in (r, e, v, d, au, so, vs, si, fd, cx, ae, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
|
|
126
|
+
for p in (r, e, v, d, au, so, vs, si, fd, cx, ae, an, ac, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
|
|
120
127
|
p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
|
|
121
128
|
a = ap.parse_args(argv)
|
|
122
129
|
t0 = time.time()
|
|
@@ -135,8 +142,16 @@ def run(a) -> dict:
|
|
|
135
142
|
if a.cmd in ("edit", "arxml-edit"):
|
|
136
143
|
try:
|
|
137
144
|
a.ops = _ops(a.ops)
|
|
145
|
+
except json.JSONDecodeError as ex:
|
|
146
|
+
at = f" near: ...{ex.doc[max(0, ex.pos - 80):ex.pos]} <<HERE>> {ex.doc[ex.pos:ex.pos + 40]}..." if ex.doc else ""
|
|
147
|
+
return {"error": f"ops is not valid JSON ({ex.msg} at char {ex.pos}){at}. Nothing was changed. Send ops "
|
|
148
|
+
"as a JSON array of objects (not one long string), and split a very large edit into "
|
|
149
|
+
"several calls."}
|
|
138
150
|
except (ValueError, OSError) as ex:
|
|
139
151
|
return {"error": f"ops is not valid JSON: {ex}"}
|
|
152
|
+
if a.cmd == "arxml-new": # no Project: the directory may hold no ARXML at all yet
|
|
153
|
+
from .arxml_new import arxml_new
|
|
154
|
+
return arxml_new(a.root, a.file, a.package, schema=a.schema, dry_run=a.dry_run)
|
|
140
155
|
if a.cmd == "diff":
|
|
141
156
|
from .diff import diff
|
|
142
157
|
root = os.path.abspath(a.root)
|
|
@@ -178,7 +193,7 @@ def run(a) -> dict:
|
|
|
178
193
|
from .specids import check_ids
|
|
179
194
|
return check_ids(os.path.abspath(a.root), a.ids, docs=a.docs)
|
|
180
195
|
from .model import Project
|
|
181
|
-
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit", "find", "crosscheck", "signal-owners", "arxml-edit", "add-signal"))
|
|
196
|
+
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit", "find", "crosscheck", "signal-owners", "arxml-edit", "add-signal", "arxml-check"))
|
|
182
197
|
if a.cmd == "add-signal":
|
|
183
198
|
from .add_signal import AddSignalError, add_signal
|
|
184
199
|
try:
|
|
@@ -187,6 +202,9 @@ def run(a) -> dict:
|
|
|
187
202
|
transfer_property=a.transfer_property, dry_run=a.dry_run)
|
|
188
203
|
except AddSignalError as ex:
|
|
189
204
|
return {"error": str(ex), "written": False}
|
|
205
|
+
if a.cmd == "arxml-check":
|
|
206
|
+
from .arxml_edit import arxml_check
|
|
207
|
+
return arxml_check(project, a.file or None)
|
|
190
208
|
if a.cmd == "arxml-edit":
|
|
191
209
|
from .arxml_edit import arxml_edit
|
|
192
210
|
return arxml_edit(project, a.ops, dry_run=a.dry_run)
|
|
@@ -19,6 +19,9 @@ Path language (a list of steps, evaluated left to right from the row container):
|
|
|
19
19
|
finds its neighbour's PDU handle (TPS_ECUC 3.4.1.3, via EcucPduCollection/Pdu)
|
|
20
20
|
call:<name> a module generator's resolver (Generator.path_steps()) for what the defs cannot
|
|
21
21
|
express as a path, e.g. Com's notification callouts (terminal, token)
|
|
22
|
+
child the one chosen sub-container of a choice container (any def)
|
|
23
|
+
def def short name of the current container (terminal), e.g. which choice is configured
|
|
24
|
+
count:<Def> number of direct sub-containers of def <Def> (terminal)
|
|
22
25
|
sym TPS_ECUC_02108 symbol of the current container (terminal)
|
|
23
26
|
module | name | index the current container's module name / SHORT-NAME / table index (terminal)
|
|
24
27
|
Macro values: count:<Def> | count:@<Set> | max:<Def>/<Param>, optionally followed by +N or -N.
|
|
@@ -29,7 +32,12 @@ Module binding keys besides tables/objects/macros:
|
|
|
29
32
|
vars RAM objects per row that tables point to (e.g. I-PDU buffers), sized by a path
|
|
30
33
|
require [{"rows": "<Def>", "path": <path>, "in": [...], "why": "..."}]: what the static code supports;
|
|
31
34
|
a configuration outside it is an error, never silently mis-generated
|
|
32
|
-
|
|
35
|
+
Table keys besides name/type/rows/fields: order (sort param), contiguous (that param must be exactly
|
|
36
|
+
0..n-1 after sorting, e.g. a position index), designated, per (<ParentDef>: one table per parent instance,
|
|
37
|
+
rows = its direct <Def> children, "{}" in name = parent SHORT-NAME; a parent with no rows is an error,
|
|
38
|
+
C has no empty arrays). unsupported entries may set "max": N (more than N containers is the error).
|
|
39
|
+
Field keys: path, default, enum (literal or token -> C token), scale (multiply, must give an integer:
|
|
40
|
+
seconds -> ms), fmt ("hex" | "int" | template with {} / {MIP} / {Mip}),
|
|
33
41
|
divide (a path from the row whose value divides this one, rounded: seconds -> main-function ticks),
|
|
34
42
|
agg ({"over": <multi-ref>, "params": [...], "op": "min_nonzero" | "max", "skipIf": <param>}).
|
|
35
43
|
"""
|
|
@@ -147,6 +155,16 @@ class Eval:
|
|
|
147
155
|
(not wdef or c.def_name == wdef)]
|
|
148
156
|
hits.sort(key=lambda c: (c.module.name, self.cg.by_path_index(c)))
|
|
149
157
|
cur = hits[0] if hits else None
|
|
158
|
+
elif op == "child":
|
|
159
|
+
kids = list(cur.children)
|
|
160
|
+
if len(kids) > 1:
|
|
161
|
+
raise BindingError(f"{cur.path}: path step child: {len(kids)} sub-containers, expected the one "
|
|
162
|
+
"chosen sub-container of a choice container")
|
|
163
|
+
cur = kids[0] if kids else None
|
|
164
|
+
elif op == "def":
|
|
165
|
+
return ("token", cur.def_name)
|
|
166
|
+
elif op == "count":
|
|
167
|
+
return ("token", W.c_uint(len(cur.sub(arg))))
|
|
150
168
|
elif op == "sym":
|
|
151
169
|
s = self.cg.symbol(cur)
|
|
152
170
|
if s and self.ctx is not None:
|
|
@@ -231,6 +249,12 @@ class Eval:
|
|
|
231
249
|
s = self.ctx.xsym(v, self.mc, self.used_mips)
|
|
232
250
|
return (s, "")
|
|
233
251
|
if kind == "token":
|
|
252
|
+
if "enum" in spec:
|
|
253
|
+
if v not in spec["enum"]:
|
|
254
|
+
raise BindingError(f"{row.path}: {spec['path']} = {v} is not supported by the static code "
|
|
255
|
+
f"(the {self.mc.name} binding maps only {', '.join(spec['enum'])})")
|
|
256
|
+
e = spec["enum"][v]
|
|
257
|
+
return (W.c_uint(e) if isinstance(e, int) else str(e), v)
|
|
234
258
|
if fmt and fmt not in ("hex", "int"):
|
|
235
259
|
return (fmt.replace("{MIP}", v.upper()).replace("{Mip}", v).replace("{}", v), "")
|
|
236
260
|
return (v, "")
|
|
@@ -244,6 +268,8 @@ class Eval:
|
|
|
244
268
|
return (W.c_uint(e) if isinstance(e, int) else str(e), raw.strip())
|
|
245
269
|
if "divide" in spec:
|
|
246
270
|
return (self._ticks(row, raw, spec), "")
|
|
271
|
+
if "scale" in spec:
|
|
272
|
+
return (self._scaled(row, raw, spec), "")
|
|
247
273
|
if fmt == "int": # a numeric value held in a STRING param (e.g. ComSignalInitValue)
|
|
248
274
|
n = parse_int(raw.strip())
|
|
249
275
|
if n is None:
|
|
@@ -261,6 +287,20 @@ class Eval:
|
|
|
261
287
|
raise BindingError(f"{row.path}: cannot convert {spec.get('name')} = {raw} to ticks of {spec['divide']} = {base}")
|
|
262
288
|
return W.c_uint(int(round(v / b)))
|
|
263
289
|
|
|
290
|
+
def _scaled(self, row: Container, raw, spec: dict) -> str:
|
|
291
|
+
"""value * scale as an integer (e.g. LinIfDelay seconds -> ms); a value that is not a whole multiple is
|
|
292
|
+
an error, never rounded silently."""
|
|
293
|
+
from ..values import parse_float
|
|
294
|
+
v = parse_float(str(raw).strip())
|
|
295
|
+
if v is None:
|
|
296
|
+
raise BindingError(f"{row.path}: {spec.get('name')} = {raw} is not a number")
|
|
297
|
+
x = v * spec["scale"]
|
|
298
|
+
n = int(round(x))
|
|
299
|
+
if abs(x - n) > 1e-6 * max(1.0, abs(x)):
|
|
300
|
+
raise BindingError(f"{row.path}: {spec['path']} = {str(raw).strip()} is not a whole multiple of "
|
|
301
|
+
f"1/{spec['scale']}; the static code's {spec.get('name')} field holds an integer")
|
|
302
|
+
return W.c_uint(n)
|
|
303
|
+
|
|
264
304
|
def _agg(self, row: Container, spec: dict) -> tuple:
|
|
265
305
|
"""Aggregate params over the targets of a multi-valued reference of the row (e.g. the smallest non-zero
|
|
266
306
|
timeout of an I-PDU's signals, SWS_Com_00291)."""
|
|
@@ -373,6 +373,124 @@
|
|
|
373
373
|
"init": ["Com_TxSignalInfo", "Com_RxSignalInfo", "Com_TxIPduInfo", "Com_RxIPduInfo"]
|
|
374
374
|
}
|
|
375
375
|
]
|
|
376
|
+
},
|
|
377
|
+
"LinIf": {
|
|
378
|
+
"detect": {"header": "LinIf.h", "contains": "LinIf_FrameType"},
|
|
379
|
+
"sourceIncludes": ["LinIf.h"],
|
|
380
|
+
"sets": {"TxPdus": {"rows": "LinIfTxPdu"}},
|
|
381
|
+
"handleLayout": [{"param": "LinIfTxPduId", "sets": ["TxPdus"]}],
|
|
382
|
+
"require": [
|
|
383
|
+
{
|
|
384
|
+
"rows": "LinIfFrame",
|
|
385
|
+
"path": "param:LinIfFrameType",
|
|
386
|
+
"in": ["UNCONDITIONAL"],
|
|
387
|
+
"why": "LinIf_FrameType has no frame type: LinIf.c sends every slot as an unconditional frame (no sporadic, event-triggered, diagnostic MRF/SRF or node configuration frames)"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"rows": "LinIfFrame",
|
|
391
|
+
"path": ["sub:LinIfPduDirection", "child", "def"],
|
|
392
|
+
"in": ["LinIfTxPdu", "LinIfRxPdu"],
|
|
393
|
+
"why": "LinIf_FrameType.Direction is LINIF_DIR_TX or LINIF_DIR_RX only (no slave-to-slave or internal PDUs)"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"rows": "LinIfScheduleTable",
|
|
397
|
+
"path": "param:LinIfRunMode",
|
|
398
|
+
"in": ["RUN_CONTINUOUS"],
|
|
399
|
+
"why": "LinIf_ScheduleType has no run mode: LinIf_MainFunction cycles its entries continuously"
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
"unsupported": [
|
|
403
|
+
{
|
|
404
|
+
"rows": "LinIfChannel",
|
|
405
|
+
"max": 1,
|
|
406
|
+
"why": "LinIf_ConfigType has one frame and one schedule list and no channel field"
|
|
407
|
+
},
|
|
408
|
+
{"rows": "LinIfSlave", "why": "LinIf.h is a master-node LinIf (LinIf_MainFunction runs the schedule)"},
|
|
409
|
+
{"rows": "LinIfSubstitutionFrames", "why": "LinIf_FrameType has no substitution frame list"},
|
|
410
|
+
{"rows": "LinIfFixedFrameSdu", "why": "LinIf_FrameType has no fixed response SDU"}
|
|
411
|
+
],
|
|
412
|
+
"macros": [
|
|
413
|
+
{"name": "LINIF_NUMBER_OF_CHANNELS", "value": "count:LinIfChannel"},
|
|
414
|
+
{"name": "LINIF_NUMBER_OF_TX_PDUS", "value": "count:LinIfTxPdu"},
|
|
415
|
+
{"name": "LINIF_NUMBER_OF_FRAMES", "value": "count:LinIfFrame"},
|
|
416
|
+
{"name": "LINIF_NUMBER_OF_SCHEDULES", "value": "count:LinIfScheduleTable"}
|
|
417
|
+
],
|
|
418
|
+
"tables": [
|
|
419
|
+
{
|
|
420
|
+
"name": "LinIf_Frames",
|
|
421
|
+
"type": "LinIf_FrameType",
|
|
422
|
+
"qualifier": "LINIF_CONST",
|
|
423
|
+
"static": true,
|
|
424
|
+
"rows": "LinIfFrame",
|
|
425
|
+
"size": "LINIF_NUMBER_OF_FRAMES",
|
|
426
|
+
"doc": "LinIfFrame in configuration order; LinIf_EntryType.FrameIndex indexes this table",
|
|
427
|
+
"fields": [
|
|
428
|
+
{"name": "FrameId", "path": "param:LinIfFrameId", "fmt": "hex"},
|
|
429
|
+
{
|
|
430
|
+
"name": "Length",
|
|
431
|
+
"path": [
|
|
432
|
+
["sub:LinIfPduDirection", "sub:LinIfTxPdu", "ref:LinIfTxPduRef", "param:PduLength"],
|
|
433
|
+
["sub:LinIfPduDirection", "sub:LinIfRxPdu", "ref:LinIfRxPduRef", "param:PduLength"]
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"name": "Checksum",
|
|
438
|
+
"path": "param:LinIfChecksumType",
|
|
439
|
+
"enum": {"CLASSIC": "LINIF_CHECKSUM_CLASSIC", "ENHANCED": "LINIF_CHECKSUM_ENHANCED"}
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"name": "Direction",
|
|
443
|
+
"path": ["sub:LinIfPduDirection", "child", "def"],
|
|
444
|
+
"enum": {"LinIfTxPdu": "LINIF_DIR_TX", "LinIfRxPdu": "LINIF_DIR_RX"}
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"name": "PduRPduId",
|
|
448
|
+
"path": [
|
|
449
|
+
["sub:LinIfPduDirection", "sub:LinIfTxPdu", "peer:LinIfTxPduRef@PduR/PduRDestPdu", "sym"],
|
|
450
|
+
["sub:LinIfPduDirection", "sub:LinIfRxPdu", "peer:LinIfRxPduRef@PduR/PduRSrcPdu", "sym"]
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
]
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "LinIf_Entries_{}",
|
|
457
|
+
"type": "LinIf_EntryType",
|
|
458
|
+
"qualifier": "LINIF_CONST",
|
|
459
|
+
"static": true,
|
|
460
|
+
"per": "LinIfScheduleTable",
|
|
461
|
+
"rows": "LinIfEntry",
|
|
462
|
+
"order": "LinIfEntryIndex",
|
|
463
|
+
"contiguous": true,
|
|
464
|
+
"doc": "{} slots in LinIfEntryIndex order (ECUC_LinIf_00011: position in the schedule table)",
|
|
465
|
+
"fields": [
|
|
466
|
+
{"name": "FrameIndex", "path": ["ref:LinIfFrameRef", "index"]},
|
|
467
|
+
{"name": "DelayMs", "path": "param:LinIfDelay", "scale": 1000}
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"name": "LinIf_Schedules",
|
|
472
|
+
"type": "LinIf_ScheduleType",
|
|
473
|
+
"qualifier": "LINIF_CONST",
|
|
474
|
+
"static": true,
|
|
475
|
+
"rows": "LinIfScheduleTable",
|
|
476
|
+
"size": "LINIF_NUMBER_OF_SCHEDULES",
|
|
477
|
+
"designated": true,
|
|
478
|
+
"doc": "indexed by LinIfScheduleTableIndex",
|
|
479
|
+
"comment": "name",
|
|
480
|
+
"fields": [
|
|
481
|
+
{"name": "Entries", "path": "name", "fmt": "LinIf_Entries_{}"},
|
|
482
|
+
{"name": "NumEntries", "path": "count:LinIfEntry"}
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
"objects": [
|
|
487
|
+
{
|
|
488
|
+
"name": "LinIf_Config",
|
|
489
|
+
"type": "LinIf_ConfigType",
|
|
490
|
+
"qualifier": "LINIF_CONST",
|
|
491
|
+
"init": ["LinIf_Frames", "LinIf_Schedules", "LINIF_NUMBER_OF_FRAMES", "LINIF_NUMBER_OF_SCHEDULES"]
|
|
492
|
+
}
|
|
493
|
+
]
|
|
376
494
|
}
|
|
377
495
|
}
|
|
378
496
|
}
|