openremap 0.1.0__py3-none-any.whl
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.
- openremap/__init__.py +7 -0
- openremap/cli/__init__.py +0 -0
- openremap/cli/commands/__init__.py +0 -0
- openremap/cli/commands/cook.py +224 -0
- openremap/cli/commands/identify.py +180 -0
- openremap/cli/commands/patch.py +366 -0
- openremap/cli/commands/scan.py +551 -0
- openremap/cli/commands/tune.py +378 -0
- openremap/cli/commands/validate.py +598 -0
- openremap/cli/commands/workflow.py +302 -0
- openremap/cli/main.py +127 -0
- openremap/tuning/__init__.py +0 -0
- openremap/tuning/manufacturers/__init__.py +32 -0
- openremap/tuning/manufacturers/base.py +322 -0
- openremap/tuning/manufacturers/bosch/__init__.py +142 -0
- openremap/tuning/manufacturers/bosch/edc1/__init__.py +0 -0
- openremap/tuning/manufacturers/bosch/edc1/extractor.py +359 -0
- openremap/tuning/manufacturers/bosch/edc15/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/edc15/extractor.py +253 -0
- openremap/tuning/manufacturers/bosch/edc15/patterns.py +186 -0
- openremap/tuning/manufacturers/bosch/edc16/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/edc16/extractor.py +423 -0
- openremap/tuning/manufacturers/bosch/edc16/patterns.py +300 -0
- openremap/tuning/manufacturers/bosch/edc17/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/edc17/extractor.py +410 -0
- openremap/tuning/manufacturers/bosch/edc17/patterns.py +265 -0
- openremap/tuning/manufacturers/bosch/edc3x/__init__.py +0 -0
- openremap/tuning/manufacturers/bosch/edc3x/extractor.py +578 -0
- openremap/tuning/manufacturers/bosch/lh/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/lh/extractor.py +364 -0
- openremap/tuning/manufacturers/bosch/m1x/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/m1x/extractor.py +632 -0
- openremap/tuning/manufacturers/bosch/m1x55/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/m1x55/extractor.py +323 -0
- openremap/tuning/manufacturers/bosch/m2x/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/m2x/extractor.py +335 -0
- openremap/tuning/manufacturers/bosch/m3x/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/m3x/extractor.py +572 -0
- openremap/tuning/manufacturers/bosch/m5x/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/m5x/extractor.py +399 -0
- openremap/tuning/manufacturers/bosch/m5x/patterns.py +239 -0
- openremap/tuning/manufacturers/bosch/me7/__init__.py +3 -0
- openremap/tuning/manufacturers/bosch/me7/extractor.py +525 -0
- openremap/tuning/manufacturers/bosch/me7/patterns.py +276 -0
- openremap/tuning/manufacturers/bosch/motronic_legacy/__init__.py +0 -0
- openremap/tuning/manufacturers/bosch/motronic_legacy/extractor.py +583 -0
- openremap/tuning/schemas/__init__.py +0 -0
- openremap/tuning/schemas/analyzer.py +94 -0
- openremap/tuning/schemas/patcher.py +192 -0
- openremap/tuning/services/__init__.py +0 -0
- openremap/tuning/services/identifier.py +90 -0
- openremap/tuning/services/patcher.py +356 -0
- openremap/tuning/services/recipe_builder.py +272 -0
- openremap/tuning/services/validate_exists.py +334 -0
- openremap/tuning/services/validate_patched.py +273 -0
- openremap/tuning/services/validate_strict.py +264 -0
- openremap-0.1.0.dist-info/METADATA +189 -0
- openremap-0.1.0.dist-info/RECORD +61 -0
- openremap-0.1.0.dist-info/WHEEL +4 -0
- openremap-0.1.0.dist-info/entry_points.txt +2 -0
- openremap-0.1.0.dist-info/licenses/LICENSE +21 -0
openremap/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openremap cook <original> <modified> --output recipe.json
|
|
3
|
+
|
|
4
|
+
Cook a recipe by diffing an original and a modified ECU binary.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
openremap cook stock.bin stage1.bin --output recipe.json
|
|
8
|
+
openremap cook stock.bin stage1.bin --output recipe.json --pretty
|
|
9
|
+
openremap cook stock.bin stage1.bin --context-size 64 --output recipe.json
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from openremap.tuning.services.recipe_builder import ECUDiffAnalyzer
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
help="Cook a recipe by diffing an original and a modified ECU binary.",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_ALLOWED = (".bin", ".ori")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_bin(path: Path, label: str) -> None:
|
|
36
|
+
"""Validate that a path points to an allowed, non-empty binary file."""
|
|
37
|
+
if path.suffix.lower() not in _ALLOWED:
|
|
38
|
+
typer.echo(
|
|
39
|
+
typer.style(
|
|
40
|
+
f"Error: {label} file '{path.name}' must be a .bin or .ori file.",
|
|
41
|
+
fg=typer.colors.RED,
|
|
42
|
+
bold=True,
|
|
43
|
+
),
|
|
44
|
+
err=True,
|
|
45
|
+
)
|
|
46
|
+
raise typer.Exit(code=1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_bin(path: Path, label: str) -> bytes:
|
|
50
|
+
"""Read binary file contents with user-friendly error handling."""
|
|
51
|
+
_check_bin(path, label)
|
|
52
|
+
try:
|
|
53
|
+
data = path.read_bytes()
|
|
54
|
+
except OSError as exc:
|
|
55
|
+
typer.echo(
|
|
56
|
+
typer.style(
|
|
57
|
+
f"Error reading {label} file: {exc}", fg=typer.colors.RED, bold=True
|
|
58
|
+
),
|
|
59
|
+
err=True,
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit(code=1)
|
|
62
|
+
|
|
63
|
+
if not data:
|
|
64
|
+
typer.echo(
|
|
65
|
+
typer.style(
|
|
66
|
+
f"Error: {label} file '{path.name}' is empty.",
|
|
67
|
+
fg=typer.colors.RED,
|
|
68
|
+
bold=True,
|
|
69
|
+
),
|
|
70
|
+
err=True,
|
|
71
|
+
)
|
|
72
|
+
raise typer.Exit(code=1)
|
|
73
|
+
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_summary(recipe: dict, output: Optional[Path]) -> None:
|
|
78
|
+
"""Print a compact summary of the produced recipe to stdout."""
|
|
79
|
+
ecu = recipe.get("ecu", {})
|
|
80
|
+
stats = recipe.get("statistics", {})
|
|
81
|
+
meta = recipe.get("metadata", {})
|
|
82
|
+
|
|
83
|
+
family = ecu.get("ecu_family") or "Unknown"
|
|
84
|
+
manufacturer = ecu.get("manufacturer") or "Unknown"
|
|
85
|
+
match_key = ecu.get("match_key") or "n/a"
|
|
86
|
+
total_changes = stats.get("total_changes", 0)
|
|
87
|
+
total_bytes = stats.get("total_bytes_changed", 0)
|
|
88
|
+
fmt_version = meta.get("format_version", "?")
|
|
89
|
+
|
|
90
|
+
typer.echo("")
|
|
91
|
+
typer.echo(
|
|
92
|
+
typer.style(" ✅ Recipe built successfully", fg=typer.colors.GREEN, bold=True)
|
|
93
|
+
)
|
|
94
|
+
typer.echo("")
|
|
95
|
+
|
|
96
|
+
col = 22
|
|
97
|
+
rows = [
|
|
98
|
+
("ECU", f"{manufacturer} · {family}"),
|
|
99
|
+
("Match Key", match_key),
|
|
100
|
+
("Format Version", fmt_version),
|
|
101
|
+
("Instructions", f"{total_changes:,}"),
|
|
102
|
+
("Bytes Changed", f"{total_bytes:,}"),
|
|
103
|
+
("Original", meta.get("original_file", "?")),
|
|
104
|
+
("Modified", meta.get("modified_file", "?")),
|
|
105
|
+
]
|
|
106
|
+
for label, value in rows:
|
|
107
|
+
typer.echo(f" {label:<{col}} {value}")
|
|
108
|
+
|
|
109
|
+
typer.echo("")
|
|
110
|
+
if output:
|
|
111
|
+
typer.echo(
|
|
112
|
+
f" Recipe saved to {typer.style(str(output), fg=typer.colors.CYAN, bold=True)}"
|
|
113
|
+
)
|
|
114
|
+
typer.echo("")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Command
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.callback(invoke_without_command=True)
|
|
123
|
+
def cook(
|
|
124
|
+
original: Path = typer.Argument(
|
|
125
|
+
...,
|
|
126
|
+
help="The unmodified (stock) ECU binary (.bin or .ori).",
|
|
127
|
+
exists=True,
|
|
128
|
+
file_okay=True,
|
|
129
|
+
dir_okay=False,
|
|
130
|
+
readable=True,
|
|
131
|
+
resolve_path=True,
|
|
132
|
+
),
|
|
133
|
+
modified: Path = typer.Argument(
|
|
134
|
+
...,
|
|
135
|
+
help="The tuned ECU binary (.bin or .ori).",
|
|
136
|
+
exists=True,
|
|
137
|
+
file_okay=True,
|
|
138
|
+
dir_okay=False,
|
|
139
|
+
readable=True,
|
|
140
|
+
resolve_path=True,
|
|
141
|
+
),
|
|
142
|
+
output: Optional[Path] = typer.Option(
|
|
143
|
+
None,
|
|
144
|
+
"--output",
|
|
145
|
+
"-o",
|
|
146
|
+
help=(
|
|
147
|
+
"File path to write the recipe JSON to. "
|
|
148
|
+
"If omitted, the recipe is printed to stdout."
|
|
149
|
+
),
|
|
150
|
+
writable=True,
|
|
151
|
+
resolve_path=True,
|
|
152
|
+
),
|
|
153
|
+
context_size: int = typer.Option(
|
|
154
|
+
32,
|
|
155
|
+
"--context-size",
|
|
156
|
+
"-c",
|
|
157
|
+
help="Number of bytes of context to capture before each changed block (default: 32).",
|
|
158
|
+
min=8,
|
|
159
|
+
max=128,
|
|
160
|
+
),
|
|
161
|
+
pretty: bool = typer.Option(
|
|
162
|
+
True,
|
|
163
|
+
"--pretty/--compact",
|
|
164
|
+
help="Pretty-print JSON output with indentation (default: pretty).",
|
|
165
|
+
),
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Cook a recipe by diffing an original and a modified ECU binary.
|
|
169
|
+
|
|
170
|
+
The recipe captures every changed byte block along with its offset,
|
|
171
|
+
original bytes (ob), modified bytes (mb), and a context anchor (ctx)
|
|
172
|
+
used during patching. The ECU identity block is derived automatically
|
|
173
|
+
from the original binary.
|
|
174
|
+
|
|
175
|
+
Save the output recipe — it is the input for all validate and patch commands.
|
|
176
|
+
"""
|
|
177
|
+
original_data = _read_bin(original, "Original")
|
|
178
|
+
modified_data = _read_bin(modified, "Modified")
|
|
179
|
+
|
|
180
|
+
typer.echo(
|
|
181
|
+
f"\n Cooking recipe from "
|
|
182
|
+
f"{typer.style(original.name, fg=typer.colors.CYAN)} vs "
|
|
183
|
+
f"{typer.style(modified.name, fg=typer.colors.CYAN)} …"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
analyzer = ECUDiffAnalyzer(
|
|
188
|
+
original_data=original_data,
|
|
189
|
+
modified_data=modified_data,
|
|
190
|
+
original_filename=original.name,
|
|
191
|
+
modified_filename=modified.name,
|
|
192
|
+
context_size=context_size,
|
|
193
|
+
)
|
|
194
|
+
recipe = analyzer.build_recipe()
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
typer.echo(
|
|
197
|
+
typer.style(
|
|
198
|
+
f"\n Error: cook failed — {exc}", fg=typer.colors.RED, bold=True
|
|
199
|
+
),
|
|
200
|
+
err=True,
|
|
201
|
+
)
|
|
202
|
+
raise typer.Exit(code=1)
|
|
203
|
+
|
|
204
|
+
indent = 2 if pretty else None
|
|
205
|
+
json_content = json.dumps(recipe, indent=indent, ensure_ascii=False)
|
|
206
|
+
|
|
207
|
+
if output:
|
|
208
|
+
try:
|
|
209
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
output.write_text(json_content, encoding="utf-8")
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
typer.echo(
|
|
213
|
+
typer.style(
|
|
214
|
+
f"\n Error: could not write recipe to '{output}': {exc}",
|
|
215
|
+
fg=typer.colors.RED,
|
|
216
|
+
bold=True,
|
|
217
|
+
),
|
|
218
|
+
err=True,
|
|
219
|
+
)
|
|
220
|
+
raise typer.Exit(code=1)
|
|
221
|
+
else:
|
|
222
|
+
typer.echo(json_content)
|
|
223
|
+
|
|
224
|
+
_print_summary(recipe, output)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openremap identify <file>
|
|
3
|
+
|
|
4
|
+
Identify a single ECU binary and print its metadata.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
openremap identify ecu.bin
|
|
8
|
+
openremap identify ecu.bin --json
|
|
9
|
+
openremap identify ecu.bin --json --output result.json
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from openremap.tuning.services.identifier import identify_ecu
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
help="Identify an ECU binary — manufacturer, family, software version, and more.",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_LABELS: list[tuple[str, str]] = [
|
|
33
|
+
("manufacturer", "Manufacturer"),
|
|
34
|
+
("ecu_family", "ECU Family"),
|
|
35
|
+
("ecu_variant", "ECU Variant"),
|
|
36
|
+
("software_version", "Software Version"),
|
|
37
|
+
("hardware_number", "Hardware Number"),
|
|
38
|
+
("calibration_id", "Calibration ID"),
|
|
39
|
+
("match_key", "Match Key"),
|
|
40
|
+
("file_size", "File Size"),
|
|
41
|
+
("sha256", "SHA-256"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_table(result: dict) -> str:
|
|
46
|
+
"""Render the identity dict as a two-column aligned table."""
|
|
47
|
+
rows: list[tuple[str, str]] = []
|
|
48
|
+
for key, label in _LABELS:
|
|
49
|
+
value = result.get(key)
|
|
50
|
+
if key == "file_size" and value is not None:
|
|
51
|
+
display = f"{value:,} bytes"
|
|
52
|
+
elif value is None:
|
|
53
|
+
display = typer.style("unknown", fg=typer.colors.YELLOW)
|
|
54
|
+
else:
|
|
55
|
+
display = str(value)
|
|
56
|
+
rows.append((label, display))
|
|
57
|
+
|
|
58
|
+
col_width = max(len(label) for label, _ in rows)
|
|
59
|
+
lines = []
|
|
60
|
+
for label, value in rows:
|
|
61
|
+
lines.append(f" {label:<{col_width}} {value}")
|
|
62
|
+
return "\n".join(lines)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _write_output(content: str, output: Optional[Path]) -> None:
|
|
66
|
+
"""Write content to a file or stdout."""
|
|
67
|
+
if output:
|
|
68
|
+
output.write_text(content, encoding="utf-8")
|
|
69
|
+
typer.echo(f"Saved to {output}")
|
|
70
|
+
else:
|
|
71
|
+
typer.echo(content)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Command
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.callback(invoke_without_command=True)
|
|
80
|
+
def identify(
|
|
81
|
+
file: Path = typer.Argument(
|
|
82
|
+
...,
|
|
83
|
+
help="ECU binary file to identify (.bin or .ori).",
|
|
84
|
+
exists=True,
|
|
85
|
+
file_okay=True,
|
|
86
|
+
dir_okay=False,
|
|
87
|
+
readable=True,
|
|
88
|
+
resolve_path=True,
|
|
89
|
+
),
|
|
90
|
+
as_json: bool = typer.Option(
|
|
91
|
+
False,
|
|
92
|
+
"--json",
|
|
93
|
+
help="Output result as JSON instead of a human-readable table.",
|
|
94
|
+
),
|
|
95
|
+
output: Optional[Path] = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--output",
|
|
98
|
+
"-o",
|
|
99
|
+
help="Save the result to a file instead of printing to stdout.",
|
|
100
|
+
writable=True,
|
|
101
|
+
resolve_path=True,
|
|
102
|
+
),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Identify a single ECU binary.
|
|
106
|
+
|
|
107
|
+
Prints manufacturer, ECU family, software version, hardware number,
|
|
108
|
+
calibration ID, match key, file size, and SHA-256 hash.
|
|
109
|
+
"""
|
|
110
|
+
suffix = file.suffix.lower()
|
|
111
|
+
if suffix not in (".bin", ".ori"):
|
|
112
|
+
typer.echo(
|
|
113
|
+
typer.style(
|
|
114
|
+
f"Error: '{file.name}' is not a .bin or .ori file.",
|
|
115
|
+
fg=typer.colors.RED,
|
|
116
|
+
bold=True,
|
|
117
|
+
),
|
|
118
|
+
err=True,
|
|
119
|
+
)
|
|
120
|
+
raise typer.Exit(code=1)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
data = file.read_bytes()
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
typer.echo(
|
|
126
|
+
typer.style(f"Error reading file: {exc}", fg=typer.colors.RED, bold=True),
|
|
127
|
+
err=True,
|
|
128
|
+
)
|
|
129
|
+
raise typer.Exit(code=1)
|
|
130
|
+
|
|
131
|
+
if not data:
|
|
132
|
+
typer.echo(
|
|
133
|
+
typer.style(
|
|
134
|
+
f"Error: '{file.name}' is empty.", fg=typer.colors.RED, bold=True
|
|
135
|
+
),
|
|
136
|
+
err=True,
|
|
137
|
+
)
|
|
138
|
+
raise typer.Exit(code=1)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
result = identify_ecu(data=data, filename=file.name)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
typer.echo(
|
|
144
|
+
typer.style(
|
|
145
|
+
f"Identification failed: {exc}", fg=typer.colors.RED, bold=True
|
|
146
|
+
),
|
|
147
|
+
err=True,
|
|
148
|
+
)
|
|
149
|
+
raise typer.Exit(code=1)
|
|
150
|
+
|
|
151
|
+
if as_json:
|
|
152
|
+
content = json.dumps(result, indent=2)
|
|
153
|
+
_write_output(content, output)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# --- Table output ---
|
|
157
|
+
header = typer.style(f"\n {file.name}", fg=typer.colors.CYAN, bold=True)
|
|
158
|
+
|
|
159
|
+
identified = result.get("ecu_family") is not None
|
|
160
|
+
status_colour = typer.colors.GREEN if identified else typer.colors.YELLOW
|
|
161
|
+
status_label = (
|
|
162
|
+
f"{result['manufacturer']} · {result['ecu_family']}"
|
|
163
|
+
if identified
|
|
164
|
+
else "Unknown ECU — no extractor matched this binary"
|
|
165
|
+
)
|
|
166
|
+
status_line = " " + typer.style(status_label, fg=status_colour, bold=True)
|
|
167
|
+
|
|
168
|
+
table = _format_table(result)
|
|
169
|
+
|
|
170
|
+
full_output = f"{header}\n{status_line}\n\n{table}\n"
|
|
171
|
+
|
|
172
|
+
if output:
|
|
173
|
+
# Strip ANSI codes when writing to a file
|
|
174
|
+
import re
|
|
175
|
+
|
|
176
|
+
plain = re.sub(r"\x1b\[[0-9;]*m", "", full_output)
|
|
177
|
+
output.write_text(plain, encoding="utf-8")
|
|
178
|
+
typer.echo(f"Saved to {output}")
|
|
179
|
+
else:
|
|
180
|
+
typer.echo(full_output)
|