version: sunnypilot v2026.002.000 (staging) date: 2026-07-21T07:44:02 master commit: 2c334ede443d7391d27575af8c854a095ba702a8
107 lines
3.3 KiB
Python
Executable File
107 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|
|
|
This file is part of sunnypilot and is licensed under the MIT License.
|
|
See the LICENSE.md file in the root directory for more details.
|
|
"""
|
|
import os
|
|
from itertools import accumulate
|
|
|
|
from openpilot.cereal import messaging
|
|
from opendbc.car.structs import car
|
|
from panda import Panda
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
|
|
FW_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rivian_long_fw.bin.signed")
|
|
SECTOR_SIZES = [0x4000] * 4 + [0x10000] + [0x20000] * 11
|
|
|
|
|
|
def _is_rivian() -> bool:
|
|
params = Params()
|
|
|
|
# check fixed fingerprint
|
|
if bundle := params.get("CarPlatformBundle"):
|
|
if bundle.get("brand") == "rivian":
|
|
return True
|
|
|
|
# check cached fingerprint
|
|
CP_bytes = params.get("CarParamsPersistent")
|
|
if CP_bytes is not None:
|
|
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
|
|
if CP.brand == "rivian":
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _flash_static(handle, code):
|
|
assert Panda.flasher_present(handle)
|
|
last_sector = next((i + 1 for i, v in enumerate(accumulate(SECTOR_SIZES[1:])) if v > len(code)), -1)
|
|
assert 1 <= last_sector < 7, "Invalid firmware size"
|
|
|
|
handle.controlWrite(Panda.REQUEST_IN, 0xb1, 0, 0, b'')
|
|
for i in range(1, last_sector + 1):
|
|
handle.controlWrite(Panda.REQUEST_IN, 0xb2, i, 0, b'')
|
|
for i in range(0, len(code), 0x10):
|
|
handle.bulkWrite(2, code[i:i + 0x10])
|
|
try:
|
|
handle.controlWrite(Panda.REQUEST_IN, 0xd8, 0, 0, b'', expect_disconnect=True)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _flash_panda(panda: Panda) -> None:
|
|
expected_sig = Panda.get_signature_from_firmware(FW_PATH)
|
|
if not panda.bootstub and panda.get_signature() == expected_sig:
|
|
cloudlog.info(f"F4 panda {panda.get_usb_serial()} already up to date")
|
|
return
|
|
|
|
cloudlog.info(f"Flashing F4 panda {panda.get_usb_serial()}")
|
|
with open(FW_PATH, "rb") as f:
|
|
code = f.read()
|
|
|
|
if not panda.bootstub:
|
|
# enter bootstub directly, panda.reset() rejects deprecated hw types
|
|
try:
|
|
panda._handle.controlWrite(Panda.REQUEST_IN, 0xd1, 1, 0, b'', timeout=15000, expect_disconnect=True)
|
|
except Exception:
|
|
pass
|
|
panda.close()
|
|
panda.reconnect()
|
|
|
|
_flash_static(panda._handle, code)
|
|
panda.reconnect()
|
|
cloudlog.info(f"Successfully flashed xnor's Rivian Longitudinal Upgrade Kit: {panda.get_usb_serial()}")
|
|
|
|
|
|
def flash_rivian_long(panda_serials: list[str]) -> None:
|
|
if not os.path.isfile(FW_PATH):
|
|
cloudlog.error(f"Rivian longitudinal upgrade firmware not found at {FW_PATH}")
|
|
return
|
|
|
|
if not _is_rivian():
|
|
cloudlog.info("Not a Rivian, skipping longitudinal upgrade...")
|
|
return
|
|
|
|
# only check USB connected pandas, internal panda uses SPI and is never an external panda
|
|
usb_serials = set(Panda.usb_list())
|
|
for serial in panda_serials:
|
|
if serial not in usb_serials:
|
|
continue
|
|
panda = Panda(serial)
|
|
# only flash external black pandas (HW_TYPE_BLACK = 0x03)
|
|
if panda.get_type() == b'\x03' and not panda.is_internal():
|
|
try:
|
|
_flash_panda(panda)
|
|
except Exception:
|
|
cloudlog.exception(f"Failed to flash xnor's Rivian Longitudinal Upgrade Kit: {serial}")
|
|
panda.close()
|
|
|
|
return
|
|
|
|
|
|
if __name__ == '__main__':
|
|
flash_rivian_long(Panda.list())
|