mirror of
https://jihulab.com/mr-one/openpilot.git
synced 2026-10-27 13:03:42 +08:00
revert: remove EPS flashing integration
This commit is contained in:
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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`.
|
||||
@@ -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",)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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())
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user