revert: remove EPS flashing integration

This commit is contained in:
nrdr integration
2026-09-10 21:54:20 +08:00
parent cd76e94eba
commit 6f1ab77d3f
70 changed files with 2 additions and 3477 deletions
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Run the NRDR RWD validator from the repository root."""
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
if __name__ == "__main__":
exec_tool("check_rwd.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Run the NRDR EPS diagnostic tool from the repository root."""
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
if __name__ == "__main__":
exec_tool("eps-diag.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Run the NRDR EPS firmware updater from the repository root."""
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
if __name__ == "__main__":
exec_tool("eps-update.py")
-8
View File
@@ -1,8 +0,0 @@
# EPS tools moved
The EPS implementation, firmware images, parser, and documentation now live at
[`openpilot/sunnypilot/nrdr/tools/eps`](../openpilot/sunnypilot/nrdr/tools/eps/README.md).
These compatibility launchers keep existing `eps_tools/...` commands working.
New instructions use the shorter commands from the repository root, such as
`python3 flash.py`.
-14
View File
@@ -1,14 +0,0 @@
"""Compatibility bootstrap for the former eps_tools command paths."""
from pathlib import Path
import sys
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
if str(REPOSITORY_ROOT) not in sys.path:
sys.path.insert(0, str(REPOSITORY_ROOT))
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
__all__ = ("exec_tool",)
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Compatibility launcher for the canonical NRDR RWD validator."""
from _bootstrap import exec_tool
if __name__ == "__main__":
exec_tool("check_rwd.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Compatibility launcher for the canonical NRDR EPS diagnostic tool."""
from _bootstrap import exec_tool
if __name__ == "__main__":
exec_tool("eps-diag.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Compatibility launcher for the canonical NRDR EPS updater."""
from _bootstrap import exec_tool
if __name__ == "__main__":
exec_tool("eps-update.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Compatibility launcher for the canonical NRDR EPS flasher."""
from _bootstrap import exec_tool
if __name__ == "__main__":
exec_tool("flash.py")
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
"""Run the guided NRDR EPS flasher from the repository root."""
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
if __name__ == "__main__":
exec_tool("flash.py")
@@ -1,345 +0,0 @@
"""EPS firmware flasher settings panel."""
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import time
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.widgets.list_view import LineSeparatorSP, button_item_sp
EPS_DIR = Path("/data/openpilot/openpilot/sunnypilot/nrdr/tools/eps")
RUNNER = EPS_DIR / "ui_flash_runner.py"
STATUS_PATH = Path("/data/eps_flash/status.json")
PANDAD_BLOCK_FILE = Path("/data/eps_flash/block_pandad")
if str(EPS_DIR) not in sys.path:
sys.path.insert(0, str(EPS_DIR))
def _load_eps_flash():
spec = importlib.util.spec_from_file_location("eps_flash_module", EPS_DIR / "flash.py")
if spec is None or spec.loader is None:
raise ImportError("Could not load EPS flash module")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
flash = _load_eps_flash()
class EPSFlashLayout(Widget):
def __init__(self):
super().__init__()
self._status: dict = {}
self._last_poll = 0.0
self._last_match_poll = 0.0
self._launch_grace_until = 0.0
self._detected_fw = None
self._matched_images: list[Path] = []
self._selected_image: Path | None = None
self._status_item = button_item_sp(
lambda: tr("EPS Flash"),
lambda: self._status_label(),
lambda: self._status_description(),
callback=self._show_status,
)
self._release_item = button_item_sp(
lambda: tr("Panda USB"),
lambda: tr("RELEASE"),
lambda: tr("Offroad only. Stops pandad so Python can open Panda directly."),
callback=self._confirm_release,
enabled=self._can_release,
)
self._eps_info_item = button_item_sp(
lambda: tr("EPS Info") + " · " + self._eps_info_label(),
lambda: self._eps_info_button(),
lambda: self._eps_info_description(),
callback=self._read_eps,
enabled=self._can_read_eps,
)
self._restore_item = button_item_sp(
lambda: tr("Panda USB"),
lambda: tr("RESTORE"),
lambda: tr("Removes the release lock and lets manager restart pandad."),
callback=self._restore_panda,
enabled=self._can_restore,
)
self._match_item = button_item_sp(
lambda: tr("Detected EPS Firmware"),
lambda: tr("FLASH"),
lambda: self._match_description(),
callback=self._confirm_flash,
enabled=self._can_flash,
)
self._no_match_item = button_item_sp(
lambda: tr("Detected EPS Firmware"),
lambda: tr("NO MATCH"),
lambda: self._no_match_description(),
callback=None,
enabled=False,
)
self._selection_items: dict[Path, object] = {}
self._items = [
self._status_item,
LineSeparatorSP(40),
self._release_item,
LineSeparatorSP(40),
self._eps_info_item,
LineSeparatorSP(40),
self._restore_item,
LineSeparatorSP(40),
self._match_item,
self._no_match_item,
]
for raw_path in flash.find_images():
image = Path(raw_path).resolve()
category = tr("Proper Torque Mod") if "Proper Torque Mod" in str(image) else tr("Stock recovery")
item = button_item_sp(
flash.image_display_name(str(image)),
lambda: tr("SELECT"),
category,
callback=lambda _=None, p=image: self._select_image(p),
enabled=lambda: not self._is_running(),
)
item.set_visible(False)
self._selection_items[image] = item
self._items.extend([item, LineSeparatorSP(40)])
self._scroller = Scroller(self._items, line_separator=False, spacing=0)
self._child(self._scroller)
self._refresh_matches(force=True)
def _refresh_status(self):
now = time.monotonic()
if now - self._last_poll < 0.5:
return
self._last_poll = now
try:
self._status = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError):
self._status = {}
def _refresh_matches(self, force: bool = False):
now = time.monotonic()
if not force and now - self._last_match_poll < 1.0:
return
self._last_match_poll = now
self._refresh_status()
car_fw = flash.car_eps_fw() or self._status.get("detected_fw")
car_norm = flash.norm_fw(car_fw) if car_fw else None
matched: list[Path] = []
if car_norm:
for raw_path in flash.find_images():
image = Path(raw_path).resolve()
versions = flash.rwd_supported_versions(str(image))
if versions and car_norm in versions and flash.validate(str(image)):
matched.append(image)
matched.sort(key=lambda p: (0 if "Proper Torque Mod" in str(p) else 1, str(p)))
self._detected_fw = car_fw
self._matched_images = matched
if self._selected_image not in matched:
self._selected_image = next((p for p in matched if "Proper Torque Mod" in str(p)), matched[0] if matched else None)
for image, item in self._selection_items.items():
item.set_visible(image in matched)
self._match_item.set_visible(bool(matched))
self._no_match_item.set_visible(not matched)
def _is_running(self) -> bool:
if time.monotonic() < self._launch_grace_until:
return True
self._refresh_status()
if self._status.get("state") != "running":
return False
try:
return time.time() - float(self._status.get("updated_at", 0)) < 30.0
except (TypeError, ValueError):
return False
def _has_panda_lock(self) -> bool:
return PANDAD_BLOCK_FILE.exists()
def _can_release(self) -> bool:
return ui_state.is_offroad() and not self._is_running() and not self._has_panda_lock()
def _can_restore(self) -> bool:
return not self._is_running() and self._has_panda_lock()
def _can_read_eps(self) -> bool:
return ui_state.is_offroad() and not self._is_running() and self._has_panda_lock()
def _eps_info_button(self) -> str:
self._refresh_status()
if self._is_running() and self._status.get("action") == "identify":
return tr("READING")
if self._status.get("eps_read_at"):
return tr("RE-READ")
return tr("READ")
def _eps_info_label(self) -> str:
self._refresh_status()
if self._is_running() and self._status.get("action") == "identify":
return tr("READING")
if self._status.get("eps_read_at"):
return tr("ONLINE") if self._status.get("eps_online") else tr("NOT FOUND")
return tr("NOT READ")
def _eps_info_description(self) -> str:
self._refresh_status()
if not self._has_panda_lock():
return tr("Release Panda first, then press READ to query EPS software ID and VIN.")
if self._is_running() and self._status.get("action") == "identify":
return tr("Reading EPS software ID and VIN...")
if not self._status.get("eps_read_at"):
return tr("Panda is released. Press READ to query EPS.")
if not self._status.get("eps_online"):
return tr("No EPS response was received.")
parts = [tr("Bus: {}").format(self._status.get("eps_bus"))]
if self._status.get("eps_part_number"):
parts.append(tr("Part: {}").format(self._status.get("eps_part_number")))
if self._status.get("eps_vin"):
parts.append(tr("VIN: {}").format(self._status.get("eps_vin")))
return " · ".join(parts)
def _read_eps(self):
self._start_action("identify")
def _can_flash(self) -> bool:
return (ui_state.is_offroad() and not self._is_running() and self._has_panda_lock() and
self._selected_image is not None and self._selected_image in self._matched_images)
def _status_label(self) -> str:
self._refresh_status()
state = self._status.get("state")
if state == "success":
phase = self._status.get("phase")
if phase == "panda_released":
return tr("PANDA RELEASED")
if phase == "panda_restored":
return tr("PANDA RESTORED")
if phase == "complete":
return tr("FLASH SUCCESS")
return tr("SUCCESS")
if state == "failed":
return tr("FAILED")
if self._is_running():
progress = self._status.get("progress")
if isinstance(progress, (int, float)) and progress > 0:
return f"{int(progress)}%"
return str(self._status.get("phase", "running")).replace("_", " ").upper()
return tr("PANDA RESTORED") if not self._has_panda_lock() else tr("PANDA RELEASED")
def _status_description(self) -> str:
self._refresh_status()
if not self._status:
return tr("Use RELEASE first, then select a matched firmware and FLASH.")
image = self._status.get("image", "")
message = self._status.get("message", "")
phase = str(self._status.get("phase", "")).replace("_", " ").title()
prefix = f"{image}: " if image else ""
detail = f"{phase}: {message}" if phase and message else str(message)
return prefix + detail
def _match_description(self) -> str:
if not self._detected_fw:
return tr("No EPS firmware reported by CarParams. Flashing is disabled.")
if not self._matched_images:
return tr("No curated firmware matches {}. Flashing is disabled.").format(self._detected_fw)
name = flash.image_display_name(str(self._selected_image))
return tr("Matched {} image(s). Selected: {}").format(len(self._matched_images), name)
def _no_match_description(self) -> str:
fw = self._detected_fw or tr("(not reported)")
return tr("No curated firmware matches {}. Flashing is disabled until the exact EPS firmware is supported.").format(fw)
def _select_image(self, image: Path):
self._selected_image = image
def _show_status(self):
self._refresh_status()
if not self._status:
gui_app.push_widget(alert_dialog(tr("No EPS helper action has run yet.")))
return
gui_app.push_widget(alert_dialog(f"{self._status_label()}: {self._status.get('message', '')}"))
def _confirm_release(self):
text = tr(
"Release Panda?<br><br>"
"This stops pandad and keeps the Panda USB interface reserved for EPS flashing. "
"Use RESTORE afterward. Only run this while the vehicle is offroad."
)
dialog = ConfirmDialog(text, tr("RELEASE"), tr("CANCEL"), rich=True,
callback=lambda result: self._start_action("release") if result == DialogResult.CONFIRM else None)
gui_app.push_widget(dialog)
def _restore_panda(self):
self._start_action("restore")
def _confirm_flash(self):
image = self._selected_image
if image is None or image not in self._matched_images:
gui_app.push_widget(alert_dialog(tr("No matching firmware; flashing is disabled.")))
return
if not self._has_panda_lock():
gui_app.push_widget(alert_dialog(tr("Release Panda first.")))
return
text = tr(
"<b>Flash matched firmware?</b><br><br>"
"Firmware: {}<br><br>"
"Keep the vehicle in ACCESSORY mode and do not interrupt power.<br><br>"
"The tool runs a dry run and then flashes automatically. A bad or incompatible "
"firmware can permanently damage the EPS."
).format(flash.image_display_name(str(image)))
dialog = ConfirmDialog(text, tr("FLASH"), tr("CANCEL"), rich=True,
callback=lambda result: self._start_action("flash", image) if result == DialogResult.CONFIRM else None)
gui_app.push_widget(dialog)
def _start_action(self, action: str, image: Path | None = None):
command = [sys.executable, str(RUNNER), "--action", action]
if image is not None:
command.extend(["--rwd", str(image)])
try:
subprocess.Popen(
command,
cwd=EPS_DIR,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as exc:
gui_app.push_widget(alert_dialog(tr("Unable to start EPS action: {}").format(exc)))
return
self._launch_grace_until = time.monotonic() + 5.0
def _update_state(self):
super()._update_state()
self._refresh_status()
self._refresh_matches()
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._refresh_matches(force=True)
self._scroller.show_event()
@@ -15,7 +15,6 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayo
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.eps_flash import EPSFlashLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
@@ -44,7 +43,6 @@ OP.PanelType = IntEnum( # type: ignore[assignment] # ty: ignore[invalid-assignm
"SUNNYLINK",
"MODELS",
"STEERING",
"EPS_FLASH",
"CRUISE",
"VISUALS",
"DISPLAY",
@@ -119,7 +117,6 @@ class SettingsLayoutSP(OP.SettingsLayout):
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
OP.PanelType.EPS_FLASH: PanelInfo(tr_noop("EPS Flash"), EPSFlashLayout(), icon="icons/shell.png"),
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"),
@@ -1 +0,0 @@
"""NRDR-owned offline and device tools."""
@@ -1,197 +0,0 @@
# EPS tools
Standalone tooling for reading, validating, and flashing Honda/Acura EPS
firmware (`.rwd`). Works on the current opendbc layout — no panda-submodule or
`selfdrive.car` dependencies.
Credit to mmmorks for the original flasher scripts, and to
[RiskyBiscuit-arc](https://github.com/RiskyBiscuit-arc) for updating them to the
current opendbc layout and adding the safety checks.
> **Disclaimer:** Flashing EPS firmware can damage or permanently brick your
> power-steering ECU. **You alone are responsible for your EPS and your vehicle,
> and you use these tools entirely at your own risk** — no warranty of any kind,
> express or implied. Always validate the image (`check_rwd.py`) and do a dry run
> before flashing.
```
openpilot/sunnypilot/nrdr/tools/eps/
flash.py guided flash: stop op -> pick image -> dry run -> --danger
eps-update.py UDS .rwd flasher (validate + erase/program over CAN)
check_rwd.py offline .rwd checksum validator (stdlib only)
eps-diag.py EPS CAN liveness/diagnostic (sniff, UDS ping, part number)
rwd_format/ vendored Python-3 .rwd container parser (0x5A/0x31)
rwd/ curated nested firmware library + hashes/safety catalog
```
The normal commands run from the **openpilot repository root**. The small root
launchers preserve arguments, prompts, and exit codes while finding the parser
and firmware here. Historical `eps_tools/...` commands remain compatibility
launchers, but contain no flashing implementation or firmware.
## Guided flash (recommended)
`flash.py` walks the whole thing for you: reads the car's current EPS firmware
and searches an explicit 15-file menu containing the seven Proper Torque Mod
images and eight clearly labelled stock recovery images. It lists only choices
compatible with the detected EPS (with checksum status), lets you pick one,
auto-detects the EPS CAN bus via a live UDS probe of the selected image's address
(asks if more than one bus responds), offers a dry run, and only flashes after
you confirm. If nothing matches your EPS (or the car isn't fingerprinted yet),
it asks before listing all curated choices. Nested category paths are shown so
similar filenames cannot be mistaken for one another.
Legacy/testing images, the T6Z offline reference, TVA x31 files, the
extensionless Pilot WIP, and raw binaries are intentionally absent from the
guided menu. See `rwd/README.md` before using anything outside that menu.
```bash
python3 flash.py # auto-detect bus (fallback default 1)
python3 flash.py --bus 0 # pin to a bus
python3 flash.py --skip-dry-run # skip dry-run prompt + dry run (not recommended)
python3 flash.py --seed-timeout 600 # wait up to 10 min for a security lockout to clear
```
It confirms the car is OFF, stops openpilot, then prompts you to switch to
accessory mode (ignition ON, engine OFF) before bus detect / dry run / flash.
**Dry run:** by default you get an in-script choice — run dry run first
(recommended) or skip (not recommended). Some EPS units lock security access
after a dry run; if the real flash then fails with a timeout/security error,
`flash.py` offers a retry menu (choosing retry is enough — no second `FLASH`
prompt). `--skip-dry-run` bypasses the prompt and skips the dry run entirely.
After the flash it offers a menu: run `eps-diag.py` as a post-flash check (which
loops back to the menu), restore openpilot (it was stopped for flashing), or
reboot. The manual steps below are the same flow if you'd rather run each
command yourself.
## How to flash (manual)
1. Make sure the comma power is connected to the car's OBD2 port.
2. With the car **OFF**, stop openpilot over SSH:
```
sudo systemctl stop comma
tmux kill-session -t comma 2>/dev/null || true
systemctl is-active comma
```
Do not continue if the final command reports `active`, `activating`,
`deactivating`, or `reloading`. Do not use `pkill -f openpilot`; a command
path containing `openpilot` can make it kill the flashing helper itself.
3. Put the car in full **accessory mode** (ignition ON, engine OFF). Turn off the
A/C to avoid draining the battery.
4. **Dry run first** — validates the image and walks the UDS/security flow but
aborts *before* any mutating action (`eps-update.py` defaults to bus **1**):
```
python3 eps-update.py "rwd/MODEL/CATEGORY/REPLACE_WITH_YOUR_FIRMWARE.rwd" -b 1
```
When it aborts before performing mutating actions, that's your sign it's ready.
**Note:** some cars lock security access after a dry run. If the real flash
then fails, wait / power-cycle (OFF → accessory) and retry `--danger`, or skip
the dry run on a subsequent attempt. See
[Security-access lockout](#security-access-lockout-nrc-0x37) below.
5. **Flash for real** once you're committed:
```
python3 eps-update.py "rwd/MODEL/CATEGORY/REPLACE_WITH_YOUR_FIRMWARE.rwd" -b 1 --danger
```
You'll see warnings/errors on the dash while it flashes — that's normal. When
it reaches "Resetting ECU" with no traceback, turn the car off. Done.
Example:
```
python3 eps-update.py "rwd/39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd" -b 1 --danger
```
`eps-update.py` is the power-user flasher: you pass `-b` explicitly (default **1**).
Bus auto-detect lives only in `flash.py`.
### Security-access lockout (NRC 0x37)
If a run dies with:
```
NegativeResponseError: SECURITY_ACCESS - required time delay not expired
```
the EPS responded far enough to report its security-access delay. That confirms
this stage of communication only; it does **not** prove that the selected image
matches the vehicle or that the full CAN/power setup is safe for flashing. The
EPS is refusing to hand out an unlock seed because its delay timer is running
(typically a settling window enforced after power-up).
**You do not need any flags for this.** A normal `flash.py` run already retries:
`eps-update.py` re-requests the seed every 10s for up to 120s and reports how long
the unlock took (so you learn your car's actual delay), and if 120s isn't enough
the dry run simply asks whether to keep waiting another 5 minutes:
```
The EPS security-access delay hasn't cleared after 120s.
The ECU reported its delay timer. Waiting may clear this response;
it does not prove image compatibility.
Keep waiting another 300s? [Y/n]:
```
`--seed-timeout N` is only for **unattended** runs (no operator to answer that
prompt): it pins the retry window to N seconds and skips the keep-waiting question.
```
python3 flash.py --seed-timeout 600 # scripted, 10-min window
python3 eps-update.py rwd/YOUR_FIRMWARE.rwd -b 1 --seed-timeout 600 # raw flasher
```
Leave the car in accessory mode while it retries; turning the ignition off
restarts the timer. Only the *seed request* is retried — a rejected seed request
doesn't touch the attempt counter, but re-sending a **key** (NRC 0x35, invalid
key) increments it and restarts the delay, so that error is never retried.
### `--skip-checksum` (not recommended)
If a firmware isn't covered by the checksum checker, you can add `--skip-checksum`.
**Be careful — flashing a `.rwd` with invalid checksums can brick the EPS.** Only
use it on an image you trust.
```
python3 eps-update.py rwd/SOME_FIRMWARE.rwd -b 1 --skip-checksum --danger
```
## Verify it worked
Turn the car back on and move the wheel by hand. Basic power-steering assist
only shows that the EPS is responding enough to provide assist; it does **not**
prove that the image, calibration, or safety behavior is correct. Confirm over
CAN and check that the reported part number is the one you expected:
```
python3 eps-diag.py # checks buses 0 and 1 by default
python3 eps-diag.py -b 1 # pin to one bus
```
`eps-diag.py` reports which bus saw EPS activity and the detected part number when
available. For recovery it points you back to `flash.py` and choosing the **stock**
image for your EPS (it does not invent a specific `.rwd` filename).
## If a flash fails / crashes
A failed or interrupted flash can leave the car without power-steering assist.
Do not drive it in that state. Recovery may be possible, but permanent EPS
damage or a nonrecoverable brick remains possible; repeated flashing is not a
guaranteed fix.
Only retry with a verified, compatible image after diagnosing why the first run
failed, confirming stable vehicle power, checking the Panda/CAN connection, and
following a recovery procedure known to apply to that exact EPS. Keep the
verified stock image for that EPS available before starting any flash. If the
failure mode or compatibility is uncertain, stop and get experienced help
instead of guessing with `--danger`.
For an image that is not tracked in this repository, flash from a persistent
copy under `/data/media/0/`. The updater may delete untracked files from
`/data/openpilot`, which could otherwise remove a custom image during a manual
workflow.
## Validate an image offline
```
python3 check_rwd.py "rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd"
python3 check_rwd.py "rwd/**/*.rwd" # includes unsupported x31 references; overall nonzero is expected
```
See `rwd/README.md` for the complete catalog, guided/reference boundaries,
source archive hash, missing slots, and rules for adding another image.
`rwd/SHA256SUMS` records every shipped image. The validation test requires the
manifest and directory to contain the same exact filenames and bytes.
@@ -1,6 +0,0 @@
"""Honda/Acura EPS firmware inspection, diagnostics, and flashing tools."""
from openpilot.sunnypilot.nrdr.tools.eps.launcher import exec_tool
__all__ = ("exec_tool",)
@@ -1,169 +0,0 @@
#!/usr/bin/env python3
# Usage: python3 check_rwd.py <file.rwd> [file2.rwd ...]
import struct
import sys
DECRYPT_LOOKUP = {144: 72, 218: 55, 255: 255, 164: 1, 195: 26, 99: 2, 28: 178, 205: 158, 125: 138, 45: 118, 222: 98, 142: 78, 62: 58, 243: 38, 163: 18, 83: 254, 3: 234, 172: 214, 92: 194, 12: 174, 189: 154, 109: 134, 29: 114, 206: 94, 126: 74, 46: 54, 227: 34, 147: 14, 113: 0, 67: 250, 236: 230, 156: 210, 76: 190, 252: 170, 173: 150, 93: 130, 13: 110, 148: 253, 120: 159, 199: 148, 198: 137, 77: 126, 23: 104, 73: 83, 203: 73, 78: 62, 123: 53, 254: 42, 43: 33, 90: 23, 161: 12, 10: 3, 132: 249, 191: 239, 226: 220, 197: 201, 248: 191, 117: 181, 34: 172, 37: 161, 88: 151, 141: 142, 8: 131, 134: 121, 185: 111, 54: 101, 190: 90, 57: 79, 128: 68, 139: 57, 14: 46, 138: 35, 131: 10, 100: 241, 1: 228, 146: 200, 133: 185, 168: 171, 104: 155, 40: 139, 251: 85, 94: 66, 91: 45, 103: 124, 55: 112, 231: 156, 80: 56, 224: 92, 102: 113, 96: 60, 98: 188, 97: 252, 140: 206, 122: 31, 232: 187, 16: 40, 202: 51, 26: 7, 239: 251, 5: 153, 219: 77, 119: 128, 21: 157, 238: 102, 180: 5, 217: 119, 30: 50, 7: 100, 32: 44, 183: 144, 50: 176, 110: 70, 157: 146, 2: 164, 44: 182, 145: 8, 58: 15, 27: 29, 64: 52, 9: 67, 31: 199, 179: 22, 42: 11, 193: 20, 211: 30, 129: 4, 241: 32, 74: 19, 178: 208, 247: 160, 112: 64, 242: 224, 114: 192, 165: 193, 0: 36, 59: 37, 196: 9, 154: 39, 75: 41, 72: 147, 249: 127, 162: 204, 130: 196, 229: 209, 182: 133, 48: 48, 86: 109, 240: 96, 137: 99, 151: 136, 209: 24, 108: 198, 181: 197, 212: 13, 244: 21, 11: 25, 118: 117, 228: 17, 214: 141, 52: 229, 160: 76, 115: 6, 106: 27, 56: 143, 25: 71, 36: 225, 194: 212, 208: 88, 187: 69, 171: 65, 153: 103, 38: 97, 207: 243, 82: 184, 184: 175, 188: 218, 213: 205, 121: 95, 15: 195, 81: 248, 24: 135, 70: 105, 150: 125, 174: 86, 158: 82, 220: 226, 201: 115, 71: 116, 51: 246, 177: 16, 176: 80, 22: 93, 39: 108, 159: 231, 223: 247, 186: 47, 169: 107, 245: 213, 235: 81, 192: 84, 124: 202, 175: 235, 84: 237, 79: 211, 234: 59, 143: 227, 237: 166, 33: 236, 253: 106, 65: 244, 111: 219, 200: 179, 101: 177, 17: 232, 20: 221, 166: 129, 60: 186, 61: 122, 167: 140, 204: 222, 87: 120, 41: 75, 135: 132, 136: 163, 49: 240, 250: 63, 107: 49, 170: 43, 18: 168, 221: 162, 35: 242, 225: 28, 149: 189, 85: 173, 152: 167, 95: 215, 53: 165, 89: 87, 66: 180, 6: 89, 47: 203, 210: 216, 215: 152, 233: 123, 116: 245, 127: 223, 19: 238, 69: 169, 105: 91, 4: 217, 216: 183, 68: 233, 63: 207, 155: 61, 246: 149, 230: 145}
FIRMWARE_CHECKSUMS = {
0x6c000: [(0, 0x6bf80, "sum"), (1, 0x6bffe, "negative-sum")],
0x4c000: [(0, 0x4bf80, "sum"), (1, 0x4bffe, "negative-sum")],
}
PILOT_APP_ID = b'39990-TG7-A060\x00\x00'
T6Z_APP_IDS = (
b'39990-T6Z-A110\x00\x00',
b'39990-TG7-A040\x00\x00',
b'39990-TG7-A030\x00\x00',
b'39990-TG7-A020\x00\x00',
)
T6Z_SECRETS = (b'\x00\x11\x00\x12\x10\x20',) * 4
V850_APP_ID_GROUPS = ((PILOT_APP_ID,), T6Z_APP_IDS)
T6Z_REFERENCE_NOTICE = "".join((
"T6Z checksum validation is offline/catalog-only; the current ",
"eps-update.py and guided flash.py do not support flashing this image.",
))
PILOT_KEYS = b'\x01\x02\x03'
PILOT_FLASH_START = 0x10000
PILOT_CHECKSUM_ENDS = (0xa000, 0x1d000, 0x4ff00)
T6Z_HEADER_PROFILE = (
(b'\x00',),
(),
(b'0',), # tester address byte 0x30 -> UDS address 0x18DA30F1
T6Z_APP_IDS,
T6Z_SECRETS,
(PILOT_KEYS,),
)
T6Z_IDENTITY_OFFSET = 0x3438
def check(path):
print(f"\n{'─'*60}")
print(f" {path}")
print(f"{'─'*60}")
try:
with open(path, 'rb') as f:
raw = f.read()
except FileNotFoundError:
print(f" ERROR: file not found")
return False
print(f" Size: {len(raw)} bytes (0x{len(raw):x})")
# 1. File checksum (last 4 bytes, little-endian 32-bit)
stored_fc = struct.unpack('<L', raw[-4:])[0]
calc_fc = sum(raw[:-4]) & 0xFFFFFFFF
file_ok = stored_fc == calc_fc
print(f"\n [1] File checksum")
print(f" Stored: 0x{stored_fc:08x}")
print(f" Calculated: 0x{calc_fc:08x} {'PASS ✓' if file_ok else 'FAIL ✗'}")
# 2. Parse 0x5A header to locate firmware block
if raw[0:1] != b'\x5a':
print(f"\n ERROR: not a 0x5A format RWD (got 0x{raw[0]:02x})")
return False
idx = 3
headers = []
for _ in range(6):
cnt = raw[idx]; idx += 1
values = []
for _ in range(cnt):
length = raw[idx]; idx += 1
values.append(raw[idx:idx+length])
idx += length
headers.append(values)
fw_start = struct.unpack('!I', raw[idx:idx+4])[0]; idx += 4
fw_len = struct.unpack('!I', raw[idx:idx+4])[0]; idx += 4
print(f"\n [2] Firmware block")
print(f" Flash address: 0x{fw_start:08x} Length: 0x{fw_len:x} ({fw_len} bytes)")
# 3. Decrypt
enc = raw[idx:idx+fw_len]
honda_v850 = fw_len == 0x50000
t6z_reference = tuple(headers[3]) == T6Z_APP_IDS
if honda_v850:
if (tuple(headers[3]) not in V850_APP_ID_GROUPS or headers[5] != [PILOT_KEYS] or
fw_start != PILOT_FLASH_START or idx + fw_len != len(raw) - 4):
print("\n ERROR: unexpected 0x50000 firmware identity, layout, or encryption keys")
return False
if t6z_reference and tuple(tuple(values) for values in headers) != T6Z_HEADER_PROFILE:
print("\n ERROR: unexpected T6Z header, CAN address, software secret, or encryption key")
return False
if t6z_reference:
print(f"\n NOTICE: {T6Z_REFERENCE_NOTICE}")
k0, k1, k2 = PILOT_KEYS
dec = bytes(((((b + k0) ^ k1) - k2) & 0xff) for b in enc)
if t6z_reference:
identity_end = T6Z_IDENTITY_OFFSET + len(T6Z_APP_IDS[0])
if dec[T6Z_IDENTITY_OFFSET:identity_end] != T6Z_APP_IDS[0]:
print("\n ERROR: decrypted T6Z payload identity does not match 39990-T6Z-A110")
return False
else:
try:
dec = bytes(DECRYPT_LOOKUP[b] for b in enc)
except KeyError as e:
print(f"\n ERROR: encrypted byte 0x{e.args[0]:02x} not in lookup table")
return False
# 4. Firmware checksums
print(f"\n [3] Firmware checksums")
cs_defs = FIRMWARE_CHECKSUMS.get(fw_len)
if honda_v850:
all_fw_ok = True
checksum = 0
start = 0
for off in PILOT_CHECKSUM_ENDS:
words = (struct.unpack('<I', dec[i:i+4])[0] for i in range(start, off, 4))
checksum = (checksum + sum(words)) & 0xffffffff
ok = checksum == 0
all_fw_ok = all_fw_ok and ok
print(f" 0x{off:05x} cumulative LE32 0x{checksum:08x} {'PASS ✓' if ok else 'FAIL ✗'}")
start = off
elif cs_defs is None:
print(f" WARNING: unknown firmware length 0x{fw_len:x} — checksums not verified")
all_fw_ok = None
else:
all_fw_ok = True
for func_idx, off, label in cs_defs:
stored = struct.unpack('!H', dec[off:off+2])[0]
if func_idx == 0:
calc = sum(struct.unpack('!H', dec[i:i+2])[0] for i in range(0, off, 2)) & 0xFFFF
else:
calc = sum(-struct.unpack('!H', dec[i:i+2])[0] for i in range(0, off, 2)) & 0xFFFF
ok = stored == calc
all_fw_ok = all_fw_ok and ok
status = 'PASS ✓' if ok else f'FAIL ✗ (expected 0x{calc:04x})'
print(f" 0x{off:05x} {label:<14} stored: 0x{stored:04x} {status}")
# 5. Summary
fw_str = ('PASS ✓' if all_fw_ok else 'FAIL ✗') if all_fw_ok is not None else 'SKIP'
overall = file_ok and (all_fw_ok is not False)
overall_label = 'ALL GOOD ✓' if overall else 'CHECKSUM FAILURE ✗'
if overall and t6z_reference:
overall_label += ' (OFFLINE ONLY; NOT FLASH APPROVAL)'
print(f"\n {'='*40}")
print(f" File checksum: {'PASS ✓' if file_ok else 'FAIL ✗'}")
print(f" Firmware checksum: {fw_str}")
print(f" Overall: {overall_label}")
return overall
if __name__ == '__main__':
if len(sys.argv) < 2:
print(f"Usage: python3 {sys.argv[0]} <file.rwd> [file2.rwd ...]")
sys.exit(1)
results = [(path, check(path)) for path in sys.argv[1:]]
if len(results) > 1:
print(f"\n{'─'*60}")
print(" SUMMARY")
print(f"{'─'*60}")
for path, ok in results:
print(f" {'PASS ✓' if ok else 'FAIL ✗'} {path}")
sys.exit(0 if all(ok for _, ok in results) else 1)
@@ -1,352 +0,0 @@
"""
EPS CAN bus diagnostic tool for Honda/Acura EPS ECUs.
Checks whether the EPS (Electric Power Steering) ECU is alive on the CAN bus
after a potentially bad flash. Runs three checks:
1. Passive sniff – listen for any raw CAN traffic from the EPS physical addr
2. UDS ping – send Tester Present and wait for a positive response
3. UDS session – try Default → Extended diagnostic sessions and read the
Application Software ID (part number)
By default checks buses 0 and 1. Pass --bus to pin to a single bus.
Usage (on the comma device via SSH):
python eps-diag.py # check buses 0 and 1, address 0x18DA30F1
python eps-diag.py -b 1 # pin to bus 1
python eps-diag.py --scan # brute-force scan all common Honda ECU addrs
"""
import argparse
import os
import sys
import time
from panda import Panda
from opendbc.car.structs import CarParams
from opendbc.car.uds import (
UdsClient,
SESSION_TYPE,
DATA_IDENTIFIER_TYPE,
NegativeResponseError,
)
# ── addresses ────────────────────────────────────────────────────────────────
EPS_ADDR = 0x18DA30F1 # tester → EPS (29-bit, ISO 15765-4 physical)
DEFAULT_BUSES = (0, 1)
def response_addr_for(req_addr: int) -> int:
"""Honda ISO-TP physical response for a 29-bit request (0x18DAXXF1 → 0x18DAF1XX)."""
return (req_addr & 0xFFFF0000) | 0x0000F100 | ((req_addr >> 8) & 0xFF)
# A handful of other Honda ECU addresses useful for --scan
HONDA_SCAN_ADDRS = [
(0x18DA30F1, "EPS"),
(0x18DA10F1, "PCM/ECM"),
(0x18DA60F1, "Combination meter"),
(0x18DA28F1, "SRS airbag"),
(0x18DA0EF1, "VSA/ABS"),
(0x18DA21F1, "Transmission"),
(0x18DA3DF1, "LKAS camera"),
(0x18DAB0F1, "LKAS camera alt"),
]
SNIFF_DURATION_S = 3.0 # seconds to passively listen
UDS_TIMEOUT_S = 3.0 # seconds to wait for each UDS response
def connect_panda() -> Panda:
p = Panda(disable_checks=True)
p.set_safety_mode(CarParams.SafetyModel.elm327)
print(f" Connected to Panda (serial: {p.get_serial()})")
return p
def format_app_id(data) -> str:
if isinstance(data, (bytes, bytearray)):
return bytes(data).decode("latin-1", "replace").strip("\x00").strip()
return str(data).strip("\x00").strip()
def car_eps_fw_from_params():
"""EPS firmware string from CarParamsPersistent / CarParams, or None."""
try:
from opendbc.car.structs import car
except Exception:
if os.path.isdir("/data/openpilot"):
sys.path.append("/data/openpilot")
try:
from opendbc.car.structs import car
except Exception:
try:
from cereal import car
except Exception:
return None
for p in ("/data/params/d/CarParamsPersistent", "/data/params/d/CarParams"):
try:
data = open(p, "rb").read()
except OSError:
continue
try:
with car.CarParams.from_bytes(data) as CP:
for fw in CP.carFw:
if fw.ecu == "eps":
v = format_app_id(bytes(fw.fwVersion))
if v:
return v
except Exception:
continue
return None
def passive_sniff(panda: Panda, buses: list[int], resp_addr: int,
duration: float = SNIFF_DURATION_S) -> dict[int, bool]:
"""Listen for raw CAN frames from the EPS response address on each bus."""
bus_list = list(buses)
label = ", ".join(str(b) for b in bus_list)
print(f"\n{'='*60}")
print(f"[1/3] Passive CAN sniff on bus(es) {label} for {duration:.0f}s …")
print(f" Watching for frames from 0x{resp_addr:08X} (EPS response addr)")
for bus in bus_list:
panda.can_clear(bus)
deadline = time.monotonic() + duration
eps_counts = {b: 0 for b in bus_list}
all_extended: dict[int, set[int]] = {b: set() for b in bus_list}
while time.monotonic() < deadline:
for addr, dat, recv_bus in panda.can_recv():
if recv_bus not in eps_counts:
continue
if addr > 0x7FF: # 29-bit extended frame
all_extended[recv_bus].add(addr)
if addr == resp_addr:
eps_counts[recv_bus] += 1
print(f" *** EPS frame on bus {recv_bus}! addr=0x{addr:08X} data={dat.hex()}")
results = {}
for bus in bus_list:
count = eps_counts[bus]
results[bus] = count > 0
if count:
print(f" RESULT bus {bus}: EPS responding passively ({count} frames)")
else:
print(f" RESULT bus {bus}: No frames from EPS response addr")
ext = all_extended[bus]
if ext:
print(f" Other 29-bit addresses seen: " +
", ".join(f"0x{a:08X}" for a in sorted(ext)))
else:
print(" No 29-bit extended frames seen on this bus.")
return results
def uds_tester_present(uds: UdsClient, bus: int) -> bool:
"""Send a single Tester Present and check whether the EPS is present on this bus.
A negative response still means the ECU is alive (same idea as flash.py probe).
"""
print(f"\n{'='*60}")
print(f"[2/3] UDS Tester Present ping (bus {bus}) …")
try:
uds.tester_present()
print(" RESULT: EPS acknowledged Tester Present ✓")
return True
except NegativeResponseError as e:
print(f" RESULT: EPS present (negative response): {e}")
return True
except Exception as e:
print(f" RESULT: No response / timeout: {e}")
return False
def uds_session_and_id(uds: UdsClient, bus: int) -> tuple[bool, str | None]:
"""Try default → extended session and read the software part number.
Returns (session_ok, app_id_or_None).
"""
print(f"\n{'='*60}")
print(f"[3/3] UDS session + Application Software ID read (bus {bus}) …")
app_id = None
# Default session (0x01) — most ECUs come up here even after a bad flash
try:
uds.diagnostic_session_control(SESSION_TYPE.DEFAULT)
print(" Default session: OK")
except Exception as e:
print(f" Default session failed: {e}")
# Try reading the part number in default session first
for did, label in [
(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION, "Application SW ID (F181)"),
(DATA_IDENTIFIER_TYPE.VIN, "VIN (F190)"),
]:
try:
data = uds.read_data_by_identifier(did)
print(f" {label}: {data!r}")
if did == DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION:
app_id = format_app_id(data) or app_id
except NegativeResponseError as e:
print(f" {label}: negative response – {e}")
except Exception as e:
print(f" {label}: timeout/error – {e}")
# Extended diagnostic session
try:
uds.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
print(" Extended diagnostic session: OK")
try:
data = uds.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
print(f" Application SW ID (extended): {data!r}")
app_id = format_app_id(data) or app_id
return True, app_id
except Exception as e:
print(f" Application SW ID (extended): {e}")
except Exception as e:
print(f" Extended diagnostic session failed: {e}")
return False, app_id
def scan_all(panda: Panda, bus: int):
"""Probe every common Honda ECU address with a Tester Present."""
print(f"\n{'='*60}")
print(f"Scanning all common Honda UDS addresses on bus {bus} …\n")
for addr, name in HONDA_SCAN_ADDRS:
uds = UdsClient(panda, addr, bus=bus, timeout=1.5)
try:
uds.tester_present()
print(f" 0x{addr:08X} {name:<25} ALIVE ✓")
except NegativeResponseError as e:
print(f" 0x{addr:08X} {name:<25} negative response: {e}")
except Exception:
print(f" 0x{addr:08X} {name:<25} no response")
def print_recovery_advice(kind: str, detected_eps: str | None):
"""Print next-step guidance without inventing a specific .rwd filename."""
if kind == "dead":
print("""
DIAGNOSIS: EPS is not responding at all on the checked bus(es).
Possible causes:
- Ignition/ACC not on — EPS needs power (key in ON position)
- EPS is bricked (bad flash left it in bootloader with no CAN output)
- Wiring / Panda connection issue
- Wrong bus (if you pinned --bus, try without it to check 0 and 1)
Next steps:
1. Confirm ignition is ON (not just ACC) — some Bosch EPS needs full ON
2. Re-run without --bus to check buses 0 and 1
3. Run a full scan: python3 eps-diag.py --scan
4. If completely unresponsive, stop. Any bench recovery requires an
EPS-specific verified procedure, experienced help, and stable power.
""")
return
if kind == "bootloader":
print("""
DIAGNOSIS: EPS is broadcasting CAN frames but not responding to UDS.
This is consistent with a bad flash that left the bootloader active
but the application layer non-functional. The ECU is alive but stuck.
""")
else:
print("""
DIAGNOSIS: EPS is responding to diagnostics.
That proves basic liveness only. It does not prove the previous image was
compatible or that another flash attempt is safe.
""")
if detected_eps:
print(f" Detected EPS: {detected_eps}")
print(""" Before any recovery attempt, identify the exact EPS/software part number,
diagnose the original failure, and confirm stable vehicle power and CAN/Panda.
If you have a verified matching stock image and a recovery procedure known to
apply to this EPS, python3 flash.py can show the reviewed candidates.
Do not blindly reuse the previous .rwd or add --danger; a wrong image may have
caused the failure. If compatibility is uncertain, get experienced help.
""")
def main():
ap = argparse.ArgumentParser(description="Honda/Acura EPS CAN bus diagnostic")
ap.add_argument("-b", "--bus", default=None, type=lambda x: int(x, 0),
help="Pin to a single CAN bus (default: check buses 0 and 1)")
ap.add_argument("--addr", default=EPS_ADDR, type=lambda x: int(x, 0),
help=f"EPS UDS address (default: 0x{EPS_ADDR:08X})")
ap.add_argument("--scan", action="store_true",
help="Scan all common Honda ECU addresses")
ap.add_argument("--sniff-only", action="store_true",
help="Only run the passive sniff, skip UDS probes")
args = ap.parse_args()
buses = [args.bus] if args.bus is not None else list(DEFAULT_BUSES)
resp_addr = response_addr_for(args.addr)
print("Honda/Acura EPS diagnostic")
print(f" Bus(es): {', '.join(str(b) for b in buses)}")
print(f" EPS addr: 0x{args.addr:08X} (response 0x{resp_addr:08X})")
try:
panda = connect_panda()
except Exception as e:
print(f"\nFailed to connect to Panda: {e}")
print("Make sure the Panda is connected via USB and the car is in ACC ON / ignition.")
return
if args.scan:
for bus in buses:
scan_all(panda, bus)
return
# ── check 1: passive sniff ─────────────────────────────────────────────────
sniff_by_bus = passive_sniff(panda, buses, resp_addr)
eps_alive_passive = any(sniff_by_bus.values())
if args.sniff_only:
return
# ── checks 2 & 3: UDS probes (per bus) ─────────────────────────────────────
ping_by_bus: dict[int, bool] = {}
session_by_bus: dict[int, bool] = {}
app_id = None
for bus in buses:
uds = UdsClient(panda, args.addr, bus=bus, timeout=UDS_TIMEOUT_S)
ping_by_bus[bus] = uds_tester_present(uds, bus)
ok, found_id = uds_session_and_id(uds, bus)
session_by_bus[bus] = ok
if found_id and not app_id:
app_id = found_id
eps_alive_ping = any(ping_by_bus.values())
eps_alive_session = any(session_by_bus.values())
detected_eps = app_id or car_eps_fw_from_params()
# ── summary ────────────────────────────────────────────────────────────────
print(f"\n{'='*60}")
print("SUMMARY")
for bus in buses:
print(f" Bus {bus}:")
print(f" Passive CAN frames: {'YES' if sniff_by_bus.get(bus) else 'NO'}")
print(f" Tester Present: {'YES' if ping_by_bus.get(bus) else 'NO'}")
print(f" UDS session/ID read: {'YES' if session_by_bus.get(bus) else 'NO'}")
responding = [b for b in buses if sniff_by_bus.get(b) or ping_by_bus.get(b) or session_by_bus.get(b)]
if responding:
print(f" EPS activity seen on bus(es): {', '.join(str(b) for b in responding)}")
if detected_eps:
print(f" Detected EPS part number: {detected_eps}")
if not any([eps_alive_passive, eps_alive_ping, eps_alive_session]):
print_recovery_advice("dead", detected_eps)
elif eps_alive_passive and not eps_alive_ping:
print_recovery_advice("bootloader", detected_eps)
else:
print_recovery_advice("alive", detected_eps)
if __name__ == "__main__":
main()
@@ -1,352 +0,0 @@
import gzip
import os
import struct
import sys
import time
import tqdm
import traceback
from argparse import ArgumentParser
from opendbc.car.structs import CarParams
from rwd_format.x5a import x5a
from panda import Panda
from opendbc.car.uds import UdsClient, SESSION_TYPE, ACCESS_TYPE, ROUTINE_CONTROL_TYPE, ROUTINE_IDENTIFIER_TYPE, DATA_IDENTIFIER_TYPE, RESET_TYPE, NegativeResponseError
from unittest import mock
# Honda EPS firmware decryption-key DID. Not present in stock opendbc
# DATA_IDENTIFIER_TYPE (only the old flasher-branch opendbc defined it).
FLASH_DECRYPTION_KEY = 0xF101
# NRCs that mean the ECU's security-access delay timer is running — waiting and
# re-requesting the seed is the only way through. 0x35 (invalid key) is
# deliberately absent: re-sending a key increments the ECU's failed-attempt
# counter and restarts the timer, making the lockout worse.
SEED_DELAY_NRCS = {0x36: 'exceed number of attempts', 0x37: 'required time delay not expired'}
SEED_RETRY_INTERVAL_S = 10.0
DEFAULT_SEED_TIMEOUT_S = 120.0
# The EPS refuses security access for a period after power-up and immediately
# after a session change; don't send the first seed request into that window.
SESSION_SETTLE_S = 0.5
DECRYPT_LOOKUP = {144: 72, 218: 55, 255: 255, 164: 1, 195: 26, 99: 2, 28: 178, 205: 158, 125: 138, 45: 118, 222: 98, 142: 78, 62: 58, 243: 38, 163: 18, 83: 254, 3: 234, 172: 214, 92: 194, 12: 174, 189: 154, 109: 134, 29: 114, 206: 94, 126: 74, 46: 54, 227: 34, 147: 14, 113: 0, 67: 250, 236: 230, 156: 210, 76: 190, 252: 170, 173: 150, 93: 130, 13: 110, 148: 253, 120: 159, 199: 148, 198: 137, 77: 126, 23: 104, 73: 83, 203: 73, 78: 62, 123: 53, 254: 42, 43: 33, 90: 23, 161: 12, 10: 3, 132: 249, 191: 239, 226: 220, 197: 201, 248: 191, 117: 181, 34: 172, 37: 161, 88: 151, 141: 142, 8: 131, 134: 121, 185: 111, 54: 101, 190: 90, 57: 79, 128: 68, 139: 57, 14: 46, 138: 35, 131: 10, 100: 241, 1: 228, 146: 200, 133: 185, 168: 171, 104: 155, 40: 139, 251: 85, 94: 66, 91: 45, 103: 124, 55: 112, 231: 156, 80: 56, 224: 92, 102: 113, 96: 60, 98: 188, 97: 252, 140: 206, 122: 31, 232: 187, 16: 40, 202: 51, 26: 7, 239: 251, 5: 153, 219: 77, 119: 128, 21: 157, 238: 102, 180: 5, 217: 119, 30: 50, 7: 100, 32: 44, 183: 144, 50: 176, 110: 70, 157: 146, 2: 164, 44: 182, 145: 8, 58: 15, 27: 29, 64: 52, 9: 67, 31: 199, 179: 22, 42: 11, 193: 20, 211: 30, 129: 4, 241: 32, 74: 19, 178: 208, 247: 160, 112: 64, 242: 224, 114: 192, 165: 193, 0: 36, 59: 37, 196: 9, 154: 39, 75: 41, 72: 147, 249: 127, 162: 204, 130: 196, 229: 209, 182: 133, 48: 48, 86: 109, 240: 96, 137: 99, 151: 136, 209: 24, 108: 198, 181: 197, 212: 13, 244: 21, 11: 25, 118: 117, 228: 17, 214: 141, 52: 229, 160: 76, 115: 6, 106: 27, 56: 143, 25: 71, 36: 225, 194: 212, 208: 88, 187: 69, 171: 65, 153: 103, 38: 97, 207: 243, 82: 184, 184: 175, 188: 218, 213: 205, 121: 95, 15: 195, 81: 248, 24: 135, 70: 105, 150: 125, 174: 86, 158: 82, 220: 226, 201: 115, 71: 116, 51: 246, 177: 16, 176: 80, 22: 93, 39: 108, 159: 231, 223: 247, 186: 47, 169: 107, 245: 213, 235: 81, 192: 84, 124: 202, 175: 235, 84: 237, 79: 211, 234: 59, 143: 227, 237: 166, 33: 236, 253: 106, 65: 244, 111: 219, 200: 179, 101: 177, 17: 232, 20: 221, 166: 129, 60: 186, 61: 122, 167: 140, 204: 222, 87: 120, 41: 75, 135: 132, 136: 163, 49: 240, 250: 63, 107: 49, 170: 43, 18: 168, 221: 162, 35: 242, 225: 28, 149: 189, 85: 173, 152: 167, 95: 215, 53: 165, 89: 87, 66: 180, 6: 89, 47: 203, 210: 216, 215: 152, 233: 123, 116: 245, 127: 223, 19: 238, 69: 169, 105: 91, 4: 217, 216: 183, 68: 233, 63: 207, 155: 61, 246: 149, 230: 145}
# Checksum offsets embedded in decrypted firmware, keyed by firmware data length.
# Each entry is (checksum_func_index, offset, label):
# 0 = sum of 16-bit BE words from 0..offset must equal value stored at offset
# 1 = negative-sum variant (same range, negated)
FIRMWARE_CHECKSUM_OFFSETS = {
0x6c000: [(0, 0x6bf80, "sum"), (1, 0x6bffe, "negative-sum")], # CR-V TLA, Insight TXM (524288 byte bin)
0x4c000: [(0, 0x4bf80, "sum"), (1, 0x4bffe, "negative-sum")], # Civic TBA/TEA/TGG/TGN, Clarity TRW (393216 byte bin)
}
PILOT_APP_ID = b'39990-TG7-A060\x00\x00'
PILOT_KEYS = b'\x01\x02\x03'
PILOT_FLASH_START = 0x10000
PILOT_CHECKSUM_ENDS = (0xa000, 0x1d000, 0x4ff00)
def auto_int(i):
return int(i, 0)
def emit_progress(phase: str, percent: float) -> None:
value = max(0, min(100, int(round(percent))))
print(f"PROGRESS {phase} {value}", flush=True)
def read_file(fn):
f_name, f_ext = os.path.splitext(fn)
open_fn = open
if f_ext == ".gz":
open_fn = gzip.open
f_name, f_ext = os.path.splitext(f_name)
with open_fn(fn, 'rb') as f:
f_data = f.read()
return f_data
def validate_fw(fw_encrypted):
assert len(fw_encrypted.firmware_blocks) == 1
fw_len = fw_encrypted.firmware_blocks[0]["length"]
print(f"\n [1] Firmware checksums")
print(f" Firmware length: 0x{fw_len:x} ({fw_len} bytes)")
pilot_v850 = fw_len == 0x50000
cs_defs = FIRMWARE_CHECKSUM_OFFSETS.get(fw_len)
if pilot_v850:
app_ids = [value.value for value in fw_encrypted.file_headers[3].values]
flash_start = fw_encrypted.firmware_blocks[0]["start"]
assert app_ids == [PILOT_APP_ID] and bytes(fw_encrypted.keys) == PILOT_KEYS and flash_start == PILOT_FLASH_START, \
"Unexpected 0x50000 firmware identity, layout, or encryption keys"
k0, k1, k2 = PILOT_KEYS
decrypted = bytes(((((b + k0) ^ k1) - k2) & 0xff) for b in fw_encrypted.firmware_encrypted[0])
checksum = 0
start = 0
all_passed = True
for off in PILOT_CHECKSUM_ENDS:
checksum = (checksum + sum(struct.unpack('<I', decrypted[i:i+4])[0] for i in range(start, off, 4))) & 0xffffffff
ok = checksum == 0
all_passed = all_passed and ok
print(f" 0x{off:05x} cumulative LE32 0x{checksum:08x} {'PASS ✓' if ok else 'FAIL ✗'}")
start = off
elif cs_defs is None:
print(f" WARNING: unknown firmware length 0x{fw_len:x} — checksums not verified")
return
else:
decrypted = bytes(DECRYPT_LOOKUP[b] for b in fw_encrypted.firmware_encrypted[0])
all_passed = True
for func_idx, off, label in cs_defs:
stored = struct.unpack('!H', decrypted[off:off+2])[0]
if func_idx == 0:
calc = sum(struct.unpack('!H', decrypted[i:i+2])[0] for i in range(0, off, 2)) & 0xFFFF
else:
calc = sum(-struct.unpack('!H', decrypted[i:i+2])[0] for i in range(0, off, 2)) & 0xFFFF
ok = stored == calc
all_passed = all_passed and ok
status = 'PASS ✓' if ok else f'FAIL ✗ (expected 0x{calc:04x})'
print(f" 0x{off:05x} {label:<14} stored: 0x{stored:04x} {status}")
print(f"\n {'='*40}")
print(f" Firmware checksum: {'PASS ✓' if all_passed else 'FAIL ✗'}")
assert all_passed, "Firmware checksum validation failed — do not flash this file"
def calculate_session_key(const_bytes, seed_bytes):
k0, k1, k2 = struct.unpack('!HHH', const_bytes)
seed = struct.unpack('!H', seed_bytes)[0]
if k2 == 0:
k2 = 0x10000
key = ((seed + k0) ^ (seed * k1) % k2) & 0xFFFF
return struct.pack('!H', key)
def get_uds_client(can_addr, bus):
try:
panda = Panda(disable_checks=True)
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, can_addr, bus=bus)
print("Using real client")
except Exception:
mock_helper = mock.patch('opendbc.car.uds.UdsClient', autospec=True)
uds_client = mock_helper.start()
uds_client.security_access.return_value = b'1234'
uds_client.request_download.return_value = 514
uds_client.read_data_by_identifier.return_value = b'39990-TG7-A060\x00\x00'
print("Using mock client")
return uds_client
def _normalize_app_id(value):
# TRW/Honda software IDs use '-' and ',' interchangeably as field separators,
# and the ECU reports them differently than they're stored in the .rwd headers
# (e.g. ECU 'b39990,TRW,A020' or a modded 'b39990-TLA,A040' vs header
# 'b39990-TLA-A040'). Normalize both separators to ',' and drop trailing NULs
# so the seed lookup matches on content.
return value.replace(b'-', b',').rstrip(b'\x00')
def get_seed_secret(fw, app_id):
headers = fw.file_headers
target = _normalize_app_id(app_id)
for i in range(len(headers[4].values)):
if _normalize_app_id(headers[3].values[i].value) == target:
return headers[4].values[i].value
raise RuntimeError(f"Couldn't find software seed for software application ID {app_id}")
def get_can_address(fw):
return 0x18da00f1 | struct.unpack('!B', fw.file_headers[2].values[0].value)[0] << 8
def request_seed(uds_client, timeout_s):
"""Request a security-access seed, waiting out the ECU's delay timer.
Per ISO 14229 the ECU restarts its security-access delay timer at power-up if
the failed-attempt counter was non-zero when it lost power, so a seed request
made seconds after ignition-on can be refused with NRC 0x37 even on the first
attempt of a session. Retry on a slow interval until the timer expires.
"""
deadline = time.monotonic() + timeout_s
started = time.monotonic()
attempt = 0
while True:
attempt += 1
try:
seed = uds_client.security_access(ACCESS_TYPE.REQUEST_SEED)
if attempt > 1:
print(f" seed granted after {time.monotonic() - started:.0f}s ({attempt} attempts)")
return seed
except NegativeResponseError as e:
if e.error_code not in SEED_DELAY_NRCS:
raise
reason = SEED_DELAY_NRCS[e.error_code]
remaining = deadline - time.monotonic()
if remaining <= 0:
print(f" still refused ({reason}) after {timeout_s:.0f}s over {attempt} attempts.")
print(" The EPS security-access lockout has not expired. Leave the car in")
print(" accessory mode and retry with a longer --seed-timeout.")
raise
wait = min(SEED_RETRY_INTERVAL_S, remaining)
print(f" attempt {attempt}: {reason} — waiting {wait:.0f}s "
f"({remaining:.0f}s of {timeout_s:.0f}s budget left)")
time.sleep(wait)
def leave_diagnostic_session(uds_client):
"""Drop back to the default session so the EPS isn't left in extended/programming."""
try:
uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT)
print("Returned EPS to default diagnostic session.")
except Exception as e:
print(f"Could not return EPS to default session ({e}) — power-cycle the car.")
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("rwd", help="RWD firmware file to flash")
parser.add_argument("-b", "--bus", default=1, type=auto_int, help="CAN bus number (default 1)")
parser.add_argument("--debug", action="store_true", help="Enable debug output")
parser.add_argument("--danger", action="store_true", help="Run in danger mode that actually performs mutating actions")
parser.add_argument("--skip-checksum", action="store_true", help="Skip firmware checksum validation (not recommended — a bad checksum will cause the EPS to reject the flash)")
parser.add_argument("--seed-timeout", default=DEFAULT_SEED_TIMEOUT_S, type=float,
help=f"Seconds to keep retrying the security-access seed request while the EPS "
f"reports a delay/lockout (default {DEFAULT_SEED_TIMEOUT_S:.0f})")
args = parser.parse_args()
fw = x5a(read_file(args.rwd))
if not args.skip_checksum:
validate_fw(fw)
emit_progress("validated", 0)
print(fw)
can_addr = get_can_address(fw)
print(f"Connecting to CAN address 0x{can_addr:08X}")
uds_client = get_uds_client(can_addr, args.bus)
# --danger must never proceed on the mock fallback — it would "succeed" without
# programming the EPS.
if args.danger and isinstance(uds_client, mock.Mock):
raise SystemExit("ERROR: --danger requires a real Panda connection (got mock client)")
debug_output: list[bytes | None] = list()
print("tester present ...")
uds_client.tester_present()
try:
print("Getting software version")
app_id = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
print(f"Application Software ID = {app_id}")
print("Set diagnostic session type to 3 (extended diagnostic)")
data = uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
debug_output = debug_output + [data]
time.sleep(SESSION_SETTLE_S)
print("Security access request key for seed 1")
data = request_seed(uds_client, args.seed_timeout)
debug_output = debug_output + [data]
secret_key = get_seed_secret(fw, app_id)
if data is None:
raise ValueError("Security access request returned no seed data")
key = calculate_session_key(secret_key, data[-2:])
print("key = ", key)
print("Security access send key for seed 1")
data = uds_client.security_access(ACCESS_TYPE.SEND_KEY, key)
debug_output = debug_output + [data]
print("Set diagnostic session type to programming")
data = uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)
debug_output = debug_output + [data]
if not args.danger:
raise RuntimeError('Safe mode: aborting before mutating actions')
print("Erasing flash")
emit_progress("erase", 0)
data = uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, ROUTINE_IDENTIFIER_TYPE.ERASE_MEMORY)
debug_output = debug_output + [data]
emit_progress("erase", 10)
print("Setting firmware decryption key")
data = uds_client.write_data_by_identifier(FLASH_DECRYPTION_KEY, fw.keys)
debug_output = debug_output + [data]
print("Requesting download")
emit_progress("program", 10)
assert len(fw.firmware_blocks) == 1
block = fw.firmware_blocks[0]
length = block["length"]
max_chunk_size = uds_client.request_download(block["start"], length)
max_chunk_size -= 2 # subtract header bytes
with tqdm.tqdm(total=length, unit='B', unit_scale=True) as t:
cursor = 0x0
seq = 1
last_progress = -1
while cursor < length:
block_size = min(max_chunk_size, length - cursor)
data = uds_client.transfer_data(seq, fw.firmware_encrypted[0][cursor:cursor+block_size])
debug_output = debug_output + [data]
seq = (seq + 1) & 0xFF
cursor += block_size
t.update(block_size)
percent = 10 + (cursor / length) * 85
if int(percent) >= last_progress + 2 or cursor >= length:
emit_progress("program", percent)
last_progress = int(percent)
print("Requesting transfer exit")
emit_progress("verify", 95)
data = uds_client.request_transfer_exit()
debug_output = debug_output + [data]
print("Checking programming dependencies")
data = uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, ROUTINE_IDENTIFIER_TYPE.CHECK_PROGRAMMING_DEPENDENCIES)
debug_output = debug_output + [data]
emit_progress("reset", 99)
print("Resetting ECU")
data = uds_client.ecu_reset(RESET_TYPE.HARD)
debug_output = debug_output + [data]
emit_progress("complete", 100)
except Exception as e:
print(traceback.format_exc())
if not isinstance(uds_client, mock.Mock):
leave_diagnostic_session(uds_client)
# Dry-run intentionally raises here; treat that as success. Any other failure
# must be a non-zero exit so callers (e.g. flash.py) don't treat it as done.
if isinstance(e, RuntimeError) and str(e) == "Safe mode: aborting before mutating actions":
pass
else:
sys.exit(1)
if isinstance(uds_client, mock.Mock):
from unittest.mock import ANY, call
#print(uds_client.method_calls)
calls = []
calls += [call.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)]
calls += [call.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)]
calls += [call.security_access(ACCESS_TYPE.REQUEST_SEED)]
calls += [call.security_access(ACCESS_TYPE.SEND_KEY, ANY)]
calls += [call.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)]
calls += [call.routine_control(ROUTINE_CONTROL_TYPE.START, ROUTINE_IDENTIFIER_TYPE.ERASE_MEMORY)]
calls += [call.write_data_by_identifier(FLASH_DECRYPTION_KEY, fw.keys)]
calls += [call.request_download(0x10000, 0x50000)]
calls += [call.transfer_data(1, fw.firmware_encrypted[0][0:512])]
calls += [call.transfer_data(2, fw.firmware_encrypted[0][512:1024])]
uds_client.assert_has_calls(calls)
num_blocks = -(len(fw.firmware_encrypted[0]) // -512) # sneaky math ceil
assert uds_client.transfer_data.call_count == num_blocks
calls = []
calls += [call.transfer_data(num_blocks & 0xFF - 1, fw.firmware_encrypted[0][((num_blocks-1)*512):])]
calls += [call.request_transfer_exit()]
calls += [call.routine_control(ROUTINE_CONTROL_TYPE.START, ROUTINE_IDENTIFIER_TYPE.CHECK_PROGRAMMING_DEPENDENCIES)]
calls += [call.ecu_reset(RESET_TYPE.HARD)]
uds_client.assert_has_calls(calls)
if args.debug:
print("\nDebug output:")
print(*debug_output, sep="\n")
@@ -1,586 +0,0 @@
#!/usr/bin/env python3
"""
Guided EPS flash helper.
Steps it runs for you:
1. Reads the car's current EPS firmware and shows the curated Proper Torque
Mod and clearly-labelled stock images compatible with your EPS —
pre-validating each with check_rwd.py. Reference, legacy, and experimental
files stay out of this menu. If none are compatible (or the car isn't
fingerprinted yet), it asks before listing the curated images.
2. Shows a numbered list; you pick one.
3. Confirms the car is OFF, then stops openpilot (frees the panda), then
prompts you to switch the car to accessory mode (ignition ON, engine OFF).
4. Auto-detects the EPS CAN bus via live UDS probe of the selected .rwd's
address (asks if ambiguous) unless --bus was passed.
5. Offers a dry run (recommended) or skip (not recommended); --skip-dry-run
bypasses that prompt. Confirms dry run reaches the safe abort when run.
6. Asks for explicit confirmation (FLASH), then runs the real flash (--danger).
On failure, offers retry (menu choice is enough — no second FLASH prompt).
7. Post-flash menu: run eps-diag.py (loops back here), restore openpilot, or
reboot the device.
Security-access delay: the EPS refuses the seed request with NRC 0x37 while its
delay timer runs (e.g. a settling window after power-up). Normal `flash.py` runs
handle this for you — eps-update.py retries the seed on a slow interval, and if the
default window elapses the dry run just asks whether to keep waiting. No flag needed.
--seed-timeout is only for unattended runs: it pins the window and skips that prompt.
Run from the openpilot repository root on the comma:
python3 flash.py # normal use — auto bus + auto seed retry
python3 flash.py --bus 0 # pin to a bus
python3 flash.py --skip-dry-run # skip dry-run prompt + dry run (not recommended)
python3 flash.py --seed-timeout 600 # unattended: wait up to 10 min, no keep-waiting prompt
"""
import argparse
import contextlib
import glob
import io
import os
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..", ".."))
RWD_DIR = os.path.join(HERE, "rwd")
EPS_UPDATE = os.path.join(HERE, "eps-update.py")
DRY_RUN_OK_MARKER = "Safe mode: aborting before mutating actions"
REAL_CLIENT_MARKER = "Using real client"
# Printed by eps-update.py when the security-access seed stayed refused for the
# whole --seed-timeout window.
SEED_LOCKOUT_MARKER = "still refused"
# Normal-use seed-retry window (mirrors eps-update.py's DEFAULT_SEED_TIMEOUT_S) and
# how much longer each interactive "keep waiting?" extension waits.
DEFAULT_SEED_TIMEOUT_S = 120.0
SEED_EXTEND_S = 300.0
EPS_ADDR = 0x18DA30F1
DEFAULT_BUS = 1
PROBE_BUSES = (0, 1)
# This is deliberately narrower than "every valid .rwd in the library". Only
# the current Proper Torque Mod images and unambiguous stock recovery images are
# safe choices for the ordinary guided menu. Legacy, testing, WIP, T6Z, and x31
# files remain available as catalogued reference material for expert/manual use.
GUIDED_FLASH_FILES = (
"39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd",
"39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd",
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd",
"39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd",
"39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd",
"39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd",
"39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd",
"39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd",
"39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd",
"39990-TGG-A120/stock-39990-TGG-A120.rwd",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd",
"39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd",
"39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd",
"39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd",
)
sys.path.insert(0, HERE)
import check_rwd # noqa: E402 (reuse the validated checksum logic)
def subprocess_env():
"""opendbc/panda live in the openpilot root; rwd_format lives next to the script."""
env = os.environ.copy()
paths = [HERE, REPO_ROOT]
if os.path.isdir("/data/openpilot"):
paths.append("/data/openpilot")
if env.get("PYTHONPATH"):
paths.append(env["PYTHONPATH"])
env["PYTHONPATH"] = os.pathsep.join(paths)
return env
def validate(path):
"""True if the image passes check_rwd (run quietly)."""
try:
with contextlib.redirect_stdout(io.StringIO()):
return bool(check_rwd.check(path))
except Exception:
return False
def find_images():
files = {
os.path.join(RWD_DIR, *relative_path.split("/"))
for relative_path in GUIDED_FLASH_FILES
if os.path.isfile(os.path.join(RWD_DIR, *relative_path.split("/")))
}
# Preserve the historical escape hatch for an operator's own flat .rwd:
# immediately inside rwd/ or beside this script. Nested library files must
# be explicitly allowlisted above, so a reference image cannot leak in.
files |= set(glob.glob(os.path.join(RWD_DIR, "*.rwd")))
files |= set(glob.glob(os.path.join(HERE, "*.rwd")))
return sorted(files)
def image_display_name(path):
"""Readable library-relative path, including its safety/category folders."""
absolute = os.path.abspath(path)
try:
inside_library = os.path.commonpath((RWD_DIR, absolute)) == RWD_DIR
except ValueError:
inside_library = False
base = RWD_DIR if inside_library else HERE
return os.path.relpath(absolute, base).replace(os.sep, "/")
def norm_fw(s):
"""Normalize a part number for matching: '-'/',' interchangeable, strip NULs/space."""
if isinstance(s, (bytes, bytearray)):
s = bytes(s).decode("latin-1", "replace")
return s.replace("-", ",").strip("\x00").strip()
def car_eps_fw():
"""EPS firmware string from CarParamsPersistent / CarParams, or None.
Same source the nrdr settings UI uses: the EPS entry in CarParams.carFw.
(Do not use CarFw.bus — Cap'n Proto UInt8 defaults to 0 when unset.)
"""
try:
from opendbc.car.structs import car
except Exception:
if os.path.isdir("/data/openpilot"):
sys.path.append("/data/openpilot")
try:
from opendbc.car.structs import car
except Exception:
try:
from cereal import car
except Exception:
return None
for p in ("/data/params/d/CarParamsPersistent", "/data/params/d/CarParams"):
try:
data = open(p, "rb").read()
except OSError:
continue
try:
with car.CarParams.from_bytes(data) as CP:
for fw in CP.carFw:
if fw.ecu == "eps":
v = bytes(fw.fwVersion).decode("latin-1", "replace").strip("\x00").strip()
if v:
return v
except Exception:
continue
return None
def rwd_supported_versions(path):
"""Normalized set of part numbers an .rwd supports (its header[3]); None if unparseable."""
try:
from rwd_format.x5a import x5a
fw = x5a(open(path, "rb").read())
return {norm_fw(bytes(v.value)) for v in fw.file_headers[3].values}
except Exception:
return None
def rwd_can_address(path):
"""UDS tester address from the .rwd (same as eps-update.get_can_address); None if unparseable."""
try:
import struct
from rwd_format.x5a import x5a
fw = x5a(open(path, "rb").read())
return 0x18da00f1 | struct.unpack('!B', fw.file_headers[2].values[0].value)[0] << 8
except Exception:
return None
def probe_eps_buses(addr):
"""Live-probe buses 0/1 for Tester Present at addr. Return responding bus list.
A negative UDS response still means the ECU is on that bus (same as eps-diag).
"""
try:
from panda import Panda
from opendbc.car.structs import CarParams
from opendbc.car.uds import UdsClient, NegativeResponseError
except Exception as e:
print(f" Live bus probe unavailable ({e})")
return []
panda = None
try:
panda = Panda(disable_checks=True)
panda.set_safety_mode(CarParams.SafetyModel.elm327)
alive = []
for bus in PROBE_BUSES:
uds = UdsClient(panda, addr, bus=bus, timeout=1.5)
try:
uds.tester_present()
alive.append(bus)
print(f" EPS responded on bus {bus}")
except NegativeResponseError as e:
alive.append(bus)
print(f" EPS on bus {bus} (negative response: {e})")
except Exception:
print(f" No EPS response on bus {bus}")
return alive
except Exception as e:
print(f" Live bus probe: could not open/configure Panda ({e})")
return []
finally:
# Always release USB so eps-update can open the Panda for dry run / flash.
if panda is not None:
try:
panda.close()
except Exception:
pass
def ask_pick_bus(choices, default):
"""Prompt for a bus from choices. Empty input selects default."""
choices = list(choices)
if default not in choices:
default = choices[0]
label = "/".join(str(b) for b in choices)
while True:
raw = input(f"Select bus to flash [{label}] (default {default}): ").strip()
if raw == "":
return default
try:
bus = int(raw, 0)
except ValueError:
print(" Enter a bus number.")
continue
if bus not in choices:
print(f" Pick one of: {label}")
continue
return bus
def resolve_bus(cli_bus, rwd_path):
"""Pick the flash bus: --bus override, else live probe, else ask the user."""
if cli_bus is not None:
print(f"Using bus {cli_bus} (--bus override)")
return cli_bus
addr = rwd_can_address(rwd_path)
if addr is None:
addr = EPS_ADDR
print(f"Could not read CAN address from .rwd — probing default 0x{addr:08X}")
else:
print(f"Probing for EPS bus (UDS 0x{addr:08X} from selected .rwd) ...")
alive = probe_eps_buses(addr)
if len(alive) == 1:
print(f"Detected EPS on bus {alive[0]}")
return alive[0]
if len(alive) > 1:
print(f"EPS responded on multiple buses: {', '.join(str(b) for b in alive)}")
# Prefer the usual Honda default when both look alive (gateway echo, etc.).
default = DEFAULT_BUS if DEFAULT_BUS in alive else alive[0]
return ask_pick_bus(alive, default=default)
print("Live probe found no EPS response on buses 0 or 1.")
return ask_pick_bus(list(PROBE_BUSES), default=DEFAULT_BUS)
def stop_openpilot():
# Stop the managed service and tear down the comma tmux session so openpilot
# cannot respawn and reclaim the panda. Do NOT `pkill -f openpilot` — that
# matches this script's argv when launched from a path containing "openpilot".
print("Stopping openpilot to free the panda ...")
subprocess.run(["sudo", "systemctl", "stop", "comma"], check=False)
subprocess.run(
["tmux", "kill-session", "-t", "comma"],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
time.sleep(2)
status = subprocess.run(
["systemctl", "is-active", "comma"],
capture_output=True, text=True, check=False,
).stdout.strip()
if status in ("active", "activating", "deactivating", "reloading"):
sys.exit(
f"Failed to stop openpilot (comma service is '{status}'). "
"Run `sudo systemctl stop comma` manually and retry."
)
print(" done.\n")
def run_eps_update(rel_path, bus, skip_checksum, danger, seed_timeout=None):
cmd = [sys.executable, EPS_UPDATE, rel_path, "-b", str(bus)]
if skip_checksum:
cmd.append("--skip-checksum")
if danger:
cmd.append("--danger")
if seed_timeout is not None:
cmd += ["--seed-timeout", str(seed_timeout)]
return cmd
def restore_openpilot():
print("\nRestoring openpilot (restarting the comma service) ...")
subprocess.run("tmux kill-session -t comma 2>/dev/null; sudo systemctl restart comma",
shell=True, check=False)
print(" Requested restart — openpilot should come up in ~30s. If not, reboot.")
def reboot_device():
print("\nRebooting ...")
subprocess.run(["sudo", "reboot"], check=False)
def exit_with_op_stopped(msg):
"""Abort after stop_openpilot(); offer to restore so the device isn't left dead."""
print(msg)
print("\nNote: openpilot is still stopped.")
if input("Restore openpilot now? type 'yes': ").strip().lower() == "yes":
restore_openpilot()
else:
print("Restore later with: sudo systemctl restart comma (or reboot).")
sys.exit(1)
def post_flash_menu(env, bus, eps_addr=None):
"""After a flash: optionally run eps-diag (loops back), restore op, or reboot."""
diag = os.path.join(HERE, "eps-diag.py")
while True:
print("\nWhat next?")
print(" [1] Run eps-diag.py — post-flash EPS check (returns here after)")
print(" [2] Restore openpilot — it was stopped for flashing")
print(" [3] Reboot the device (sudo reboot)")
choice = input("Select 1/2/3 (q = leave openpilot stopped and exit): ").strip().lower()
if choice == "1":
print()
# Pin to the bus/address we just flashed so the check matches the flash path.
cmd = [sys.executable, diag, "-b", str(bus)]
if eps_addr is not None:
cmd += ["--addr", hex(eps_addr)]
subprocess.run(cmd, env=env, cwd=HERE)
elif choice == "2":
restore_openpilot()
return
elif choice == "3":
reboot_device()
return
elif choice in ("q", "quit", ""):
print("Leaving openpilot stopped. Restore later with: sudo systemctl restart comma (or reboot).")
return
else:
print(" Invalid choice — pick 1, 2, or 3.")
def choose_dry_run(skip_dry_run_flag):
"""Return True if a dry run should run. Flag bypasses the in-script prompt."""
if skip_dry_run_flag:
print("\nSkipping dry run (--skip-dry-run). Not recommended.")
return False
print("\nDry run validates the image and security flow, then aborts before erase.")
print("Some EPS units lock security access after a dry run; skip only if you need to.")
print(" [1] Run dry run first (recommended)")
print(" [2] Skip dry run (not recommended)")
choice = input("Select 1/2 [1]: ").strip() or "1"
if choice == "2":
print("Skipping dry run (not recommended).")
return False
if choice != "1":
print(" Invalid choice — running dry run.")
return True
def run_and_tee(cmd, env):
"""Run cmd, printing output as it arrives while capturing it for marker checks.
The seed request can retry for minutes against an EPS security lockout, so the
output has to stream — a captured run looks like a hang.
"""
proc = subprocess.Popen(cmd, env=env, cwd=HERE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, bufsize=1)
lines = []
for line in proc.stdout:
print(line, end="", flush=True)
lines.append(line)
proc.wait()
return proc.returncode, "".join(lines)
def run_dry_run(rel, bus, skip_checksum, env, name, seed_timeout=None):
# seed_timeout None = normal use: fall back to eps-update.py's own default and
# let an interactive lockout be handled by prompting to keep waiting, so plain
# `flash.py` never needs a flag. An explicit --seed-timeout pins the window and
# skips the prompt (for unattended runs).
interactive = seed_timeout is None
effective = DEFAULT_SEED_TIMEOUT_S if seed_timeout is None else seed_timeout
while True:
print(f"\n=== DRY RUN: {name} (validates + auth flow, stops before erase) ===\n")
_, combined = run_and_tee(
run_eps_update(rel, bus, skip_checksum, danger=False, seed_timeout=effective), env)
# Mock UDS client also reaches the safe-abort marker; require a real Panda.
if REAL_CLIENT_MARKER not in combined:
exit_with_op_stopped(
"\nDry run did NOT talk to a real Panda/EPS (mock client) — NOT flashing. "
"Fix the Panda connection and retry."
)
if SEED_LOCKOUT_MARKER in combined:
print(f"\nThe EPS security-access delay hasn't cleared after {effective:.0f}s.")
print("The ECU reported its delay timer. Waiting may clear this response;")
print("it does not prove that the selected image matches this EPS.")
if interactive and input(
f"Keep waiting another {SEED_EXTEND_S:.0f}s? [Y/n]: ").strip().lower() in ("", "y", "yes"):
effective = SEED_EXTEND_S
continue
exit_with_op_stopped("".join((
"\nStopping while the EPS delay is still active — NOT flashing.\n",
"After re-checking image compatibility, leave the car in accessory mode (ignition off restarts the timer) and retry the seed flow.",
)))
if DRY_RUN_OK_MARKER not in combined:
exit_with_op_stopped(
"\nDry run did NOT reach the safe abort point — NOT flashing. Review the output above."
)
break
print("Dry run OK — real client + safe abort (the traceback above is expected for a dry run).")
print("Note: some EPS units lock security access briefly after a dry run.")
print("If the real flash fails with a security/timeout error, wait or power-cycle, then retry.")
def run_real_flash(rel, bus, skip_checksum, env, name, seed_timeout=None):
print(f"\n=== FLASHING: {name} (bus {bus}) ===\n")
return subprocess.run(run_eps_update(rel, bus, skip_checksum, danger=True, seed_timeout=seed_timeout),
env=env, cwd=HERE).returncode
def flash_failure_menu(rel, bus, skip_checksum, env, name, rc, seed_timeout=None):
"""After a failed flash: retry (--danger only) or continue to post-flash menu.
Choosing retry is enough confirmation — no second FLASH prompt.
Returns the final return code.
"""
while rc != 0:
print(f"\nFlash process exited ({rc}).")
print("FLASH FAILED or incomplete — power-steering assist may be unavailable.")
print("A retry is not guaranteed recovery. First verify this image matches the exact")
print("EPS, diagnose the failure, and confirm stable vehicle power and CAN/Panda.")
print("A security lockout after a dry run is common on some cars:")
print(" wait a bit, power-cycle the car (OFF → accessory), then retry.")
print(" [r] Retry flash (--danger only, no dry run)")
print(" [m] Continue to post-flash menu")
print(" [q] Quit (leave openpilot stopped)")
choice = input("Select r/m/q: ").strip().lower()
if choice == "r":
rc = run_real_flash(rel, bus, skip_checksum, env, name, seed_timeout)
if rc == 0:
print(f"\nFlash process exited ({rc}).")
print("Flash reported success (exit 0).")
return rc
continue
if choice == "m":
print("Continuing to post-flash menu without a successful flash.")
return rc
if choice in ("q", "quit"):
exit_with_op_stopped("Leaving after failed flash — openpilot is still stopped.")
print(" Invalid choice — pick r, m, or q.")
return rc
def main():
ap = argparse.ArgumentParser(description="Guided EPS flash helper")
ap.add_argument("-b", "--bus", type=int, default=None,
help="CAN bus number (default: auto-detect; fallback 1)")
ap.add_argument("--skip-dry-run", action="store_true",
help="Skip the dry-run prompt and dry run (not recommended)")
ap.add_argument("--seed-timeout", type=float, default=None,
help="Seconds to keep retrying the security-access seed request while the "
"EPS reports a delay/lockout (passed to eps-update.py)")
args = ap.parse_args()
images = find_images()
if not images:
sys.exit(f"No .rwd files found in {RWD_DIR} or {HERE}")
# Filter to images compatible with the car's current EPS firmware.
car_fw = car_eps_fw()
car_norm = norm_fw(car_fw) if car_fw else None
if car_norm:
print(f"Detected EPS firmware: {car_fw}")
compatible = [f for f in images if (sv := rwd_supported_versions(f)) and car_norm in sv]
if compatible:
hidden = len(images) - len(compatible)
extra = f" ({hidden} incompatible image(s) hidden)" if hidden else ""
print(f"Showing firmware compatible with your EPS{extra}:\n")
shown = compatible
else:
print(f"\n!! No .rwd here is compatible with your EPS ({car_fw}).")
if input(" Show all curated guided images and continue anyway? type 'yes': ").strip().lower() != "yes":
sys.exit("Aborted.")
print("\nAll curated guided firmware (compatibility NOT guaranteed — pre-validated):\n")
shown = images
else:
print("Could not read the car's EPS firmware (not fingerprinted yet?). "
"Showing all curated guided images.\n")
shown = images
rows = []
for i, f in enumerate(shown):
ok = validate(f)
rows.append((f, ok))
status = "valid ✓" if ok else "INVALID ✗ -> will need --skip-checksum (brick risk)"
print(f" [{i + 1:>2}] {image_display_name(f)} {status}")
sel = input("\nSelect a firmware number to flash (q to quit): ").strip()
if sel.lower() in ("", "q", "quit"):
sys.exit("Aborted.")
try:
idx = int(sel) - 1
except ValueError:
sys.exit("Invalid selection.")
# Reject 0 and out-of-range: int("0")-1 == -1 would silently pick the last row.
if idx < 0 or idx >= len(rows):
sys.exit("Invalid selection.")
path, ok = rows[idx]
rel = os.path.relpath(path, HERE)
name = image_display_name(path)
skip_checksum = not ok
if not ok:
print(f"\n!! {name} FAILED checksum validation.")
print("!! It can only be flashed with --skip-checksum. A bad image can BRICK the EPS.")
if input(" Type 'yes' to continue with this unvalidated image: ").strip().lower() != "yes":
sys.exit("Aborted.")
# Power sequence: kill openpilot with the car OFF, then switch to accessory
# mode — so openpilot doesn't re-grab the panda when CAN comes alive.
if input("\nConfirm the car is fully OFF (ignition off) — type 'yes': ").strip().lower() != "yes":
sys.exit("Aborted — turn the car off first, then re-run.")
stop_openpilot()
input("Now switch the car to ACCESSORY mode (ignition ON, engine OFF, A/C off).\n"
"Press Enter when the dash is on and ready: ")
bus = resolve_bus(args.bus, path)
eps_addr = rwd_can_address(path)
env = subprocess_env()
if choose_dry_run(args.skip_dry_run):
run_dry_run(rel, bus, skip_checksum, env, name, args.seed_timeout)
print(f"\n=== READY TO FLASH: {name} (bus {bus}) ===")
print("Make sure the car is in accessory mode (ignition ON, engine OFF, A/C off).")
print("An interrupted flash can remove steering assist or permanently damage the EPS;")
print("recovery is not guaranteed.")
if input("Type 'FLASH' to commit the real flash (--danger): ").strip() != "FLASH":
exit_with_op_stopped("Aborted — no flash performed.")
rc = run_real_flash(rel, bus, skip_checksum, env, name, args.seed_timeout)
if rc == 0:
print(f"\nFlash process exited ({rc}).")
print("Flash reported success (exit 0).")
else:
rc = flash_failure_menu(rel, bus, skip_checksum, env, name, rc, args.seed_timeout)
post_flash_menu(env, bus, eps_addr=eps_addr)
sys.exit(rc)
if __name__ == "__main__":
main()
@@ -1,68 +0,0 @@
"""Stable launch boundary for canonical and compatibility EPS commands."""
import glob
import os
from pathlib import Path
import subprocess
import sys
EPS_DIR = Path(__file__).resolve().parent
REPOSITORY_ROOT = EPS_DIR.parents[4]
TOOLS = frozenset(("flash.py", "eps-update.py", "eps-diag.py", "check_rwd.py"))
_RWD_ALIASES = ("rwd/", "eps_tools/rwd/")
def _expand_rwd_alias(argument: str) -> list[str]:
normalized = argument.replace("\\", "/")
for prefix in _RWD_ALIASES:
if not normalized.startswith(prefix):
continue
canonical = EPS_DIR / "rwd" / normalized.removeprefix(prefix)
matches = sorted(glob.glob(str(canonical), recursive=True))
return matches or [str(canonical)]
return [argument]
def normalized_arguments(tool: str, arguments: list[str]) -> list[str]:
if tool not in TOOLS:
raise ValueError(f"Unknown EPS tool: {tool}")
if tool not in ("check_rwd.py", "eps-update.py"):
return list(arguments)
normalized: list[str] = []
for argument in arguments:
expanded = _expand_rwd_alias(argument)
if tool == "eps-update.py" and len(expanded) > 1:
raise SystemExit(f"Firmware path matched more than one image: {argument}")
normalized.extend(expanded)
return normalized
def exec_tool(tool: str) -> None:
"""Replace the launcher process with the canonical tool.
Using exec rather than importing the command preserves its arguments, exit
status, signals, prompts, and __file__-relative firmware/parser lookup.
"""
if tool not in TOOLS:
raise ValueError(f"Unknown EPS tool: {tool}")
environment = os.environ.copy()
python_path = [str(EPS_DIR), str(REPOSITORY_ROOT)]
if environment.get("PYTHONPATH"):
python_path.append(environment["PYTHONPATH"])
environment["PYTHONPATH"] = os.pathsep.join(python_path)
target = EPS_DIR / tool
arguments = normalized_arguments(tool, sys.argv[1:])
command = [sys.executable, str(target), *arguments]
if os.name == "nt":
# Windows Store Python cannot reliably replace itself with os.execve.
# Keep Linux/device launchers as true exec boundaries while preserving
# the same arguments and exit status for off-device Windows validation.
raise SystemExit(subprocess.call(command, env=environment))
os.execve(sys.executable, command, environment)
__all__ = ("EPS_DIR", "REPOSITORY_ROOT", "TOOLS", "exec_tool", "normalized_arguments")
@@ -1,119 +0,0 @@
# Curated Honda EPS firmware library
This directory is the byte-for-byte firmware catalog imported from Brett's
`Modded Honda RWDs-20260828T042240Z-1-001.zip` archive. The outer archive's
SHA-256 is:
```text
b36446d02ee5f67250495528881a2ea239c3df3a40b0c463be7bc9d9a9f8721b
```
The source's model/category layout is preserved here, with trailing whitespace
removed from directory names so the paths work consistently on Windows and the
comma. There are 37 firmware artifacts: 33 named `.rwd` containers, one
extensionless Pilot work-in-progress container, and three raw `.bin` dumps.
`SHA256SUMS` pins every artifact recursively.
## What `flash.py` shows
The guided command deliberately shows only 15 reviewed choices:
- all seven files in a `Proper Torque Mod` directory; and
- eight `.rwd` recovery files whose filename clearly says `stock`.
Those choices are an explicit allowlist in `flash.py`. A valid checksum alone
does not make an old tune appropriate for a car, so files labelled legacy,
linear, max, spike, mod, testing, dated, or work in progress are not added to
the ordinary menu. The nested category path is displayed alongside every menu
choice so similarly named images are not confused.
Run the guided tool from the openpilot root:
```bash
python3 flash.py
```
## Guided catalog
| EPS family | Proper Torque Mod | clearly labelled stock recovery |
| --- | --- | --- |
| TBA-A030 | `Proper Torque Mod/39990-TBA,A030-PTM.rwd` | `Legacy and Stock/stock-39990-TBA-A030.rwd` |
| TBA-C020 | `Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd` | `Legacy and Stock/stock-39990-TBA-C020.rwd` |
| TBA-C120 | `Proper Torque Mod/39990-TBA,C120-PTM.rwd` | `Legacy and Stock/stock-39990-TBA-C120.rwd` |
| TEG-A010 | `Proper Torque Mod/39990-TEG,A010-PTM.rwd` | not present in the archive |
| TG7-A060 Pilot | not present in the archive | `Legacy and Stock/39990-TG7-A060-STOCK.rwd` |
| TGG-A020 | not present in the archive | `Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd` |
| TGG-A120 | not labelled as PTM in the archive | `stock-39990-TGG-A120.rwd` |
| TLA-A040 CR-V | `Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd` | `Legacy and Stock/39990-TLA-A040-stock.rwd` |
| TRW-A020 Clarity | `Proper Torque Mod/ClarityMax-PTM.rwd` | not present in the archive |
| TXM-A040 Insight | `Proper Torque Mod/39990-TXM,A040-PTM.rwd` | `Legacy and Stock/TXM-A040-STOCK.rwd` |
The archive included three empty category slots. No fake file or empty directory
was created for them:
- `39990-TG7-A060 (2019 Honda Pilot)/Proper Torque Mod`
- `39990-TGG-A020/Proper Torque Mod`
- `39990-TRW-A020 (Honda Clarity)/Legacy and Stock`
## Catalogued but not guided or approved for flashing
- **Legacy/testing x5A RWDs:** retained under their source paths for comparison
and research. They are checksum-valid but are not automatically offered.
- **T6Z-A110:** the offline checker accepts only its exact four-ID group
(T6Z-A110 plus TG7-A040/A030/A020), CAN address `0x18DA30F1`, all four
`00 11 00 12 10 20` software secrets, key `01 02 03`, start `0x10000`, length
`0x50000`, known checksum boundaries, and the decrypted T6Z identity. It
prints a reference-only notice. The current `eps-update.py` intentionally
rejects this identity, and `flash.py` never lists it. Offline validation is
not flash approval.
- **TVA-A160:** the stock and 2x files use the different `0x31` container format.
They are retained as reference material. `check_rwd.py`, `eps-update.py`, and
the guided tool do not support or offer them.
- **Pilot 2X work in progress:** retained with its original extensionless name
under `Work in Progress`; it is never discovered by the guided tool.
- **Raw `.bin` dumps:** retained for research and recovery analysis only. No EPS
command discovers or flashes raw binaries.
Do not bypass these boundaries just because a file exists in the repository.
Hardware compatibility and a valid recovery procedure must be established
before any reference file becomes a guided choice.
## Integrity and offline validation
All 34 RWD/container artifacts pass their outer little-endian file checksum.
The full offline checker passes all 32 x5A containers: 30 ordinary `.rwd`
files, the one explicitly profiled T6Z `.rwd`, and the extensionless Pilot WIP.
The two x31 files are expected to be rejected by the x5A checker. Raw binaries
are not RWD containers.
Quote nested paths because several directory names contain spaces:
```bash
python3 check_rwd.py "rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd"
python3 check_rwd.py "rwd/39990-T6Z-A110/39990-T6Z-A110.rwd"
```
The root and historical `eps_tools/` launchers also expand recursive quoted
patterns such as `rwd/**/*.rwd`. That broad pattern includes the two unsupported
x31 reference files, so a nonzero overall result is expected.
## Deliberately excluded archive files
The following three attachment-borne documents/scripts are not firmware and
were not imported or executed:
- `39990-TVA-A160 (Honda Accord)/Flashing script and instructions via ssh/eps-update-tva.py`
- `39990-TVA-A160 (Honda Accord)/Flashing script and instructions via ssh/ONBOARDING.md`
- `39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/readme.txt`
The TVA script describes an unverified flashing path outside the safe x5A tool,
and its onboarding text would conflict with the canonical root workflow. The
TXM note refers to a file that is not in the archive.
## Adding another image
Add real files only; never create placeholders for missing firmware. Preserve a
clear model/category path, record the exact SHA-256 in `SHA256SUMS`, validate the
container and decrypted firmware where supported, and document whether it is
guided or reference-only. Adding a file to the guided menu is a separate safety
decision and requires an explicit `GUIDED_FLASH_FILES` entry.
@@ -1,37 +0,0 @@
7fac5c25732761f60cb123ba8ea34d010486f3e7bea1859f9c2d42ff53af4f9d 39990-T6Z-A110/39990-T6Z-A110.rwd
835b99414d4927bac327996ac6771a8f2dcd3633baac1933b2ed2d091a568fb9 39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd
b023258dd774729cfcca3f0d3f6b9fefe1bc111f8b4d1b6bb0e83d788cbfec6f 39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd
50aa2a555b44ac80149cd5528290a6ac9b3452d6d127a03a6c9f9cf6758c9226 39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd
3694903e9ffa242097b4f649c61acf6df77d4b76c4f5d97f67194613731e8520 39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd
077d2dda35d43bf17c631837c28c980fa0b274de7a151efff0b75439ed87bc6b 39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd
b112945d28b0f3031bcf5f35c9a3609f9153110edd0dd6342c7e5a1a48b4576e 39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-linear-max-V2.1-Linear45-FlatGain.rwd
7a5ddf7fb20724647d7b251b48d43dd60ab351eae96e099d31644bec60ac90bf 39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd
ad30cbd9faade0e2011f97229632c6c43e9c08e72aa5160f0a282c79843e9367 39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd
2d8836e81a53d5e1cb44fb7645ab81336481b2676361805d8de07a199347b33d 39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd
ac9c0fd6e57ad5fb118122863ae86d5aba40f88d8edd786dbfa82653a96f3dcd 39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd
273726b6bbcfc83b49a07be3a75bcffcaa9f87e8692277a918a9d8a89c18cd38 39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd
15e63414b3137a9c6906305bbfe90c207b48083f2b173207a8084ff7312002e3 39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd
4ebf643a99eda27c68ec162c639627a43aa07c3ce5924a9fd8910c13d2022040 39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd
80fbd0e023453553719dc7363923a5d036d05f025f705809b4f0603e664cf8b2 39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd
d28c27b573ef610a479875107002ac7d9a84689ba5f3660686d0aecf1afb7555 39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin
f13d6b28e60ed5a39d2117c461c0c759a3d8b742b39ea9bfd6861ed7cc9fe184 39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd
6ef205bceb5128f48e27182fa4e128173f2f6c0995192efd57809c237b81e40f 39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot)
3a8cdf57ad75306e74d387d399306aa355628f2101c4589f9c7c1721901e235c 39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd
9b43471586cd39e31e7d932d31684b5c4bee056bc4de53d638bb3b02ee9ad73d 39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd
470559fa7e02d11b8531fb83fcac19ca609a451dc3bd931ff5810343d9f4e8b4 39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd
357b2571cd65c2100c07b33b1326101f9ce8ea9790aaa67358df69f98c089fc7 39990-TGG-A120/mod-39990-TGG,A120.rwd
3780431a47003e13d3654bf767697c73b52c9fa0be9a2d96cf1e5f6fba6c73b6 39990-TGG-A120/stock-39990-TGG-A120.rwd
042fe60ba415f9214d82196efc595f78aea2e54cf95f9b8f1e2216e6ca7268ad 39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd
0ceab577022d7504562c9cbb9fe882b7995283c072baeffee54e78e18d5f3740 39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin
f84968c6b2b1fbba8b0d538b80a1782213186f92f2fc706be8eece485cb717d1 39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd
ac6dce68bc3c36e7fa990170f7f56df678f9edd92617fd62cdf74a3e7274bc42 39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd
26f5390b654ace80b759bd20156d4c8ef834d97dc0e22889c2d247c052980afb 39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd
d4fe903bcf347495f4321be65a3c650c8c931f1650c3bef6443721da0dd80f7d 39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd
8a809d1b320957e4846c9145ee662858759d514681ce46c247a4a197784202da 39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd
7ad6d49d4a97b661693423bc5ad9f1a754e7f80523010491e9097708294dcfea 39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd
ccc59bb576114ae6ee906281bdc6041e425fb3fb1a320d4e7816bee96b599782 39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin
ad7457d97dd165fc6426ad9f7948b7eac652dbbcf5e3490a7e401f25e773a59e 39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd
02cf82ca9992f18796a64484cf0fe5470d77a09bd5fc5cd1a098dc61a5c3d2fb 39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd
563e15e3895b7b6da4a036e98d7998bdfb272b871c4ccfa9e4c7cafe0755d828 39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd
834a2fc06e625553994580b1b8fbfd627e0572f5d1f86123f36bc5055c545d4a 39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd
833a41bf9c3a8b4051c37271e0f7e1b970532e58b143d7882729203fa399b6e8 39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd
@@ -1,60 +0,0 @@
import struct
class Base(object):
def __init__(self, data, headers, keys, addr_blocks, encrypted):
self._file_format = data[0:1]
self._file_headers = headers
self._file_checksum = struct.unpack('<L', data[-4:])[0]
self._firmware_blocks = addr_blocks
self._firmware_encrypted = encrypted
self._keys = keys
self.validate_file_checksum(data)
@property
def file_format(self):
return self._file_format
@property
def file_checksum(self):
return self._file_checksum
@property
def file_headers(self):
return self._file_headers
@property
def firmware_blocks(self):
return self._firmware_blocks
@property
def firmware_encrypted(self):
return self._firmware_encrypted
@property
def keys(self):
return self._keys
def validate_file_checksum(self, data):
calculated = sum(data[0:-4]) & 0xFFFFFFFF
assert calculated == self.file_checksum, "file checksum mismatch"
def __str__(self):
info = [
"file format: {}".format(self.file_format),
"file checksum: {}".format(hex(self.file_checksum)),
]
info.append("headers:")
info.extend([str(h) for h in self._file_headers])
info.append("keys:")
info.extend([
"k{} = {}".format(i, hex(self._keys[i]))
for i in range(len(self._keys))
])
info.append("address blocks:")
info.extend([
"start = {} len = {}".format(hex(i["start"]), hex(i["length"]))
for i in self._firmware_blocks
])
return '\n'.join(info)
@@ -1,35 +0,0 @@
class Header():
def __init__(self, id, prefix, suffix):
self._id = id
self._prefix = prefix
self._suffix = suffix
self._values = list()
@property
def id(self):
return self._id
@property
def prefix(self):
return self._prefix
@property
def suffix(self):
return self._suffix
@property
def values(self):
return self._values
def generate(self):
value = self._prefix
for header_value in self._values:
value += header_value.generate
value += self._suffix
return value
def __str__(self):
return "{} = {}".format(self._id, str([str(v) for v in self._values]))
@@ -1,24 +0,0 @@
class HeaderValue():
def __init__(self, prefix, suffix, value):
self._prefix = prefix
self._suffix = suffix
self._value = value
@property
def prefix(self):
return self._prefix
@property
def suffix(self):
return self._suffix
@property
def value(self):
return self._value
@property
def generate(self):
return self._prefix + self._value + self._suffix
def __str__(self):
return str(self._value)
@@ -1,91 +0,0 @@
from binascii import a2b_hex
from .base import Base
from .header import Header
from .header_value import HeaderValue
class x31(Base):
def __init__(self, data):
start_idx = 3 # skip file type indicator bytes
headers, header_data_len = self._parse_file_headers(data[start_idx:])
keys = self._get_keys(headers)
start_idx += header_data_len
addr_blocks, encrypted = self._get_firmware(data[start_idx:-4]) # exclude file checksum
Base.__init__(self, data, headers, keys, addr_blocks, encrypted)
def _parse_file_headers(self, data):
headers = list()
d_idx = 0
for h_idx in range(6):
h_prefix = data[d_idx:d_idx+3]
d_idx += 3
# delimiter is 0x__0D0A
assert h_prefix[1:] == "\x0D\x0A", "header delimiter not found!"
f_header = Header(h_prefix[0], h_prefix, h_prefix)
# stop when delimiter is repeat ed
while data[d_idx:d_idx+3] != h_prefix:
# values delimited by 0x0D0A
end_idx = data.find("\x0D\x0A", d_idx)
assert end_idx != -1, "field delimiter not found!"
v_data = data[d_idx:end_idx]
d_idx += len(v_data)
# skip past field delimiter
v_suffix = data[d_idx:d_idx+2]
d_idx += 2
h_value = HeaderValue("", v_suffix, v_data)
f_header.values.append(h_value)
# skip past delimiter
h_suffix = data[d_idx:d_idx+3]
d_idx += 3
assert h_prefix == h_suffix, "header prefix and suffix do not match"
headers.append(f_header)
return headers, d_idx
def _get_keys(self, headers):
for header in headers:
if header.id == "&":
assert len(header.values) == 1, "encryption key header does not have exactly one value!"
value = a2b_hex(header.values[0].value)
assert len(value) == 3, "encryption key header not three bytes!"
return value
raise Exception("could not find encryption key header!")
def _get_firmware(self, data):
firmware = list()
addr_blocks = list()
chunk_size = 130
data_size = chunk_size - 2
addr_next = 0
block_start = 0
block_data = ""
for i in range(0, len(data), chunk_size):
addr = (data[i] << 12) | (data[i+1] << 4)
assert addr >= addr_next, "address decreased"
if addr != addr_next:
if len(block_data) > 0:
firmware.append(block_data)
addr_blocks.append({"start": block_start, "length": len(block_data)})
block_start = addr
block_data = ""
block_data += data[i+2:i+data_size+2]
addr_next = addr + data_size
if len(block_data) > 0:
firmware.append(block_data)
addr_blocks.append({"start": block_start, "length": len(block_data)})
assert len(addr_blocks) > 0, "could not find firmware address blocks!"
return addr_blocks, firmware
@@ -1,60 +0,0 @@
import struct
from .base import Base
from .header import Header
from .header_value import HeaderValue
class x5a(Base):
def __init__(self, data):
start_idx = 3 # skip file type indicator bytes
headers, header_data_len = self._parse_file_headers(data[start_idx:])
keys = self._get_keys(headers)
start_idx += header_data_len
addr_blocks, encrypted = self._get_firmware(data[start_idx:-4]) # exclude file checksum
Base.__init__(self, data, headers, keys, addr_blocks, encrypted)
def _parse_file_headers(self, data):
headers = list()
d_idx = 0
for h_idx in range(6):
h_prefix = data[d_idx]
d_idx += 1
# first byte is number of values
cnt = h_prefix
f_header = Header(h_idx, h_prefix, "")
for v_idx in range(cnt):
v_prefix = data[d_idx]
d_idx += 1
# first byte is length of value
length = v_prefix
v_data = data[d_idx:d_idx+length]
d_idx += length
h_value = HeaderValue(v_prefix, "", v_data)
f_header.values.append(h_value)
headers.append(f_header)
return headers, d_idx
def _get_keys(self, headers):
for header in headers:
if header.id == 5:
assert len(header.values) == 1, "encryption key header does not have exactly one value!"
assert len(header.values[0].value) == 3, "encryption key header not three bytes!"
return header.values[0].value
raise Exception("could not find encryption key header!")
def _get_firmware(self, data):
start = struct.unpack('!I', data[0:4])[0]
length = struct.unpack('!I', data[4:8])[0]
firmware = data[8:]
assert len(firmware) == length, "firmware length incorrect!"
return [{"start": start, "length": length}], [firmware]
@@ -1 +0,0 @@
"""Tests for the canonical NRDR EPS tool bundle."""
@@ -1,440 +0,0 @@
import contextlib
import hashlib
import importlib.util
import io
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
import types
import unittest
from unittest import mock
from openpilot.sunnypilot.nrdr.tools.eps import flash
from openpilot.sunnypilot.nrdr.tools.eps import launcher
from openpilot.sunnypilot.nrdr.tools.eps.rwd_format.x5a import x5a
ROOT = Path(__file__).resolve().parents[6]
EPS_DIR = ROOT / "openpilot" / "sunnypilot" / "nrdr" / "tools" / "eps"
CHECK_RWD_PATH = EPS_DIR / "check_rwd.py"
EPS_UPDATE_PATH = EPS_DIR / "eps-update.py"
RWD_DIR = EPS_DIR / "rwd"
SHA256_MANIFEST = RWD_DIR / "SHA256SUMS"
EXPECTED_ASSETS = {
"39990-T6Z-A110/39990-T6Z-A110.rwd": "7fac5c25732761f60cb123ba8ea34d010486f3e7bea1859f9c2d42ff53af4f9d",
"39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd": "835b99414d4927bac327996ac6771a8f2dcd3633baac1933b2ed2d091a568fb9",
"39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd": "b023258dd774729cfcca3f0d3f6b9fefe1bc111f8b4d1b6bb0e83d788cbfec6f",
"39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd": "50aa2a555b44ac80149cd5528290a6ac9b3452d6d127a03a6c9f9cf6758c9226",
"39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd": "3694903e9ffa242097b4f649c61acf6df77d4b76c4f5d97f67194613731e8520",
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd": "077d2dda35d43bf17c631837c28c980fa0b274de7a151efff0b75439ed87bc6b",
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-linear-max-V2.1-Linear45-FlatGain.rwd": (
"b112945d28b0f3031bcf5f35c9a3609f9153110edd0dd6342c7e5a1a48b4576e"
),
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd": "7a5ddf7fb20724647d7b251b48d43dd60ab351eae96e099d31644bec60ac90bf",
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd": "ad30cbd9faade0e2011f97229632c6c43e9c08e72aa5160f0a282c79843e9367",
"39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd": (
"2d8836e81a53d5e1cb44fb7645ab81336481b2676361805d8de07a199347b33d"
),
"39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd": "ac9c0fd6e57ad5fb118122863ae86d5aba40f88d8edd786dbfa82653a96f3dcd",
"39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd": "273726b6bbcfc83b49a07be3a75bcffcaa9f87e8692277a918a9d8a89c18cd38",
"39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd": "15e63414b3137a9c6906305bbfe90c207b48083f2b173207a8084ff7312002e3",
"39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd": "4ebf643a99eda27c68ec162c639627a43aa07c3ce5924a9fd8910c13d2022040",
"39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd": "80fbd0e023453553719dc7363923a5d036d05f025f705809b4f0603e664cf8b2",
"39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin": "d28c27b573ef610a479875107002ac7d9a84689ba5f3660686d0aecf1afb7555",
"39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd": "f13d6b28e60ed5a39d2117c461c0c759a3d8b742b39ea9bfd6861ed7cc9fe184",
"39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot)": "6ef205bceb5128f48e27182fa4e128173f2f6c0995192efd57809c237b81e40f",
"39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd": "3a8cdf57ad75306e74d387d399306aa355628f2101c4589f9c7c1721901e235c",
"39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd": "9b43471586cd39e31e7d932d31684b5c4bee056bc4de53d638bb3b02ee9ad73d",
"39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd": "470559fa7e02d11b8531fb83fcac19ca609a451dc3bd931ff5810343d9f4e8b4",
"39990-TGG-A120/mod-39990-TGG,A120.rwd": "357b2571cd65c2100c07b33b1326101f9ce8ea9790aaa67358df69f98c089fc7",
"39990-TGG-A120/stock-39990-TGG-A120.rwd": "3780431a47003e13d3654bf767697c73b52c9fa0be9a2d96cf1e5f6fba6c73b6",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd": "042fe60ba415f9214d82196efc595f78aea2e54cf95f9b8f1e2216e6ca7268ad",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin": "0ceab577022d7504562c9cbb9fe882b7995283c072baeffee54e78e18d5f3740",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd": "f84968c6b2b1fbba8b0d538b80a1782213186f92f2fc706be8eece485cb717d1",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd": (
"ac6dce68bc3c36e7fa990170f7f56df678f9edd92617fd62cdf74a3e7274bc42"
),
"39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd": (
"26f5390b654ace80b759bd20156d4c8ef834d97dc0e22889c2d247c052980afb"
),
"39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd": "d4fe903bcf347495f4321be65a3c650c8c931f1650c3bef6443721da0dd80f7d",
"39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd": "8a809d1b320957e4846c9145ee662858759d514681ce46c247a4a197784202da",
"39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd": "7ad6d49d4a97b661693423bc5ad9f1a754e7f80523010491e9097708294dcfea",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin": "ccc59bb576114ae6ee906281bdc6041e425fb3fb1a320d4e7816bee96b599782",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd": "ad7457d97dd165fc6426ad9f7948b7eac652dbbcf5e3490a7e401f25e773a59e",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd": "02cf82ca9992f18796a64484cf0fe5470d77a09bd5fc5cd1a098dc61a5c3d2fb",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd": "563e15e3895b7b6da4a036e98d7998bdfb272b871c4ccfa9e4c7cafe0755d828",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd": "834a2fc06e625553994580b1b8fbfd627e0572f5d1f86123f36bc5055c545d4a",
"39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd": "833a41bf9c3a8b4051c37271e0f7e1b970532e58b143d7882729203fa399b6e8",
}
GUIDED_IMAGES = {
"39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd",
"39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd",
"39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd",
"39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd",
"39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd",
"39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd",
"39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd",
"39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd",
"39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd",
"39990-TGG-A120/stock-39990-TGG-A120.rwd",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd",
"39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd",
"39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd",
"39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd",
}
T6Z_IMAGE = "39990-T6Z-A110/39990-T6Z-A110.rwd"
X31_IMAGES = {
"39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd",
"39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd",
}
WIP_IMAGE = "39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot)"
RAW_IMAGES = {
"39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin",
"39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin",
"39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin",
}
RWD_IMAGES = {name for name in EXPECTED_ASSETS if name.endswith(".rwd")}
X5A_RWD_IMAGES = RWD_IMAGES - X31_IMAGES
X5A_CONTAINERS = X5A_RWD_IMAGES | {WIP_IMAGE}
OUTER_CHECKSUM_IMAGES = RWD_IMAGES | {WIP_IMAGE}
spec = importlib.util.spec_from_file_location("check_rwd", CHECK_RWD_PATH)
assert spec is not None and spec.loader is not None
check_rwd = importlib.util.module_from_spec(spec)
spec.loader.exec_module(check_rwd)
def block_offset(data):
idx = 3
for _ in range(6):
count = data[idx]
idx += 1
for _ in range(count):
length = data[idx]
idx += length + 1
return idx
def header_value_offset(data, wanted_header, wanted_value):
idx = 3
for header_index in range(6):
count = data[idx]
idx += 1
for value_index in range(count):
length = data[idx]
idx += 1
if (header_index, value_index) == (wanted_header, wanted_value):
return idx
idx += length
raise ValueError(f"No header value {wanted_header}:{wanted_value}")
def repair_file_checksum(data):
struct.pack_into("<I", data, len(data) - 4, sum(data[:-4]) & 0xffffffff)
def load_eps_update_with_dependency_stubs():
"""Load the flasher's validation function without Panda/device dependencies."""
fake_tqdm = types.ModuleType("tqdm")
fake_tqdm.tqdm = lambda iterable, **_kwargs: iterable
fake_opendbc = types.ModuleType("opendbc")
fake_opendbc.__path__ = []
fake_car = types.ModuleType("opendbc.car")
fake_car.__path__ = []
fake_structs = types.ModuleType("opendbc.car.structs")
fake_structs.CarParams = type("CarParams", (), {})
fake_uds = types.ModuleType("opendbc.car.uds")
fake_uds.UdsClient = type("UdsClient", (), {})
for name in (
"SESSION_TYPE", "ACCESS_TYPE", "ROUTINE_CONTROL_TYPE",
"ROUTINE_IDENTIFIER_TYPE", "DATA_IDENTIFIER_TYPE", "RESET_TYPE",
):
setattr(fake_uds, name, types.SimpleNamespace())
fake_uds.NegativeResponseError = type("NegativeResponseError", (Exception,), {})
fake_panda = types.ModuleType("panda")
fake_panda.Panda = type("Panda", (), {})
fake_opendbc.car = fake_car
fake_car.structs = fake_structs
fake_car.uds = fake_uds
stubs = {
"tqdm": fake_tqdm,
"opendbc": fake_opendbc,
"opendbc.car": fake_car,
"opendbc.car.structs": fake_structs,
"opendbc.car.uds": fake_uds,
"panda": fake_panda,
}
module_spec = importlib.util.spec_from_file_location("eps_update_offline_test", EPS_UPDATE_PATH)
assert module_spec is not None and module_spec.loader is not None
module = importlib.util.module_from_spec(module_spec)
with mock.patch.dict(sys.modules, stubs):
module_spec.loader.exec_module(module)
return module
class TestCuratedRwdLibrary(unittest.TestCase):
def check_quietly(self, path):
with contextlib.redirect_stdout(io.StringIO()):
return check_rwd.check(path)
def test_exact_37_asset_paths_hashes_and_manifest(self):
self.assertEqual(len(EXPECTED_ASSETS), 37)
manifest = {}
for line in SHA256_MANIFEST.read_text().splitlines():
if not line.strip():
continue
digest, name = line.split(maxsplit=1)
self.assertNotIn(name, manifest)
manifest[name] = digest
self.assertEqual(manifest, EXPECTED_ASSETS)
actual = {
path.relative_to(RWD_DIR).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
for path in RWD_DIR.rglob("*")
if path.is_file() and path.name not in {"README.md", "SHA256SUMS"}
}
self.assertEqual(actual, EXPECTED_ASSETS)
def test_all_34_rwd_containers_have_valid_outer_checksums(self):
self.assertEqual(len(OUTER_CHECKSUM_IMAGES), 34)
for name in OUTER_CHECKSUM_IMAGES:
with self.subTest(name=name):
raw = (RWD_DIR / name).read_bytes()
self.assertGreaterEqual(len(raw), 4)
self.assertEqual(struct.unpack("<I", raw[-4:])[0], sum(raw[:-4]) & 0xffffffff)
def test_all_32_x5a_containers_pass_full_offline_validation(self):
self.assertEqual(len(X5A_CONTAINERS), 32)
for name in X5A_CONTAINERS:
with self.subTest(name=name):
self.assertTrue(self.check_quietly(RWD_DIR / name))
def test_guided_menu_is_exactly_seven_ptm_and_eight_stock_images(self):
self.assertEqual(len(GUIDED_IMAGES), 15)
self.assertEqual(sum("/Proper Torque Mod/" in name for name in GUIDED_IMAGES), 7)
self.assertEqual(sum("stock" in Path(name).name.lower() for name in GUIDED_IMAGES), 8)
self.assertEqual(set(flash.GUIDED_FLASH_FILES), GUIDED_IMAGES)
self.assertEqual(
{Path(path).relative_to(RWD_DIR).as_posix() for path in flash.find_images()},
GUIDED_IMAGES,
)
def test_t6z_is_offline_only_and_eps_update_remains_fail_closed(self):
output = io.StringIO()
with contextlib.redirect_stdout(output):
self.assertTrue(check_rwd.check(RWD_DIR / T6Z_IMAGE))
self.assertIn(check_rwd.T6Z_REFERENCE_NOTICE, output.getvalue())
self.assertNotIn(T6Z_IMAGE, flash.GUIDED_FLASH_FILES)
eps_update = load_eps_update_with_dependency_stubs()
firmware = x5a((RWD_DIR / T6Z_IMAGE).read_bytes())
with contextlib.redirect_stdout(io.StringIO()):
with self.assertRaisesRegex(AssertionError, "Unexpected 0x50000 firmware identity"):
eps_update.validate_fw(firmware)
def test_t6z_exact_profile_rejects_header_can_secret_key_start_and_payload_mutations(self):
original = (RWD_DIR / T6Z_IMAGE).read_bytes()
mutations = {}
for label, header_index, value_index in (
("header-zero", 0, 0),
("can-address", 2, 0),
("app-id", 3, 0),
("software-secret", 4, 0),
("encryption-key", 5, 0),
):
changed = bytearray(original)
changed[header_value_offset(changed, header_index, value_index)] ^= 1
repair_file_checksum(changed)
mutations[label] = changed
wrong_start = bytearray(original)
struct.pack_into(">I", wrong_start, block_offset(wrong_start), 0x20000)
repair_file_checksum(wrong_start)
mutations["flash-start"] = wrong_start
wrong_payload_identity = bytearray(original)
payload_identity = block_offset(wrong_payload_identity) + 8 + check_rwd.T6Z_IDENTITY_OFFSET
wrong_payload_identity[payload_identity] ^= 1
repair_file_checksum(wrong_payload_identity)
mutations["decrypted-identity"] = wrong_payload_identity
with tempfile.TemporaryDirectory() as tmpdir:
for label, data in mutations.items():
with self.subTest(label=label):
path = Path(tmpdir) / f"t6z-{label}.rwd"
path.write_bytes(data)
self.assertFalse(self.check_quietly(path))
def test_exact_two_x31_files_are_reference_only_and_rejected(self):
self.assertEqual(len(X31_IMAGES), 2)
self.assertTrue(X31_IMAGES.isdisjoint(flash.GUIDED_FLASH_FILES))
for name in X31_IMAGES:
with self.subTest(name=name):
output = io.StringIO()
with contextlib.redirect_stdout(output):
self.assertFalse(check_rwd.check(RWD_DIR / name))
self.assertIn("not a 0x5A format RWD", output.getvalue())
def test_wip_raw_and_noncurated_library_files_are_not_guided(self):
self.assertNotIn(WIP_IMAGE, flash.GUIDED_FLASH_FILES)
self.assertTrue(RAW_IMAGES.isdisjoint(flash.GUIDED_FLASH_FILES))
self.assertTrue((set(EXPECTED_ASSETS) - GUIDED_IMAGES).isdisjoint(flash.GUIDED_FLASH_FILES))
def test_no_old_flat_payload_or_trailing_space_path_component(self):
self.assertEqual(
{path.name for path in RWD_DIR.iterdir() if path.is_file()},
{"README.md", "SHA256SUMS"},
)
for name in EXPECTED_ASSETS:
with self.subTest(name=name):
self.assertTrue(all(part == part.rstrip() for part in Path(name).parts))
def test_payload_corruption_fails_and_cli_exits_nonzero(self):
stock = "39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd"
data = bytearray((RWD_DIR / stock).read_bytes())
data[block_offset(data) + 8 + 0x100] ^= 1
repair_file_checksum(data)
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "corrupt.rwd"
path.write_bytes(data)
self.assertFalse(self.check_quietly(path))
env = os.environ.copy()
env["PYTHONUTF8"] = "1"
result = subprocess.run([sys.executable, str(CHECK_RWD_PATH), str(path)],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
self.assertNotEqual(result.returncode, 0)
def test_wrong_v850_identity_and_flash_start_fail_with_valid_outer_checksum(self):
stock = "39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd"
original = (RWD_DIR / stock).read_bytes()
mutations = {}
wrong_identity = bytearray(original)
identity_at = wrong_identity.index(b"39990-TG7-A060")
original_identity = b"39990-TG7-A060"
wrong_identity[identity_at:identity_at + len(original_identity)] = b"39990-TG7-A999"
repair_file_checksum(wrong_identity)
mutations["wrong-identity.rwd"] = wrong_identity
wrong_start = bytearray(original)
struct.pack_into(">I", wrong_start, block_offset(wrong_start), 0x20000)
repair_file_checksum(wrong_start)
mutations["wrong-start.rwd"] = wrong_start
with tempfile.TemporaryDirectory() as tmpdir:
for filename, data in mutations.items():
with self.subTest(filename=filename):
path = Path(tmpdir) / filename
path.write_bytes(data)
self.assertFalse(self.check_quietly(path))
def test_launcher_maps_nested_aliases_and_recursive_globs(self):
nested = "39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd"
expected = str(RWD_DIR / Path(nested))
self.assertEqual(launcher.normalized_arguments("check_rwd.py", [f"rwd/{nested}"]), [expected])
self.assertEqual(launcher.normalized_arguments("eps-update.py", [f"eps_tools/rwd/{nested}"]), [expected])
self.assertEqual(launcher.normalized_arguments("flash.py", ["--bus", "1"]), ["--bus", "1"])
expanded = launcher.normalized_arguments("check_rwd.py", ["rwd/**/*.rwd"])
self.assertEqual(
{Path(path).relative_to(RWD_DIR).as_posix() for path in expanded},
RWD_IMAGES,
)
with self.assertRaisesRegex(SystemExit, "matched more than one image"):
launcher.normalized_arguments("eps-update.py", ["rwd/**/*.rwd"])
def test_guided_flasher_keeps_danger_explicit(self):
dry_run = flash.run_eps_update("rwd/example.rwd", 1, skip_checksum=False, danger=False)
real_flash = flash.run_eps_update("rwd/example.rwd", 1, skip_checksum=False, danger=True)
self.assertNotIn("--danger", dry_run)
self.assertEqual(real_flash.count("--danger"), 1)
self.assertIn(str(EPS_DIR / "eps-update.py"), dry_run)
self.assertIn(str(ROOT), flash.subprocess_env()["PYTHONPATH"].split(os.pathsep))
def test_recovery_guidance_does_not_promise_success_or_blind_retry(self):
readme = (EPS_DIR / "README.md").read_text()
flash_source = (EPS_DIR / "flash.py").read_text()
diag_source = (EPS_DIR / "eps-diag.py").read_text()
self.assertNotIn("the EPS is not permanently bricked", readme)
self.assertNotIn("the EPS is recoverable: just run this again", flash_source)
self.assertNotIn("EPS is responding — try the flash again", diag_source)
self.assertNotIn("re-run eps-update.py with the same .rwd", diag_source)
self.assertIn("recovery is not guaranteed", flash_source)
self.assertIn("it does **not** prove that the selected image", readme)
self.assertIn("Do not blindly reuse the previous .rwd", diag_source)
def test_root_and_historical_commands_accept_nested_paths_with_spaces(self):
nested = "rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd"
environment = os.environ.copy()
environment["PYTHONUTF8"] = "1"
commands = (
([sys.executable, str(ROOT / "check_rwd.py"), nested], ROOT),
([sys.executable, "check_rwd.py", nested], ROOT / "eps_tools"),
)
for command, cwd in commands:
with self.subTest(command=command, cwd=cwd):
result = subprocess.run(command, cwd=cwd, env=environment, capture_output=True, text=True, check=False)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("ALL GOOD", result.stdout)
def test_legacy_eps_directory_contains_launchers_only(self):
legacy = ROOT / "eps_tools"
self.assertFalse((legacy / "rwd").exists())
self.assertFalse((legacy / "rwd_format").exists())
self.assertEqual(
{path.name for path in legacy.iterdir() if path.name != "__pycache__"},
{"README.md", "_bootstrap.py", "check_rwd.py", "eps-diag.py", "eps-update.py", "flash.py"},
)
def test_root_guided_command_lists_only_curated_nested_paths_before_quit(self):
environment = os.environ.copy()
environment["PYTHONUTF8"] = "1"
result = subprocess.run(
[sys.executable, str(ROOT / "flash.py")],
cwd=ROOT,
env=environment,
input="q\n",
capture_output=True,
text=True,
timeout=60,
check=False,
)
output = result.stdout + result.stderr
self.assertNotEqual(result.returncode, 0)
self.assertIn("Aborted", output)
for name in GUIDED_IMAGES:
with self.subTest(guided=name):
self.assertIn(name, output)
for name in set(EXPECTED_ASSETS) - GUIDED_IMAGES:
with self.subTest(reference=name):
self.assertNotIn(name, output)
def test_eps_bundle_and_launchers_ship_in_release(self):
output = subprocess.check_output([sys.executable, "tools/release/release_files.py"], cwd=ROOT)
released = {os.fsdecode(path) for path in output.split(b"\0") if path}
expected = {
path.relative_to(ROOT).as_posix()
for path in EPS_DIR.rglob("*")
if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc"
}
expected.update(("flash.py", "eps-update.py", "eps-diag.py", "check_rwd.py"))
expected.update(
f"eps_tools/{name}"
for name in ("README.md", "_bootstrap.py", "check_rwd.py", "eps-diag.py", "eps-update.py", "flash.py")
)
self.assertEqual(expected - released, set())
if __name__ == "__main__":
unittest.main()
@@ -1,430 +0,0 @@
#!/usr/bin/env python3
"""Detached EPS UI runner.
The settings UI watches status.json while this process releases only the Panda
USB interface, validates and flashes the selected image, then restores pandad.
It never stops the comma service or the UI.
"""
from __future__ import annotations
import argparse
import contextlib
import fcntl
import importlib.util
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import time
import traceback
from datetime import datetime, timezone
EPS_DIR = Path(__file__).resolve().parent
REPO_ROOT = EPS_DIR.parents[4]
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(EPS_DIR))
def _load_eps_flash():
spec = importlib.util.spec_from_file_location("eps_flash_module", EPS_DIR / "flash.py")
if spec is None or spec.loader is None:
raise ImportError("Could not load EPS flash module")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
flash = _load_eps_flash()
from openpilot.cereal import messaging # noqa: E402
STATE_DIR = Path("/data/eps_flash")
STATUS_PATH = STATE_DIR / "status.json"
LOG_PATH = STATE_DIR / "run.log"
LOCK_PATH = STATE_DIR / "runner.lock"
PANDAD_BLOCK_FILE = STATE_DIR / "block_pandad"
DRY_RUN_MARKERS = ("Using real client", "Safe mode: aborting before mutating actions")
def _timestamp() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def _log(message: str) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(f"[{_timestamp()}] {message.rstrip()}\n")
def _write_status(record: dict, **updates) -> None:
record.update(updates)
record["updated_at"] = time.time()
STATE_DIR.mkdir(parents=True, exist_ok=True)
tmp = STATUS_PATH.with_suffix(f".tmp.{os.getpid()}")
with tmp.open("w", encoding="utf-8") as f:
json.dump(record, f, ensure_ascii=False, sort_keys=True)
os.chmod(tmp, 0o644)
os.replace(tmp, STATUS_PATH)
def _vehicle_is_offroad() -> bool:
messaging.reset_context()
sm = messaging.SubMaster(["deviceState"])
# A fresh SubMaster can need a few cycles before deviceState is valid. Poll
# instead of treating one unlucky timeout as an onroad state.
for _ in range(20):
sm.update(250)
if sm.all_checks(["deviceState"]):
return not sm["deviceState"].started
return False
def _pandad_pids() -> list[int]:
pids: list[int] = []
for entry in Path("/proc").iterdir():
if not entry.name.isdigit():
continue
try:
raw_tokens = [t for t in (entry / "cmdline").read_bytes().split(b"\0") if t]
except (OSError, PermissionError):
continue
if any(token == b"openpilot.selfdrive.pandad.pandad" for token in raw_tokens) or \
any(Path(os.fsdecode(token)).name == "pandad" for token in raw_tokens):
pids.append(int(entry.name))
return sorted(pids)
def _wait_for_pandad(absent: bool, timeout: float) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
pids = _pandad_pids()
if bool(not pids) == absent:
return True
time.sleep(0.2)
return bool(not _pandad_pids()) == absent
def _set_block_pandad(blocked: bool) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
if blocked:
PANDAD_BLOCK_FILE.write_text(f"{os.getpid()}\n", encoding="utf-8")
os.chmod(PANDAD_BLOCK_FILE, 0o644)
else:
PANDAD_BLOCK_FILE.unlink(missing_ok=True)
def _run_streamed(state: dict, command: list[str], env: dict[str, str], phase: str) -> tuple[int, str]:
_log("RUN " + " ".join(command))
_write_status(state, phase=phase, message=f"Running {Path(command[1]).name}")
proc = subprocess.Popen(
command,
cwd=EPS_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
lines: list[str] = []
assert proc.stdout is not None
for raw in proc.stdout:
line = raw.rstrip("\n")
lines.append(raw)
if line.strip():
_log(line)
if line.startswith("PROGRESS "):
try:
_, progress_phase, percent = line.split(maxsplit=2)
progress = max(0, min(100, int(percent)))
_write_status(state, phase=progress_phase, progress=progress,
message=f"{progress_phase} {progress}%")
except (TypeError, ValueError):
_write_status(state, phase=phase, message=line.strip()[-240:])
else:
_write_status(state, phase=phase, message=line.strip()[-240:])
rc = proc.wait()
_write_status(state, phase=phase, message=f"{Path(command[1]).name} exited {rc}")
return rc, "".join(lines)
def _validated_image(raw_path: str) -> Path:
path = Path(raw_path)
if not path.is_absolute():
path = EPS_DIR / path
path = path.resolve()
allowed = {Path(p).resolve() for p in flash.find_images()}
if path not in allowed:
raise ValueError("Firmware is not in the curated EPS guided-flash list")
if not flash.validate(str(path)):
raise ValueError("Firmware failed check_rwd validation")
return path
def _image_matches_car(image: Path) -> bool:
car_fw = flash.car_eps_fw()
if not car_fw:
try:
car_fw = json.loads(STATUS_PATH.read_text(encoding="utf-8")).get("detected_fw")
except (OSError, ValueError, TypeError):
car_fw = None
if not car_fw:
return False
versions = flash.rwd_supported_versions(str(image)) or set()
return flash.norm_fw(car_fw) in versions
def _read_live_eps_info() -> dict:
info = {
"eps_online": False,
"eps_bus": None,
"eps_part_number": None,
"eps_vin": None,
"eps_read_at": time.time(),
}
try:
from panda import Panda
from opendbc.car.structs import CarParams
from opendbc.car.uds import DATA_IDENTIFIER_TYPE, SESSION_TYPE, UdsClient, NegativeResponseError
except Exception as exc:
_log(f"Live EPS identify unavailable: {exc}")
return info
panda = None
try:
panda = Panda(disable_checks=True)
panda.set_safety_mode(CarParams.SafetyModel.elm327)
for bus in (1, 0):
uds = UdsClient(panda, flash.EPS_ADDR, bus=bus, timeout=2.0)
try:
uds.tester_present()
except NegativeResponseError:
pass
except Exception:
continue
info["eps_online"] = True
info["eps_bus"] = bus
_log(f"EPS responded on bus {bus}")
try:
uds.diagnostic_session_control(SESSION_TYPE.DEFAULT)
except Exception:
pass
try:
data = uds.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
part_number = bytes(data).decode("latin-1", "replace").strip("\x00").strip()
if part_number:
info["eps_part_number"] = part_number
_log(f"Live EPS part number: {part_number}")
except Exception as exc:
_log(f"EPS part number read failed on bus {bus}: {exc}")
try:
data = uds.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VIN)
vin = bytes(data).decode("latin-1", "replace").strip("\x00").strip()
if vin:
info["eps_vin"] = vin
_log(f"EPS VIN: {vin}")
except Exception:
pass
break
except Exception as exc:
_log(f"Live EPS identify failed: {exc}")
finally:
if panda is not None:
panda.close()
return info
def _resolve_bus(requested: str, image: Path, state: dict) -> int:
if requested != "auto":
return int(requested, 0)
_write_status(state, phase="bus_detect", message="Detecting EPS CAN bus")
addr = flash.rwd_can_address(str(image)) or flash.EPS_ADDR
output = io.StringIO()
with contextlib.redirect_stdout(output):
alive = flash.probe_eps_buses(addr)
for line in output.getvalue().splitlines():
if line.strip():
_log(line)
if 1 in alive:
return 1
if alive:
return alive[0]
_log("No live EPS bus detected; falling back to bus 1")
return 1
def _new_state(action: str, image: Path | None = None) -> dict:
return {
"state": "running",
"phase": f"{action}_starting",
"message": f"Starting {action}",
"action": action,
"image": flash.image_display_name(str(image)) if image else "",
"image_path": str(image) if image else "",
"started_at": time.time(),
"updated_at": time.time(),
"bus": None,
"progress": 0,
"returncode": None,
}
def _failed(state: dict, exc: Exception) -> int:
_log(f"FAILED: {exc}")
_log(traceback.format_exc())
_write_status(state, state="failed", phase="failed", message=str(exc))
return 1
def _release_panda(state: dict, args: argparse.Namespace) -> int:
try:
if not _vehicle_is_offroad():
raise RuntimeError("Vehicle must be offroad before releasing Panda")
_write_status(state, phase="releasing_panda", message="Stopping pandad and releasing Panda USB")
_set_block_pandad(True)
if not _wait_for_pandad(absent=True, timeout=args.release_timeout):
_set_block_pandad(False)
raise RuntimeError("pandad did not exit; Panda was not released")
time.sleep(args.release_delay)
eps_info = _read_live_eps_info()
detected_fw = eps_info.get("eps_part_number")
if detected_fw:
message = f"Panda released; EPS {detected_fw} on bus {eps_info.get('eps_bus')}"
else:
message = "Panda released; EPS did not respond"
_write_status(state, detected_fw=detected_fw, state="success", phase="panda_released", **eps_info)
_write_status(state, message=message)
_log("Panda released")
return 0
except Exception as exc:
_set_block_pandad(False)
return _failed(state, exc)
def _restore_panda(state: dict, args: argparse.Namespace) -> int:
try:
_write_status(state, phase="restoring_panda", message="Removing Panda release lock")
_set_block_pandad(False)
if not _wait_for_pandad(absent=False, timeout=args.restore_timeout):
raise RuntimeError("pandad did not restart before timeout")
_write_status(state, state="success", phase="panda_restored", message="pandad restored")
_log("pandad restored")
return 0
except Exception as exc:
return _failed(state, exc)
def _identify_eps(state: dict, args: argparse.Namespace) -> int:
try:
if not _vehicle_is_offroad():
raise RuntimeError("Vehicle must be offroad before reading EPS")
if not PANDAD_BLOCK_FILE.exists():
raise RuntimeError("Release Panda first")
if not _wait_for_pandad(absent=True, timeout=args.release_timeout):
raise RuntimeError("pandad is still running")
_write_status(state, phase="identify", message="Reading EPS software ID and VIN")
eps_info = _read_live_eps_info()
detected_fw = eps_info.get("eps_part_number")
message = f"EPS {detected_fw} on bus {eps_info.get('eps_bus')}" if detected_fw else "No EPS part number received"
_write_status(state, detected_fw=detected_fw, state="success", phase="eps_identified", message=message, **eps_info)
return 0
except Exception as exc:
return _failed(state, exc)
def _flash_panda(state: dict, args: argparse.Namespace, image: Path) -> int:
try:
if not _vehicle_is_offroad():
raise RuntimeError("Vehicle must be offroad before flashing EPS")
if not _image_matches_car(image):
raise RuntimeError("Selected firmware does not match the reported EPS firmware")
if not PANDAD_BLOCK_FILE.exists():
raise RuntimeError("Release Panda first")
if not _wait_for_pandad(absent=True, timeout=args.release_timeout):
raise RuntimeError("pandad is still running; restore and release Panda again")
env = flash.subprocess_env()
bus = _resolve_bus(args.bus, image, state)
_write_status(state, bus=bus)
_log(f"Using CAN bus {bus}")
dry_cmd = [
sys.executable,
str(EPS_DIR / "eps-update.py"),
str(image),
"-b",
str(bus),
"--seed-timeout",
str(args.seed_timeout),
]
dry_rc, dry_output = _run_streamed(state, dry_cmd, env, "dry_run")
if not all(marker in dry_output for marker in DRY_RUN_MARKERS):
raise RuntimeError(f"Dry run did not reach the safe abort point (exit {dry_rc})")
if not _vehicle_is_offroad():
raise RuntimeError("Vehicle left offroad during the dry run; refusing to flash")
_write_status(state, phase="dry_run_ok", message="Dry run passed; flashing now")
flash_rc, _ = _run_streamed(state, [*dry_cmd, "--danger"], env, "flashing")
_write_status(state, returncode=flash_rc)
if flash_rc != 0:
raise RuntimeError(f"EPS flash failed with exit code {flash_rc}")
_write_status(state, state="success", phase="complete", message="EPS flash completed; restore Panda when ready")
_log("EPS flash completed successfully")
return 0
except Exception as exc:
return _failed(state, exc)
def run(args: argparse.Namespace) -> int:
image = None
if args.action == "flash":
try:
image = _validated_image(args.rwd)
except Exception as exc:
return _failed(_new_state(args.action), exc)
state = _new_state(args.action, image)
_write_status(state)
if args.action == "release":
return _release_panda(state, args)
if args.action == "restore":
return _restore_panda(state, args)
if args.action == "identify":
return _identify_eps(state, args)
return _flash_panda(state, args, image)
def main() -> int:
parser = argparse.ArgumentParser(description="UI-owned Honda/Acura EPS helper")
parser.add_argument("--action", choices=("release", "restore", "identify", "flash"), default="flash")
parser.add_argument("--rwd", help="Curated .rwd path or path relative to eps/")
parser.add_argument("--bus", default="auto", help="auto, 0, or 1")
parser.add_argument("--seed-timeout", type=float, default=600.0, help="Security-access seed wait in seconds")
parser.add_argument("--release-timeout", type=float, default=20.0, help="Seconds to wait for pandad to exit")
parser.add_argument("--release-delay", type=float, default=2.0, help="Extra USB release settling time")
parser.add_argument("--restore-timeout", type=float, default=20.0, help="Seconds to wait for pandad to restart")
args = parser.parse_args()
if args.action == "flash" and not args.rwd:
parser.error("--rwd is required for --action flash")
STATE_DIR.mkdir(parents=True, exist_ok=True)
lock_file = LOCK_PATH.open("a+")
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
_log("Another EPS UI flash runner is already active")
return 2
try:
return run(args)
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
lock_file.close()
if __name__ == "__main__":
raise SystemExit(main())
+2 -15
View File
@@ -15,9 +15,6 @@ from openpilot.common.text_window import TextWindow
from openpilot.common.hardware import HARDWARE, PC
from openpilot.system.manager.helpers import unblock_stdout, save_bootlog
from openpilot.system.manager.process import ensure_running
PANDAD_BLOCK_FILE = "/data/eps_flash/block_pandad"
from openpilot.system.manager.process_config import managed_processes
from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler
@@ -133,21 +130,11 @@ def manager_thread() -> None:
ignore.append("pandad")
ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0]
# A stale lock must never survive a manager restart. The EPS UI creates this
# file only while it owns the Panda USB interface, then removes it when done.
try:
os.unlink(PANDAD_BLOCK_FILE)
except FileNotFoundError:
pass
def get_not_run() -> list[str]:
return [*ignore, *(["pandad"] if os.path.exists(PANDAD_BLOCK_FILE) else [])]
sm = messaging.SubMaster(['deviceState', 'carParams', 'pandaStates'], poll='deviceState')
pm = messaging.PubMaster(['managerState'])
params.put_bool("IsOffroad", True, block=True)
ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=get_not_run())
ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore)
started_prev = False
ignition_prev = False
@@ -173,7 +160,7 @@ def manager_thread() -> None:
started_prev = started
ignition_prev = ignition
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=get_not_run())
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name)
for p in managed_processes.values() if p.proc)