Repository navigation
Releases
0.9.0
0.9.0 — 2026-10-24
This file accumulates everything landed since 0.7.7. There is no 0.8.0: that
line was the cross-firmware relocation experiment, and it was dropped on purpose —
moving a calibration between firmware revisions is only safe with a professional
tuner validating the result, and without one the output would be dangerous by
default (the reasoning is in the README roadmap). Three things changed shape:
- the apply side — a new engine with an explicit identity policy, built on the
schema 5.0 recipe specification it reads, exposed as a ladder of four commands
(tune-strict→tune-shift→tune-unique→tune-any) with a matching pair
of producers (cook-strict/cook-forced). The engine was verified against the
flag-driventunebyte-for-byte on 47 real pairs before that command was
deleted, which is why the old code is gone rather than deprecated; - the TUI — the 2 625-line single module was retired and rebuilt screen by
screen on top of the API gate: BOOT, HOME, IDENTIFY, INSPECT, MAPS, COOK, DIFF,
APPLY, BATCH, CONTRIBUTE and HELP; - the contribution wizard — a screen, a wider dump intake, and a generated
extractor that now measures the identity instead of leaving fourTODOs.
Added
-
The DIFF screen — what changed between two dumps, table by table. COOK
answers "make this pair portable"; DIFF answers the question that comes first,
and it is now a screen of its own (HOME card 3) with the pair shared in both
directions:[D]in COOK hands the two files over,[C]in DIFF hands them
back. The summary never lets the explained part stand in for the whole — the
reference pair has 34 changed tables and 69 changed regions no table accounts
for, and both are shown, with[U]listing the second and[⏎]rendering
both sides of a region byte by byte.[D]opens the selected table as
stock / tuned / Δ per cell against its measured breakpoints (a view the CLI
cannot give at all);[P]writes both sides as SVG using those same
breakpoints;[X]writes the delta as long-form CSV with breakpoints. Filters
(changed/realigned/no axis/all) keep exactly what their labels
say, and axis-backed tables rank above inferred ones — ano axistable's
-100%is computed against row shapes the scanner guessed and must not lead.
A region's size is printed as "largest N B", never as a changed-byte total: a
region is an extent, and a number that cannot be reconciled is worse than two
honest ones. Nothing is named anywhere on the screen. -
The product is now tested off Linux. Wheels were built for macOS and
Windows, but the suite had only ever run on Ubuntu — so "the TUI works on
Windows" was an assumption. Aportabilityjob now runs the TUI and console
tests onwindows-latestandmacos-latest. It is advisory (continue-on-error)
until it has been green twice, because the suite was written on Linux and the
first run is worth reading rather than obeying. -
Console output is UTF-8 by default. On Windows a redirected stream uses the
ANSI code page, soopenremap identify x.bin > out.txtraised
UnicodeEncodeErroron the first em dash — and the copy is full of→,✓,
⚠,Δon purpose.openremap.console.force_utf8()reconfiguresstdout/
stderr(witherrors="replace", so a glyph a terminal cannot draw becomes
?instead of killing the command) and is a no-op on a UTF-8 stream. Wired into
the CLI entry point and the JSON-RPC transport; the TUI's own terminal handling
is left alone. -
The picker lists the drives on Windows. A tree rooted at
C:\Users\…
can only climb as far asC:\— no arrow key leaves a drive — so the drives are
named in the header and typed into the path field (D:, normalised toD:\).
No jump key is invented: in this screen a printable key belongs to the path
field. The row is hidden off Windows, where drive letters are not a concept. -
CI runs the TUI off Linux — and that is how these were found. The new
portabilityjob (windows-latest + macos-latest) caught two real defects in the
picker on its first run, plus one test that only worked because the corpus was
present locally.D:is rewritten toD:\\only when it is a bare drive spec;
the first version also rewrote UNC paths. The rule moved into
normalize_typed()— pure text in, text out — because two tests in that file
assertedstr(_typed_path(...)), which runs throughWindowsPathand therefore
measured the platform, not the rule. The drive row's test asserted "hidden" — true on POSIX,
false on Windows — and now forces both directions. AndTestClearingin
test_recent.pyused the corpus without the guard its siblings had, so it failed
on every platform wheretests/data/is absent, which is all of CI. -
The file picker's path field stopped failing silently. Typing a folder that
does not exist left the tree exactly where it was, with no message at all —
against the picker's own stated rule that a rejected file is always named. It
now saysno such folder: …and stays open. The same path also stops mangling
the Windows idiom for changing drives:D:is drive-relative there (meaning
"the current directory on D:", often not a directory at all), so it is read as
D:\— while on Linux and macOS the identical text is a filename and is left
alone. -
hex_region— a byte range of a binary as hex, so the TUI never opens a
file itself.map_imageaccepts the caller's breakpoints (x_axis/
y_axis) instead of re-deriving them from a descriptor that may not carry the
offsets: the DIFF screen already measured them, and the picture must not fall
back to cell indices while the grid next to it shows real breakpoints. -
The apply side says out loud that it does not correct checksums. Every
apply command printedAPPLIED, a sha256 and the bytes written — all true, and
all about the recipe. Nothing in this engine rewrites a stored checksum, so
the written file is expected to be stale wherever the family carries one, and
the operator learned that from the ECU instead of from us. The report now
carries a measured verdict on the bytes it produced
(report.result.checksums:ok/fail/skip), shown by the CLI and by the
TUI'sCHECKSUMSrow, together with the command that re-checks the written
file. It is a disclaimer, never a refusal: a stale checksum does not make the
recipe wrong, it makes the file unflashable.skipis the honest answer for a
family whose checksum we have never cracked (Bosch EDC17 among them) and is
never presented as a pass. Cost: one detector pass, 22–62 ms on a 4 MB dump.
Every successful apply also prints a standing disclaimer, set apart from the
check rows: OpenRemap does not correct checksums automatically, a tune that
will run in a real vehicle must be reviewed by a dedicated tuner shop, and the
supported use is right to repair and track racing — not modifying an everyday
car. It is a constant in the report (result.disclaimer), so the CLI and the
TUI cannot drift apart on it. -
The wizard now says what your dumps can prove. A marker is "stable across
your dumps", and that is evidence of a family signature only if the dumps are
different calibrations — two dumps of one calibration cannot, with any
algorithm, tell a family token from that calibration's version string. Measured
on two real families: Bosch EDC17 with two dumps of one calibration produced an
extractor covering 0 of 17 known dumps; two dumps of different calibrations
produced one covering 14 of 17, exactly the set the hand-written marker
covers. Denso SH7058: 22/35 from one model year with a marker made of
repeated data, 35/35 from two dumps six years apart using the ECU's own OS
banner. Soanalyzenow reportscalibration_diversity
(same-calibration/different-calibrations/near-identical/
distinct-builds, by identity when the family is known and by shared bytes when
it is not), the CLI and the CONTRIBUTE screen print it before the candidates,
and the PR description gains a What your dumps can prove section. The intake
copy changed with it: the advice was "two or more dumps", which counts dumps
when what matters is that they are different calibrations. -
A repeated data pattern can no longer lead the candidate ranking.
signatures.repeated_motif()flags a run built from one short motif (≥6
repetitions over ≥60% of its length, any alignment): the measured
'gCaCA$3:f3F$&j4j3j#WD$3TD$3TD…'— 152 printable bytes, 120 of them$3TD—
ranked first and was recommended. It stays in the list with its reason, like
every other generic run, but it can no longer be the recommendation. -
The contribution wizard measures the identity, not just the marker. The
generated skeleton used to ship with everyextract()field asNoneand a
TODO— a shape, not a contribution — while the wizard had already measured the
very fields it left blank. It now fills software version, hardware number,
variant and calibration version from the pattern that found them in the
contributor's dumps, so the skeleton'smatch_keyagrees with the engine's from
the first run. The pattern is what travels, never the measured value: a value
copied in as a literal would match exactly one calibration, which is the same
mistake the wizard exists to prevent incan_handle(). Whatever the dumps could
not decide stays aTODO, and the report lists which. The PR description gains
a Fields measured section, because the measurement a reviewer most needs
(the ident descriptor carrying variant and calibration version) used to reach
nobody: it was in the JSON and in none of the files handed over. -
contribute extractortakes as many dumps as you have. The positional
argument is var...
0.7.7
0.7.7 — 2026-10-17
This file accumulates everything landed since the 0.7.6 release; 0.7.7 is the
next release. It starts with correctness fixes surfaced while reviewing the
remaining items of the 0.7.6 post-release audit, plus a layer/cleanup pass. Two
of the fixes change detection/scan results on real binaries; all of them are
covered by new regression tests. It then adds the synthetic dump generator and
the golden corpus built on it, and the contribution wizard — the tooling half of
this release, which is what a new ECU family now arrives through.
Added
- Contribution wizard (
openremap contribute) — the engine that turns "I own
a dump of an unsupported ECU" into a pull request. The problem it solves is not
writing the extractor: it is knowing which bytes are stable in that family
and absent from every family the engine already recognises, which nobody can
answer by reading their own file. Two verbs:openremap contribute extractor dump.bin --also dump2.bin --brand X --family Y
measures: who claims the dump today, ranked candidate signatures each tested
against every known family, collision offsets, and candidate identity fields
split into constant in the family (belongs incan_handle()) versus
varies per dump (what amatch_keyneeds).--writeadditionally emits
four artifacts: the fixture recipe that goes in the PR instead of the
dump, an extractor skeleton that already satisfies the review bar
(measured marker, size gate, exclusion guards harvested from the colliding
family's own ident token, evidence tags on everyreturn False), the
fixture-driven test, and a PR report with a Not verified section.openremap contribute check <extractor.py>runs the same bar over an
existing extractor: it still claims its own fixture, it accepts no other
family's, a rejected binary leaves no stale evidence,supported_families
matches whatextract()reports, everyFalsepath clears the evidence
(checked by parsing the module, not by pattern-matching text), and the
extractor is reachable from the registry.
The wizard executes the skeleton it just rendered against all 89
known-family fixtures before calling anything verified, and exits non-zero when
no marker is collision-free or no guard can work — a generated file that has
never been run is worse than no file. It never writes the contributor's dump
anywhere, and it refuses a marker that does not occur in their own dumps (that
would produce a working fixture for a signature the family does not carry).
Business logic inopenremap/core/services/wizard/, thin gates in
openremap/api/commands/contribute.pyandopenremap/core/cli/commands/.
Documented where a reader actually looks:README.md(the Supported ECUs
section, where somebody checks whether their car is covered, and the
Contributing section with the two commands) and
contributing/extractors.md. The known-family fixtures are the repository's own
_dev/contrib/recipes/— committed, so anybody with a checkout has them, and
deliberately not copied into the package. An installed wheel therefore has
no recipes, and the default refuses with instructions rather than degrading
silently;--measure-only(the service'smeasure_only) is the explicit opt-in
for the half that is pure local computation — who claims the dump, the runs
stable across the dumps, the family name the dump suggests — with no collision
check, no recommended marker and no artifacts, and it says all three. It also
needs neither--brandnor--family: a first look should not require the
answer it exists to produce (those two name the family the extractor declares,
so they stay required for the full run). Same
strict-by-default shape ascook --allow-non-unique. Contributing means opening
a pull request, which needs a clone anyway; a second copy of the fixtures inside the wheel was tried and
removed, because it duplicated the knowledge and made the wizard blind to a
recipe it had itself just written.
- Synthetic dump generator (
openremap.core.synth) — builds ECU-like
binaries from scratch: flash layout, calibration axes, smooth map surfaces,
a known ground truth, and no byte of any real ROM. Because it is
deterministic (same parameters + seed → identical bytes), the file can be
regenerated everywhere instead of shipped: usable as a test fixture, a demo,
or a reproducer in a bug report — none of which was possible before, since
real dumps can neither be committed (copyright) nor handed to users.
Exposed three ways, like every other feature:generate_dumpover the API /
stdio RPC (payload base64) andopenremap synth -o file.binon the CLI.
Generated files carry no manufacturer fingerprints, soidentifyreports
Unknownfor them — by design, and pinned as such. - Golden corpus over synthetic dumps (
tests/corpus/) — cases are
parameter sets inmanifest.json, so the corpus is committable (~50 KB for
three cases) and runs in CI, with no ECU data and no personal data
involved. Eachcases/<id>/golden.jsonpins: input hashes, planted maps,
identity, detected map candidates and their scores, the cooked.remap
recipe (wall-clock fields normalised), the re-patch result — including the
round-trip propertyapply(cook(a, b), a) == b— and both refusal layers
for a tampered recipe (ctx→ the patcher refuses with an actionable
message;ob→ strict validation refuses silently; either way nothing is
written). Regeneration is a reviewed act:_dev/corpus/update_golden.py,
read the diff, commit it with the change that caused it.
Fixed
- VAG part-number scan no longer stops at an all-digit lookalike. The shared
scanner (manufacturers/bosch/_vag.py::find_vag_part_number, used by the
EDC15/EDC16/EDC17/EDC3.x extractors) took the first structurally matching
run in the dump and applied its "a part number must contain a letter" guard
afterwards. An all-digit Bosch calibration code of the same shape sitting
earlier in the file therefore ended the search: the dump reported no part
number at all even though a valid one existed further on. The scan now walks
every structural match and returns the first that contains a letter.
Corpus sweep (1,685 files, before/after): exactly two fields change, both
gains — two VW Crafter 2.5 TDI EDC16 dumps that reported no OEM part number
now resolve074906032ATand074906032AG(both confirmed by their source
filenames). No other field moved anywhere in the corpus: no dump lost a part
number, and nomatch_keyor family changed. scan_map_tablesno longer serves a stale cached result. The two-stage
map-scan caches were keyed on(path, mtime, size)alone, so parameters were
ignored whenever the cache was warm: a second call on the same file with a
differentmin_table_scorereturned the first call's tables (reproduced:
0.55→ 120 tables, then0.99→ the same 120 instead of 0). Both caches
now include every parameter that shapes the result, so an identical request
still hits the cache while a changed one recomputes.- Two-stage map scanning decodes HEX/S-Record containers.
scan_map_axes
andscan_map_tablesread the file with a raw byte read, bypassing the shared
loader, so a text Intel HEX / Motorola S-Record input produced axes over ASCII
text while every other command decoded it first. Both stages now load through
the API's sharedload_binary. CONTRIBUTING.mdno longer points at a layout that was deleted. Eleven
references tosrc/openremap/tuning/manufacturers/…— including the import
line in the copy-paste example — survived the package re-layout, so the
public "Adding a New ECU Extractor" guide dead-ended at step 1: the directory
does not exist and the module does not import. Paths now read
openremap/core/manufacturers/…. While repairing it, two more gaps in the
same guide were closed: the worked example now shows the evidence-tag
contract (_set_evidence()plus tags such asSIZE_MATCH,
EXCLUSION_CLEAR,FAMILY_STRING) that the confidence scorer depends on and
which the guide had omitted entirely, and the test-tree snippet no longer
invents a per-brand sub-directory undertests/tuning/manufacturers/. Two
further drifts in the same file were corrected: the "Submitting a Pull
Request" steps saidmasterwhile the default branch ismain.CONTRIBUTING.mdis now an index, not a textbook. It had grown to 311
lines of which ~130 were the extractor guide alone — the loudest domain had
swallowed the page, and every other area of the codebase (checksums,
decoders, map scoring, instruction scanners, tests) was compressed into a
table row or missing. It is now an intro plus the binding cross-cutting rules
(safety notice, setup, code style, PR process, bug reports, binary-file
policy) and routes to per-domain guides incontributing/:
extractors.md,checksums.md,decoders.md,map-scoring.md,
scanners.mdandtests.md. Each guide states where the code lives, the
conventions of that area, and what a PR must demonstrate — which is the
part that genuinely differs per domain (an extractor must not steal another
family's claims; a checksum needs family consensus; a decoder needs oracle
parity; a scoring change must show a corpus before/after).
Changed
scan_map_tablesdeclares the axis parameters it already read. The
fallback branch (stage 2 without a prior stage 1) readmin_axis_length,
max_axis_length,min_stepandmax_step, but none were in the method's
input_schema— invisible tolist_methodsconsumers and exempt from the
registry's default/validation handling. All four are now declared, matching
scan_map_axes.- RPC
list_methodshonourskind. The...
0.7.6
0.7.6 — 2026-09-06
This release turns two previously-unidentified lines into full identities and
fixes VAG part-number detection: a new ME7.9.x Bosch extractor, a new Siemens
GS20 (BMW AGS) extractor, and a shared, false-positive-proof VAG part-number
scanner for the EDC15/EDC16/EDC17/EDC3.x extractors. All changes were
validated against the full corpus before and after, with zero regressions.
Added
- ME7.9.x (ME7.9.10) extractor — new Bosch family covering the
Fiat/Alfa/Lancia 1.4 T-Jet and PSA variants (2006+). These 832KB dumps
were previously unidentified: their ident block at 0x10000 uses the
ZZ<<dialect, which the classic ME7 extractor'sZZ<digit>guard
deliberately skips. The new extractor keys on that dialect plus the
ME7910/ME7.9family markers in the calibration area, and extracts the
10-digit SW number, the0261…/0261S…HW number and the match key
(ME7.9.10::<sw>). Registered right after the ME7 extractor — the
corpus-wide claims sweep shows it claims only the two dumps that were
previously unclaimed (no detection reordering anywhere else). - Siemens GS20 extractor — new family covering the 4 previously
unidentified 64 KB dumps (tests/data/ECUs/Siemens/GS20/GS20_90C0_64KB*).
Oracle ground truth (ms4x wiki + a community BMW flash-tool corpus): GS20
is a Siemens BMW automatic-gearbox controller (AGS), HWNR7544721.
Detection is a 64 KB
size gate + the header codeGADC9010+ the footer project string
G2210_0090C0AD10(both corpus-unique;G2210_is the oracle's GS20
ident key). Extractssoftware_version(7-digit7557997) and
calibration_id(GADC9010) with match keyGS20::<sw>;hardware_number
staysNonebecause the oracle HWNR7544721is not stored as ASCII in the
dumps. Field mapping is heuristic — the oracle's GS20 ident numbers are all
7-digit (HW/ZB/SW), so7557997is kept as the version identifier for a
stable match key (documented in the extractor). A before/after corpus scan
(1,685 files) shows exactly those 4 files flip from unidentified → GS20 and
nothing else changes. analyze --xref-mode v2— opt-in code-reference mode whose confirmation
bonus is score-gated: a table earns the +0.06 only when it is already
high-confidence (structuralscore >= 0.85, the corpus-validated
"calibration, never code" floor) AND referenced by code anywhere in its data
block. Decoder calibration (load-base detection + arch cascade) stays
byte-identical to the defaultv1. Measured on 19 real corpus bins:
base/arch unchanged (19/19), bonus tightened 396 → 40 with all 40 high-score
referenced maps kept (median boosted score 0.767 → 0.882). The design
evolution (v2.0 span-gating → v2.1 first-cell → v2.2 score-gated) is
documented incore/services/maps/xrefs_v2.py.- Declarative workflow engine — a small engine (
openremap/api/workflows/)
turns.tomlstep lists into registered methods withkind="workflow". A
workflow is a linear, unconditional pipeline: each step is acallto a
registered method with$input.*/$prev.*/$<as>.*data plumbing, and
the last step (or an explicitoutput) is the result. Built-in workflows
auto-load on import; extra dirs load viaload_workflows()or
OPENREMAP_WORKFLOW_DIRS. Shipsidentify_cook(identify both binaries →
cook) as the reference workflow. No code execution — dropping a.tomlis
the third-party extension surface. Malformed or invalid.tomlfiles in
user-supplied dirs are skipped with a warning (the built-in workflows fail
fast), so one bad extension file cannot breakimport openremap.api.
Changed
- diff-maps cell machinery moved to Rust —
_read_cells(strided path),
_best_alignment, and_diff_cellsincli/commands/diff_maps.pyare now
thin wrappers over a new_rs/src/maps/diff_cells.rsmodule
(read_cells/best_alignment/diff_cells). The new Rustread_cells
is a shared primitive that also replaces the duplicated_read_cellscopies
inmap_classifier.pyandmap_exporter.py. The per-cell Python loops are
gone (~1.2 s of the ~5.9 sdiff-mapsCLI on the 4 MB pair, est. ~50–100×);
parity is byte-exact — verified 1:1 against the original pure-Python
implementations on the real 4 MB tune pair (5,257read_cellstables incl.
398 strided, 2,432best_alignmentpairs, 2,432diff_cellspairs, the
latter bit-exact including the raw unrounded floats) plus ME7.1.1 and MS43. - Shared VAG part-number scanner (
manufacturers/bosch/_vag.py) — a
letter-guarded regex used by the EDC15/EDC16/EDC17/EDC3.x extractors'
OEM part-number resolution. It fixes two real gaps:- part numbers with a digit at position 3 (
038906016L,
045906019BQ,022906032CS) were missed by the old
0[2-9][A-Z]…pattern; - platform-code prefixes (
4B0906018DF,3B0907401) were missed
entirely.
The letter-content guard keeps all-digit runs (Bosch HW0261…/0281…,
SW1037…, 9-digit calibration codes) out — a 654-file sweep across
non-VAG families shows zero false positives, down from the naive
broadening's 3.
- part numbers with a digit at position 3 (
- EDC15 non-Format-D dumps (ALH/AGR etc.) and EDC3.x dumps whose ident
parsers produced no OEM number now fall back to the shared scanner. - Denso Subaru family relabel — the petrol
ecu_familylabels were one
CPU generation off (confirmed by two independent oracles). Detection is
unchanged (each size bucket still maps to one clean CPU); only the names
shifted:SH7055(160/192 KB) →68HC16Y5(Motorola HC16),SH7058
(512 KB) →SH7055(SuperH),SH72531(1 MB) →SH7058(SuperH). The
arch table no longer disassembles the 16-bit units as SuperH (capstone has
no HC16 support), and the health map-count envelopes were re-keyed to the
new names. Verified on the full corpus before/after — zero claim changes. - API thin-gate refactor (internal, no output change) — every
*_binary/*_recipe/*_one/*_imagecore function moved out of
openremap/api/commands/+openremap/api/composite/into
openremap/core/services/(the engine), so the API is now only@register
handlers + a single sharedload_binaryhelper. The canonical
to_json_safe()now lives inopenremap/core/serialize.py(re-exported by
api/serialize.py); service functions raise plainValueError/
MergeConflictand the gate translates toGuardError. The
analyze/convertCLI commands now go through the API layer too.
Fixed
oem_part_numberwasnullfor several EDC15/EDC16 dumps that do carry
a VAG part number (e.g.038906019CJ,038906016L) — now extracted and
matching the external reference labels on the validation set
(previously 4 misses, now 0).- Marelli IAW 1AP part number —
oem_part_numberwas alwaysnull; the
extractor now resolves the 10–11 digit part-number run in the code+cal area
(e.g.55598889641at 0x051C0 on the Peugeot 206 dump). Labelled
heuristic — closed sources report the same value minus the leading 5
(5598889641).
Removed
- Dead
openremap-serverJSON-RPC daemon —openremap/server.py(the
"OpenRemap Studio" daemon, long since superseded) is deleted, and the
openremap-serverconsole script is replaced byopenremap-api, which
points at the new stdio JSON-RPC transport
(python -m openremap.api.transport.stdio). The wire format is unchanged
in shape but errors are now JSON-RPC 2.0{code, message}objects instead
of bare strings; every command is dispatched through theopenremap.api
registry (27 methods).