Skip to content

feat(detector): Bosch EDC16CP31 (Mercedes OM642/OM646) family - #13

Draft
COSSART-FR wants to merge 18 commits into
LeZed97:masterfrom
COSSART-FR:edc16cp31-calibration
Draft

feat(detector): Bosch EDC16CP31 (Mercedes OM642/OM646) family#13
COSSART-FR wants to merge 18 commits into
LeZed97:masterfrom
COSSART-FR:edc16cp31-calibration

Conversation

@COSSART-FR

Copy link
Copy Markdown

What this adds

Support for the Bosch EDC16CP31, the common-rail ECU of the Mercedes CDI
engines (OM642 V6, OM646 4-cyl). Follows the layout described in
CONTRIBUTING.md: new detector module, strict identifier, routing, frontend
declaration.

Calibrated and verified on a real OM642 dump (CLK 320 CDI W209, Daimler
project CR4-642, SW 1037393817, 2 MB). No dump is committed — the tests that
need one read it from ZEDSUITE_CP31_DUMP and are #[ignore]d otherwise.

Detection engine (Rust)

  • detector/ecu/bosch/edc16cp31/EDC16CP31Detector with
    signatures.rs. 16 families (14 maps + 2 curves): rail pressure
    set-point and limiter, boost set-point and limiters, smoke limiter,
    thermal quantity limiter, quantity limit by boost and by rpm, driver wish
    ×2, torque→quantity, main-injection timing, air-mass set-point, boost
    controller base duty.
  • The CP31 stores its calibration as self-describing blocks
    (Kf_Xs16_Ys16_Ws16: nx, ny, X[], Y[], Z[] column-major;
    Kl_Xs16_Ws16 for curves). The detector reads them instead of assuming
    dimensions, so a 12×12 map in a 16×16 allocation is read correctly.
  • Families are located by exact axis key ([nx][ny] + X + Y, byte for
    byte), independent of address. A map whose Z values were remapped is still
    found, and a software not in the corpus can still yield maps. Byte
    markers and a bounded zone sweep are the fallbacks, in that order. Curves
    are keyed only — a blind sweep would match any increasing vector.
  • Big-endian (MPC5xx) throughout.

Identifier

ecu_identifier.rs: positive evidence only — Bosch software number at the
calibration start (positional, not a substring search), Daimler engine tag,
Daimler part-number format. A VAG part number vetoes the Mercedes gate.
Tests include foreign_2mb_file_is_not_cp31 and
cp31_dump_is_never_labelled_vag.

Frontend (TypeScript)

  • ecus.json, SUPPORTED_ECUS, ECU list, i18n strings.
  • EDC16 checksum: the region end is now only required to be
    dword-aligned. The 0x100 end alignment was a property of the six VAG pairs
    in the original corpus, not of the format — the CP31 region is
    0x190000..0x1FCFFB and was rejected as "unsupported checksum" although
    its sum is exactly reproducible. Start alignment (which constrains the
    signature position) is unchanged, and MIN_REGION_LENGTH compensates.
    The six VAG pairs still reproduce.
  • Solution edc16cp31_egr_off: replayed from a verified stock/EGR-off
    pair (21 ranges, 60 bytes, all inside the EGR block). Anchored by
    signature with a guard on the original bytes; returns nothing on any
    mismatch, no hard-coded address.
  • Power estimation: OM642 engine preset and a CP31-specific minimum AFR
    (the model has no torque limiter in Nm on this family, so treat CP31
    figures as an order of magnitude).

Docs

  • docs/PORTING-EDC16CP31.md — how the port was done, reusable for other
    Kf/Kl-based EDC16 variants.
  • docs/EDC16CP31-OM642-maps.md — reference sheet of the located maps
    (addresses on SW 1037393817, grids, factors, axes, pitfalls).

