diff --git a/check_rwd.py b/check_rwd.py
deleted file mode 100755
index 4ba785421..000000000
--- a/check_rwd.py
+++ /dev/null
@@ -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")
diff --git a/eps-diag.py b/eps-diag.py
deleted file mode 100755
index 6c53f65c7..000000000
--- a/eps-diag.py
+++ /dev/null
@@ -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")
diff --git a/eps-update.py b/eps-update.py
deleted file mode 100755
index 8029ac00f..000000000
--- a/eps-update.py
+++ /dev/null
@@ -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")
diff --git a/eps_tools/README.md b/eps_tools/README.md
deleted file mode 100644
index e56976954..000000000
--- a/eps_tools/README.md
+++ /dev/null
@@ -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`.
diff --git a/eps_tools/_bootstrap.py b/eps_tools/_bootstrap.py
deleted file mode 100644
index f83b202b5..000000000
--- a/eps_tools/_bootstrap.py
+++ /dev/null
@@ -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",)
diff --git a/eps_tools/check_rwd.py b/eps_tools/check_rwd.py
deleted file mode 100755
index 81fe3aa9e..000000000
--- a/eps_tools/check_rwd.py
+++ /dev/null
@@ -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")
diff --git a/eps_tools/eps-diag.py b/eps_tools/eps-diag.py
deleted file mode 100755
index 2e94cec47..000000000
--- a/eps_tools/eps-diag.py
+++ /dev/null
@@ -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")
diff --git a/eps_tools/eps-update.py b/eps_tools/eps-update.py
deleted file mode 100755
index 1c19e54e1..000000000
--- a/eps_tools/eps-update.py
+++ /dev/null
@@ -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")
diff --git a/eps_tools/flash.py b/eps_tools/flash.py
deleted file mode 100755
index 1c1faf4af..000000000
--- a/eps_tools/flash.py
+++ /dev/null
@@ -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")
diff --git a/flash.py b/flash.py
deleted file mode 100755
index 8a5f08573..000000000
--- a/flash.py
+++ /dev/null
@@ -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")
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/eps_flash.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/eps_flash.py
deleted file mode 100644
index 30daa872d..000000000
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/eps_flash.py
+++ /dev/null
@@ -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?
"
- "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(
- "Flash matched firmware?
"
- "Firmware: {}
"
- "Keep the vehicle in ACCESSORY mode and do not interrupt power.
"
- "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()
\ No newline at end of file
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py
index 6ee98a2af..a5b66b3d3 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py
@@ -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"),
diff --git a/openpilot/sunnypilot/nrdr/__init__.py b/openpilot/sunnypilot/nrdr/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openpilot/sunnypilot/nrdr/tools/__init__.py b/openpilot/sunnypilot/nrdr/tools/__init__.py
deleted file mode 100644
index d5b103ab4..000000000
--- a/openpilot/sunnypilot/nrdr/tools/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""NRDR-owned offline and device tools."""
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/README.md b/openpilot/sunnypilot/nrdr/tools/eps/README.md
deleted file mode 100644
index 0a9b13c72..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/README.md
+++ /dev/null
@@ -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.
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/__init__.py b/openpilot/sunnypilot/nrdr/tools/eps/__init__.py
deleted file mode 100644
index 612fe6afb..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/__init__.py
+++ /dev/null
@@ -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",)
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/check_rwd.py b/openpilot/sunnypilot/nrdr/tools/eps/check_rwd.py
deleted file mode 100644
index f3a28b8fe..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/check_rwd.py
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/usr/bin/env python3
-# Usage: python3 check_rwd.py [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(' [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)
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/eps-diag.py b/openpilot/sunnypilot/nrdr/tools/eps/eps-diag.py
deleted file mode 100644
index cf62afaaf..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/eps-diag.py
+++ /dev/null
@@ -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()
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/eps-update.py b/openpilot/sunnypilot/nrdr/tools/eps/eps-update.py
deleted file mode 100644
index 2347fd6fd..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/eps-update.py
+++ /dev/null
@@ -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(' 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")
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/flash.py b/openpilot/sunnypilot/nrdr/tools/eps/flash.py
deleted file mode 100644
index f61534675..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/flash.py
+++ /dev/null
@@ -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()
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/launcher.py b/openpilot/sunnypilot/nrdr/tools/eps/launcher.py
deleted file mode 100644
index 305d7ec9a..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/launcher.py
+++ /dev/null
@@ -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")
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-T6Z-A110/39990-T6Z-A110.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-T6Z-A110/39990-T6Z-A110.rwd
deleted file mode 100644
index 97948069f..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-T6Z-A110/39990-T6Z-A110.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd
deleted file mode 100644
index 35afb8397..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd
deleted file mode 100644
index 7984085f0..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd
deleted file mode 100644
index d4d9fc383..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd
deleted file mode 100644
index 61bcb971e..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd
deleted file mode 100644
index fa6e6ff4e..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-linear-max-V2.1-Linear45-FlatGain.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-linear-max-V2.1-Linear45-FlatGain.rwd
deleted file mode 100644
index 76b2e43e2..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-linear-max-V2.1-Linear45-FlatGain.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd
deleted file mode 100644
index a4d66f859..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd
deleted file mode 100644
index 85010e614..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd
deleted file mode 100644
index 90a97d506..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd
deleted file mode 100644
index 439d706ab..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd
deleted file mode 100644
index b25a946c0..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd
deleted file mode 100644
index 20ac3983d..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd
deleted file mode 100644
index bfa2fb713..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd
deleted file mode 100644
index 0d94afcfa..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin
deleted file mode 100644
index e2774910b..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd
deleted file mode 100644
index 92905e68c..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot) b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot)
deleted file mode 100644
index f6b1b0886..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot) and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd
deleted file mode 100644
index 170b26a13..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd
deleted file mode 100644
index f1ab581af..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd
deleted file mode 100644
index b52dc560b..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/mod-39990-TGG,A120.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/mod-39990-TGG,A120.rwd
deleted file mode 100644
index 1841e66f4..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/mod-39990-TGG,A120.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/stock-39990-TGG-A120.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/stock-39990-TGG-A120.rwd
deleted file mode 100644
index 196c2803c..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/stock-39990-TGG-A120.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd
deleted file mode 100644
index d35c170c6..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin
deleted file mode 100644
index 3871b45ff..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd
deleted file mode 100644
index 8ef9f90e5..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd
deleted file mode 100644
index 5621fe122..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd
deleted file mode 100644
index 2ae0f20b9..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Proper Torque Mod/39990-TLA-A040_Clarity_FF_tune_telemety_8cf8e537.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd
deleted file mode 100644
index 617fea117..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd
deleted file mode 100644
index d73f31635..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd
deleted file mode 100644
index 42fe6796c..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin
deleted file mode 100644
index 84807b82b..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd
deleted file mode 100644
index e1a134602..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd
deleted file mode 100644
index 9a4f0c3cd..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd
deleted file mode 100644
index a2a3d5d2c..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd
deleted file mode 100644
index 809ddb394..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd
deleted file mode 100644
index dc123b677..000000000
Binary files a/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd and /dev/null differ
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md b/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md
deleted file mode 100644
index 048ea9d5e..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md
+++ /dev/null
@@ -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.
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/SHA256SUMS b/openpilot/sunnypilot/nrdr/tools/eps/rwd/SHA256SUMS
deleted file mode 100644
index 5a5b5689f..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/rwd/SHA256SUMS
+++ /dev/null
@@ -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
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/__init__.py b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py
deleted file mode 100644
index 74b6bb015..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py
+++ /dev/null
@@ -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('= 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
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/x5a.py b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/x5a.py
deleted file mode 100644
index 064cb36ae..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/x5a.py
+++ /dev/null
@@ -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]
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/tests/__init__.py b/openpilot/sunnypilot/nrdr/tools/eps/tests/__init__.py
deleted file mode 100644
index b10c77366..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the canonical NRDR EPS tool bundle."""
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/tests/test_rwd_validation.py b/openpilot/sunnypilot/nrdr/tools/eps/tests/test_rwd_validation.py
deleted file mode 100644
index d63c5961b..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/tests/test_rwd_validation.py
+++ /dev/null
@@ -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", 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()
diff --git a/openpilot/sunnypilot/nrdr/tools/eps/ui_flash_runner.py b/openpilot/sunnypilot/nrdr/tools/eps/ui_flash_runner.py
deleted file mode 100755
index 426e9645f..000000000
--- a/openpilot/sunnypilot/nrdr/tools/eps/ui_flash_runner.py
+++ /dev/null
@@ -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())
\ No newline at end of file
diff --git a/openpilot/system/manager/manager.py b/openpilot/system/manager/manager.py
index 80cc0d5e3..7befb7f4a 100755
--- a/openpilot/system/manager/manager.py
+++ b/openpilot/system/manager/manager.py
@@ -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)