From 082cf39d736ad10a10ca33c7ba3f01f12e0a9f00 Mon Sep 17 00:00:00 2001 From: Christopher Haucke <132518562+CHaucke89@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:01:27 -0500 Subject: [PATCH 01/10] UI: Fix option control display for floating point params (#1711) --- system/ui/sunnypilot/widgets/option_control.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index 91e9650eb..291d8f6ff 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -44,7 +44,8 @@ class OptionControlSP(ItemAction): self.current_value = int(key) break else: - self.current_value = int(self.params.get(self.param_key, return_default=True)) + value = self.params.get(self.param_key, return_default=True) + self.current_value = int(float(value) * 100.0) if self.use_float_scaling else int(value) # Initialize font and button styles self._font = gui_app.font(FontWeight.MEDIUM) From 111e8897be79f4c99734e77d0c389612f14a8e50 Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 23 Feb 2026 22:23:08 -0500 Subject: [PATCH 02/10] models: fix default & index "0" (#1718) * default be gone * zero is the most significant number, don't ignore it. * thanks sentry!! --- common/params_keys.h | 2 +- sunnypilot/models/manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 6f27d9582..8f3ee858f 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -190,7 +190,7 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, - {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT, "0"}}, + {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 2d3d670bf..8fee0798b 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -171,7 +171,7 @@ class ModelManagerSP: self.available_models = self.model_fetcher.get_available_bundles() self.active_bundle = get_active_bundle(self.params) - if index_to_download := self.params.get("ModelManager_DownloadIndex"): + if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) From 8a64cc57a96845c1ea67b537d648441631f97a6d Mon Sep 17 00:00:00 2001 From: Angus Dippenaar Date: Tue, 24 Feb 2026 06:22:40 +0100 Subject: [PATCH 03/10] [TIZI/TICI] visuals: Improved speed limit (#1713) improved speed limit Co-authored-by: royjr Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index e8b5c5cfc..39a9848d3 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -4,6 +4,7 @@ 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. """ + from dataclasses import dataclass import math import pyray as rl @@ -33,7 +34,7 @@ SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source class Colors: WHITE = rl.WHITE BLACK = rl.BLACK - RED = rl.RED + RED = rl.Color(235, 32, 32, 255) GREY = rl.Color(145, 155, 149, 255) DARK_GREY = rl.Color(77, 77, 77, 255) SUB_BG = rl.Color(0, 0, 0, 180) @@ -201,22 +202,17 @@ class SpeedLimitRenderer(Widget): center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2) radius = (rect.width + 18) / 2 - white = rl.Color(255, 255, 255, int(255 * alpha)) - red = rl.Color(255, 0, 0, int(255 * alpha)) - - if hasattr(color, 'r'): - text_color = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - else: - text_color = rl.Color(color[0], color[1], color[2], int(255 * alpha)) - - black = rl.Color(0, 0, 0, int(255 * alpha)) - dark_grey = rl.Color(77, 77, 77, int(255 * alpha)) + white = rl.color_alpha(Colors.WHITE, alpha) + red = rl.color_alpha(Colors.RED, alpha) + black = rl.color_alpha(Colors.BLACK, alpha) + dark_grey = rl.color_alpha(Colors.DARK_GREY, alpha) + text_color = rl.color_alpha(color, alpha) rl.draw_circle_v(center, radius, white) - rl.draw_ring(center, radius * 0.80, radius, 0, 360, 36, red) + rl.draw_ring(center, radius * 0.75, radius, 0, 360, 36, red) - f_size = 70 if len(val) >= 3 else 85 - self._draw_text_centered(self.font_bold, val, f_size, center, text_color) + font_size = 70 if len(val) >= 3 else 85 + self._draw_text_centered(self.font_bold, val, font_size, center, text_color) if sub and has_limit: s_radius = radius * 0.4 @@ -225,22 +221,23 @@ class SpeedLimitRenderer(Widget): rl.draw_circle_v(s_center, s_radius, black) rl.draw_ring(s_center, s_radius - 3, s_radius, 0, 360, 36, dark_grey) - f_scale = 0.5 if len(sub) < 3 else 0.45 - self._draw_text_centered(self.font_bold, sub, int(s_radius * 2 * f_scale), s_center, white) + font_scale = 0.5 if len(sub) < 3 else 0.45 + self._draw_text_centered(self.font_bold, sub, int(s_radius * 2 * font_scale), s_center, white) def _render_mutcd(self, rect, val, sub, color, has_limit, alpha=1.0): - white = rl.Color(255, 255, 255, int(255 * alpha)) - black = rl.Color(0, 0, 0, int(255 * alpha)) - dark_grey = rl.Color(77, 77, 77, int(255 * alpha)) - - if hasattr(color, 'r'): - text_color = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - else: - text_color = rl.Color(color[0], color[1], color[2], int(255 * alpha)) + white = rl.color_alpha(Colors.WHITE, alpha) + black = rl.color_alpha(Colors.BLACK, alpha) + dark_grey = rl.color_alpha(Colors.DARK_GREY, alpha) + text_color = rl.color_alpha(color, alpha) rl.draw_rectangle_rounded(rect, 0.35, 10, white) + inner = rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20) - rl.draw_rectangle_rounded_lines_ex(inner, 0.35, 10, 4, black) + outer_radius = 0.35 * rect.width / 2.0 + inner_radius = outer_radius - 10.0 + inner_roundness = inner_radius / (inner.width / 2.0) + + rl.draw_rectangle_rounded_lines_ex(inner, inner_roundness, 10, 4, black) self._draw_text_centered(self.font_demi, "SPEED", 40, rl.Vector2(rect.x + rect.width / 2, rect.y + 40), black) self._draw_text_centered(self.font_demi, "LIMIT", 40, rl.Vector2(rect.x + rect.width / 2, rect.y + 80), black) From c48b724de1e3f44e0eaea43b1c2cf79c9b5ece1e Mon Sep 17 00:00:00 2001 From: James Mikesell Date: Tue, 24 Feb 2026 02:38:19 -0500 Subject: [PATCH 04/10] ICBM: ensure button timers update on disable to clear stale presses (#1688) * ICBM: ensure button timers update on disable to clear stale presses * fix race condition --------- Co-authored-by: Jason Wen --- selfdrive/car/cruise.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 9973862b8..572dfabc0 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -48,14 +48,14 @@ class VCruiseHelper(VCruiseHelperSP): self.get_minimum_set_speed(is_metric) + _enabled = self.update_enabled_state(CS, enabled) + if CS.cruiseState.available: - _enabled = self.update_enabled_state(CS, enabled) if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled): # if stock cruise is completely disabled, then we can use our own set speed logic self._update_v_cruise_non_pcm(CS, _enabled, is_metric) self.update_speed_limit_assist_v_cruise_non_pcm() self.v_cruise_cluster_kph = self.v_cruise_kph - self.update_button_timers(CS, enabled) else: self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH @@ -69,6 +69,9 @@ class VCruiseHelper(VCruiseHelperSP): self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET + if not self.CP.pcmCruise or not self.CP_SP.pcmCruiseSpeed: + self.update_button_timers(CS, enabled) + def _update_v_cruise_non_pcm(self, CS, enabled, is_metric): # handle button presses. TODO: this should be in state_control, but a decelCruise press # would have the effect of both enabling and changing speed is checked after the state transition From 03a14ff864af684f9f894a4f8b207e76e690c3b6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 24 Feb 2026 03:02:36 -0500 Subject: [PATCH 05/10] [TIZI/TICI] ui: simplify Smart Cruise Control text rendering (#1719) --- .../sunnypilot/onroad/smart_cruise_control.py | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py index ca71fcac4..b4848141e 100644 --- a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -25,7 +25,6 @@ class SmartCruiseControlRenderer(Widget): self.long_override = False self.font = gui_app.font(FontWeight.BOLD) - self.scc_tex = rl.load_render_texture(256, 128) def update(self): sm = ui_state.sm @@ -65,44 +64,24 @@ class SmartCruiseControlRenderer(Widget): sz = measure_text_cached(self.font, text, font_size) box_height = int(sz.y + padding_v * 2) - texture_width = 256 - texture_height = 128 - - rl.begin_texture_mode(self.scc_tex) - rl.clear_background(rl.Color(0, 0, 0, 0)) - if self.long_override: box_color = COLORS.OVERRIDE else: box_color = rl.Color(0, 255, 0, 255) - # Center box in texture - box_x = (texture_width - box_width) // 2 - box_y = (texture_height - box_height) // 2 + screen_y = rect_height / 4 + y_offset + box_x = rect_center_x + x_offset - box_width / 2 + box_y = screen_y - box_height / 2 + + # Draw rounded background box rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) - # Draw text with custom blend mode to punch hole - rl.rl_set_blend_factors(rl.RL_ZERO, rl.RL_ONE_MINUS_SRC_ALPHA, 0x8006) - rl.rl_set_blend_mode(rl.BLEND_CUSTOM) - + # Draw text centered in the box (black color for contrast against bright green/grey) text_pos_x = box_x + (box_width - sz.x) / 2 text_pos_y = box_y + (box_height - sz.y) / 2 - rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.WHITE) - - rl.rl_set_blend_mode(rl.BLEND_ALPHA) # Reset - rl.end_texture_mode() - - screen_y = rect_height / 4 + y_offset - - dest_x = rect_center_x + x_offset - texture_width / 2 - dest_y = screen_y - texture_height / 2 - - src_rect = rl.Rectangle(0, 0, texture_width, -texture_height) - dst_rect = rl.Rectangle(dest_x, dest_y, texture_width, texture_height) - - rl.draw_texture_pro(self.scc_tex.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0, rl.WHITE) + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.BLACK) def _render(self, rect: rl.Rectangle): x_offset = -260 From 7a43e2cb6736421e45b4b5e2e51cb381ac3ecd9e Mon Sep 17 00:00:00 2001 From: Zeph Date: Wed, 25 Feb 2026 00:52:48 -0600 Subject: [PATCH 06/10] controlsd: fix steer_limited_by_safety not updating under MADS (#1716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `steer_limited_by_safety` update in `publish()` is gated by `selfdriveState.active`, which is False during MADS lateral-only control. This causes the flag to never update once cruise deactivates — it stays stuck at whatever value it had during the last ramp-up (typically True), permanently suppressing the saturation timer in `_check_saturation` and preventing the "Turn Exceeds Steering Limit" alert from firing. Use `CC.latActive` instead, which already accounts for MADS via `get_lat_active()` in ControlsExt. Bug was introduced in #446 (MADS), which updated `CC.latActive` to use `mads.active` but missed updating the `steer_limited_by_safety` gate in `publish()`. --- selfdrive/controls/controlsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 12b146e80..1c8e9f76b 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -185,7 +185,7 @@ class Controls(ControlsExt): hudControl.leftLaneDepart = self.sm['driverAssistance'].leftLaneDeparture hudControl.rightLaneDepart = self.sm['driverAssistance'].rightLaneDeparture - if self.sm['selfdriveState'].active: + if self.get_lat_active(self.sm): CO = self.sm['carOutput'] if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ From 538e1e8a9a77ababf8492108867b8adccc59e7d3 Mon Sep 17 00:00:00 2001 From: Zeph Date: Wed, 25 Feb 2026 01:00:07 -0600 Subject: [PATCH 07/10] soundd: trigger timeout warning during MADS lateral-only (#1717) * soundd: trigger timeout warning during MADS lateral-only The selfdrive timeout alert (warningImmediate) only fires when selfdriveState.enabled is True. During MADS lateral-only mode, enabled is False even though the system is actively steering. If selfdrived stops publishing while MADS lateral is active, the driver gets no audible warning that steering has become unresponsive. Add selfdriveStateSP to the SubMaster and check mads.active alongside selfdriveState.enabled so the timeout alert fires whenever the system is actuating steering. * test_soundd: add MADS lateral-only timeout test Test that the selfdrive timeout warning fires when selfdriveState.enabled is False but selfdriveStateSP.mads.active is True. --------- Co-authored-by: Jason Wen --- selfdrive/ui/soundd.py | 4 ++-- selfdrive/ui/tests/test_soundd.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index c41fec267..edf28b712 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -65,7 +65,7 @@ def check_selfdrive_timeout_alert(sm): ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] if ss_missing > SELFDRIVE_STATE_TIMEOUT: - if sm['selfdriveState'].enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10: + if (sm['selfdriveState'].enabled or sm['selfdriveStateSP'].mads.enabled) and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10: return True return False @@ -158,7 +158,7 @@ class Soundd(QuietMode): # sounddevice must be imported after forking processes import sounddevice as sd - sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) + sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure']) with self.get_stream(sd) as stream: rk = Ratekeeper(20) diff --git a/selfdrive/ui/tests/test_soundd.py b/selfdrive/ui/tests/test_soundd.py index a9da8455e..226117ae8 100644 --- a/selfdrive/ui/tests/test_soundd.py +++ b/selfdrive/ui/tests/test_soundd.py @@ -10,8 +10,8 @@ AudibleAlert = car.CarControl.HUDControl.AudibleAlert class TestSoundd: def test_check_selfdrive_timeout_alert(self): - sm = SubMaster(['selfdriveState']) - pm = PubMaster(['selfdriveState']) + sm = SubMaster(['selfdriveState', 'selfdriveStateSP']) + pm = PubMaster(['selfdriveState', 'selfdriveStateSP']) for _ in range(100): cs = messaging.new_message('selfdriveState') @@ -31,5 +31,31 @@ class TestSoundd: assert check_selfdrive_timeout_alert(sm) + def test_check_selfdrive_timeout_alert_mads_lateral_only(self): + sm = SubMaster(['selfdriveState', 'selfdriveStateSP']) + pm = PubMaster(['selfdriveState', 'selfdriveStateSP']) + + for _ in range(100): + cs = messaging.new_message('selfdriveState') + cs.selfdriveState.enabled = False + + ss_sp = messaging.new_message('selfdriveStateSP') + ss_sp.selfdriveStateSP.mads.enabled = True + + pm.send("selfdriveState", cs) + pm.send("selfdriveStateSP", ss_sp) + + time.sleep(0.01) + + sm.update(0) + + assert not check_selfdrive_timeout_alert(sm) + + for _ in range(SELFDRIVE_STATE_TIMEOUT * 110): + sm.update(0) + time.sleep(0.01) + + assert check_selfdrive_timeout_alert(sm) + # TODO: add test with micd for checking that soundd actually outputs sounds From a4166563e1fe28d9f64637f7049ebe65cc59bfed Mon Sep 17 00:00:00 2001 From: Lukas Heintz <61192133+lukasloetkolben@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:39:50 +0100 Subject: [PATCH 08/10] pandad: flasher for Rivian long upgrade module (#1712) * flasher for Rivian long upgrade * self-contained no dependency on Panda.F4_DEVICES * standalone flasher * move to sp module * use brand field directly * use brand field directly * use brand field directly * bump * add some logging --------- Co-authored-by: Jason Wen --- opendbc_repo | 2 +- selfdrive/pandad/pandad.py | 5 + sunnypilot/selfdrive/pandad/__init__.py | 0 .../selfdrive/pandad/rivian_long_flasher.py | 96 ++++++++++++++++++ .../pandad/rivian_long_fw.bin.signed | Bin 0 -> 59500 bytes 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/selfdrive/pandad/__init__.py create mode 100755 sunnypilot/selfdrive/pandad/rivian_long_flasher.py create mode 100644 sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed diff --git a/opendbc_repo b/opendbc_repo index 613a562bb..cbcd12695 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 613a562bba80997b5c1c74cd799641d06fb2b38c +Subproject commit cbcd126951094311ab5a43dd384a495cd881c080 diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index d21e60f83..c9ddfd633 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -12,6 +12,8 @@ from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.selfdrive.pandad.rivian_long_flasher import flash_rivian_long + def get_expected_signature() -> bytes: try: @@ -129,6 +131,9 @@ def main() -> None: for serial in panda_serials: pandas.append(flash_panda(serial)) + # flash Rivian longitudinal upgrade panda + flash_rivian_long(pandas) + # Ensure internal panda is present if expected internal_pandas = [panda for panda in pandas if panda.is_internal()] if HARDWARE.has_internal_panda() and len(internal_pandas) == 0: diff --git a/sunnypilot/selfdrive/pandad/__init__.py b/sunnypilot/selfdrive/pandad/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sunnypilot/selfdrive/pandad/rivian_long_flasher.py b/sunnypilot/selfdrive/pandad/rivian_long_flasher.py new file mode 100755 index 000000000..58ff0683b --- /dev/null +++ b/sunnypilot/selfdrive/pandad/rivian_long_flasher.py @@ -0,0 +1,96 @@ +#!/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 cereal import car, messaging +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() + + +def flash_rivian_long(pandas: list[Panda]) -> 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 + + for panda in pandas: + # 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 F4 panda {panda.get_usb_serial()}") + + +if __name__ == '__main__': + flash_rivian_long([Panda(s) for s in Panda.list()]) diff --git a/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed b/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed new file mode 100644 index 0000000000000000000000000000000000000000..bdbd237ba99589813bdda2199ad71f42bfc2937a GIT binary patch literal 59500 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 Date: Wed, 25 Feb 2026 02:51:39 -0500 Subject: [PATCH 09/10] bump --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index a95e060e8..7b9eb4b24 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit a95e060e855dc59898dd3bf2274a598dacdd2571 +Subproject commit 7b9eb4b247e09e4aa40b78394e45d92f5b42a569 From f43dc93bd91ef16fb374954b4c106d2aa1bb9cc8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 25 Feb 2026 03:34:05 -0500 Subject: [PATCH 10/10] Revert "bump" This reverts commit 06a5a380df70244a2a7c82218ca7748095aa9d6d. --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 7b9eb4b24..a95e060e8 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 7b9eb4b247e09e4aa40b78394e45d92f5b42a569 +Subproject commit a95e060e855dc59898dd3bf2274a598dacdd2571