Validation

  • cargo test green (52 tests added: identifier, block reader, every
    family, build_expected_report_edc16cp31 consistency).
  • tsc --noEmit clean, tauri build OK on Windows x64.
  • Real dumps imported in the app: stock, EGR-off, and a modified file all
    report the same 27 elements; 2 MB dumps of other ECUs are rejected.
  • Rebased on 1.1.7. DETECTOR_VERSION bumped to 43 (39–42 were used by
    both this branch and 1.1.7, so every EDC15/CP31 project is re-scanned).

Known limits

  • Corpus of one software (1037393817, OM642 165 kW). The OM646 4-cyl
    is declared but has not been verified on a dump.
  • tuned_mode is never requested by the UI (the three detectMaps calls
    pass false); worked around for keyed families, not fixed at the root.
  • FlMng_facFullLd_CUR deliberately not shipped: its declared
    COMPU_METHOD (1/128) contradicts the values found (up to 2.0).
  • No TypeScript test runner in the project, so the EGR solution has no
    permanent test. Happy to add Vitest in a follow-up if you want it.

Unrelated: cargo clippy on 1.98 fails on three pre-existing
ifs_same_cond in edc15p/mod.rs (4048/4515, 4075/4539, 4340/5038). Not
touched here — I'll open a separate issue.

COSSART-FR and others added 16 commits September 7, 2026 13:17
Adds the Bosch EDC16CP31 family as an uncalibrated skeleton, following the
walkthrough in CONTRIBUTING.md.

What works out of the box:
- generic axis-first scan, scoring and axis validation (family-agnostic)
- big-endian MPC5xx decoding
- EDC16 checksum correction and frontend endianness, inherited via the
  existing includes("EDC16") routing - no code change needed
- Mercedes identification gate placed BEFORE the VAG variant logic, so a
  Mercedes 2MB dump can no longer be labelled EDC16U31

What is deliberately empty (needs a corpus of real CP31 dumps):
- CP31_SIGNATURES / CP31_MARKERS
- CP31_ZONES address windows
- every MAP_TEMPLATE carries calibrated: false

Safety contract: detect() returns no maps while uncalibrated and never falls
back to the VAG detectors. Locked by uncalibrated_detector_reports_no_maps.

Frontend stays disabled (ecus.json enabled: false, SUPPORTED_ECUS commented).

See docs/PORTING-EDC16CP31.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXgAH9nQKiN7FwQAmneXXb
Validated against one KESS V2 read of an OM642 3.0 CDI 165 kW (SW 1037393817):

- dump is exactly 2MB and everything below 0x190000 is 0xFF (the tool returns
  the calibration area only), so CP31_ZONES.calibration moves from the copied
  VAG guess (0x180000) to the real (0x190000, 0x1FD000)
- the single EDC16 checksum descriptor sits at 0x19003C and declares
  0x190000..0x1FCFFB; the BE dword sum over that region is exactly
  0xD01FE500, so edc16-checksum.ts reads this file unmodified
- identification gains two confirmed markers: the Bosch 1037 SW number at the
  fixed offset 0x190010 (positional, not a substring search) and the Daimler
  engine descriptor CR<n>-<group>-

Adds identifier tests built from synthetic fixtures reproducing that layout
(no .bin committed), including the required negative test and a regression
test proving a CP31 dump is never labelled EDC16U31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXgAH9nQKiN7FwQAmneXXb
…ilies

The skeleton guessed grids the way the VAG detectors do. That is unnecessary
on CP31: every 2D map uses the Bosch Kf_Xs16_Ys16_Ws16 record layout, which
carries its own dimensions -

  +0x00 u16 nx | +0x02 u16 ny | i16[nx] X | i16[ny] Y | i16[nx*ny] Z

with Z in COLUMN direction (Y is the fast index). So the detector now reads
the grid instead of guessing it, which removes the worst failure mode of the
axis-first engine: a wrong grid can no longer produce a plausible-looking
map. Presentation follows the file: rows = nx (engine speed), cols = ny
(load), so nothing is transposed and y_axis_address points at the first axis
in the block.

