diff --git a/check_rwd.py b/check_rwd.py new file mode 100755 index 000000000..4ba785421 --- /dev/null +++ b/check_rwd.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..6c53f65c7 --- /dev/null +++ b/eps-diag.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..8029ac00f --- /dev/null +++ b/eps-update.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100644 index 000000000..e56976954 --- /dev/null +++ b/eps_tools/README.md @@ -0,0 +1,8 @@ +# 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 new file mode 100644 index 000000000..f83b202b5 --- /dev/null +++ b/eps_tools/_bootstrap.py @@ -0,0 +1,14 @@ +"""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 new file mode 100755 index 000000000..81fe3aa9e --- /dev/null +++ b/eps_tools/check_rwd.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..2e94cec47 --- /dev/null +++ b/eps_tools/eps-diag.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..1c19e54e1 --- /dev/null +++ b/eps_tools/eps-update.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..1c1faf4af --- /dev/null +++ b/eps_tools/flash.py @@ -0,0 +1,8 @@ +#!/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 new file mode 100755 index 000000000..8a5f08573 --- /dev/null +++ b/flash.py @@ -0,0 +1,8 @@ +#!/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/sunnypilot/nrdr/__init__.py b/openpilot/sunnypilot/nrdr/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openpilot/sunnypilot/nrdr/tools/__init__.py b/openpilot/sunnypilot/nrdr/tools/__init__.py new file mode 100644 index 000000000..d5b103ab4 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/__init__.py @@ -0,0 +1 @@ +"""NRDR-owned offline and device tools.""" diff --git a/openpilot/sunnypilot/nrdr/tools/eps/README.md b/openpilot/sunnypilot/nrdr/tools/eps/README.md new file mode 100644 index 000000000..0a9b13c72 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/README.md @@ -0,0 +1,197 @@ +# 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 new file mode 100644 index 000000000..612fe6afb --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/__init__.py @@ -0,0 +1,6 @@ +"""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 new file mode 100644 index 000000000..f3a28b8fe --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/check_rwd.py @@ -0,0 +1,169 @@ +#!/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 new file mode 100644 index 000000000..cf62afaaf --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/eps-diag.py @@ -0,0 +1,352 @@ +""" +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 new file mode 100644 index 000000000..e172c3fc3 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/eps-update.py @@ -0,0 +1,335 @@ +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 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) + + 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") + data = uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, ROUTINE_IDENTIFIER_TYPE.ERASE_MEMORY) + debug_output = debug_output + [data] + + 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") + 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 + 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) + + print("Requesting transfer exit") + 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] + + print("Resetting ECU") + data = uds_client.ecu_reset(RESET_TYPE.HARD) + debug_output = debug_output + [data] + + 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 new file mode 100644 index 000000000..f61534675 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/flash.py @@ -0,0 +1,586 @@ +#!/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 new file mode 100644 index 000000000..305d7ec9a --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/launcher.py @@ -0,0 +1,68 @@ +"""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 new file mode 100644 index 000000000..97948069f Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-T6Z-A110/39990-T6Z-A110.rwd 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 new file mode 100644 index 000000000..35afb8397 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/39990-TBA,A030-linear-max.rwd 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 new file mode 100644 index 000000000..7984085f0 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/mod-39990-TBA,A030.rwd 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 new file mode 100644 index 000000000..d4d9fc383 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Legacy and Stock/stock-39990-TBA-A030.rwd 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 new file mode 100644 index 000000000..61bcb971e Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-A030/Proper Torque Mod/39990-TBA,A030-PTM.rwd 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 new file mode 100644 index 000000000..fa6e6ff4e Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/2.5x-mod-39990-TBA,C020.rwd 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 new file mode 100644 index 000000000..76b2e43e2 Binary files /dev/null and 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 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 new file mode 100644 index 000000000..a4d66f859 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/39990-TBA-C020-max.rwd 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 new file mode 100644 index 000000000..85010e614 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Legacy and Stock/stock-39990-TBA-C020.rwd 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 new file mode 100644 index 000000000..90a97d506 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C020 (2019 Honda Civic Sport)/Proper Torque Mod/39990-TBA,C020-Trk4000-PTM.rwd 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 new file mode 100644 index 000000000..439d706ab Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/mod-39990-TBA,C120.rwd 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 new file mode 100644 index 000000000..b25a946c0 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Legacy and Stock/stock-39990-TBA-C120.rwd 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 new file mode 100644 index 000000000..20ac3983d Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TBA-C120/Proper Torque Mod/39990-TBA,C120-PTM.rwd 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 new file mode 100644 index 000000000..bfa2fb713 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Legacy and Stock/39990-TEG-A010-linear-max.rwd 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 new file mode 100644 index 000000000..0d94afcfa Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TEG-A010/Proper Torque Mod/39990-TEG,A010-PTM.rwd 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 new file mode 100644 index 000000000..e2774910b Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.bin 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 new file mode 100644 index 000000000..92905e68c Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Legacy and Stock/39990-TG7-A060-STOCK.rwd 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) new file mode 100644 index 000000000..f6b1b0886 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TG7-A060 (2019 Honda Pilot)/Work in Progress/39990-TG7-A060-2X (2019 Honda Pilot) 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 new file mode 100644 index 000000000..170b26a13 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/mod-39990-TGG,A020-HatchbackSport.rwd 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 new file mode 100644 index 000000000..f1ab581af Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A020/Legacy and Stock/stock-39990-TGG-A020-HatchbackSport.rwd 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 new file mode 100644 index 000000000..b52dc560b Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/39990-TGG-A120-08072026-4250.rwd 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 new file mode 100644 index 000000000..1841e66f4 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/mod-39990-TGG,A120.rwd 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 new file mode 100644 index 000000000..196c2803c Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TGG-A120/stock-39990-TGG-A120.rwd 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 new file mode 100644 index 000000000..d35c170c6 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-linear-max.rwd 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 new file mode 100644 index 000000000..3871b45ff Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.bin 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 new file mode 100644 index 000000000..8ef9f90e5 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040-stock.rwd 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 new file mode 100644 index 000000000..5621fe122 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TLA-A040 - (Honda_CRV_5G)/Legacy and Stock/39990-TLA-A040_tq30000_a9000_44256c0b.rwd 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 new file mode 100644 index 000000000..2ae0f20b9 Binary files /dev/null and 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 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 new file mode 100644 index 000000000..617fea117 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TRW-A020 (Honda Clarity)/Proper Torque Mod/ClarityMax-PTM.rwd 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 new file mode 100644 index 000000000..d73f31635 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/39990-TVA,A160-V37-2xTorque.rwd 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 new file mode 100644 index 000000000..42fe6796c Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TVA-A160 (Honda Accord)/Stock/39990-TVA-A160-V9B-Stock.rwd 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 new file mode 100644 index 000000000..84807b82b Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/39990-TXM-A040 Bin/user.bin 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 new file mode 100644 index 000000000..e1a134602 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2.5x-spike.rwd 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 new file mode 100644 index 000000000..9a4f0c3cd Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-2x-linear.rwd 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 new file mode 100644 index 000000000..a2a3d5d2c Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-STOCK.rwd 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 new file mode 100644 index 000000000..809ddb394 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Legacy and Stock/TXM-A040-linear-max.rwd 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 new file mode 100644 index 000000000..dc123b677 Binary files /dev/null and b/openpilot/sunnypilot/nrdr/tools/eps/rwd/39990-TXM-A040 (2019-2022 Honda Insight)/Proper Torque Mod/39990-TXM,A040-PTM.rwd differ diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md b/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md new file mode 100644 index 000000000..048ea9d5e --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/rwd/README.md @@ -0,0 +1,119 @@ +# 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 new file mode 100644 index 000000000..5a5b5689f --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/rwd/SHA256SUMS @@ -0,0 +1,37 @@ +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 new file mode 100644 index 000000000..e69de29bb diff --git a/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py new file mode 100644 index 000000000..74b6bb015 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/base.py @@ -0,0 +1,60 @@ +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 new file mode 100644 index 000000000..064cb36ae --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/rwd_format/x5a.py @@ -0,0 +1,60 @@ +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 new file mode 100644 index 000000000..b10c77366 --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/tests/__init__.py @@ -0,0 +1 @@ +"""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 new file mode 100644 index 000000000..d63c5961b --- /dev/null +++ b/openpilot/sunnypilot/nrdr/tools/eps/tests/test_rwd_validation.py @@ -0,0 +1,440 @@ +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()