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.
Files changed (61) hide show
  1. openremap/__init__.py +7 -0
  2. openremap/cli/__init__.py +0 -0
  3. openremap/cli/commands/__init__.py +0 -0
  4. openremap/cli/commands/cook.py +224 -0
  5. openremap/cli/commands/identify.py +180 -0
  6. openremap/cli/commands/patch.py +366 -0
  7. openremap/cli/commands/scan.py +551 -0
  8. openremap/cli/commands/tune.py +378 -0
  9. openremap/cli/commands/validate.py +598 -0
  10. openremap/cli/commands/workflow.py +302 -0
  11. openremap/cli/main.py +127 -0
  12. openremap/tuning/__init__.py +0 -0
  13. openremap/tuning/manufacturers/__init__.py +32 -0
  14. openremap/tuning/manufacturers/base.py +322 -0
  15. openremap/tuning/manufacturers/bosch/__init__.py +142 -0
  16. openremap/tuning/manufacturers/bosch/edc1/__init__.py +0 -0
  17. openremap/tuning/manufacturers/bosch/edc1/extractor.py +359 -0
  18. openremap/tuning/manufacturers/bosch/edc15/__init__.py +3 -0
  19. openremap/tuning/manufacturers/bosch/edc15/extractor.py +253 -0
  20. openremap/tuning/manufacturers/bosch/edc15/patterns.py +186 -0
  21. openremap/tuning/manufacturers/bosch/edc16/__init__.py +3 -0
  22. openremap/tuning/manufacturers/bosch/edc16/extractor.py +423 -0
  23. openremap/tuning/manufacturers/bosch/edc16/patterns.py +300 -0
  24. openremap/tuning/manufacturers/bosch/edc17/__init__.py +3 -0
  25. openremap/tuning/manufacturers/bosch/edc17/extractor.py +410 -0
  26. openremap/tuning/manufacturers/bosch/edc17/patterns.py +265 -0
  27. openremap/tuning/manufacturers/bosch/edc3x/__init__.py +0 -0
  28. openremap/tuning/manufacturers/bosch/edc3x/extractor.py +578 -0
  29. openremap/tuning/manufacturers/bosch/lh/__init__.py +3 -0
  30. openremap/tuning/manufacturers/bosch/lh/extractor.py +364 -0
  31. openremap/tuning/manufacturers/bosch/m1x/__init__.py +3 -0
  32. openremap/tuning/manufacturers/bosch/m1x/extractor.py +632 -0
  33. openremap/tuning/manufacturers/bosch/m1x55/__init__.py +3 -0
  34. openremap/tuning/manufacturers/bosch/m1x55/extractor.py +323 -0
  35. openremap/tuning/manufacturers/bosch/m2x/__init__.py +3 -0
  36. openremap/tuning/manufacturers/bosch/m2x/extractor.py +335 -0
  37. openremap/tuning/manufacturers/bosch/m3x/__init__.py +3 -0
  38. openremap/tuning/manufacturers/bosch/m3x/extractor.py +572 -0
  39. openremap/tuning/manufacturers/bosch/m5x/__init__.py +3 -0
  40. openremap/tuning/manufacturers/bosch/m5x/extractor.py +399 -0
  41. openremap/tuning/manufacturers/bosch/m5x/patterns.py +239 -0
  42. openremap/tuning/manufacturers/bosch/me7/__init__.py +3 -0
  43. openremap/tuning/manufacturers/bosch/me7/extractor.py +525 -0
  44. openremap/tuning/manufacturers/bosch/me7/patterns.py +276 -0
  45. openremap/tuning/manufacturers/bosch/motronic_legacy/__init__.py +0 -0
  46. openremap/tuning/manufacturers/bosch/motronic_legacy/extractor.py +583 -0
  47. openremap/tuning/schemas/__init__.py +0 -0
  48. openremap/tuning/schemas/analyzer.py +94 -0
  49. openremap/tuning/schemas/patcher.py +192 -0
  50. openremap/tuning/services/__init__.py +0 -0
  51. openremap/tuning/services/identifier.py +90 -0
  52. openremap/tuning/services/patcher.py +356 -0
  53. openremap/tuning/services/recipe_builder.py +272 -0
  54. openremap/tuning/services/validate_exists.py +334 -0
  55. openremap/tuning/services/validate_patched.py +273 -0
  56. openremap/tuning/services/validate_strict.py +264 -0
  57. openremap-0.1.0.dist-info/METADATA +189 -0
  58. openremap-0.1.0.dist-info/RECORD +61 -0
  59. openremap-0.1.0.dist-info/WHEEL +4 -0
  60. openremap-0.1.0.dist-info/entry_points.txt +2 -0
  61. openremap-0.1.0.dist-info/licenses/LICENSE +21 -0
openremap/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ openremap — ECU binary analysis and patching toolkit.
3
+
4
+ Diff, validate, and apply tuning recipes to automotive ECU binaries.
5
+ """
6
+
7
+ __version__ = "0.1.0"
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)