Calibration source: one real dump (OM642 3.0 CDI 165 kW, Bosch SW
1037393817, KESS V2 read) cross-referenced with the Daimler ASAP2 description
of the same project (B209 / V080000 / CR4-642). The A2L describes a DIFFERENT
calibration build - 55% of the calibration bytes differ and no constant offset
exists - so the labels were transferred structurally: exact axis-vector match
plus a longest-increasing-subsequence alignment on block order (238 maps),
then piecewise-constant delta interpolation validated against the A2L physical
limits (374 of 392). Every family below was then re-read from the dump and
checked by hand. The damos and the .bin are not committed; only the derived
signature data is.

Eleven families, all calibrated:
  Rail_pSetPointBase / Rail_pSetPointLimN / PCR_pDesBas /
  PCR_pBDesMaxAPGear{Hi,Lo} / PCR_rCtlBas / FlMng_qSmk / AccPed_trqEng{,2} /
  FlMng_qLimBstPres / FMTC_trq2qBas / InjCrv_phiMI1Bas1..3+Max1..2 /
  AirCtl_mDesBas.

Two hypotheses inherited from the VAG templates were wrong and are fixed:
  - the CR4 smoke limiter is a function of engine speed and CORRECTED BOOST
    PRESSURE (PCR_pBPSCor), not of air mass;
  - pedal and duty-cycle axes are 1/8192 per bit (8192 == 100%), not the VAG
    0.01 %/bit. AxisType::Pedal is replaced by AxisType::Percent; the old
    factor would have scaled every pedal map by 1.22.

CP31_ZONES now carries a window per category, and each template a tighter one
of its own. CP31_SIGNATURES and CP31_MARKERS are populated with markers cut
from the dump at the exact block start and extended until unique inside their
window; detect_by_signatures is implemented as a real marker walk. A marker
hit stays a hint - the block is still decoded, both axes validated and the
data range-checked before anything is emitted.

Tests: 14 unit tests, no binary in the repo (synthetic Kf blocks), including
rejection of an out-of-range block and of a block planted outside its zone.
The fixture test on a real dump is separate and #[ignore]d behind
ZEDSUITE_CP31_DUMP; on the reference dump it finds 25 blocks, all of them
real members of the eleven families, zero false positives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
The detector reports real maps now, so EDC16CP31 goes into SUPPORTED_ECUS and
ecus.json flips to status "beta", enabled: true. Mappack export stays off:
the corpus is one software build, and an export prices a confidence the data
does not support yet.

docs/PORTING-EDC16CP31.md is rewritten around what was actually found: the
self-describing record layout, the method used to transfer a damos whose
addresses do not line up, the table of the eleven confirmed families, and
what is still missing (a second software build, OM646, checksum CORRECTION on
a stock/modified pair, and the 1D Kl curves).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
The editor showed "Checksum non supporté" on a Mercedes EDC16CP31 dump whose
checksum is perfectly readable. Routing was never the problem -
`isChecksumSupported` matches on the EDC16 substring and returns true - but
`findEdc16Regions` rejected the region, `correctEDC16Checksum` returned null,
and the caller reads null as "unsupported".

The cause is an assumption inherited from the original six VAG pairs: the
region had to be 0x100-aligned at BOTH ends. That is a property of that
corpus, not of the format.

  VAG U31/U34   start 0x100000 (%0x100 = 0)   end+1 0x200000 (%0x100 = 0)
  CP31 OM642    start 0x190000 (%0x100 = 0)   end+1 0x1FCFFC (%0x100 = 0xFC)

The end only needs to be dword-aligned, since the checksum IS the last dword
of the region. REGION_END_ALIGNMENT drops to 4. To keep the guard against a
stray occurrence of the magic just as tight, two constraints are added in the
same place: a 4 KB minimum region length, and the requirement that the
signature lives inside the region it announces. The start alignment stays at
0x100 - that is the constraint that actually pins the signature's position.

