openremap scan-maps
Structural calibration-map scanner: finds monotonically-increasing 16-bit sequences (RPM/load breakpoints) and the 2D data tables that follow them — no manufacturer identification required. Works on any binary regardless of ECU family.
New here? Read the plain-English introduction first.
It is the same scan you can run from
Python code or JSON-RPC (there the method is
called scan_maps).
Usage
openremap scan-maps <FILE|DIRECTORY> [OPTIONS]
| Argument | Required | Description |
|---|---|---|
PATH |
Yes | An ECU binary (.bin / .ori / .hex / .s19 / .srec / .mot) or a directory of binaries to scan. |
With a directory, use --recursive and/or --export for batch work (see
below).
Options
| Option | Short | Default | Description |
|---|---|---|---|
--top N |
-n |
20 |
Number of top-scoring tables to show per file. |
--min-score S |
-s |
0.85 |
Minimum table score in [0, 1]. Higher = fewer false positives. |
--region RANGE |
-r |
(calibration region) | Restrict scanning to a byte range: 0xSTART-0xEND or START-END. Overrides the calibration-region default. |
--whole-file |
off | Scan the whole file instead of only the detected calibration region. | |
--classify |
off | Annotate tables with probabilistic content labels (fuel 0.72, …). |
|
--xrefs |
off | Add the code-reference signal (see below). Adds ~4 s per file on a 4 MB EDC17. | |
--max-series-tables N |
16 |
Max consecutive shared-axis tables to probe after each anchor. 1 disables. |
|
--show-series |
off | Group tables that share identical X/Y axes with indented └─ continuation rows. |
|
--export PATH |
— | Export found tables as CSV files. With a directory: one sub-folder per file. | |
--recursive |
-R |
off | Recurse into sub-directories when scanning a directory. |
--verbose |
off | Per-file progress output in batch mode. | |
--json |
off | Output as JSON instead of human-readable text. | |
--help |
Show help and exit. |
Calibration-region default
By default the scan is limited to the calibration region — the flash
area the layout segmenter labels as calibration. Junk tables found in
code / erased / mixed sectors are hidden and counted (in JSON as
tables_hidden); the human output notes them:
304 table(s) outside the calibration region hidden — use --whole-file to scan the whole file.
--whole-filescans everything (code-sector junk included).--regionoverrides the default entirely — an explicit range wins.- No calibration signal (small / synthetic / unfamiliar binaries) falls back to whole-file behaviour automatically — the default never hides tables where it cannot find a calibration region.
The layout estimate is structural inference, so it can be wrong on unusual binaries — but a wrong estimate only changes what the report shows, never recipe or patch output.
Shared-axis tables
Real ECUs often place several calibration tables (fuel, timing, boost, EGR…) consecutively after a single pair of RPM × Load axes:
[RPM axis][Load axis][Fuel table][Timing table][Boost table][EGR table]
The scanner detects the first table, then probes forward for additional
blocks with identical dimensions sharing the same axes. Each block must
pass the same scoring as the anchor — garbage or mismatched geometry stops
the series. Use --max-series-tables 1 to report only the primary table
per axis pair, and --show-series to see the grouping.
Code references (--xrefs)
With --xrefs, the code regions are disassembled (capstone) and every
byte offset a real instruction statically references is collected. A table
whose data block is referenced by code is almost certainly a genuine
calibration map:
- its score gets a small bonus (+0.06, capped at 1.0);
- it is marked
⟶codein the human listing; - JSON gains a top-level
xrefssummary (status,arch,base_address,reference_count, …) and anxrefevidence block on each table (referenced_by_code,data_refs,axis_refs,insns).
Safety contract:
- Presence-only — a table is never demoted for lacking a reference (most ECU code reaches maps via base-register addressing that is not statically resolvable). Absence proves nothing.
- Arch-gated — supported families only: EDC17 (TriCore) and Denso/Hitachi SuperH; everything else skips silently.
- Load base auto-detected — the address base is inferred per file from table-data hits; no hardcoded manufacturer knowledge.
Cost: ~4 s per file on a 4 MB EDC17 — opt-in for batch scans.
Scoring
Each 2D table is scored on a multi-dimension heuristic: axis quality (smooth, monotonic breakpoints), table smoothness (row/column gradient continuity — real calibration data is smooth, code is not), dimensionality (plausible map dimensions) and a stripe penalty (repeated rows/columns, common in padding).
| Score | Meaning |
|---|---|
| ≥ 0.90 | High confidence — smooth calibration surface, genuine axis values |
| 0.85–0.90 | Plausible — likely a real calibration map |
| 0.75–0.85 | Mixed — some real maps, some encoded data |
| < 0.75 | Low confidence — mostly coincidental structures |
The default --min-score of 0.85 keeps >90% of genuine maps with far
fewer false positives than 0.75. Lower it to 0.55 for exhaustive
scanning; raise to 0.90 for the cleanest signal.
Health signal
| Axes found | Signal | Meaning |
|---|---|---|
| ≥ 1,000 | ✓ Genuine calibration binary | Expected for a real ECU |
| 100–999 | ⚠ Few axes | Possibly corrupted, trimmed, or a partial dump |
| < 100 | ✗ Very few axes | Likely encrypted, non-ECU, or empty |
Example output
original.bin
✓ Genuine calibration binary
15,967 axes • 1,985 tables • 4,194,304 bytes
Offset Dim Cells Score X Axis Y Axis
──────────────────────────────────────────────────────────────
0x000376F2 32×16 u16 LIT 0.977 0x00037692 0x376D2
0x002214D4 32×6 u16 LIT 0.965 0x00221488 0x2214C8
… and 1983 more. Use --top 1985 to see all, or --min-score 0.8 to filter.
JSON output
openremap scan-maps ecu.bin --json --top 5
{
"file": "ecu.bin",
"file_size": 4194304,
"axes_count": 15967,
"tables_count": 1985,
"layout_filtered": true,
"tables_hidden": 304,
"tables": [
{
"offset": 227058,
"cols": 32,
"rows": 16,
"cell_width": 2,
"byte_order": "little",
"x_axis_offset": 226962,
"y_axis_offset": 227026,
"score": 0.977
}
]
}
layout_filtered is true when the calibration-region default applied;
tables_hidden counts tables outside it (both false/0 with
--whole-file). Axes are capped at 200 in JSON output to keep the payload
reasonable. Offsets are always absolute file offsets, regardless of
--region.
Batch mode
# Scan a whole folder, recurse, and export one CSV per file
openremap scan-maps ./my_bins/ --recursive --export ./csv_exports/ --verbose
Notes
- The scanner detects consecutive
[X axis][Y axis][data]layouts and shared-axis table series. It does not know whether a table is fuel, timing, boost or EGR — that is what--classify(best guess with a label) and you, the tuner, are for. - Runtime is ~0.5–3 seconds per file for a typical 1–4 MB binary.
See also
- scan-maps — API — the same scan from Python and JSON-RPC
- layout — CLI — finding the calibration region first
- identify — CLI — identify before you scan