@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.
- package/README.md +4 -0
- package/agents/autosar-integrator.md +34 -2
- package/package.json +4 -1
- package/plugins/index.js +53 -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 +372 -69
- package/python/ecuc_engine/cli.py +119 -10
- package/python/ecuc_engine/codegen/__init__.py +185 -0
- package/python/ecuc_engine/codegen/binding.py +210 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -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 +395 -0
- package/python/ecuc_engine/codegen/handles.py +89 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +259 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -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 +133 -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/diff.py +13 -5
- package/python/ecuc_engine/find.py +162 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +33 -29
- package/python/ecuc_engine/paths.py +221 -0
- 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/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +52 -3
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +57 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +11 -2
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -74,9 +74,11 @@ class RuleContext:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def run_rules(project: Project) -> Iterable[dict]:
|
|
77
|
-
from . import com, handles # noqa: F401 (registers the packs)
|
|
77
|
+
from . import com, handles, mem, nvm, pdur, system # noqa: F401 (registers the packs)
|
|
78
78
|
ctx = RuleContext(project)
|
|
79
79
|
for r in RULES:
|
|
80
|
-
for
|
|
80
|
+
for item in r.fn(ctx):
|
|
81
|
+
e, line, msg, def_ref = item[:4]
|
|
82
|
+
sev = item[4] if len(item) > 4 else r.severity # a rule may downgrade one finding (e.g. NOT CHECKED -> info)
|
|
81
83
|
yield {"file": project.rel(e.file), "line": line, "path": e.path, "rule": r.id,
|
|
82
|
-
"severity":
|
|
84
|
+
"severity": sev, "message": f"{msg} [{r.cite}]", "defRef": def_ref}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""H2L-MEM-001: do the Fee/Ea blocks fit the memory they are configured to use?
|
|
2
|
+
|
|
3
|
+
Followed generically: each Fee/Ea block -> its address-area reference (MemAcc address area) -> sub address
|
|
4
|
+
areas -> sector batch (number of sectors x erase sector size). Per-block cost = blockSize + block overhead,
|
|
5
|
+
rounded up to the virtual page size (FeeBlockOverhead/FeeVirtualPageSize from the Fee config when present),
|
|
6
|
+
times the number of NV instances NvM keeps (NvMNvBlockNum of the referencing NvM block). Fee/Ea need spare
|
|
7
|
+
space for sector switching / garbage collection, so the check compares against half the area (warning).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from . import rule
|
|
12
|
+
from ..values import parse_int
|
|
13
|
+
|
|
14
|
+
CITE = ("ECUC_Fee_00148 FeeBlockSize, FeeBlockOverhead/FeeVirtualPageSize; ECUC_NvM_00479 NvMNvBlockNum; MemAcc "
|
|
15
|
+
"address area / Mem sector batch sizes (SWS_MemAcc, SWS_Mem)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _up(n: int, page: int) -> int:
|
|
19
|
+
return n if page <= 1 else ((n + page - 1) // page) * page
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _first(ctx, rel):
|
|
23
|
+
c = ctx.containers(rel)
|
|
24
|
+
return c[0] if c else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@rule("H2L-MEM-001", "warning", CITE, "Fee/Ea blocks fit their configured memory area")
|
|
28
|
+
def capacity(ctx):
|
|
29
|
+
for mod, blkdef, size_p in (("Fee", "Fee/FeeBlockConfiguration", "FeeBlockSize"),
|
|
30
|
+
("Ea", "Ea/EaBlockConfiguration", "EaBlockSize")):
|
|
31
|
+
blocks = ctx.containers(blkdef)
|
|
32
|
+
if not blocks:
|
|
33
|
+
continue
|
|
34
|
+
gen = _first(ctx, f"{mod}/{mod}General")
|
|
35
|
+
pub = _first(ctx, f"{mod}/{mod}PublishedInformation")
|
|
36
|
+
page = parse_int(ctx.text(gen, f"{mod}VirtualPageSize") or "") if gen is not None else None
|
|
37
|
+
over = parse_int(ctx.text(pub, f"{mod}BlockOverhead") or "") if pub is not None else None
|
|
38
|
+
instances: dict = {}
|
|
39
|
+
for nb in ctx.containers("NvM/NvMBlockDescriptor"):
|
|
40
|
+
stack = [nb]
|
|
41
|
+
n = parse_int(ctx.text(nb, "NvMNvBlockNum") or "") or 1
|
|
42
|
+
while stack:
|
|
43
|
+
e = stack.pop()
|
|
44
|
+
for g in ctx.p.find(e.path):
|
|
45
|
+
stack.extend(g.children)
|
|
46
|
+
for pv in g.params:
|
|
47
|
+
if pv.is_ref and pv.value:
|
|
48
|
+
instances[pv.value.strip()] = max(instances.get(pv.value.strip(), 1), n)
|
|
49
|
+
per_area: dict = {}
|
|
50
|
+
for b in blocks:
|
|
51
|
+
size = parse_int(ctx.text(b, size_p) or "")
|
|
52
|
+
if size is None:
|
|
53
|
+
continue
|
|
54
|
+
area = None
|
|
55
|
+
for g in ctx.p.find(b.path):
|
|
56
|
+
for pv in g.params:
|
|
57
|
+
t = ctx.target(pv) if pv.is_ref else None
|
|
58
|
+
if t is not None and "AddressArea" in (t.def_ref or ""):
|
|
59
|
+
area = t
|
|
60
|
+
cost = _up(size + (over or 0), page or 1) * instances.get(b.path, 1)
|
|
61
|
+
key = area.path if area is not None else None
|
|
62
|
+
per_area.setdefault(key, [0, 0, area])
|
|
63
|
+
per_area[key][0] += cost
|
|
64
|
+
per_area[key][1] += 1
|
|
65
|
+
for key, (need, count, area) in per_area.items():
|
|
66
|
+
if area is None:
|
|
67
|
+
continue
|
|
68
|
+
cap, parts = 0, []
|
|
69
|
+
for sub in ctx.p.find(area.path)[0].children:
|
|
70
|
+
ns = parse_int(ctx.text(sub, f"MemAccNumberOfSectors") or "")
|
|
71
|
+
batch = ctx.target(ctx.value(sub, "MemAccSectorBatchRef"))
|
|
72
|
+
ss = parse_int(ctx.text(batch, "MemEraseSectorSize") or "") if batch is not None else None
|
|
73
|
+
if ns and ss:
|
|
74
|
+
cap += ns * ss
|
|
75
|
+
parts.append(f"{sub.name}: {ns} x {ss} B")
|
|
76
|
+
if not cap:
|
|
77
|
+
continue
|
|
78
|
+
usable = cap // 2
|
|
79
|
+
if need > usable:
|
|
80
|
+
anchor = ctx.value(blocks[0], size_p)
|
|
81
|
+
yield blocks[0], anchor.node.line if anchor else blocks[0].line, (
|
|
82
|
+
f"{count} {mod} block(s) on {area.name} need {need} B (size + overhead {over or 0} B, rounded to "
|
|
83
|
+
f"{page or 1} B pages, x NV instances) but the area holds {cap} B ({', '.join(parts)}); with space "
|
|
84
|
+
f"for sector switching only ~{usable} B are usable"), anchor.def_ref if anchor else None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""NvM block length vs the C object behind its permanent RAM block, and the Fee/Ea block that stores it.
|
|
2
|
+
|
|
3
|
+
NvMRamBlockDataAddress names a C object; its size comes from a compile-only sizeof probe built with the
|
|
4
|
+
project's own compiler/flags (ecuc_engine/probe.py). NvMNvBlockLength is the NV *data* length
|
|
5
|
+
(ECUC_NvM_00479: "NV block data length in bytes"; CRC is managed by NvM on top). The Fee/Ea block is found by
|
|
6
|
+
following the block's references to a FeeBlockConfiguration / EaBlockConfiguration.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from . import rule
|
|
14
|
+
from .. import paths, probe
|
|
15
|
+
from ..values import parse_int
|
|
16
|
+
|
|
17
|
+
CITE = ("ECUC_NvM_00479 NvMNvBlockLength (NV block data length), ECUC_NvM_00482 NvMRamBlockDataAddress, "
|
|
18
|
+
"ECUC_Fee_00148 FeeBlockSize / ECUC_Ea_00128 EaBlockSize")
|
|
19
|
+
SYMBOL = re.compile(r"^[&(\s]*(?:\(\s*\w+\s*\*?\s*\)\s*)?&?\s*([A-Za-z_]\w*)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _storage_blocks(ctx, blk):
|
|
23
|
+
"""Fee/Ea block configurations referenced (at any depth) from an NvM block descriptor."""
|
|
24
|
+
out = []
|
|
25
|
+
stack = [blk]
|
|
26
|
+
while stack:
|
|
27
|
+
e = stack.pop()
|
|
28
|
+
for g in ctx.p.find(e.path):
|
|
29
|
+
stack.extend(g.children)
|
|
30
|
+
for pv in g.params:
|
|
31
|
+
if pv.is_ref and pv.value:
|
|
32
|
+
t = ctx.target(pv)
|
|
33
|
+
if t is not None and (t.def_ref or "").endswith(("/FeeBlockConfiguration", "/EaBlockConfiguration")):
|
|
34
|
+
out.append(t)
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def blocks_with_ram(ctx):
|
|
39
|
+
for blk in ctx.containers("NvM/NvMBlockDescriptor"):
|
|
40
|
+
addr = (ctx.text(blk, "NvMRamBlockDataAddress") or "").strip()
|
|
41
|
+
m = SYMBOL.match(addr)
|
|
42
|
+
if m:
|
|
43
|
+
yield blk, addr, m.group(1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def probe_sizes(ctx):
|
|
47
|
+
"""{block path: (size|None, Decl|None)} and notes; cached on the context."""
|
|
48
|
+
if getattr(ctx, "_nvm_sizes", None) is not None:
|
|
49
|
+
return ctx._nvm_sizes
|
|
50
|
+
root = ctx.p.root
|
|
51
|
+
tree = paths.walk(root, paths.is_c).files
|
|
52
|
+
from ..audit import is_generated
|
|
53
|
+
own = [f for f in tree if not is_generated(f)]
|
|
54
|
+
headers = [f for f in own if f.lower().endswith(".h")] + [f for f in tree if f.lower().endswith(".h") and f not in own]
|
|
55
|
+
cfiles = [f for f in own if f.lower().endswith(".c")]
|
|
56
|
+
decls, where = {}, {}
|
|
57
|
+
for blk, _addr, sym in blocks_with_ram(ctx):
|
|
58
|
+
d = probe.find_decl(sym, cfiles, headers)
|
|
59
|
+
where[blk.path] = d
|
|
60
|
+
if d is not None:
|
|
61
|
+
if not d.header:
|
|
62
|
+
d.header = probe.type_header(d.type, headers)
|
|
63
|
+
if d.header is None and d.type not in ("uint8", "uint16", "uint32", "sint8", "sint16", "sint32", "boolean",
|
|
64
|
+
"char", "int", "short", "long", "float", "double"):
|
|
65
|
+
continue
|
|
66
|
+
decls[blk.path] = d
|
|
67
|
+
ctx._nvm_probed = set(decls)
|
|
68
|
+
found, notes, tc = probe.sizes(root, probe.load_cfg(root), decls) if decls else ({}, [], None)
|
|
69
|
+
ctx._nvm_sizes = ({k: (found.get(k), where.get(k)) for k in where}, notes, tc)
|
|
70
|
+
return ctx._nvm_sizes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@rule("H2L-NVM-001", "error", CITE, "NvM block length / Fee-Ea block size match the RAM block's C object size")
|
|
74
|
+
def nvm_length_vs_c(ctx):
|
|
75
|
+
"""Every NvM block descriptor gets an outcome: a size verdict (error on mismatch), or a NOT CHECKED info that
|
|
76
|
+
says why. A summary info is always emitted, so the rule never yields nothing when NvM is configured."""
|
|
77
|
+
blocks = ctx.containers("NvM/NvMBlockDescriptor")
|
|
78
|
+
if not blocks:
|
|
79
|
+
return
|
|
80
|
+
first = blocks[0]
|
|
81
|
+
with_ram = list(blocks_with_ram(ctx))
|
|
82
|
+
if not with_ram:
|
|
83
|
+
yield first, first.line, (f"NOT CHECKED: none of the {len(blocks)} NvM block(s) has an NvMRamBlockDataAddress "
|
|
84
|
+
"naming a C object"), None, "info"
|
|
85
|
+
return
|
|
86
|
+
table, notes, tc = probe_sizes(ctx)
|
|
87
|
+
for n in notes:
|
|
88
|
+
yield first, first.line, f"NOT CHECKED: {n}", None, "info"
|
|
89
|
+
checked = mism = 0
|
|
90
|
+
unchecked = []
|
|
91
|
+
for blk, addr, sym in with_ram:
|
|
92
|
+
size, decl = table.get(blk.path, (None, None))
|
|
93
|
+
if size is None:
|
|
94
|
+
why = ("not declared in any C source" if decl is None else
|
|
95
|
+
f"{decl.type} has no header defining it" if blk.path not in getattr(ctx, "_nvm_probed", {blk.path})
|
|
96
|
+
else "compiler did not report its size")
|
|
97
|
+
unchecked.append(blk.name)
|
|
98
|
+
yield blk, blk.line, f"NOT CHECKED: {blk.name}: sizeof({sym}) undetermined ({why})", None, "info"
|
|
99
|
+
continue
|
|
100
|
+
checked += 1
|
|
101
|
+
where = f"{os.path.relpath(decl.file, ctx.p.root)}:{decl.line}" if decl else "?"
|
|
102
|
+
ln = parse_int(ctx.text(blk, "NvMNvBlockLength") or "")
|
|
103
|
+
pv = ctx.value(blk, "NvMNvBlockLength")
|
|
104
|
+
crc = (ctx.text(blk, "NvMBlockUseCrc") or "").lower() == "true"
|
|
105
|
+
tail = f" (RAM block {addr}: {decl.type if decl else '?'} at {where}, sizeof = {size} via {tc.origin if tc else '?'}" \
|
|
106
|
+
f"{'; CRC ' + (ctx.text(blk, 'NvMBlockCrcType') or '') + ' is added by NvM on top' if crc else ''})"
|
|
107
|
+
if ln is not None and ln != size:
|
|
108
|
+
mism += 1
|
|
109
|
+
yield blk, pv.node.line, f"{blk.name}: NvMNvBlockLength={ln} but sizeof({sym}) = {size}" + tail, pv.def_ref
|
|
110
|
+
for st in _storage_blocks(ctx, blk):
|
|
111
|
+
sname = "FeeBlockSize" if (st.def_ref or "").endswith("/FeeBlockConfiguration") else "EaBlockSize"
|
|
112
|
+
bs = parse_int(ctx.text(st, sname) or "")
|
|
113
|
+
spv = ctx.value(st, sname)
|
|
114
|
+
if bs is not None and bs < size:
|
|
115
|
+
mism += 1
|
|
116
|
+
yield st, spv.node.line, (f"{st.name}: {sname}={bs} cannot hold the {size}-byte RAM block of {blk.name}"
|
|
117
|
+
+ tail), spv.def_ref
|
|
118
|
+
yield first, first.line, (f"sizeof check: {checked} of {len(with_ram)} RAM block(s) sized via "
|
|
119
|
+
f"{tc.origin if tc else 'no probe'}, {mism} mismatch(es), {len(unchecked)} NOT CHECKED"), None, "info"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@rule("H2L-NVM-000", "info", "NvM RAM block coverage", "RAM blocks whose C object could not be found")
|
|
123
|
+
def nvm_unresolved(ctx):
|
|
124
|
+
if not ctx.containers("NvM/NvMBlockDescriptor"):
|
|
125
|
+
return
|
|
126
|
+
table, _notes, _tc = probe_sizes(ctx)
|
|
127
|
+
miss = [p for p, (s, d) in table.items() if d is None]
|
|
128
|
+
if miss:
|
|
129
|
+
first = ctx.containers("NvM/NvMBlockDescriptor")[0]
|
|
130
|
+
yield first, first.line, (f"{len(miss)} NvM RAM block symbol(s) not declared in any C source "
|
|
131
|
+
f"(e.g. {', '.join(miss[:3])}); H2L-NVM-001 skipped for them"), None
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""PduR routing vs the System / ECU extract.
|
|
2
|
+
|
|
3
|
+
A routing path forwards one I-PDU: its source and destination EcuC Pdus must stand for the same system
|
|
4
|
+
I-PDU (the same IPdu triggered on two channels, or an I-PDU with the same I-SIGNAL content), or for a
|
|
5
|
+
source/target pair of an extract GATEWAY I-PDU-MAPPING when the extract has one.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from . import rule
|
|
10
|
+
from .system import channels, foreign_targets, system_ipdus
|
|
11
|
+
|
|
12
|
+
CITE = ("SWS_PduR routing path (PduRSrcPdu/PduRDestPdu); System Template Gateway.iPduMappings / IPdu triggered on "
|
|
13
|
+
"source and target channels")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _pdu(ctx, container, ref_name):
|
|
17
|
+
return ctx.target(ctx.value(container, ref_name)) if container is not None else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _gateway_map(ctx) -> dict:
|
|
21
|
+
"""source PDU-TRIGGERING -> {target PDU-TRIGGERING} from extract I-PDU-MAPPINGs (if any)."""
|
|
22
|
+
out: dict = {}
|
|
23
|
+
for m in ctx.p.system.tags("I-PDU-MAPPING"):
|
|
24
|
+
src = m.values.get("SOURCE-I-PDU-REF", [])
|
|
25
|
+
tgt = m.values.get("TARGET-I-PDU-REF", [])
|
|
26
|
+
for s in src:
|
|
27
|
+
out.setdefault(s, set()).update(tgt)
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _paths(ctx):
|
|
32
|
+
for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
|
|
33
|
+
src = ctx.target(ctx.value(rp, "PduRSrcPduRRef"))
|
|
34
|
+
dests = [ctx.target(pv) for pv in ctx.values(rp, "PduRDestPduRRef")]
|
|
35
|
+
yield rp, src, [d for d in dests if d is not None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@rule("H2L-PDUR-001", "error", CITE, "a routing path's destination carries the same I-PDU as its source")
|
|
39
|
+
def route_vs_extract(ctx):
|
|
40
|
+
if not ctx.p.system.by_path:
|
|
41
|
+
return
|
|
42
|
+
gw = _gateway_map(ctx)
|
|
43
|
+
sysi = ctx.p.system
|
|
44
|
+
for rp, src, dests in _paths(ctx):
|
|
45
|
+
spdu = _pdu(ctx, src, "PduRSrcPduRef")
|
|
46
|
+
s_ip = system_ipdus(ctx, spdu)
|
|
47
|
+
if not s_ip:
|
|
48
|
+
continue
|
|
49
|
+
s_sig = {x for ip in s_ip for x in sysi.signals_of_ipdu(ip)}
|
|
50
|
+
for d in dests:
|
|
51
|
+
dpdu = _pdu(ctx, d, "PduRDestPduRef")
|
|
52
|
+
d_ip = system_ipdus(ctx, dpdu)
|
|
53
|
+
if not d_ip or set(d_ip) & set(s_ip):
|
|
54
|
+
continue
|
|
55
|
+
d_sig = {x for ip in d_ip for x in sysi.signals_of_ipdu(ip)}
|
|
56
|
+
if s_sig and s_sig == d_sig:
|
|
57
|
+
continue
|
|
58
|
+
src_trig = [t.path for _pv, t in foreign_targets(ctx, spdu)]
|
|
59
|
+
if any(t in gw for t in src_trig):
|
|
60
|
+
allowed = set().union(*(gw[t] for t in src_trig if t in gw))
|
|
61
|
+
dst_trig = {t.path for _pv, t in foreign_targets(ctx, dpdu)}
|
|
62
|
+
if allowed & dst_trig:
|
|
63
|
+
continue
|
|
64
|
+
pv = ctx.value(rp, "PduRDestPduRRef")
|
|
65
|
+
others = [o.name for o, s2, ds in _paths(ctx) if o.path != rp.path and any(x.path == d.path for x in ds)]
|
|
66
|
+
yield rp, pv.node.line, (
|
|
67
|
+
f"{rp.name}: source {spdu.name if spdu else src.name} carries system I-PDU {', '.join(s_ip)} "
|
|
68
|
+
f"(channel {', '.join(channels(ctx, spdu)) or '?'}) but destination {d.name} -> "
|
|
69
|
+
f"{dpdu.name if dpdu else '?'} carries {', '.join(d_ip)} (channel {', '.join(channels(ctx, dpdu)) or '?'}), "
|
|
70
|
+
f"with different I-SIGNALs ({len(s_sig)} vs {len(d_sig)}, {len(s_sig & d_sig)} shared)"
|
|
71
|
+
+ (f"; that destination is also the target of {', '.join(others)}" if others else "")), pv.def_ref
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@rule("H2L-PDUR-002", "warning", "SWS_PduR routing path (one source per destination PDU)",
|
|
75
|
+
"a destination PDU is fed by more than one routing path")
|
|
76
|
+
def fan_in(ctx):
|
|
77
|
+
feeds: dict = {}
|
|
78
|
+
for rp, src, dests in _paths(ctx):
|
|
79
|
+
for d in dests:
|
|
80
|
+
feeds.setdefault(d.path, []).append((rp, src))
|
|
81
|
+
for dpath, rps in feeds.items():
|
|
82
|
+
srcs = {s.path for _r, s in rps if s is not None}
|
|
83
|
+
if len(srcs) > 1:
|
|
84
|
+
for rp, _s in rps:
|
|
85
|
+
pv = ctx.value(rp, "PduRDestPduRRef")
|
|
86
|
+
yield rp, pv.node.line, (f"{rp.name}: destination {dpath} is also fed by "
|
|
87
|
+
f"{', '.join(r.name for r, _ in rps if r.path != rp.path)} (different sources)"), pv.def_ref
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""ECU configuration vs the System / ECU extract (the ECUC must agree with the extract it was derived from).
|
|
2
|
+
|
|
3
|
+
Links are discovered from the configuration's own foreign references and the extract's metaclasses:
|
|
4
|
+
ComSignal --(any foreign ref: I-SIGNAL-TO-I-PDU-MAPPING | I-SIGNAL | SYSTEM-SIGNAL)--> mapping;
|
|
5
|
+
EcuC Pdu --(any foreign ref: PDU-TRIGGERING | *FRAME-TRIGGERING | *I-PDU)--> system I-PDU.
|
|
6
|
+
No element names are assumed.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from . import rule
|
|
13
|
+
from ..values import parse_int
|
|
14
|
+
|
|
15
|
+
ENDIAN = {"MOST-SIGNIFICANT-BYTE-LAST": "LITTLE_ENDIAN", "MOST-SIGNIFICANT-BYTE-FIRST": "BIG_ENDIAN", "OPAQUE": "OPAQUE"}
|
|
16
|
+
CITE_SIG = ("System Template ISignalToIPduMapping.startPosition/packingByteOrder, ISignal.length vs "
|
|
17
|
+
"ECUC_Com_00259 ComBitPosition, ECUC_Com_00158 ComBitSize, ECUC_Com_00157 ComSignalEndianness")
|
|
18
|
+
CITE_PDU = "System Template IPdu.length vs ECUC_EcuC_00003 PduLength"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def foreign_targets(ctx, e):
|
|
22
|
+
"""(param value, system Ident) for every reference of an ECUC element that lands in the system index."""
|
|
23
|
+
out = []
|
|
24
|
+
for g in ctx.p.find(e.path):
|
|
25
|
+
for pv in g.params:
|
|
26
|
+
if pv.is_ref and pv.value:
|
|
27
|
+
t = ctx.p.system.get(pv.value)
|
|
28
|
+
if t is not None:
|
|
29
|
+
out.append((pv, t))
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def system_ipdus(ctx, pdu) -> list:
|
|
34
|
+
"""EcuC Pdu container -> system I-PDU paths it stands for."""
|
|
35
|
+
if pdu is None:
|
|
36
|
+
return []
|
|
37
|
+
out = []
|
|
38
|
+
for _pv, t in foreign_targets(ctx, pdu):
|
|
39
|
+
if t.tag.endswith("TRIGGERING"):
|
|
40
|
+
out.extend(ctx.p.system.ipdus_of_triggering(t.path))
|
|
41
|
+
elif t.tag.endswith("PDU"):
|
|
42
|
+
out.append(t.path)
|
|
43
|
+
return sorted(set(out))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def channels(ctx, pdu) -> list:
|
|
47
|
+
if pdu is None:
|
|
48
|
+
return []
|
|
49
|
+
return sorted({ctx.p.system.channel_of(t.path) for _pv, t in foreign_targets(ctx, pdu)
|
|
50
|
+
if t.tag.endswith("TRIGGERING") and ctx.p.system.channel_of(t.path)})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def owning_ipdus(ctx):
|
|
54
|
+
"""ComSignal path -> list of ComIPdu Elems that reference it."""
|
|
55
|
+
own: dict = {}
|
|
56
|
+
for ip in ctx.containers("Com/ComConfig/ComIPdu"):
|
|
57
|
+
for pv in ctx.values(ip, "ComIPduSignalRef"):
|
|
58
|
+
if pv.value:
|
|
59
|
+
own.setdefault(pv.value.strip(), []).append(ip)
|
|
60
|
+
return own
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def mappings_for(ctx, sig, sys_ipdus: list) -> list:
|
|
64
|
+
sysi = ctx.p.system
|
|
65
|
+
out = []
|
|
66
|
+
for _pv, t in foreign_targets(ctx, sig):
|
|
67
|
+
if t.tag == "I-SIGNAL-TO-I-PDU-MAPPING":
|
|
68
|
+
out.append(t)
|
|
69
|
+
elif t.tag in ("I-SIGNAL", "SYSTEM-SIGNAL"):
|
|
70
|
+
isigs = [t.path] if t.tag == "I-SIGNAL" else [i.path for i in sysi.refs_to(t.path) if i.tag == "I-SIGNAL"]
|
|
71
|
+
for isig in isigs:
|
|
72
|
+
for m in sysi.refs_to(isig):
|
|
73
|
+
if m.tag == "I-SIGNAL-TO-I-PDU-MAPPING" and (not sys_ipdus or m.parent in sys_ipdus):
|
|
74
|
+
out.append(m)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _motorola_msb(lsb: int, size: int) -> int:
|
|
79
|
+
b = lsb
|
|
80
|
+
for _ in range(size - 1):
|
|
81
|
+
b = b + 1 if b % 8 != 7 else b - 15
|
|
82
|
+
return b
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _line(m) -> str:
|
|
86
|
+
return f"{m.file.rsplit('/', 1)[-1]}:{m.line}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@rule("H2L-SYS-001", "error", CITE_SIG, "ComSignal layout and I-PDU agree with the system extract mapping")
|
|
90
|
+
def signal_vs_extract(ctx):
|
|
91
|
+
if not ctx.p.system.by_path:
|
|
92
|
+
return
|
|
93
|
+
own = owning_ipdus(ctx)
|
|
94
|
+
for sig in ctx.containers("Com/ComConfig/ComSignal") + ctx.containers("Com/ComConfig/ComSignalGroup/ComGroupSignal"):
|
|
95
|
+
ipdus = own.get(sig.path, [])
|
|
96
|
+
sys_ipdus = sorted({x for ip in ipdus for x in system_ipdus(ctx, ctx.target(ctx.value(ip, "ComPduIdRef")))})
|
|
97
|
+
maps = mappings_for(ctx, sig, sys_ipdus)
|
|
98
|
+
if not maps:
|
|
99
|
+
continue # no link: counted by the info rule below
|
|
100
|
+
m = maps[0]
|
|
101
|
+
isig = ctx.p.system.get(m.one("I-SIGNAL-REF"))
|
|
102
|
+
pos, size = parse_int(ctx.text(sig, "ComBitPosition") or ""), parse_int(ctx.text(sig, "ComBitSize") or "")
|
|
103
|
+
if size is None:
|
|
104
|
+
n = parse_int(ctx.text(sig, "ComSignalLength") or "")
|
|
105
|
+
size = n * 8 if n is not None else None
|
|
106
|
+
start = parse_int(m.one("START-POSITION") or "")
|
|
107
|
+
length = parse_int(isig.one("LENGTH") or "") if isig is not None else None
|
|
108
|
+
order = m.one("PACKING-BYTE-ORDER")
|
|
109
|
+
endian = (ctx.text(sig, "ComSignalEndianness") or "").strip()
|
|
110
|
+
anchor = ctx.value(sig, "ComBitPosition") or ctx.value(sig, "ComBitSize")
|
|
111
|
+
line = anchor.node.line if anchor is not None else sig.line
|
|
112
|
+
ev = f"extract {m.path} ({_line(m)}): START-POSITION={start}, PACKING-BYTE-ORDER={order}, " \
|
|
113
|
+
f"I-SIGNAL LENGTH={length}"
|
|
114
|
+
if pos is not None and start is not None and size:
|
|
115
|
+
ok = pos == start
|
|
116
|
+
if order == "MOST-SIGNIFICANT-BYTE-FIRST":
|
|
117
|
+
ok = ok or start == _motorola_msb(pos, size) or pos == _motorola_msb(start, size)
|
|
118
|
+
if not ok:
|
|
119
|
+
yield sig, line, f"{sig.name}: ComBitPosition={pos} but the system extract maps it at START-POSITION={start}; {ev}", \
|
|
120
|
+
ctx.value(sig, "ComBitPosition").def_ref
|
|
121
|
+
if size is not None and length is not None and size != length:
|
|
122
|
+
yield sig, line, f"{sig.name}: ComBitSize={size} but I-SIGNAL LENGTH={length} bits; {ev}", \
|
|
123
|
+
(ctx.value(sig, "ComBitSize") or ctx.value(sig, "ComSignalLength")).def_ref
|
|
124
|
+
if order in ENDIAN and endian and ENDIAN[order] != endian:
|
|
125
|
+
yield sig, line, f"{sig.name}: ComSignalEndianness={endian} but PACKING-BYTE-ORDER={order} ({ENDIAN[order]}); {ev}", \
|
|
126
|
+
ctx.value(sig, "ComSignalEndianness").def_ref
|
|
127
|
+
if sys_ipdus and m.parent not in sys_ipdus:
|
|
128
|
+
yield sig, sig.line, (f"{sig.name} is in ComIPdu {', '.join(i.name for i in ipdus)} (system I-PDU "
|
|
129
|
+
f"{', '.join(sys_ipdus)}) but the extract maps its I-SIGNAL into {m.parent} ({_line(m)})"), sig.def_ref
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@rule("H2L-SYS-002", "error", CITE_PDU, "EcuC PduLength agrees with the system I-PDU LENGTH")
|
|
133
|
+
def pdu_length_vs_extract(ctx):
|
|
134
|
+
if not ctx.p.system.by_path:
|
|
135
|
+
return
|
|
136
|
+
for pdu in ctx.containers("EcuC/EcucConfigSet/EcucPduCollection/Pdu"):
|
|
137
|
+
length = parse_int(ctx.text(pdu, "PduLength") or "")
|
|
138
|
+
if length is None:
|
|
139
|
+
continue
|
|
140
|
+
for ip in system_ipdus(ctx, pdu):
|
|
141
|
+
si = ctx.p.system.get(ip)
|
|
142
|
+
sl = parse_int(si.one("LENGTH") or "") if si is not None else None
|
|
143
|
+
if sl is not None and sl != length:
|
|
144
|
+
pv = ctx.value(pdu, "PduLength")
|
|
145
|
+
yield pdu, pv.node.line, (f"{pdu.name}: PduLength={length} but system I-PDU {ip} has LENGTH={sl} "
|
|
146
|
+
f"({_line(si)})"), pv.def_ref
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@rule("H2L-SYS-000", "info", "ECU extract link coverage", "how much of the config could be linked to the extract")
|
|
150
|
+
def coverage(ctx):
|
|
151
|
+
first: Optional[object] = next((m for m in ctx.p.modules if (m.def_ref or "").endswith("/Com")), None)
|
|
152
|
+
if first is None:
|
|
153
|
+
return
|
|
154
|
+
if not ctx.p.system.by_path:
|
|
155
|
+
yield first, first.line, ("no System/ECU extract loaded (no non-ECUC .arxml found or configured as 'input'); "
|
|
156
|
+
"H2L-SYS/H2L-PDUR cross-checks were skipped"), None
|
|
157
|
+
return
|
|
158
|
+
sigs = ctx.containers("Com/ComConfig/ComSignal")
|
|
159
|
+
unlinked = [s for s in sigs if not mappings_for(ctx, s, [])]
|
|
160
|
+
if unlinked:
|
|
161
|
+
yield first, first.line, (f"{len(unlinked)} of {len(sigs)} ComSignal(s) have no reference into the loaded "
|
|
162
|
+
f"extract (e.g. {', '.join(s.path for s in unlinked[:3])}); H2L-SYS-001 skipped for them"), None
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Index of the non-ECUC ARXML (System / ECU extract, SWCs, BSWMD) for cross-checks.
|
|
2
|
+
|
|
3
|
+
One expat pass per file. Every identifiable (element with a SHORT-NAME) becomes an Ident with its tag,
|
|
4
|
+
file, line, the identifiable children, and the leaf values *of its own* (descendants that are not
|
|
5
|
+
inside a nested identifiable): e.g. an I-SIGNAL-TO-I-PDU-MAPPING gets {"I-SIGNAL-REF": [...],
|
|
6
|
+
"START-POSITION": ["16"], "PACKING-BYTE-ORDER": [...]}. References keep their DEST type.
|
|
7
|
+
Nothing is keyed by project-specific names; rules navigate by AUTOSAR metaclass and reference.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import xml.parsers.expat as expat
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Ident:
|
|
18
|
+
path: str
|
|
19
|
+
tag: str
|
|
20
|
+
file: str
|
|
21
|
+
line: int
|
|
22
|
+
values: dict = field(default_factory=dict) # leaf tag -> [text]
|
|
23
|
+
dests: dict = field(default_factory=dict) # ref tag -> [DEST]
|
|
24
|
+
children: list = field(default_factory=list) # child identifiable paths
|
|
25
|
+
parent: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
def one(self, tag: str) -> Optional[str]:
|
|
28
|
+
v = self.values.get(tag)
|
|
29
|
+
return v[0] if v else None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SystemIndex:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.by_path: dict = {}
|
|
35
|
+
self.by_tag: dict = {}
|
|
36
|
+
self._refs_to: Optional[dict] = None
|
|
37
|
+
|
|
38
|
+
def add_file(self, path: str):
|
|
39
|
+
try:
|
|
40
|
+
with open(path, "rb") as fh:
|
|
41
|
+
data = fh.read()
|
|
42
|
+
except OSError:
|
|
43
|
+
return
|
|
44
|
+
p = expat.ParserCreate()
|
|
45
|
+
p.buffer_text = True
|
|
46
|
+
stack: list = [] # [tag, Ident|None, text parts, attrs, line]
|
|
47
|
+
idents: list = [] # current chain of Idents
|
|
48
|
+
|
|
49
|
+
def start(name, attrs):
|
|
50
|
+
stack.append([name.rsplit(":", 1)[-1], None, [], attrs, p.CurrentLineNumber])
|
|
51
|
+
|
|
52
|
+
def chars(t):
|
|
53
|
+
if stack:
|
|
54
|
+
stack[-1][2].append(t)
|
|
55
|
+
|
|
56
|
+
def end(name):
|
|
57
|
+
tag, ident, text, attrs, line = stack.pop()
|
|
58
|
+
if tag == "SHORT-NAME" and stack:
|
|
59
|
+
owner = stack[-1]
|
|
60
|
+
if owner[1] is None:
|
|
61
|
+
parent = idents[-1] if idents else None
|
|
62
|
+
sn = "".join(text).strip()
|
|
63
|
+
ip = f"{parent.path if parent else ''}/{sn}"
|
|
64
|
+
owner[1] = Ident(ip, owner[0], path, owner[4], parent=parent.path if parent else None)
|
|
65
|
+
if parent is not None:
|
|
66
|
+
parent.children.append(ip)
|
|
67
|
+
idents.append(owner[1])
|
|
68
|
+
self.by_path.setdefault(ip, owner[1])
|
|
69
|
+
self.by_tag.setdefault(owner[0], []).append(owner[1])
|
|
70
|
+
return
|
|
71
|
+
if ident is not None:
|
|
72
|
+
idents.pop()
|
|
73
|
+
return
|
|
74
|
+
if idents: # a leaf value of the innermost enclosing identifiable
|
|
75
|
+
txt = "".join(text).strip()
|
|
76
|
+
if txt:
|
|
77
|
+
cur = idents[-1]
|
|
78
|
+
cur.values.setdefault(tag, []).append(txt)
|
|
79
|
+
if "DEST" in attrs:
|
|
80
|
+
cur.dests.setdefault(tag, []).append(attrs["DEST"])
|
|
81
|
+
|
|
82
|
+
p.StartElementHandler = start
|
|
83
|
+
p.EndElementHandler = end
|
|
84
|
+
p.CharacterDataHandler = chars
|
|
85
|
+
try:
|
|
86
|
+
p.Parse(data, True)
|
|
87
|
+
except expat.ExpatError:
|
|
88
|
+
pass
|
|
89
|
+
self._refs_to = None
|
|
90
|
+
|
|
91
|
+
def get(self, path: Optional[str]) -> Optional[Ident]:
|
|
92
|
+
return self.by_path.get(path.strip()) if path else None
|
|
93
|
+
|
|
94
|
+
def tags(self, tag: str) -> list:
|
|
95
|
+
return self.by_tag.get(tag, [])
|
|
96
|
+
|
|
97
|
+
def refs_to(self, path: str) -> list:
|
|
98
|
+
"""Idents that reference ``path`` (any *-REF leaf)."""
|
|
99
|
+
if self._refs_to is None:
|
|
100
|
+
idx: dict = {}
|
|
101
|
+
for i in self.by_path.values():
|
|
102
|
+
for t, vals in i.values.items():
|
|
103
|
+
if t.endswith("-REF") or t.endswith("-TREF"):
|
|
104
|
+
for v in vals:
|
|
105
|
+
idx.setdefault(v, []).append(i)
|
|
106
|
+
self._refs_to = idx
|
|
107
|
+
return self._refs_to.get(path, [])
|
|
108
|
+
|
|
109
|
+
def foreign(self) -> dict:
|
|
110
|
+
return {p: (i.tag, i.line, i.file) for p, i in self.by_path.items()}
|
|
111
|
+
|
|
112
|
+
# ---- navigation helpers (by metaclass, not by name) ----
|
|
113
|
+
def ipdus_of_triggering(self, path: Optional[str]) -> list:
|
|
114
|
+
"""A PDU-TRIGGERING or FRAME-TRIGGERING (any bus) -> system I-PDU paths it carries."""
|
|
115
|
+
t = self.get(path)
|
|
116
|
+
if t is None:
|
|
117
|
+
return []
|
|
118
|
+
out = []
|
|
119
|
+
for ref in t.values.get("I-PDU-REF", []):
|
|
120
|
+
out.append(ref)
|
|
121
|
+
for pt in t.values.get("PDU-TRIGGERING-REF", []):
|
|
122
|
+
out.extend(self.ipdus_of_triggering(pt))
|
|
123
|
+
if not out:
|
|
124
|
+
fr = self.get(t.one("FRAME-REF"))
|
|
125
|
+
if fr is not None:
|
|
126
|
+
for m in fr.children:
|
|
127
|
+
mi = self.get(m)
|
|
128
|
+
if mi is not None:
|
|
129
|
+
out.extend(mi.values.get("PDU-REF", []))
|
|
130
|
+
seen, res = set(), []
|
|
131
|
+
for x in out:
|
|
132
|
+
if x not in seen:
|
|
133
|
+
seen.add(x)
|
|
134
|
+
res.append(x)
|
|
135
|
+
return res
|
|
136
|
+
|
|
137
|
+
def channel_of(self, path: Optional[str]) -> Optional[str]:
|
|
138
|
+
"""Physical channel (the identifiable holding the triggering) of a PDU/FRAME-TRIGGERING."""
|
|
139
|
+
t = self.get(path)
|
|
140
|
+
return t.parent if t is not None else None
|
|
141
|
+
|
|
142
|
+
def signals_of_ipdu(self, ipdu: str) -> list:
|
|
143
|
+
"""I-SIGNAL paths mapped into a system I-PDU (via its I-SIGNAL-TO-I-PDU-MAPPING children)."""
|
|
144
|
+
i = self.get(ipdu)
|
|
145
|
+
if i is None:
|
|
146
|
+
return []
|
|
147
|
+
out = []
|
|
148
|
+
for c in i.children:
|
|
149
|
+
ci = self.get(c)
|
|
150
|
+
if ci is not None and ci.values.get("I-SIGNAL-REF"):
|
|
151
|
+
out.extend(ci.values["I-SIGNAL-REF"])
|
|
152
|
+
return sorted(set(out))
|