Verified on the real CP31 dump (OM642, SW 1037393817): single descriptor at
0x19003C, region 0x190000..0x1FCFFB, 111615 dwords summing to exactly
0xD01FE500 with the stored word 0x0375D4D9 at 0x1FCFF8. Stock file -> OK,
found 1, fixed 0. One byte changed in the Boost Target -> FAIL, fixed 1, and
the region revalidates after correction. So the CORRECTION path is now
confirmed, not just verification.

VAG non-regression checked on synthetic regions: a 0x100-aligned end is still
accepted, while a non-dword end, a non-0x100 start, a sub-4KB region and a
lone magic in noise are all still rejected.

docs/PORTING-EDC16CP31.md said this worked out of the box. It did not, and
the section now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
… VAG

The editor showed "3%" on a healthy Mercedes dump. build_expected_report
routes anything containing EDC16 to the VAG rule set - Duration 00..05,
Duration Selector, Gearbox Torque Limiter, SVRL, Smoke Limiter by
MAF/MAP/Lambda, N75 duty cycle. None of those families exist under those
names on a CR4 Mercedes, so the score measured the file against the wrong
rulebook and told the user their original file looked heavily modified.

CP31 now gets its own report over the eleven calibrated families. Expected
counts are 1 each, deliberately: the corpus is one software build, and
turning its per-family counts (3 EGR, 7 SOI, 3 boost...) into expectations
would flag a legitimate different build as modified. Presence is the only
invariant a corpus of one supports. Tighten it when a second software joins
the corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
Three TDI-shaped assumptions got in the way of the CLK 320 CDI.

1. Brand list. project-creator only offered Audi/Seat/Skoda/Volkswagen, so a
   Mercedes project could not record its own brand. Mercedes-Benz is added,
   and one ecu-brand-db entry (Bosch SW 1037393817 -> Mercedes-Benz CLK 320
   CDI W209, OM642) makes the brand auto-fill on import like it does for VAG.

2. Engine presets. Every preset was a VAG TDI, and guessEnginePreset matched
   "edc16" before it matched "3.0", so a CP31 file was estimated as a 1968 cc
   four-cylinder. Adds "3.0 CDI V6 - OM642" (6 cyl, 2987 cc - a different
   displacement from the VAG 3.0 V6) and matches EDC16CP31/OM642 first.

3. The air bound. This is the part that mattered. MIN_AFR = 17 is a
   BLACK-SMOKE bound: it describes what a modified engine swallows before it
   sooties, and it fits the tuned VAG PD files of the original corpus, which
   really do fuel to that limit. A stock Euro 4 common-rail V6 with a DPF runs
   far leaner. It bound the curve because CP31 has no Nm torque limiter for
   the model to use - CP31 limits in QUANTITY - so nothing else shapes the
   full-load line and the constant decides everything. At 17 the reference
   stock dump came out at 299 hp for an engine rated 224.

   CP31_MIN_AFR = 21 puts peak power at 229 hp against 224 announced (+2 %)
   and peak torque at 448 Nm. This is a one-unknown fit, on one file, against
   one published figure - not a bench calibration, and the code says so.
   Published torque for this car does not agree with itself either: Wikipedia
   gives 510 Nm for the 165 kW OM642, two spec databases give 415 Nm for this
   body. 448 sits between them, which is consistent but proves nothing.

Also wires the CP31 quantity ceilings (FlMng_qSmk_MAP, FlMng_qLimT3BPS_MAP)
into the EDC16 fuel path. The existing comment says EDC16 smoke maps are
unusable because their rows are stored mirrored - that is true of the VAG
detectors, not of the family: the CP31 detector reads the self-describing Kf
header and emits rows in file order. They do not bind on the stock file (air
is tighter) but they will on a tuned one, which is the point.
FlMng_qLimBstPres_MAP is deliberately NOT used: it is indexed by a pressure
DIFFERENCE, and feeding it absolute boost collapses the curve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
Adresses de bloc, géométrie, axes et facteurs des onze familles confirmées
sur le dump de référence (OM642 165 kW, SW Bosch 1037393817), plus les
vecteurs d'axes lus dans le fichier.

