A diagnostic failure has no stack trace
Every other layer of an ECU tells you when it breaks. A pointer bug traps. A missing module fails the link. A wrong configuration throws at startup. The diagnostic layer does not do any of that: the ECU boots, the tester connects, the session opens, and then a request comes back 0x31 requestOutOfRange — or a DID answers with a payload three bytes shorter than the spec says, and the tool happily shows you garbage with a label on it.
The reason is structural. The two sides are written by different people in different tools:
| Artefact | What it is | Written by |
|---|---|---|
| CDD | The diagnostic specification. The tester is built from it. | Diagnostic engineer, in CANdelaStudio |
| ARXML | Dcm / Dem / CanIf / CanTp configuration. What the ECU actually runs. | BSW engineer, in DaVinci / EB tresos |
Nothing compares them. There is no build step, no schema that spans both, no CI that would notice. Change a CAN address in the CDD and the ECU keeps listening on the old one forever, because nothing in the pipeline connects the two documents.
A crash tells you where to look. A wrong diagnostic address tells you nothing: the tester says "no response", which is also what it says when the ECU is off, the wiring is bad, the CAN transceiver is asleep, or the gateway filters it. Integration debugging then starts from the wire, and the actual fault is a number in a text file.
Two sources for the same number
Before the bug, the structure. A CDD stores diagnostic addresses in two different places, and the difference matters more than it looks:
Some_ECU CAN.ReqCanId
Type 1 is configuration. Type 2 is what CANdelaStudio put there when the template was created, and it exists whether or not anyone ever filled in the CAN tab. The parser reads type 1. So when a project is created by saving a template and the CAN page is left empty, the attribute map comes back with no CAN entries at all — and the file still parses perfectly, still has 53 DIDs, still has 129 DTCs, still has every routine.
That combination is what made the bug invisible for a whole release: the document is structurally valid, and the only thing missing is the part nobody looks at when the DID list matches.
Eight checks that skipped in silence
The comparison block looked like this. Read the second and third lines again — they are the whole bug:
for code, sev, attr, role, label in id_map:
cdd_val = cdd.attrs.get(attr)
if not isinstance(cdd_val, int):
continue # <-- no value? skip the check. say nothing.
arx_val = arx.canif_ids.get(role)
...
Three addressing checks (CDX-010/011/012) and five timing checks (CDX-020 through CDX-024) all guarded themselves the same way. With no CAN attributes, all eight hit continue. The report came back with DID findings, DTC findings, routine findings, and a small tidy summary — and no mention of CAN ids anywhere.
| What the user sees | What it means |
|---|---|
| Report has no CAN id findings | Either the ids agree, or nobody checked |
Those two states are the same string on a terminal. A run where all 53 DIDs, all 129 DTCs and all 4 routines matched reads like a complete review. In fact the CDD had 0 of the 3 required CAN addresses configured — and the template defaults that sat there instead were placeholder values that no ECU on that bus listens for.
Worse: the placeholder values are not random. They are 0x700 / 0x600 / 0x750, which looks plausible enough that nobody questions it, and exporting ODX from that CDD produces a tester that sends every request to 0x700.
The fix that is not a fix
The obvious repair is to fall back to the template value and compare anyway. Do not do that. It produces a confident verdict about a number that is not this ECU's address — and a confident wrong verdict is worse than a stated gap, because the reader will act on it.
So the gap itself became a finding. Two new codes:
| Code | Severity | Fires when |
|---|---|---|
CDX-013 |
error | Any of the three diagnostic CAN addresses is unconfigured |
CDX-025 |
warn | Any of P2 / P2* / S3 / STmin / Blocksize is unconfigured |
Both name the checks that did not run. And the template defaults are kept in a separate field — attrs_template rather than attrs — so they can never be silently mistaken for configuration downstream. That separation was the actual fix; the two codes only make it visible.

