@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.
@@ -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
- ft, parent = self.tree_for(parent_path.rstrip("/"))
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], "changes": changes,
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
- Field keys: path, default, enum (literal -> C token), fmt ("hex" | "int" | template with {} / {MIP} / {Mip}),
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
  }