Données dérivées du damos, pas le damos : ni le .bin ni l'A2L ne sont dans
le dépôt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
Three things the ground rules asked for and I had not done.

"Comments in the Rust engine are English" - my build_expected_report_edc16cp31
doc block and its inline comments were French. Translated. (commands.rs is
already largely French upstream; that is not a reason to add more.)

"Add tests for anything you fix or add" - the CP31 report shipped with none.
Four now: every calibrated family is matched by its prefix, every prefix
actually exists in MAP_TEMPLATES (the two tables live in different files and
drift silently), CP31 does not fall through to the VAG rules, and the VAG
families still get theirs.

And a mistake of mine: inserting the CP31 function directly above
build_expected_report orphaned that function's doc comment onto mine. The
original comment is back where it belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
The Solutions button was gated to EDC15 - getSolutionsForECU returned null
for anything else - so a Mercedes project had none. CP31 now gets one.

What it writes is not invented: it is a byte-for-byte replay of an EGR delete
done by a professional tuner, verified against a REAL pair - the stock read of
the ECU and a read of the same ECU after flashing. 21 ranges, 60 bytes, all
inside the EGR block; nothing touches boost, rail, timing or the limiters.
Applying it to the stock file reproduces the car's calibration exactly, save
the programming signature and checksum word the flashing tool regenerates.

Core: AirCtl_qLo_CUR (EGR shutdown threshold by injected quantity) to zero
everywhere, EGRCD_swtEGROnVal_C 1 to 0, the first row of the duty-cycle
conversion 95 % to 5 %, EGRCD_rInit_C 12 % to 5 %. Diagnostics: the EGRCD
debounce and tiDia timers to 0x0000/0xFFFF, plus four DSM bytes whose damos
identification stays ambiguous - two candidates each. Those four are replayed
because they are part of the verified change, not because their role is
established, and the module says so.

The corpus is ONE software build, so no address is hardcoded. Each group is
anchored on a byte signature checked to be UNIQUE in the dump, and every write
carries the original bytes it expects. Anchor missing, anchor ambiguous, bytes
already different, solution already applied - any of those and it returns zero
patches rather than writing blind. A partial patch would be worse than none.

Verified: 21 patches / 60 bytes, result identical to the car's calibration,
re-applying emits 0, a file with the EGR area wiped emits 0, and the checksum
module reports the file as needing correction afterwards (the editor does it).

Not covered, and the module documents it: the EGR air-mass setpoint maps stay
untouched - the tuner killed the actuator rather than rewriting the targets -
so nothing here guarantees the control-deviation fault path is silenced.

i18n already carried an `emissions` category and an `egr_off` entry; the new
solution gets its own key in all five languages with wording that describes
what it actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
commands.rs states the rule: increment whenever a change alters the results
the detector produces, so projects detected by an older engine are re-scanned
on open. The CP31 calibration went in without a bump - my oversight - and the
family now returns 25 maps where it returned none.

Upstream took 38 for the EDC15VM work in 1.1.6, so this branch takes 39.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
… address

The CP31 detector located families by walking a per-family address window.
That works on the one software build the port was calibrated on and nowhere
else: on another build every zone is wrong, and the detector silently finds
nothing.

Adds a third anchor that does not depend on an address. A MapTemplate can now
carry the exact `[nx][ny] + X + Y` bytes of its block(s); the detector
searches for those bytes over the whole calibration area and hands each hit
to the same `try_block` validation as the other phases. Breakpoint grids are
calibration data a remap has no reason to touch, so a key that matched a
stock file still matches the tuned one.

The keys are not a guess. The ASAP2 package ships the reference binary its
addresses describe, which makes every characteristic readable at its declared
address. Searching the reference axis vectors in the corpus dump resolves 9 of
the 13 calibrated blocks to exactly ONE place each, and that place is the
address the previous structural alignment had produced. The other 4 are
families whose members legitimately share a grid; they were resolved by the
local offset of a uniquely-matched neighbour, which agreed. Zero
contradictions - the damos labels are now verified rather than inferred.