Which side is wrong
This is the part that makes the tool worth writing. A diff can tell you that the CDD says 0x738 and CanIf says 0x72F. It cannot tell you which one to change, and the person reading the report is usually the owner of exactly one of those two files.
There is a tie-breaker, and it is in the ARXML: the hardware acceptance filter. The CAN controller does not deliver every frame to the software — it filters on CanHwFilterCode and CanHwFilterMask, and a frame passes only when:
(frame_id & mask) == (code & mask)
Evaluate both candidate ids against every filter in the configuration. Three outcomes, and only one of them licenses a verdict:
| Filter state | Verdict |
|---|---|
| Accepts CDD's id, rejects CanIf's | CanIf is wrong |
| Accepts CanIf's id, rejects CDD's | The CDD is wrong |
| Accepts both (mask has don't-care bits) / accepts neither / no filter | Cannot decide — say so, do not guess |
On a real project this resolved to a single filter out of 51, with mask 0x7FE. The mask has one don't-care bit, which is why 0x738 and 0x739 both land in that mailbox — and why 0x72F does not:
0x738 & 0x7FE = 0x738 == code 0x738 -> accepted 0x72F & 0x7FE = 0x72E != code 0x738 -> rejected
So the controller only hands 0x738 frames upward. The CanIf PDU configured for 0x72F therefore describes an address that cannot reach the software — and the answer is not "the two files disagree", it is "change CanIfRxPduCfg_Phys_Req.CanIfRxPduCanId to 0x738".
CanTp carries its own addressing: N_SA and N_TA. On that project they decoded to 56, which is 0x38 — the low byte of 0x738. The low byte of 0x72F is 0x2F, or 47, and appears nowhere. Two artefacts built by different people agree with the CDD and disagree with CanIf.
One honest caveat, because the tool used to overclaim it. On that project CanIf hardware-software filtering was disabled (CanIfHrhSoftwareFilter = false), so CanIf does not re-validate the id in software. The wrong value is therefore a latent defect rather than an immediate failure: it works today by accident, and breaks the day someone enables the software filter, changes the HRH routing, or turns on an integration-test assertion.
A Phy is not a phys
CanIf PDU naming differs per supplier, so roles are detected by keyword rather than by an expected name. The first version looked for phys:
if 'func' in name.lower(): role = 'func_req' elif 'phys' in name.lower(): role = 'phys_req' elif 'resp' in name.lower(): role = 'resp'
Then a project arrived with RX_PDU_UDS_DiagPhyReq_LDLP. 'phys' in 'diagphyreq' is False. So the physical request PDU was never found, and the tool reported "the CDD defines a physical request CAN id but CanIf has no corresponding diagnostic PDU" — while that PDU sat in the file under a name that was three letters shorter.
The failure direction is the bad one. A missed mismatch leaves a real defect unreported; a false mismatch sends someone to create a PDU that already exists, and then they have two.
Fixed by matching phy, phys and physical. Trivial line, but it is the class of bug that only appears when the third supplier's naming convention shows up — and by then the tool has been trusted for months.
Proving 0 error is real
A checker that reports nothing is indistinguishable from a checker that broke. The only way to tell them apart is to plant known defects and confirm they still get caught — and specifically, to confirm that a passing fixture and a skipping fixture produce different output.
The self-test therefore has two fixtures:
| Fixture | Asserts |
|---|---|
| Configured CDD with 11 planted defects | 6 error / 4 warn / 3 info, and all 13 expected codes fire |
| Template save, CAN page emptied | CDX-013 / CDX-025 must fire; CDX-010/011/012/020/021/022 must NOT (nothing to disagree with) |
The second fixture also asserts that attrs stays empty while attrs_template is populated. Without that assertion, the fix itself could regress into "read the template values into attrs" — which passes the first two assertions and reintroduces the confident-wrong-verdict problem.
Beyond the fixtures: write the correct eight attributes into the unconfigured CDD manually and confirm CDX-013/CDX-025 disappear; then write 0x700 for the physical request and confirm CDX-010 fires and blames the CDD. Two directions, one file, thirty seconds — and it catches both "the check is dead" and "the verdict logic is inverted".
Two real projects
The bug was not found by reading the code. It was found by running the tool on a second project whose CDD happened to be a template save with an empty CAN page — and noticing something was off about the report.
What noticed it was not a check. It was the probe output looking different from the previous run: the line that used to report CAN.ReqCanId under diagnostic parameters was simply gone. A missing line and an empty line look the same on a terminal, which is exactly why the probe now prints the gap explicitly.
| Project | Before | After |
|---|---|---|
| CAN attributes mostly filled, Blocksize missing | 1 error / 1 warn | 1 error / 2 warn — the new CDX-025 honestly says CDX-024 never ran |
| Template save, CAN page empty | 2 error / 2 warn | 3 error / 3 warn — CDX-013 + CDX-025 appear |
The first row is the one worth staring at. Before the fix it reported 1 warn and looked fine. After the fix it reports 2 warns and one of them says a check did not run. The configuration did not change — only the honesty of the report did.
What the second project actually contained
Beyond the missing addresses, three ordinary findings that would each have cost a bench session:
| Code | Finding | Bench symptom |
|---|---|---|
CDX-006 |
3 routines in the CDD, absent from Dcm | Flashing calls 31 01 FF00 to erase and gets 0x31 back |
CDX-031 |
Security level 2 defined in the CDD only | Every service behind that level is locked forever |
CDX-002 |
3 DIDs in Dcm, absent from the CDD | Not a bug — decide whether they should be readable after sales at all |
The last row is the direction that matters. The three DIDs were named like SystemSupplierInternalECUHardware and ReadOnlyTest. Two are OEM-reserved identifiers that should not appear in an after-sales spec at all, and one is a leftover from the configuration tool's demo project. The correct action may well be to delete them from Dcm, not to add them to the CDD.
"Present in Dcm, missing from the CDD" sounds like the CDD is behind. Sometimes it is the opposite: the ECU exposes something it should not. The tool reports the asymmetry and leaves the ownership question to the engineer, because only they know which document is authoritative.
What it cannot see
Twenty-five checks passing is not the same as correct diagnostics, and the failure mode of a checking tool is that people stop reading the caveats. So the caveats are printed as part of every report, not buried in documentation.
| Not covered | Why it matters |
|---|---|
| DID data length and structure | A DID can exist on both sides and still return the wrong payload length |
| Session/security dependency per service | A service can be configured and still be unreachable from the session it belongs to |
| NRC usage per service | Only the two lists are counted, not which service must return which code |
| Security access algorithm | Seed/key lengths compared; the algorithm itself is not |
| Snapshot / extended record fields | DTC presence compared, record contents are not |
| Whether a configured value is correct | Only "configured" vs "not configured" is decidable |
The last row is the honest limit of the whole approach. If the CDD says 0x738 and the filter also says 0x738 but the vehicle actually uses 0x739, no amount of cross-checking between these two files will find it. Cross-checking finds disagreement, not wrongness.
Results and method
python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py probe ecu.cdd ecuconfig.xml python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py check ecu.cdd ecuconfig.xml python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py matrix ecu.cdd ecuconfig.xml -o inv.csv python skills/cdd-arxml-consistency/scripts/cdd_arxml_check.py selftest
Exit codes are 0 / 1 / 2 — no error, errors found, bad arguments — so it drops into CI without a wrapper. 25 codes, 10 error / 12 warn / 3 info. Standard library only, read-only, Python 3.8+.
Four things here generalise past this tool:
| Principle | Why |
|---|---|
| A skipped check must look different from a passing one | Otherwise a broken parser and a clean config produce the same report |
| Never fall back to defaults in silence | A default compares confidently against a number that is not the real one |
| Find the tie-breaker before writing the verdict | A diff without a verdict makes the reader guess which file to open |
| Assert the negative fixture | The fixture with the empty page is what catches over-reporting and regression in the fix itself |
Source and issue tracker: github.com/autosar-skills/autosar-agent-skills · MIT · the whole thing is one standard-library Python file plus two fixtures. All examples above are synthetic; no real vehicle, supplier or customer identifiers appear.
Common questions
Why do a CDD and an ARXML disagree in the first place?
They are two artefacts maintained by two groups in two tools: the CDD is the diagnostic spec, written by a diagnostic engineer in CANdelaStudio and used to build the tester; the ARXML is the ECU configuration, written by a BSW engineer in DaVinci or EB tresos and compiled into the ECU. Nothing in the pipeline compares them, so changing one never updates the other. The symptom is that it compiles, the tester connects, and then the ECU answers 0x31 requestOutOfRange.
When the CAN ids disagree, how do I know which file to change?
Use the hardware acceptance filter as the tie-breaker: read CanHwFilterCode and CanHwFilterMask and evaluate (frame_id & mask) == (code & mask). If the filter accepts the CDD's id and rejects CanIf's, the controller only hands the CDD's id upward, so CanIf is wrong. If it is the other way round, the CDD is wrong. When both or neither are accepted, say it cannot be decided rather than guessing.
Does 0 error mean the diagnostic configuration is fine?
No. It means those 25 checks found nothing. DID payload structure, the service-session-security dependency matrix, NRC usage ranges, the security algorithm and snapshot record contents are all outside the scope. More importantly, the tool has to distinguish a check that passed from a check that never ran: if a CDD has no CAN addresses at all, the three addressing checks did not execute and that is not a pass.
Why does it say the CDD has no CAN configuration when values are visible in the file?
A CDD carries diagnostic addresses in two places: attribute instances under
Which diagnostic configuration errors fail immediately on the bench?
Three most directly. A wrong physical request CAN id means no ECU listens on the address the tester sends to, so it cannot connect at all. A missing erase or check-integrity routine (0xFF00 / 0xFF01) makes the flashing sequence fail with 0x31 at that step. A security level defined in the CDD but absent from Dcm locks every service it protects, permanently.