Behaviour on the corpus is unchanged: the same 25 maps at the same addresses
on both the stock and the EGR-off file. Only the confidence of key-confirmed
blocks moves, which is the point - an exact grid match used to score below a
zone-walk guess.

`ignores_a_boost_target_planted_outside_its_zone` planted the REAL Boost
Target grid outside its window and asserted it was ignored. Phase 0 now finds
it, and correctly so: 28 exact breakpoints identify a family far better than
an address range does. The test keeps its invariant - a block that merely
looks like a family is still rejected outside its zone - by planting axes
close to but not equal to the real grid, and the exact-grid case gets its own
test.

Not committed: the reference binary and the ASAP2 file are third-party
licensed material. Only byte patterns derived from the corpus dump are stored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
…lings

Two problems, both found by opening a modified file in the editor.

1. A remapped map disappeared. `score()` range-checks against `z_range_stock`
   unless the caller asks for tuned mode - and nothing in the UI ever asks:
   all three `detectMaps` call sites leave `tuned_mode` at false. So on a file
   whose limiters had been raised, the very maps that were raised fell out of
   the stock range and were not reported. The editor hid exactly what the user
   opened it to look at.

   Fixed where the evidence is strongest rather than by widening the ranges:
   the axis-key phase now validates against the tuned range. Once the whole
   breakpoint grid has matched byte for byte, identity is settled, and the
   stock range no longer serves to identify anything - it only rejects the
   family's own map for having been modified. The zone walk keeps the stock
   range, so nothing else gets looser.

2. `FlMng_qSmk_MAP` and `FlMng_qLimT3BPS_MAP` were reported under one name.
   Same unit, same axes, adjacent addresses - but one is the visible-smoke
   ceiling and the other is exhaust-temperature protection. Reporting them as
   one family means a user raising "the smoke limiter" cannot tell which of
   the two they are editing, and one of those two choices removes a thermal
   protection. They are now separate families with their own grid, zone and
   axis key.

On the corpus: 25 maps on the stock file, 25 on the EGR-off file, and 25 on a
file with the quantity ceilings raised (22 before this change).

DETECTOR_VERSION 40 -> 41: map names change, so CP31 projects need a re-scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
…limit

The port only handled 2D blocks, so one-dimensional calibration was invisible
in the editor. That is not a cosmetic gap: `FlMng_qLimN_CUR` is the ceiling
that ends the power band - flat at 86 mm3/stroke from 1800 to 4000 rpm on the
corpus dump, then collapsing to 1.5 by 4550 - and it is one of three limiters
that have to move together for anything to change at full load. A user could
edit two of them in the editor and never see the third.

Adds `CurveTemplate` / `CalCurve` / `read_curve` for the Bosch
`[nx][X][Z]` layout, and two calibrated families: `FlMng_qLimN_CUR` and
`Rail_pMaxSetSubst_CUR`.

Curves are located by axis key ONLY - there is no zone walk for them, and
that is deliberate. A 2D block is recognised by two axes plus a grid; a curve
has a single axis, so a blind walk would report any ascending vector as
calibration data. `a_curve_without_a_matching_key_is_never_reported` pins
this down: a curve whose rpm axis is perfectly plausible but differs from the
key by ONE breakpoint must be ignored, even in exploratory mode.

Only 2 of the 47 curves that could be uniquely identified against the
reference build are shipped, because only those have an unambiguous ASAP2
conversion. `FlMng_facFullLd_CUR` is left out on purpose and the reason is in
the code: its COMPU_METHOD gives 1/128 while its declared upper limit is 1.0
and the stored values reach 2.0. Showing a number that may be twice the truth
is worse than showing nothing.

`cp31_report_matches_every_detector_family` carried a hard-coded copy of the
family names - the very duplication it exists to catch - and went red on the
new families. It now derives the list from MAP_TEMPLATES and CURVE_TEMPLATES
instead of restating it, so adding, splitting or renaming a family can no
longer break it. The prefix test reads both tables too.

On the corpus: 27 elements instead of 25 (25 maps + 2 curves), identical on
the stock file, the EGR-off file and a file with the quantity ceilings raised.
52 tests pass.

DETECTOR_VERSION 41 -> 42: CP31 projects need a re-scan to pick up the curves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMdNS7jKKmsNwuh49CHag8
DETECTOR_VERSION goes to 43: upstream used 39–40 for the EDC15P display
scaling rename and the EDC15VM MAP/MAF switch, this branch used 39–42 for
CP31, so every project scanned with either lineage is re-scanned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xs3ohatqVeW78mfeSCpb7D
Copilot AI lite review requested due to automatic review settings September 7, 2026 21:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job was not started because the account is locked due to a billing issue.

@LeZed97

LeZed97 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Thanks for this, and for the care that went into it. I read the whole thing and ran it against my bench before answering: the detector and the identifier give byte-identical results to master on every VAG file I have (EDC15P, EDC15VM, EDC16), and the checksum still works exactly as before. So nothing in this PR hurts the existing families, cargo test and tsc are clean, and the way you read the self-describing Kf blocks is honestly better than what the VAG detectors have to do. The porting doc is the kind of thing I wish I had written myself.

I am not going to merge it yet though, and I want to explain why properly, because it is not about the code quality, it is about how I decide a detector is finished. New ECU families are the contribution I want the most, so it is worth being precise.

How the existing detectors were built

Every family in the app went through the same loop. I locate the maps by hand on real dumps with the references I have (WinOLS packs, damos/A2L when there is one, the reference tool's map lists), I extract the structural signatures that identify each family (dimension headers, axis markers and layouts, selector blocks, spacing between maps), then I run the detector on a bench of real files, stock AND tuned, and I compare its output file by file with the references until they match. Then I chase the false positives, category by category, until the output is quiet. A detector is not "done" when it finds the maps on the file it was written on; it is done when it behaves on files it has never seen.

Some numbers, so you see what "finished" means here:

EDC15 and EDC16 VAG: more than 350 dumps all families together, mostly stock, with a share of tuned files, some modified on purpose and some at random, to see how the detector behaves on files that are not stock. On EDC15P the completeness check is 35 rules per codeblock written from tuner knowledge, not from statistics on the files: one EGR map, one driver wish plus two start-quantity maps, one torque limiter, two idle maps, the six injector durations 00 to 05 always with their selector, ten SOI maps, and so on. Every codeblock of the bench is at 100 %, and the gaps against the reference tool are documented and deliberate (hidden families). On EDC16 (U1, U31, U34) the invariants come from experience with these engines: every VAG EDC16 has exactly six main durations 00 to 05 with their selector, every one has the gearbox torque limiter slot, the smoke limiters come by MAF/MAP/lambda, the driver wish is 12 to 16 rpm points by 8 or 9 pedal points with the ninth being kickdown. Those rules are what let the app say "this file has been modified" instead of counting whatever it found.
Marelli MJD6: this is the one I want you to look at. I have 23 files covering every MJD6 variant, the detector matches the reference lists on all of them, and the false positives were cut category by category against a factory damos as ground truth. And I still consider that detector about 70 % done, and I chose not to ship it in the app: I have a stack of stock dumps to run it on first, and only then does the refinement start (families with shared axes, the DTC side, the small partial dumps that needed special-casing I do not fully trust). Being at 100 % on the reference was not enough for me to expose it to users.
That is the bar. Not because I enjoy being strict, but because of what the app is for. ZedSuite is meant to stay simple to understand: you open a dump, you get a short list of maps with a clear name, the right axes and the right units, and you can trust it. The whole value is in that trust. A false positive, or a real map with a wrong axis or a wrong factor, does the exact opposite of what the app promises: the user edits a "boost map" that is a diagnostic table, or reads mbar where the axis is mg, flashes it, and the app is the one that lied to them. One bad experience like that and the tool gets the reputation of being unreliable, which is worse than not supporting the family at all. So I would rather show fewer maps that are right than more maps that are almost right, and every family in the app is held to that.

Where the CP31 stands against that bar

It is calibrated on one dump, one software, one engine. Every axis key is the axis vector of that file, so on another software the key phase finds nothing and the detector falls back to the zone walk, whose false-positive rate nobody has measured yet. And this ECU is not an OM642 ECU: the same CP31 sits on the 4-cylinder OM646 of the same years with a different calibration, and the app works per ECU type, not per engine, exactly like a VAG EDC16 that serves a 1.9, a 2.0 and a 2.5 TDI. A CP31 detector has to behave on every engine the CP31 was fitted to; today the OM646 is declared but was never seen. There are no invariants yet, only "at least one of each family", because with one file you cannot know what a CP31 always carries. I have no Mercedes dump at all, so I cannot even run the bench myself. And the EGR OFF solution replays a tune from that same single file; beyond that, I am not integrating solutions of that kind for now, for several reasons I would rather not go into here. We can maybe talk about it in the future, but that part I will not take regardless of the corpus.

What would get this merged

A corpus. For a family like this I want in the range of 50 files minimum, stock and tuned, across the software versions and the engines this ECU is fitted to, OM642 and OM646 at least, power levels and years. Mercedes dumps are less common than VAG ones, so if you get to 30 real files with both engines represented, talk to me. Forum threads and the usual sources have them; people will also send files once the family is announced as being worked on.
A bench script like the ones I use: run the detector on every file, dump the map list per file, and diff against a reference (WinOLS pack, A2L, or your own verified annotations). The result I need to see is the same kind of table as above: N files, N at the expected map list, and the differences explained one by one.
Invariants from the ECU, not from the corpus. Once you have seen twenty CP31 files you will know what is always there (how many rail pressure maps, how many boost limiters, whether the smoke limiter always comes with its thermal twin, what grid the driver wish always has). Write those into the expected report, that is what turns the completeness badge into something users can trust.
Zero false positives on the VAG bench, which you already have, plus zero on whatever foreign 2 MB files you can find (PSA, BMW EDC16), because your gate runs before the VAG logic.
Drop the EGR OFF solution from the PR. Keep it in your fork if you like, it is well written; no solutions for now, that is a decision on my side, not a judgement on the code.
Until then the family stays out of the released app. I would rather it ships in a version where I can put "EDC16CP31" in the notes and mean it. When it gets there, integration will be done on my side (master keeps one commit per release, so there is no direct merge button here), with your name in the release notes and the README, and I would ask you to stay the reference person for Mercedes reports, since I cannot test them.

Two small things while you are in there: the "PORTING SKELETON" comment in bosch/mod.rs is outdated, and the package-lock version bump should go away, the version lives in the release.

I would leave the PR open and mark it as a draft in the meantime, so it stays the place where this lands. And if you want to talk about all this directly, the corpus, the invariants, how I run the benches, write to me on Instagram or wherever suits you (links on linktr.ee/zedperf), it is easier than a comment thread.

Thanks again, really. This is the best contribution the project has received, and I want it in. Just not at 70 %. Whatever happens next, you officially stay the first contributor on GitHub :)

The estimate reads the boost TARGET map as the air available, and caps fuel
at the family's AFR floor (21:1 on CP31). Both assumptions broke on the OM642
logs: the turbo never reaches the +100 hPa target (2568 hPa measured against
2633 asked, 2430 at 3800 rpm), and a build that accepts smoke runs richer
than 21:1. Two optional inputs in the power modal: a smoke floor (family
default / 19 / 17) and the measured absolute boost, which clamps the air
model at every point. Unset, nothing changes.
@LeZed97
LeZed97 marked this pull request as draft September 9, 2026 11:37
DETECTOR_VERSION 43 (local) et 47 (amont) reconcilies en 48 : les deux lignees avaient attribue les numeros 41-47 independamment, tous les projets sont a re-scanner.
@COSSART-FR
COSSART-FR force-pushed the edc16cp31-calibration branch from 7edde57 to 95cd102 Compare September 9, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants