From 31ea1850f73a0f8049dd5a655b7bc1f03d5389be Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:54:52 -0700 Subject: [PATCH 01/34] ui: remove raygui usage (#38708) * ui: remove raygui usage * match previous gui_text_box line spacing * Revert "match previous gui_text_box line spacing" This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66. * Reapply "match previous gui_text_box line spacing" This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16. --- openpilot/selfdrive/ui/body/layouts/onroad.py | 6 +- openpilot/selfdrive/ui/layouts/home.py | 4 +- openpilot/selfdrive/ui/layouts/onboarding.py | 8 +- openpilot/selfdrive/ui/mici/layouts/home.py | 6 +- .../ui/mici/layouts/offroad_alerts.py | 14 +-- .../selfdrive/ui/mici/layouts/onboarding.py | 4 +- .../ui/mici/onroad/alert_renderer.py | 6 +- .../ui/mici/onroad/augmented_road_view.py | 6 +- .../ui/mici/onroad/cabin_camera_dialog.py | 18 ++-- openpilot/selfdrive/ui/mici/widgets/button.py | 14 +-- .../selfdrive/ui/onroad/alert_renderer.py | 10 +-- .../ui/onroad/cabin_camera_dialog.py | 4 +- openpilot/system/ui/lib/application.py | 23 ++--- openpilot/system/ui/lib/utils.py | 19 ----- openpilot/system/ui/mici_setup.py | 12 +-- openpilot/system/ui/mici_updater.py | 4 +- openpilot/system/ui/tici_reset.py | 5 +- openpilot/system/ui/tici_setup.py | 26 +++--- openpilot/system/ui/tici_updater.py | 9 +- openpilot/system/ui/widgets/button.py | 6 +- openpilot/system/ui/widgets/keyboard.py | 6 +- openpilot/system/ui/widgets/label.py | 85 ++++++------------- openpilot/system/ui/widgets/list_view.py | 10 +-- openpilot/system/ui/widgets/network.py | 10 +-- openpilot/system/ui/widgets/option_dialog.py | 4 +- openpilot/system/ui/widgets/slider.py | 6 +- 26 files changed, 140 insertions(+), 185 deletions(-) delete mode 100644 openpilot/system/ui/lib/utils.py diff --git a/openpilot/selfdrive/ui/body/layouts/onroad.py b/openpilot/selfdrive/ui/body/layouts/onroad.py index a48e525628..94f1342abd 100644 --- a/openpilot/selfdrive/ui/body/layouts/onroad.py +++ b/openpilot/selfdrive/ui/body/layouts/onroad.py @@ -1,7 +1,7 @@ import time import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.ui_state import ui_state @@ -26,8 +26,8 @@ class BodyLayout(Widget): self._last_input_time = time.monotonic() self._was_active = False self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color): spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS) diff --git a/openpilot/selfdrive/ui/layouts/home.py b/openpilot/selfdrive/ui/layouts/home.py index 183c2d4588..613fc3de3d 100644 --- a/openpilot/selfdrive/ui/layouts/home.py +++ b/openpilot/selfdrive/ui/layouts/home.py @@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment from openpilot.system.ui.lib.multilang import tr, trn from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets import Widget @@ -178,7 +178,7 @@ class HomeLayout(Widget): version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, version_text_width, self.header_rect.height) - gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT) def _render_home_content(self): self._render_left_column() diff --git a/openpilot/selfdrive/ui/layouts/onboarding.py b/openpilot/selfdrive/ui/layouts/onboarding.py index 97ca89b50c..91b674d52d 100644 --- a/openpilot/selfdrive/ui/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/layouts/onboarding.py @@ -5,7 +5,7 @@ from enum import IntEnum import pyray as rl from openpilot.common.basedir import BASEDIR -from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -112,9 +112,9 @@ class TermsPage(Widget): self._on_accept = on_accept self._on_decline = on_decline - self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT) self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."), - font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT) self._decline_btn = Button(tr("Decline"), click_callback=on_decline) self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept) @@ -147,7 +147,7 @@ class DeclinePage(Widget): def __init__(self, back_callback=None): super().__init__() self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."), - font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT) self._back_btn = Button(tr("Back"), click_callback=back_callback) self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER, click_callback=self._on_uninstall_clicked) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 519580925f..64f2961326 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -8,7 +8,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES @@ -69,8 +69,8 @@ class AlertsPill(Widget): count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h) gui_label(count_rect, str(alert_count), font_size=36, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) class NetworkIcon(Widget): diff --git a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py index c27e05f752..40e074d455 100644 --- a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr REFRESH_INTERVAL = 5.0 # seconds @@ -62,12 +62,12 @@ class AlertItem(Widget): self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE) self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95) + alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95) self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95) + alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95) self._title_text = "" self._body_text = "" @@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller): # Create empty state label self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) # Build initial alert list self._build_alerts() diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py index 2b448ac73d..0590440c6b 100644 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -4,7 +4,7 @@ import pyray as rl from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.qrcode import make_texture -from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import SmallCircleIconButton from openpilot.system.ui.widgets.scroller import NavScroller, Scroller @@ -33,7 +33,7 @@ class CabinCameraSetupDialog(BaseCabinCameraDialog): if not self._camera_view.frame: gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + alignment=TextAlignment.CENTER) rl.end_scissor_mode() return diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index d2896e1807..e42ee6c438 100644 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -10,7 +10,7 @@ from opendbc.car.structs import car from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel @@ -319,7 +319,7 @@ class AlertRenderer(Widget): self._alert_text1_label.set_text(alert_text1) self._alert_text1_label.set_text_color(color) self._alert_text1_label.set_font_size(font_size) - self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) self._alert_text1_label.render(text_rect1) alert_text2 = alert.text2.lower() @@ -351,5 +351,5 @@ class AlertRenderer(Widget): self._alert_text2_label.set_text(alert_text2) self._alert_text2_label.set_text_color(color) self._alert_text2_label.set_font_size(small_font_size) - self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) self._alert_text2_label.render(text_rect2) diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 4f4e4b2f6c..ddc04d7560 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView -from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent +from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget from openpilot.common.filter_simple import BounceFilter @@ -154,8 +154,8 @@ class AugmentedRoadView(CameraView): self._confidence_ball = ConfidenceBall() self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") diff --git a/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py index e86c0aa739..b0bdb9749c 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py +++ b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py @@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget): if not self._camera_view.frame: gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + alignment=TextAlignment.CENTER) rl.end_scissor_mode() self._publish_alert_sound(None) return @@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget): awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height), f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.TOP, color=rl.Color(0, 0, 0, 180)) gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.TOP, color=rl.Color(255, 255, 255, int(255 * 0.9))) if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none: @@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget): # Show alert level alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}" - alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT + alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, alignment=alignment, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, + alignment_vertical=TextAlignmentVertical.BOTTOM, color=rl.Color(0, 0, 0, 180)) gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, alignment=alignment, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, + alignment_vertical=TextAlignmentVertical.BOTTOM, color=rl.Color(255, 255, 255, int(255 * 0.9))) def _load_eye_textures(self): diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index e2912d00cf..59a3f95191 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -6,7 +6,7 @@ from collections.abc import Callable from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import DO_ZOOM -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical from openpilot.common.filter_simple import BounceFilter if TYPE_CHECKING: @@ -125,10 +125,10 @@ class BigButton(Widget): self._rotate_icon_t: float | None = None self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD, - text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll, + text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll, line_height=0.9) self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN, - text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM) self._update_label_layout() self._load_images() @@ -167,9 +167,9 @@ class BigButton(Widget): def _update_label_layout(self): self._label.set_font_size(self._get_label_font_size()) if self.value: - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._label.set_alignment_vertical(TextAlignmentVertical.TOP) else: - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM) def set_text(self, text: str): self.text = text @@ -356,8 +356,8 @@ class GreyBigButton(BigButton): self._sub_label.set_font_size(36) self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR) - self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else + TextAlignmentVertical.BOTTOM) self._sub_label.set_line_height(0.95) @property diff --git a/openpilot/selfdrive/ui/onroad/alert_renderer.py b/openpilot/selfdrive/ui/onroad/alert_renderer.py index 62511b87db..7add53df5d 100644 --- a/openpilot/selfdrive/ui/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/onroad/alert_renderer.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from openpilot.cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -76,10 +76,10 @@ class AlertRenderer(Widget): self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) # font size is set dynamically - self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) - self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER, + text_alignment_vertical=TextAlignmentVertical.TOP) + self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER, + text_alignment_vertical=TextAlignmentVertical.TOP) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" diff --git a/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py index 7bb1917c35..58a645381b 100644 --- a/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py +++ b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py @@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.label import gui_label @@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView): tr("camera starting"), font_size=100, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + alignment=TextAlignment.CENTER, ) return -1 diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index 5e980937d7..230ddc635e 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -13,7 +13,7 @@ import subprocess from contextlib import contextmanager from collections.abc import Callable from collections import deque -from enum import StrEnum +from enum import IntEnum, StrEnum from pathlib import Path from typing import NamedTuple from importlib.resources import as_file, files @@ -115,6 +115,18 @@ class FontWeight(StrEnum): DISPLAY = "Inter-Bold.ttf" +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 + + +class TextAlignmentVertical(IntEnum): + TOP = 0 + MIDDLE = 1 + BOTTOM = 2 + + def font_fallback(font: rl.Font) -> rl.Font: """Use a Noto fallback for languages not covered by Inter.""" if multilang.requires_font_fallback(): @@ -330,7 +342,6 @@ class GuiApplication: rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps) self._target_fps = fps - self._set_styles() self._load_fonts() self._patch_text_functions() self._patch_scissor_mode() @@ -731,14 +742,6 @@ class GuiApplication: self._fonts[font_weight_file] = font if multilang.requires_font_fallback(): self.fallback_font() - rl.gui_set_font(self._fonts[FontWeight.NORMAL]) - - def _set_styles(self): - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK)) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR)) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) def _patch_text_functions(self): # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt diff --git a/openpilot/system/ui/lib/utils.py b/openpilot/system/ui/lib/utils.py deleted file mode 100644 index e97b3ba9d9..0000000000 --- a/openpilot/system/ui/lib/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -import pyray as rl -from collections.abc import Sequence - - -class GuiStyleContext: - def __init__(self, styles: Sequence[tuple[int, int, int]]): - """styles is a list of tuples (control, prop, new_value)""" - self.styles = styles - self.prev_styles: list[tuple[int, int, int]] = [] - - def __enter__(self): - for control, prop, new_value in self.styles: - prev_value = rl.gui_get_style(control, prop) - self.prev_styles.append((control, prop, prev_value)) - rl.gui_set_style(control, prop, new_value) - - def __exit__(self, exc_type, exc_value, traceback): - for control, prop, prev_value in self.prev_styles: - rl.gui_set_style(control, prop, prev_value) diff --git a/openpilot/system/ui/mici_setup.py b/openpilot/system/ui/mici_setup.py index e4977ffdf4..45d63f5d2c 100755 --- a/openpilot/system/ui/mici_setup.py +++ b/openpilot/system/ui/mici_setup.py @@ -18,7 +18,7 @@ from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid from openpilot.common.utils import run_cmd -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -105,8 +105,8 @@ class StartPage(Widget): super().__init__() self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), - font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False) self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False) @@ -197,7 +197,7 @@ class DownloadingPage(NavWidget): self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), - font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + font_weight=FontWeight.ROMAN, alignment_vertical=TextAlignmentVertical.BOTTOM) self._progress = 0 def _back_enabled(self) -> bool: @@ -261,8 +261,8 @@ class BigPillButton(BigButton): super().__init__(*args, **kwargs) self._label.set_font_size(48) - self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._label.set_alignment(TextAlignment.CENTER) + self._label.set_alignment_vertical(TextAlignmentVertical.MIDDLE) def _load_images(self): if self._green: diff --git a/openpilot/system/ui/mici_updater.py b/openpilot/system/ui/mici_updater.py index d9009b8259..cc16145793 100755 --- a/openpilot/system/ui/mici_updater.py +++ b/openpilot/system/ui/mici_updater.py @@ -7,7 +7,7 @@ import pyray as rl from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.label import UnifiedLabel @@ -30,7 +30,7 @@ class ProgressPage(NavWidget): font_weight=FontWeight.DISPLAY, line_height=0.8) self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), font_weight=FontWeight.ROMAN, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + alignment_vertical=TextAlignmentVertical.BOTTOM) def _back_enabled(self) -> bool: return False diff --git a/openpilot/system/ui/tici_reset.py b/openpilot/system/ui/tici_reset.py index 5c175dbfa6..ffe90bbb87 100755 --- a/openpilot/system/ui/tici_reset.py +++ b/openpilot/system/ui/tici_reset.py @@ -11,7 +11,7 @@ from openpilot.common.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle -from openpilot.system.ui.widgets.label import gui_label, gui_text_box +from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label USERDATA = "/dev/disk/by-partlabel/userdata" TIMEOUT = 3*60 @@ -38,6 +38,7 @@ class Reset(Widget): self._cancel_button = Button("Cancel", gui_app.request_close) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) self._reboot_button = Button("Reboot", self._reboot) + self._body_label = UnifiedLabel(self._get_body_text, 90, line_height=1 / 0.9) @staticmethod def _reboot() -> None: @@ -75,7 +76,7 @@ class Reset(Widget): gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE) - gui_text_box(text_rect, self._get_body_text(), 90) + self._body_label.render(text_rect) button_height = 160 button_spacing = 50 diff --git a/openpilot/system/ui/tici_setup.py b/openpilot/system/ui/tici_setup.py index 36f47ccf1b..17c3cf5e68 100755 --- a/openpilot/system/ui/tici_setup.py +++ b/openpilot/system/ui/tici_setup.py @@ -13,7 +13,7 @@ import pyray as rl from openpilot.cereal import log from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard @@ -67,17 +67,17 @@ class Setup(Widget): self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) - self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255), text_padding=20) self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + text_alignment=TextAlignment.LEFT, text_padding=20) self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) - self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", - BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20) self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) @@ -85,32 +85,32 @@ class Setup(Widget): button_style=ButtonStyle.PRIMARY) self._software_selection_continue_button.set_enabled(False) self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) - self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) - self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) + self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20) self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._network_setup_continue_button.set_enabled(False) - self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._custom_software_warning_continue_button.set_enabled(False) self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) - self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255), text_padding=60) self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE)) self._custom_software_warning_body_labels = [ - Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + Label(text, 68, text_alignment=TextAlignment.LEFT, + text_alignment_vertical=TextAlignmentVertical.TOP, text_padding=60, icon=self._yellow_warning_icon if has_icon else None) for text, has_icon in [ ("Use caution when installing third-party software.", False), diff --git a/openpilot/system/ui/tici_updater.py b/openpilot/system/ui/tici_updater.py index 27cf6579a8..2cb86cca88 100755 --- a/openpilot/system/ui/tici_updater.py +++ b/openpilot/system/ui/tici_updater.py @@ -10,7 +10,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle -from openpilot.system.ui.widgets.label import gui_text_box, gui_label +from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.network import WifiManagerUI # Constants @@ -50,6 +50,8 @@ class Updater(Widget): self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY) self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT)) self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot()) + self._desc_label = UnifiedLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + + "The download size is approximately 1GB.", BODY_FONT_SIZE, line_height=1 / 0.9) def set_current_screen(self, screen: Screen): self.current_screen = screen @@ -99,11 +101,8 @@ class Updater(Widget): gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) # Description - desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + - "The download size is approximately 1GB.") - desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4) - gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) + self._desc_label.render(desc_rect) # Buttons at the bottom button_y = rect.height - MARGIN - BUTTON_HEIGHT diff --git a/openpilot/system/ui/widgets/button.py b/openpilot/system/ui/widgets/button.py index 36ef3bedab..c031b2f141 100644 --- a/openpilot/system/ui/widgets/button.py +++ b/openpilot/system/ui/widgets/button.py @@ -3,7 +3,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import Label from openpilot.common.filter_simple import FirstOrderFilter @@ -86,7 +86,7 @@ class Button(Widget): font_weight: FontWeight = FontWeight.MEDIUM, button_style: ButtonStyle = ButtonStyle.NORMAL, border_radius: int = 10, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + text_alignment: int = TextAlignment.CENTER, text_padding: int = 20, icon=None, elide_right: bool = False, @@ -139,7 +139,7 @@ class ButtonRadio(Button): icon, click_callback: Callable[[], None] | None = None, font_size: int = DEFAULT_BUTTON_FONT_SIZE, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + text_alignment: int = TextAlignment.LEFT, border_radius: int = 10, text_padding: int = 20, ): diff --git a/openpilot/system/ui/widgets/keyboard.py b/openpilot/system/ui/widgets/keyboard.py index 49c59a431f..c9d2187cbb 100644 --- a/openpilot/system/ui/widgets/keyboard.py +++ b/openpilot/system/ui/widgets/keyboard.py @@ -5,7 +5,7 @@ from collections.abc import Callable import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button @@ -65,8 +65,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) + self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20) self._max_text_size = max_text_size self._min_text_size = min_text_size diff --git a/openpilot/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py index a3e827321c..60fa3e25e0 100644 --- a/openpilot/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -4,10 +4,9 @@ from collections.abc import Callable from typing import Union import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.utils import GuiStyleContext from openpilot.system.ui.lib.wrap_text import wrap_text ICON_PADDING = 15 @@ -32,8 +31,8 @@ def gui_label( font_size: int = DEFAULT_TEXT_SIZE, color: rl.Color = DEFAULT_TEXT_COLOR, font_weight: FontWeight = FontWeight.NORMAL, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + alignment: int = TextAlignment.LEFT, + alignment_vertical: int = TextAlignmentVertical.MIDDLE, elide_right: bool = True ): font = gui_app.font(font_weight) @@ -57,16 +56,16 @@ def gui_label( # Calculate horizontal position based on alignment text_x = rect.x + { - rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, - rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, - rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, + TextAlignment.LEFT: 0, + TextAlignment.CENTER: (rect.width - text_size.x) / 2, + TextAlignment.RIGHT: rect.width - text_size.x, }.get(alignment, 0) # Calculate vertical position based on alignment text_y = rect.y + { - rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, + TextAlignmentVertical.TOP: 0, + TextAlignmentVertical.MIDDLE: (rect.height - text_size.y) / 2, + TextAlignmentVertical.BOTTOM: rect.height - text_size.y, }.get(alignment_vertical, 0) # Draw the text in the specified rectangle @@ -74,42 +73,14 @@ def gui_label( rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) -def gui_text_box( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, - font_weight: FontWeight = FontWeight.NORMAL, - line_scale: float = 1.0, -): - styles = [ - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE * line_scale)), - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) - ] - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(font_weight)) - - with GuiStyleContext(styles): - rl.gui_label(rect, text) - - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) - - # Non-interactive text area. Can render an optional specified icon. class Label(Widget): def __init__(self, text: str | Callable[[], str], font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + text_alignment: int = TextAlignment.CENTER, + text_alignment_vertical: int = TextAlignmentVertical.MIDDLE, text_padding: int = 0, text_color: rl.Color = DEFAULT_TEXT_COLOR, icon: Union[rl.Texture, None] = None, @@ -181,10 +152,10 @@ class Label(Widget): self._update_text(self._text) text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) - if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: + if self._text_alignment_vertical == TextAlignmentVertical.MIDDLE: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) - elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + elif self._text_alignment_vertical == TextAlignmentVertical.BOTTOM: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height) else: @@ -193,10 +164,10 @@ class Label(Widget): if self._icon: icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 if len(self._text_wrapped) > 0: - if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._text_alignment == TextAlignment.LEFT: icon_x = self._rect.x + self._text_padding text_pos.x = self._rect.x + self._icon.width + ICON_PADDING - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._text_alignment == TextAlignment.CENTER: total_width = self._icon.width + ICON_PADDING + text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 text_pos.x = self._rect.x + self._icon.width + ICON_PADDING @@ -208,11 +179,11 @@ class Label(Widget): for text, text_size in zip(self._text_wrapped, self._text_size, strict=True): line_pos = rl.Vector2(text_pos.x, text_pos.y) - if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._text_alignment == TextAlignment.LEFT: line_pos.x += self._text_padding - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._text_alignment == TextAlignment.CENTER: line_pos.x += (self._rect.width - text_size.x) // 2 - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + elif self._text_alignment == TextAlignment.RIGHT: line_pos.x += self._rect.width - text_size.x - self._text_padding rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color) @@ -221,7 +192,7 @@ class Label(Widget): class UnifiedLabel(Widget): """ - Unified label widget that combines functionality from gui_label, gui_text_box, and Label. + Unified label widget that combines functionality from gui_label and Label. Supports: - Text wrapping @@ -241,8 +212,8 @@ class UnifiedLabel(Widget): font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, text_color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment: int = TextAlignment.LEFT, + alignment_vertical: int = TextAlignmentVertical.TOP, text_padding: int = 0, max_width: int | None = None, elide: bool = True, @@ -561,9 +532,9 @@ class UnifiedLabel(Widget): total_visible_height += size.y * self._line_height # Calculate vertical alignment offset - if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: + if self._alignment_vertical == TextAlignmentVertical.TOP: start_y = self._rect.y - elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + elif self._alignment_vertical == TextAlignmentVertical.BOTTOM: start_y = self._rect.y + self._rect.height - total_visible_height else: # TEXT_ALIGN_MIDDLE start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 @@ -640,11 +611,11 @@ class UnifiedLabel(Widget): def _render_line(self, line, size, current_y, x_offset=0.0): # Calculate horizontal position - if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._alignment == TextAlignment.LEFT: line_x = self._rect.x + self._text_padding - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._alignment == TextAlignment.CENTER: line_x = self._rect.x + (self._rect.width - size.x) / 2 - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + elif self._alignment == TextAlignment.RIGHT: line_x = self._rect.x + self._rect.width - size.x - self._text_padding else: line_x = self._rect.x + self._text_padding @@ -662,9 +633,9 @@ class UnifiedLabel(Widget): def _render_line_shimmer(self, line, line_x, current_y): # Shimmer range based on widest line so sweep is even across all lines max_width = self.text_width - if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + if self._alignment == TextAlignment.RIGHT: shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._alignment == TextAlignment.CENTER: shimmer_left = self._rect.x + (self._rect.width - max_width) / 2 else: shimmer_left = self._rect.x + self._text_padding diff --git a/openpilot/system/ui/widgets/list_view.py b/openpilot/system/ui/widgets/list_view.py index 705cb476de..d88473a491 100644 --- a/openpilot/system/ui/widgets/list_view.py +++ b/openpilot/system/ui/widgets/list_view.py @@ -3,7 +3,7 @@ import os import pyray as rl from collections.abc import Callable from abc import ABC -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -139,8 +139,8 @@ class ButtonAction(ItemAction): if value_text: value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height) gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR, - font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.MIDDLE) # TODO: just use the generic Widget click callbacks everywhere, no returning from render pressed = self._pressed @@ -168,8 +168,8 @@ class TextAction(ItemAction): def _render(self, rect: rl.Rectangle) -> bool: gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, - font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.NORMAL, alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.MIDDLE) return False def set_text(self, text: str | Callable[[], str]): diff --git a/openpilot/system/ui/widgets/network.py b/openpilot/system/ui/widgets/network.py index 4068d552b9..3aa6701943 100644 --- a/openpilot/system/ui/widgets/network.py +++ b/openpilot/system/ui/widgets/network.py @@ -3,7 +3,7 @@ from functools import partial from typing import cast import pyray as rl -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid @@ -51,7 +51,7 @@ class NavButton(Widget): def _render(self, _): color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255) rl.draw_rectangle_rounded(self._rect, 0.6, 10, color) - gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(self.rect, self.text, font_size=60, alignment=TextAlignment.CENTER) class NetworkUI(Widget): @@ -299,7 +299,7 @@ class WifiManagerUI(Widget): def _render(self, rect: rl.Rectangle): if not self._networks: - gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=TextAlignment.CENTER) return if self.state == UIState.NEEDS_AUTH and self._state_network: @@ -373,7 +373,7 @@ class WifiManagerUI(Widget): if status_text: status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) - gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(status_text_rect, status_text, font_size=48, alignment=TextAlignment.CENTER) else: # If the network is saved, show the "Forget" button if self._wifi_manager.is_connection_saved(network.ssid): @@ -439,7 +439,7 @@ class WifiManagerUI(Widget): self._networks = networks for n in self._networks: self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) + text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, font_size=45) diff --git a/openpilot/system/ui/widgets/option_dialog.py b/openpilot/system/ui/widgets/option_dialog.py index 206400a74f..816503c4a0 100644 --- a/openpilot/system/ui/widgets/option_dialog.py +++ b/openpilot/system/ui/widgets/option_dialog.py @@ -1,6 +1,6 @@ import pyray as rl from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -29,7 +29,7 @@ class MultiOptionDialog(Widget): # Create scroller with option buttons self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt), font_weight=option_font_weight, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL, + text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.NORMAL, text_padding=50, elide_right=True) for option in options] self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING) diff --git a/openpilot/system/ui/widgets/slider.py b/openpilot/system/ui/widgets/slider.py index bf965954f2..7bdf056219 100644 --- a/openpilot/system/ui/widgets/slider.py +++ b/openpilot/system/ui/widgets/slider.py @@ -3,7 +3,7 @@ from collections.abc import Callable import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter @@ -41,8 +41,8 @@ class SliderBase(Widget, abc.ABC): self._is_dragging_circle = False self._label = self._child(UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True)) + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.MIDDLE, line_height=0.9, shimmer=True)) @abc.abstractmethod def _load_assets(self): From 4cdc16031ffd8bd44cb1d3f2fd2bfd49effd66ce Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 27 Aug 2026 11:21:56 -0700 Subject: [PATCH 02/34] log chestnut supply fault (#38711) * log chestnut INA supply fault * ci --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/modeld/modeld.py | 45 ++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fdf022fa63..fb4867b2a6 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -725,6 +725,7 @@ struct ChestnutState { pcieLtssm @7 :UInt8; supplyVoltage @8 :UInt16; # mV supplyCurrent @9 :Int16; # mA + supplyFault @10 :Bool; } struct RadarState @0x9a185389d6fdd05f { diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 0db66ed880..a795f366ce 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,6 +6,7 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device +import usb1 import struct import threading import time @@ -31,6 +32,7 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob @@ -79,6 +81,37 @@ class ChestnutState: self.valid = True self.sends = 0 self.metrics = {} + self._asm_usb = None + + def _close_asm_usb(self) -> None: + if self._asm_usb is not None: + self._asm_usb.close() + self._asm_usb = None + + def _open_asm_usb(self): + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: + return handle + context.close() + + def _read_ina(self) -> tuple[int, int, bool]: + if "AMD" in Device._opened_devices and self._asm_usb is None: + try: + raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) + return struct.unpack(' int: @@ -114,13 +147,15 @@ class ChestnutState: setattr(state, k, v) asm_valid = False + try: + # ASM runs on USB-C power, these still read without a gpu + state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() + asm_valid = True + except Exception: + pass if "AMD" in Device._opened_devices: try: - # ASM runs on USB-C power, these still read without a gpu - asm = Device["AMD"].iface.pci_dev.usb - state.pcieLtssm = asm.read(0xB450, 1)[0] - state.supplyVoltage, state.supplyCurrent = struct.unpack(' Date: Thu, 27 Aug 2026 11:38:53 -0700 Subject: [PATCH 03/34] bump raylib (#38712) --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 20fe78c1fb..89b82b5452 100644 --- a/uv.lock +++ b/uv.lock @@ -209,15 +209,15 @@ wheels = [ [[package]] name = "comma-deps-raylib" -version = "6.0.0.1.post98" +version = "6.0.0.1.post101" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/21/59509b2758e3612c63336df5534ea3b36e472e06c18a50f0b96c32e8e3e0/comma_deps_raylib-6.0.0.1.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:838a6115f508ee4ca0e4c84975c0892d67c10f94b11dd2bc0e023ad65138d010", size = 2004199, upload-time = "2026-07-23T17:03:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f5/84e135ac611a6dfe8b95ff989097c200770907907afba78455d4c863981e/comma_deps_raylib-6.0.0.1.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9347fe18799c209dd1af746a44a94433bae69d4644de10e11dcc307aecd87fb1", size = 7203023, upload-time = "2026-07-23T17:03:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/ae0959dac622f7169de230ec98c6555870ae0ad4f61e7b8d32922fa960ed/comma_deps_raylib-6.0.0.1.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f9fd393fbaf1f16d7785be0b2252ef3b14a953c93d89296ed9a67bc7063de882", size = 5055448, upload-time = "2026-07-23T17:03:25.737Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/e289acd1725d71c792c33399422f1052c4b0c38aeb4222a866d30c4a2cad/comma_deps_raylib-6.0.0.1.post101-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa69d5093a92d7d2bfd2714a1afccca63b94d4c55fae88e61b80e8841de6a6cd", size = 1885392, upload-time = "2026-08-27T18:24:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/a1/17/12977631f6d86d1daa4f67a310cfdf2783ba288f42d444d6a89b2297a0a4/comma_deps_raylib-6.0.0.1.post101-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c93ff9b45df414620b3011280da77151764c027e2f95d384fe255615d78872cc", size = 21707644, upload-time = "2026-08-27T18:24:41.282Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/ef33aff790b37dfc925f94fbb3a86da19f67929c15b3725ddb18afb91e96/comma_deps_raylib-6.0.0.1.post101-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8f2a1ffe5f60cac06170b144d6ce84925c91657e7ada3ae35bc5df6ccbe0b461", size = 20722616, upload-time = "2026-08-27T18:24:45.803Z" }, ] [[package]] From cbf750de20d29cd96c545da028a45c1135475c00 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:54:06 -0700 Subject: [PATCH 04/34] cabana: replace custom non-view Qt signals w/ plain observer (#38713) --- openpilot/tools/cabana/SConscript | 2 +- openpilot/tools/cabana/binaryview.cc | 5 +- openpilot/tools/cabana/binaryview.h | 1 + openpilot/tools/cabana/cabana.cc | 10 +-- openpilot/tools/cabana/cameraview.cc | 22 ++--- openpilot/tools/cabana/cameraview.h | 19 ++--- openpilot/tools/cabana/chart/chart.cc | 9 +- openpilot/tools/cabana/chart/chart.h | 1 + openpilot/tools/cabana/chart/chartswidget.cc | 17 ++-- openpilot/tools/cabana/chart/chartswidget.h | 1 + .../tools/cabana/chart/signalselector.cc | 1 - openpilot/tools/cabana/commands.cc | 22 ++--- openpilot/tools/cabana/commands.h | 28 +------ openpilot/tools/cabana/core/observable.h | 83 +++++++++++++++++++ openpilot/tools/cabana/dbc/dbcmanager.cc | 28 +++---- openpilot/tools/cabana/dbc/dbcmanager.h | 22 ++--- openpilot/tools/cabana/dbc/dbcqt.cc | 18 ---- openpilot/tools/cabana/dbc/dbcqt.h | 27 ------ openpilot/tools/cabana/detailwidget.cc | 11 ++- openpilot/tools/cabana/detailwidget.h | 1 + openpilot/tools/cabana/historylog.cc | 10 ++- openpilot/tools/cabana/historylog.h | 3 +- openpilot/tools/cabana/mainwin.cc | 69 ++++++++------- openpilot/tools/cabana/mainwin.h | 11 ++- openpilot/tools/cabana/messageswidget.cc | 12 +-- openpilot/tools/cabana/messageswidget.h | 3 +- openpilot/tools/cabana/settings.cc | 2 +- openpilot/tools/cabana/settings.h | 8 +- openpilot/tools/cabana/signalview.cc | 19 ++--- openpilot/tools/cabana/signalview.h | 2 + .../tools/cabana/streams/abstractstream.cc | 61 +++++++++----- .../tools/cabana/streams/abstractstream.h | 40 ++++----- .../tools/cabana/streams/devicestream.cc | 16 ++-- openpilot/tools/cabana/streams/devicestream.h | 3 +- openpilot/tools/cabana/streams/livestream.cc | 12 +-- openpilot/tools/cabana/streams/livestream.h | 4 +- openpilot/tools/cabana/streams/pandastream.cc | 4 +- openpilot/tools/cabana/streams/pandastream.h | 3 +- .../tools/cabana/streams/replaystream.cc | 40 +++++---- openpilot/tools/cabana/streams/replaystream.h | 12 ++- .../tools/cabana/streams/socketcanstream.cc | 4 +- .../tools/cabana/streams/socketcanstream.h | 3 +- openpilot/tools/cabana/tests/test_cabana.cc | 9 +- openpilot/tools/cabana/tools/routeinfo.cc | 2 +- openpilot/tools/cabana/utils/util.cc | 26 ++++++ openpilot/tools/cabana/utils/util.h | 13 ++- openpilot/tools/cabana/videowidget.cc | 22 ++--- openpilot/tools/cabana/videowidget.h | 1 + 48 files changed, 403 insertions(+), 339 deletions(-) create mode 100644 openpilot/tools/cabana/core/observable.h delete mode 100644 openpilot/tools/cabana/dbc/dbcqt.cc delete mode 100644 openpilot/tools/cabana/dbc/dbcqt.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 387129709e..bc366ab6fb 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', + 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 5e919dc6a3..2f0167107c 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -1,5 +1,4 @@ #include "tools/cabana/binaryview.h" -#include "tools/cabana/dbc/dbcqt.h" #include @@ -36,8 +35,8 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { setMouseTracking(true); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); addShortcuts(); setWhatsThis(R"( diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index c49067a1a2..0b0159b422 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -100,5 +100,6 @@ private: bool is_message_active = false; const cabana::Signal *resize_sig = nullptr; const cabana::Signal *hovered_sig = nullptr; + Connections connections_; friend class BinaryItemDelegate; }; diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 8b21143faf..e3a1850346 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -146,19 +146,19 @@ int main(int argc, char *argv[]) { AbstractStream *stream = nullptr; if (args.msgq) { - stream = new DeviceStream(&app); + stream = new DeviceStream(); } else if (!args.zmq.empty()) { - stream = new DeviceStream(&app, QString::fromStdString(args.zmq)); + stream = new DeviceStream(QString::fromStdString(args.zmq)); } else if (args.panda || !args.panda_serial.empty()) { try { - stream = new PandaStream(&app, {.serial = args.panda_serial}); + stream = new PandaStream({.serial = args.panda_serial}); } catch (std::exception &e) { fprintf(stderr, "%s\n", e.what()); return 0; } #ifdef __linux__ } else if (SocketCanStream::available() && !args.socketcan.empty()) { - stream = new SocketCanStream(&app, {.device = args.socketcan}); + stream = new SocketCanStream({.device = args.socketcan}); #endif } else { uint32_t replay_flags = REPLAY_FLAG_NONE; @@ -174,7 +174,7 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } if (!route.isEmpty()) { - auto replay_stream = std::make_unique(&app); + auto replay_stream = std::make_unique(); if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { return 0; } diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc index 9bd6b9be19..b8c70ab1bf 100644 --- a/openpilot/tools/cabana/cameraview.cc +++ b/openpilot/tools/cabana/cameraview.cc @@ -9,13 +9,11 @@ #include #include "common/yuv.h" +#include "tools/cabana/utils/util.h" CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); - qRegisterMetaType>("availableStreams"); - QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); - QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); } @@ -38,10 +36,6 @@ void CameraWidget::stopVipcThread() { } } -void CameraWidget::availableStreamsUpdated(std::set streams) { - available_streams = streams; -} - void CameraWidget::paintEvent(QPaintEvent *event) { QPainter p(this); p.fillRect(rect(), bg); @@ -67,10 +61,6 @@ void CameraWidget::paintEvent(QPaintEvent *event) { p.drawImage(video_rect, rgb_frame); } -void CameraWidget::vipcFrameReceived() { - update(); -} - void CameraWidget::vipcThread() { VisionStreamType cur_stream = requested_stream_type; std::unique_ptr vipc_client; @@ -93,7 +83,11 @@ void CameraWidget::vipcThread() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } - emit vipcAvailableStreamsUpdated(streams); + utils::runOnMainThread([this, alive = std::weak_ptr(alive_), streams]() { + if (alive.expired()) return; + available_streams = streams; + availableStreamsUpdated(streams); + }); if (!vipc_client->connect(false)) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -112,7 +106,9 @@ void CameraWidget::vipcThread() { std::lock_guard lk(frame_lock); rgb_frame.swap(rgb_back); } - emit vipcThreadFrameReceived(); + utils::runOnMainThread([this, alive = std::weak_ptr(alive_)]() { + if (!alive.expired()) update(); + }); } } } diff --git a/openpilot/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h index 3b55dd9ed9..ac292a701c 100644 --- a/openpilot/tools/cabana/cameraview.h +++ b/openpilot/tools/cabana/cameraview.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,11 +12,10 @@ #include #include "openpilot/cereal/visionstream.h" +#include "tools/cabana/core/observable.h" #include "msgq/visionipc/visionipc_client.h" class CameraWidget : public QWidget { - Q_OBJECT - public: explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); ~CameraWidget(); @@ -23,16 +23,14 @@ public: VisionStreamType getStreamType() { return active_stream_type; } void stopVipcThread(); -signals: - void clicked(); - void vipcThreadFrameReceived(); - void vipcAvailableStreamsUpdated(std::set); + Observable<> clicked; + Observable> availableStreamsUpdated; // invoked on the main thread protected: void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override { stopVipcThread(); } - void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + void mouseReleaseEvent(QMouseEvent *event) override { clicked(); } void vipcThread(); void clearFrames(); @@ -47,10 +45,5 @@ protected: std::thread vipc_thread; std::atomic vipc_exit = false; std::mutex frame_lock; - -protected slots: - void vipcFrameReceived(); - void availableStreamsUpdated(std::set streams); + std::shared_ptr alive_ = std::make_shared(true); }; - -Q_DECLARE_METATYPE(std::set); diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index 8496c26994..beece12352 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/chart.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -37,10 +36,10 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par createToolButtons(); signal_value_font.setPointSize(9); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated); + connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); })); + connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); })); + connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { msgUpdated(id); })); } void ChartView::createToolButtons() { diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index b03623475a..d7227b6cf8 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -126,5 +126,6 @@ private: double tooltip_x = -1; QFont signal_value_font; ChartsWidget *charts_widget; + Connections connections_; friend class ChartsWidget; }; diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 3144c5132d..8aa095b65f 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/chartswidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -76,10 +75,10 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); }); undo_zoom_action->setEnabled(false); redo_zoom_action->setEnabled(false); - zoom_undo_stack.setCallbacks({.index_changed = [this]() { + connections_.push_back(zoom_undo_stack.indexChanged.connect([this]() { undo_zoom_action->setEnabled(zoom_undo_stack.canUndo()); redo_zoom_action->setEnabled(zoom_undo_stack.canRedo()); - }}); + })); reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -122,16 +121,16 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { align_timer->setSingleShot(true); QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll); - QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); - QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState); - QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState); - QObject::connect(can, &AbstractStream::timeRangeChanged, this, &ChartsWidget::timeRangeChanged); + connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); })); + connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); })); + connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); + connections_.push_back(can->seeking.connect([this](double) { updateState(); })); + connections_.push_back(can->timeRangeChanged.connect([this](const auto &range) { timeRangeChanged(range); })); QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); - QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); + connections_.push_back(settings.changed.connect([this]() { settingChanged(); })); QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar); QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index 8b3003dcd6..4c143242b2 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -123,6 +123,7 @@ private: QTimer *align_timer; int current_theme = 0; bool value_tip_visible_ = false; + Connections connections_; friend class ChartView; friend class ChartsContainer; }; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index 85832e796b..aab38f5372 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/signalselector.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index b47bf90b0c..c6cdd9b1b7 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -28,22 +28,22 @@ void UndoStack::clear() { bool was_clean = isClean(); commands_.clear(); index_ = clean_index_ = 0; - if (callbacks_.index_changed) callbacks_.index_changed(); - if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true); + indexChanged(); + if (!was_clean) cleanChanged(true); } void UndoStack::setClean() { if (!isClean()) { clean_index_ = index_; - if (callbacks_.clean_changed) callbacks_.clean_changed(true); + cleanChanged(true); } } void UndoStack::setIndex(int index) { bool was_clean = isClean(); index_ = index; - if (callbacks_.index_changed) callbacks_.index_changed(); - if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean()); + indexChanged(); + if (isClean() != was_clean) cleanChanged(isClean()); } UndoStack *UndoStack::instance() { @@ -51,18 +51,6 @@ UndoStack *UndoStack::instance() { return &undo_stack; } -QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) { - UndoStack::instance()->setCallbacks({ - .index_changed = [this]() { emit indexChanged(); }, - .clean_changed = [this](bool clean) { emit cleanChanged(clean); }, - }); -} - -QtUndoNotifier *undoNotifier() { - static QtUndoNotifier notifier; - return ¬ifier; -} - // EditMsgCommand EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size, diff --git a/openpilot/tools/cabana/commands.h b/openpilot/tools/cabana/commands.h index 200a4f2f5b..1e40499515 100644 --- a/openpilot/tools/cabana/commands.h +++ b/openpilot/tools/cabana/commands.h @@ -1,13 +1,11 @@ #pragma once -#include #include #include #include #include -#include - +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" @@ -21,11 +19,6 @@ public: class UndoStack { public: - struct Callbacks { - std::function index_changed; - std::function clean_changed; - }; - void push(UndoCommand *cmd); // takes ownership and calls redo() void undo(); void redo(); @@ -36,31 +29,18 @@ public: bool canRedo() const { return index_ < (int)commands_.size(); } std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; } std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; } - void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } static UndoStack *instance(); + Observable<> indexChanged; + Observable cleanChanged; + private: void setIndex(int index); std::vector> commands_; int index_ = 0; int clean_index_ = 0; - Callbacks callbacks_; }; -// emits Qt signals for the global undo stack -class QtUndoNotifier : public QObject { - Q_OBJECT - -public: - explicit QtUndoNotifier(QObject *parent = nullptr); - -signals: - void indexChanged(); - void cleanChanged(bool clean); -}; - -QtUndoNotifier *undoNotifier(); - class EditMsgCommand : public UndoCommand { public: EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node, diff --git a/openpilot/tools/cabana/core/observable.h b/openpilot/tools/cabana/core/observable.h new file mode 100644 index 0000000000..0c992bf1da --- /dev/null +++ b/openpilot/tools/cabana/core/observable.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace observable_detail { +struct HandlerTable { + virtual ~HandlerTable() = default; + virtual void erase(int id) = 0; +}; +} // namespace observable_detail + +// disconnects on destruction; safe to outlive the Observable +class Connection { +public: + Connection() = default; + Connection(std::weak_ptr table, int id) : table_(std::move(table)), id_(id) {} + Connection(Connection &&other) noexcept { *this = std::move(other); } + Connection &operator=(Connection &&other) noexcept { + if (this != &other) { + disconnect(); + table_ = std::move(other.table_); + id_ = std::exchange(other.id_, -1); + } + return *this; + } + Connection(const Connection &) = delete; + Connection &operator=(const Connection &) = delete; + ~Connection() { disconnect(); } + + void disconnect() { + if (auto table = table_.lock()) table->erase(id_); + table_.reset(); + id_ = -1; + } + +private: + std::weak_ptr table_; + int id_ = -1; +}; + +using Connections = std::vector; + +// main thread only. handlers may disconnect (or destroy the Observable) while being invoked. +template +class Observable { +public: + using Handler = std::function; + + Observable() = default; + Observable(const Observable &) = delete; + Observable &operator=(const Observable &) = delete; + + [[nodiscard]] Connection connect(Handler handler) { + int id = table_->next_id++; + table_->handlers.emplace(id, std::make_shared(std::move(handler))); + return Connection(table_, id); + } + + void operator()(Args... args) const { + auto table = table_; + std::vector ids; + ids.reserve(table->handlers.size()); + for (const auto &[id, _] : table->handlers) ids.push_back(id); + for (int id : ids) { + auto it = table->handlers.find(id); + if (it == table->handlers.end()) continue; + auto handler = it->second; + (*handler)(args...); + } + } + +private: + struct Table : observable_detail::HandlerTable { + std::map> handlers; + int next_id = 0; + void erase(int id) override { handlers.erase(id); } + }; + std::shared_ptr table_ = std::make_shared
(); +}; diff --git a/openpilot/tools/cabana/dbc/dbcmanager.cc b/openpilot/tools/cabana/dbc/dbcmanager.cc index 7a95a4f809..551619ea82 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.cc +++ b/openpilot/tools/cabana/dbc/dbcmanager.cc @@ -17,7 +17,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name return false; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); return true; } @@ -32,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s return false; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); return true; } @@ -40,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) { for (auto s : sources) { dbc_files[s] = nullptr; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::close(DBCFile *dbc_file) { for (auto &[_, f] : dbc_files) { if (f.get() == dbc_file) f = nullptr; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::closeAll() { dbc_files.clear(); - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->addSignal(sig)) { - if (callbacks_.signal_added) callbacks_.signal_added(id, s); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + signalAdded(id, s); + maskUpdated(); } } } @@ -67,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->updateSignal(sig_name, sig)) { - if (callbacks_.signal_updated) callbacks_.signal_updated(s); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + signalUpdated(s); + maskUpdated(); } } } @@ -76,9 +76,9 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) { if (auto m = msg(id)) { if (auto s = m->sig(sig_name)) { - if (callbacks_.signal_removed) callbacks_.signal_removed(s); + signalRemoved(s); m->removeSignal(sig_name); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + maskUpdated(); } } } @@ -87,15 +87,15 @@ void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_ auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->updateMsg(id, name, size, node, comment); - if (callbacks_.msg_updated) callbacks_.msg_updated(id); + msgUpdated(id); } void DBCManager::removeMsg(const MessageId &id) { auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->removeMsg(id); - if (callbacks_.msg_removed) callbacks_.msg_removed(id); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + msgRemoved(id); + maskUpdated(); } std::string DBCManager::newMsgName(const MessageId &id) { diff --git a/openpilot/tools/cabana/dbc/dbcmanager.h b/openpilot/tools/cabana/dbc/dbcmanager.h index 5a09fae03d..6bc86a8167 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.h +++ b/openpilot/tools/cabana/dbc/dbcmanager.h @@ -1,12 +1,12 @@ #pragma once -#include #include #include #include #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcfile.h" typedef std::set SourceSet; @@ -15,16 +15,6 @@ inline bool operator<(const std::shared_ptr &l, const std::shared_ptr signal_added; - std::function signal_removed; - std::function signal_updated; - std::function msg_updated; - std::function msg_removed; - std::function file_changed; - std::function mask_updated; - }; - DBCManager() = default; bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr); bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr); @@ -54,11 +44,17 @@ public: DBCFile *findDBCFile(const uint8_t source); inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); } std::set allDBCFiles(); - void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } + + Observable signalAdded; + Observable signalRemoved; + Observable signalUpdated; + Observable msgUpdated; + Observable msgRemoved; + Observable<> fileChanged; + Observable<> maskUpdated; private: std::map> dbc_files; - Callbacks callbacks_; }; DBCManager *dbc(); diff --git a/openpilot/tools/cabana/dbc/dbcqt.cc b/openpilot/tools/cabana/dbc/dbcqt.cc deleted file mode 100644 index 4354caf3f5..0000000000 --- a/openpilot/tools/cabana/dbc/dbcqt.cc +++ /dev/null @@ -1,18 +0,0 @@ -#include "tools/cabana/dbc/dbcqt.h" - -QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) { - dbc()->setCallbacks({ - .signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); }, - .signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); }, - .signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); }, - .msg_updated = [this](MessageId id) { emit msgUpdated(id); }, - .msg_removed = [this](MessageId id) { emit msgRemoved(id); }, - .file_changed = [this]() { emit DBCFileChanged(); }, - .mask_updated = [this]() { emit maskUpdated(); }, - }); -} - -QtDBCNotifier *dbcNotifier() { - static QtDBCNotifier notifier; - return ¬ifier; -} diff --git a/openpilot/tools/cabana/dbc/dbcqt.h b/openpilot/tools/cabana/dbc/dbcqt.h deleted file mode 100644 index b889f854ed..0000000000 --- a/openpilot/tools/cabana/dbc/dbcqt.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" - -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -class QtDBCNotifier : public QObject { - Q_OBJECT - -public: - explicit QtDBCNotifier(QObject *parent = nullptr); - -signals: - void signalAdded(MessageId id, const cabana::Signal *sig); - void signalRemoved(const cabana::Signal *sig); - void signalUpdated(const cabana::Signal *sig); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id); - void DBCFileChanged(); - void maskUpdated(); -}; - -QtDBCNotifier *dbcNotifier(); diff --git a/openpilot/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc index 6b62f54959..d0cccb5992 100644 --- a/openpilot/tools/cabana/detailwidget.cc +++ b/openpilot/tools/cabana/detailwidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/detailwidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -56,9 +55,9 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart); QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight); QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); - QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh); + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { if (index != -1) { @@ -97,11 +96,11 @@ void DetailWidget::createToolBar() { layout()->addWidget(toolbar); connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); - connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional> &range) { + connections_.push_back(can->timeRangeChanged.connect([=](const std::optional> &range) { auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; heatmap_all->setText(text); (range ? heatmap_all : heatmap_live)->setChecked(true); - }); + })); } void DetailWidget::showTabBarContextMenu(const QPoint &pt) { diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h index 0fe1535c7a..48d320c40f 100644 --- a/openpilot/tools/cabana/detailwidget.h +++ b/openpilot/tools/cabana/detailwidget.h @@ -57,6 +57,7 @@ private: SignalView *signal_view; ChartsWidget *charts; QSplitter *splitter; + Connections connections_; }; class CenterWidget : public QWidget { diff --git a/openpilot/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc index 3fd569c648..26c0f4168f 100644 --- a/openpilot/tools/cabana/historylog.cc +++ b/openpilot/tools/cabana/historylog.cc @@ -1,5 +1,4 @@ #include "tools/cabana/historylog.h" -#include "tools/cabana/dbc/dbcqt.h" #include @@ -10,6 +9,12 @@ #include "tools/cabana/commands.h" #include "tools/cabana/utils/export.h" +HistoryLogModel::HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) { + connections_.push_back(can->seekedTo.connect([this](double) { reset(); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); })); +} + QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { const auto &m = messages[index.row()]; const int col = index.column(); @@ -207,9 +212,6 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(filterChanged())); QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged); QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); - QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset); QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); } diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 1d3200b200..405fe60fbb 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -22,7 +22,7 @@ class HistoryLogModel : public QAbstractTableModel { Q_OBJECT public: - HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} + HistoryLogModel(QObject *parent); void setMessage(const MessageId &message_id); void updateState(bool clear = false); void setFilter(int sig_idx, const QString &value, std::function cmp); @@ -54,6 +54,7 @@ public: std::deque messages; std::vector sigs; bool hex_mode = false; + Connections connections_; }; class LogsWidget : public QFrame { diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 953f573a0b..bcfe1dde41 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -1,5 +1,4 @@ #include "tools/cabana/mainwin.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -41,15 +40,13 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW restoreGeometry(utils::qbytes(settings.geometry)); restoreState(utils::qbytes(settings.window_state)); - // install handlers + // download handlers are called from download threads static auto static_main_win = this; - qRegisterMetaType("uint64_t"); - qRegisterMetaType("SourceSet"); installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { - emit static_main_win->updateProgressBar(cur, total, success); + utils::runOnMainThread([=]() { static_main_win->updateDownloadProgress(cur, total, success); }); }); installMessageHandler([](ReplyMsgType type, const std::string msg) { - emit static_main_win->showMessage(QString::fromStdString(msg), 2000); + utils::runOnMainThread([=]() { static_main_win->statusBar()->showMessage(QString::fromStdString(msg), 2000); }); }); setStyleSheet(QString(R"(QMainWindow::separator { @@ -57,11 +54,14 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW height: %1px; /* when horizontal */ })").arg(style()->pixelMetric(QStyle::PM_SplitterWidth))); - QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); - QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged); - QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged); - QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); + connections_.push_back(dbc()->fileChanged.connect([this]() { DBCFileChanged(); })); + connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) { undoStackCleanChanged(clean); })); + connections_.push_back(settings.changed.connect([this]() { updateStatus(); })); + + // temporary pump for the non-Qt main thread queue until imgui owns the loop + auto *queue_timer = new QTimer(this); + QObject::connect(queue_timer, &QTimer::timeout, utils::drainMainThreadQueue); + queue_timer->start(10); QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); show(); @@ -136,7 +136,7 @@ void MainWindow::createActions() { undo_act->setShortcuts(QKeySequence::Undo); redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); }); redo_act->setShortcuts(QKeySequence::Redo); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { updateUndoRedoActions(); })); updateUndoRedoActions(); // View Menu @@ -259,14 +259,14 @@ void MainWindow::selectAndOpenStream() { if (dlg.exec()) { openStream(dlg.stream(), dlg.dbcFile()); } else if (!can) { - openStream(new DummyStream(this)); + openStream(new DummyStream()); } } void MainWindow::closeStream() { - openStream(new DummyStream(this)); + openStream(new DummyStream()); if (dbc()->nonEmptyDBCCount() > 0) { - emit dbcNotifier()->DBCFileChanged(); + dbc()->fileChanged(); } statusBar()->showMessage(tr("stream closed")); } @@ -336,13 +336,19 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { } } +// stream threads read the global `can` until its destructor joins them +MainWindow::~MainWindow() { + delete can; + can = nullptr; +} + void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) { - if (can) { - QObject::connect(can, &QObject::destroyed, this, [=]() { startStream(stream, dbc_file); }); - can->deleteLater(); - } else { - startStream(stream, dbc_file); - } + stream_connections_.clear(); + if (wait_dlg_) wait_dlg_->deleteLater(); + wait_dlg_ = nullptr; + delete can; + can = nullptr; + startStream(stream, dbc_file); } void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { @@ -350,8 +356,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { delete messages_widget; delete video_splitter; - can = stream; - can->setParent(this); // take ownership + can = stream; // take ownership can->start(); loadFile(dbc_file); @@ -373,18 +378,19 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { newFile(); } - QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged); + stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); })); if (has_stream) { - auto wait_dlg = new QProgressDialog( + wait_dlg_ = new QProgressDialog( can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."), tr("&Abort"), 0, 100, this); - wait_dlg->setWindowModality(Qt::WindowModal); - wait_dlg->setFixedSize(400, wait_dlg->sizeHint().height()); - QObject::connect(wait_dlg, &QProgressDialog::canceled, this, &MainWindow::close); - QObject::connect(can, &AbstractStream::eventsMerged, wait_dlg, &QProgressDialog::deleteLater); - QObject::connect(this, &MainWindow::updateProgressBar, wait_dlg, [=](uint64_t cur, uint64_t total, bool success) { - wait_dlg->setValue((int)((cur / (double)total) * 100)); + wait_dlg_->setWindowModality(Qt::WindowModal); + wait_dlg_->setFixedSize(400, wait_dlg_->sizeHint().height()); + QObject::connect(wait_dlg_, &QProgressDialog::canceled, this, &MainWindow::close); + wait_dlg_connection_ = can->eventsMerged.connect([this](const MessageEventsMap &) { + wait_dlg_->deleteLater(); + wait_dlg_ = nullptr; + wait_dlg_connection_.disconnect(); }); } } @@ -544,6 +550,7 @@ void MainWindow::remindSaveChanges() { } void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) { + if (wait_dlg_) wait_dlg_->setValue((int)((cur / (double)total) * 100)); if (success && cur < total) { progress_bar->setValue((cur / (double)total) * 100); progress_bar->setFormat(tr("Downloading %p% (%1)").arg(formattedDataSize(total).c_str())); diff --git a/openpilot/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h index 279ea3b969..a57232ceb2 100644 --- a/openpilot/tools/cabana/mainwin.h +++ b/openpilot/tools/cabana/mainwin.h @@ -19,11 +19,14 @@ #include "tools/cabana/videowidget.h" #include "tools/cabana/tools/findsimilarbits.h" +class QProgressDialog; + class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(AbstractStream *stream, const QString &dbc_file); + ~MainWindow(); void toggleChartsDocking(); void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } void loadFile(const QString &fn, SourceSet s = SOURCE_ALL); @@ -42,10 +45,6 @@ public slots: void saveAs(); void saveToClipboard(); -signals: - void showMessage(const QString &msg, int timeout); - void updateProgressBar(uint64_t cur, uint64_t total, bool success); - protected: void startStream(AbstractStream *stream, QString dbc_file); bool eventFilter(QObject *obj, QEvent *event) override; @@ -104,6 +103,10 @@ protected: QAction *redo_act = nullptr; QString car_fingerprint; std::vector default_state; + Connections connections_; + Connections stream_connections_; + Connection wait_dlg_connection_; + QProgressDialog *wait_dlg_ = nullptr; }; class HelpOverlay : public QWidget { diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index b07e3ea0bf..eea07b8711 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/messageswidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -43,9 +42,6 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); - QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -96,7 +92,7 @@ QWidget *MessagesWidget::createToolBar() { QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals); + QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, this, [](int state) { can->suppressDefinedSignals(state); }); suppressHighlighted(); return toolbar; @@ -161,6 +157,12 @@ void MessagesWidget::setMultiLineBytes(bool multi) { // MessageListModel +MessageListModel::MessageListModel(QObject *parent) : QAbstractTableModel(parent) { + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); })); +} + QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 0a9cd256d8..28cab25463 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -31,7 +31,7 @@ public: DATA, }; - MessageListModel(QObject *parent) : QAbstractTableModel(parent) {} + MessageListModel(QObject *parent); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; @@ -63,6 +63,7 @@ private: int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; int sort_threshold_ = 0; + Connections connections_; }; class MessageView : public QTreeView { diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index 0c8136b84c..cbeded0d10 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -613,6 +613,6 @@ void SettingsDlg::save() { settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text().toStdString(); settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - emit settings.changed(); + settings.changed(); QDialog::accept(); } diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 7357ecf4fc..2c21fab419 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -9,11 +9,10 @@ #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/core/settings.h" -class Settings : public QObject, public CabanaSettingsState { - Q_OBJECT - +class Settings : public CabanaSettingsState { public: Settings(); void save(); @@ -24,8 +23,7 @@ public: std::vector window_state; std::vector message_header_state; -signals: - void changed(); + Observable<> changed; }; class SettingsDlg : public QDialog { diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index ebb5374140..250d05e3b2 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -1,5 +1,4 @@ #include "tools/cabana/signalview.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -26,12 +25,12 @@ static QString signalTypeToString(cabana::Signal::Type type) { } SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &SignalModel::refresh); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &SignalModel::handleMsgChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &SignalModel::handleMsgChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalModel::handleSignalAdded); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalModel::handleSignalUpdated); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &SignalModel::handleSignalRemoved); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); })); + connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); })); + connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); + connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); })); } void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { @@ -472,11 +471,11 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalView::handleSignalAdded); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalView::handleSignalUpdated); + connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); - QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) { if (logicalIndex == 1) { value_column_width = newSize; diff --git a/openpilot/tools/cabana/signalview.h b/openpilot/tools/cabana/signalview.h index bccfbab0cc..e2a99ebbd1 100644 --- a/openpilot/tools/cabana/signalview.h +++ b/openpilot/tools/cabana/signalview.h @@ -62,6 +62,7 @@ private: MessageId msg_id; QString filter_str; std::unique_ptr root; + Connections connections_; friend class SignalView; friend class SignalItemDelegate; }; @@ -150,4 +151,5 @@ private: ChartsWidget *charts; QLabel *signal_count_lb; SignalItemDelegate *delegate; + Connections connections_; }; diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index c58a98084f..cc8b2f96ad 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,10 +1,8 @@ #include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include -#include #include "common/timing.h" #include "tools/cabana/settings.h" @@ -12,15 +10,41 @@ static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB AbstractStream *can = nullptr; -AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { - assert(parent != nullptr); +AbstractStream::AbstractStream() { event_buffer_ = std::make_unique(EVENT_NEXT_BUFFER_SIZE); - QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); - QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); - QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks); - QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks); + // connected first so the stream state is updated before any widget handlers run + connections_.push_back(seekedTo.connect([this](double sec) { updateLastMsgsTo(sec); })); + connections_.push_back(seeking.connect([this](double sec) { current_sec_ = sec; })); + connections_.push_back(dbc()->fileChanged.connect([this]() { updateMasks(); })); + connections_.push_back(dbc()->maskUpdated.connect([this]() { updateMasks(); })); +} + +void AbstractStream::postToMainThread(std::function fn) { + utils::runOnMainThread([alive = std::weak_ptr(alive_), fn = std::move(fn)]() { + if (!alive.expired()) fn(); + }); +} + +void AbstractStream::postToMainThreadAndWait(std::function fn) { + assert(!utils::isMainThread()); + std::unique_lock lock(mutex_); + if (exiting_) return; + auto done = std::make_shared(false); + postToMainThread([this, alive = std::weak_ptr(alive_), done, fn = std::move(fn)]() { + fn(); + if (alive.expired()) return; // fn deleted the stream, the waiter was released by cancelWaits() + std::lock_guard lk(mutex_); + *done = true; + wait_cv_.notify_all(); + }); + wait_cv_.wait(lock, [&]() { return *done || exiting_; }); +} + +void AbstractStream::cancelWaits() { + std::lock_guard lk(mutex_); + exiting_ = true; + wait_cv_.notify_all(); } void AbstractStream::updateMasks() { @@ -97,9 +121,8 @@ void AbstractStream::updateLastMessages() { if (sources.size() != prev_src_size) { updateMasks(); - emit sourcesUpdated(sources); } - emit msgsReceived(&msgs, prev_msg_size != last_msgs.size()); + msgsReceived(&msgs, prev_msg_size != last_msgs.size()); } void AbstractStream::setTimeRange(const std::optional> &range) { @@ -107,7 +130,7 @@ void AbstractStream::setTimeRange(const std::optional> if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) { seekTo(time_range_->first); } - emit timeRangeChanged(time_range_); + timeRangeChanged(time_range_); } void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) { @@ -175,16 +198,16 @@ void AbstractStream::updateLastMsgsTo(double sec) { std::any_of(messages_.cbegin(), messages_.cend(), [this](const auto &m) { return !last_msgs.count(m.first); }); last_msgs = messages_; - emit msgsReceived(nullptr, id_changed); + msgsReceived(nullptr, id_changed); std::lock_guard lk(mutex_); seek_finished_ = true; - seek_finished_cv_.notify_one(); + wait_cv_.notify_all(); } void AbstractStream::waitForSeekFinshed() { std::unique_lock lock(mutex_); - seek_finished_cv_.wait(lock, [this]() { return seek_finished_; }); + wait_cv_.wait(lock, [this]() { return seek_finished_ || exiting_; }); seek_finished_ = false; } @@ -218,16 +241,16 @@ void AbstractStream::mergeEvents(const std::vector &events) { } auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent()); all_events_.insert(pos, events.cbegin(), events.cend()); - emit eventsMerged(msg_events); + eventsMerged(msg_events); } } std::pair AbstractStream::eventsInRange(const MessageId &id, std::optional> time_range) const { - const auto &events = can->events(id); + const auto &events = this->events(id); if (!time_range) return {events.begin(), events.end()}; - auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent()); - auto last = std::upper_bound(first, events.end(), can->toMonoTime(time_range->second), CompareCanEvent()); + auto first = std::lower_bound(events.begin(), events.end(), toMonoTime(time_range->first), CompareCanEvent()); + auto last = std::upper_bound(first, events.end(), toMonoTime(time_range->second), CompareCanEvent()); return {first, last}; } diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 82cb899937..17d212041a 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,16 +15,15 @@ #include "openpilot/cereal/messaging/messaging.h" #include "tools/cabana/core/can_data.h" +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/utils/util.h" #include "tools/replay/util.h" -class AbstractStream : public QObject { - Q_OBJECT - +class AbstractStream { public: - AbstractStream(QObject *parent); - virtual ~AbstractStream() {} + AbstractStream(); + virtual ~AbstractStream() = default; virtual void start() = 0; virtual bool liveStreaming() const { return true; } virtual void seekTo(double ts) {} @@ -56,21 +56,22 @@ public: void clearSuppressed(); void suppressDefinedSignals(bool suppress); -signals: - void paused(); - void resume(); - void seeking(double sec); - void seekedTo(double sec); - void timeRangeChanged(const std::optional> &range); - void eventsMerged(const MessageEventsMap &events_map); - void msgsReceived(const std::set *new_msgs, bool has_new_ids); - void sourcesUpdated(const SourceSet &s); - void privateUpdateLastMsgsSignal(); + // invoked on the main thread + Observable<> paused; + Observable<> resume; + Observable seeking; + Observable seekedTo; + Observable> &> timeRangeChanged; + Observable eventsMerged; + Observable *, bool> msgsReceived; -public: SourceSet sources; protected: + void postToMainThread(std::function fn); // dropped if the stream is destroyed first + void postToMainThreadAndWait(std::function fn); + void cancelWaits(); // call before joining threads, the main thread isn't pumping events during destruction + void requestUpdateLastMessages() { postToMainThread([this]() { updateLastMessages(); }); } void mergeEvents(const std::vector &events); const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); @@ -87,11 +88,14 @@ private: MessageEventsMap events_; std::unordered_map last_msgs; std::unique_ptr event_buffer_; + std::shared_ptr alive_ = std::make_shared(true); + Connections connections_; // Members accessed in multiple threads. (mutex protected) std::mutex mutex_; - std::condition_variable seek_finished_cv_; + std::condition_variable wait_cv_; bool seek_finished_ = false; + bool exiting_ = false; std::set new_msgs_; std::unordered_map messages_; std::unordered_map> masks_; @@ -108,9 +112,7 @@ signals: }; class DummyStream : public AbstractStream { - Q_OBJECT public: - DummyStream(QObject *parent) : AbstractStream(parent) {} std::string routeName() const override { return "No Stream"; } void start() override {} }; diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3bd51c079d..3b4b54707c 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -23,7 +23,7 @@ // DeviceStream -DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { +DeviceStream::DeviceStream(QString address) : zmq_address(address) { } DeviceStream::~DeviceStream() { @@ -61,8 +61,8 @@ void DeviceStream::start() { // fails, the child writes errno and the parent aborts stream start. int err_pipe[2] = {-1, -1}; if (::pipe(err_pipe) != 0) { - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); return; } @@ -79,8 +79,8 @@ void DeviceStream::start() { ::close(err_pipe[1]); if (pid < 0) { ::close(err_pipe[0]); - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); return; } @@ -91,8 +91,8 @@ void DeviceStream::start() { // Child failed to exec; reap and surface the error. int status = 0; ::waitpid(pid, &status, 0); - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); return; } @@ -144,5 +144,5 @@ OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(p AbstractStream *OpenDeviceWidget::open() { QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); bool msgq = group->checkedId() == 0; - return new DeviceStream(qApp, msgq ? "" : ip); + return new DeviceStream(msgq ? "" : ip); } diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 0e6951c92c..1aa8ce9bff 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -5,9 +5,8 @@ #include class DeviceStream : public LiveStream { - Q_OBJECT public: - DeviceStream(QObject *parent, QString address = {}); + DeviceStream(QString address = {}); ~DeviceStream(); inline std::string routeName() const override { return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString()); diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 019a67e8f2..05a6d44c40 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -38,7 +38,7 @@ struct LiveStream::Logger { uint64_t start_ts; }; -LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { +LiveStream::LiveStream() { if (settings.log_livestream) { logger = std::make_unique(); } @@ -65,9 +65,9 @@ void LiveStream::stop() { void LiveStream::updateThread() { while (!exit_) { std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_)); - // coalesce: skip the emit if the main thread hasn't processed the previous one yet. + // coalesce: skip the request if the main thread hasn't processed the previous one yet. if (!update_pending_.exchange(true)) { - emit privateUpdateLastMsgsSignal(); + requestUpdateLastMessages(); } } } @@ -89,7 +89,7 @@ void LiveStream::handleEvent(kj::ArrayPtr data) { } } -// called on the main thread by the queued privateUpdateLastMsgsSignal connection +// called on the main thread via requestUpdateLastMessages() void LiveStream::updateLastMessages() { update_pending_ = false; fps_ = settings.fps; @@ -142,10 +142,10 @@ void LiveStream::seekTo(double sec) { first_update_ts = nanos_since_boot(); current_event_ts = first_event_ts = std::min(sec * 1e9 + begin_event_ts, lastest_event_ts); post_last_event = (first_event_ts == lastest_event_ts); - emit seekedTo((current_event_ts - begin_event_ts) / 1e9); + seekedTo((current_event_ts - begin_event_ts) / 1e9); } void LiveStream::pause(bool pause) { paused_ = pause; - emit(pause ? paused() : resume()); + pause ? paused() : resume(); } diff --git a/openpilot/tools/cabana/streams/livestream.h b/openpilot/tools/cabana/streams/livestream.h index 5d65b1743f..22587335b0 100644 --- a/openpilot/tools/cabana/streams/livestream.h +++ b/openpilot/tools/cabana/streams/livestream.h @@ -9,10 +9,8 @@ #include "tools/cabana/streams/abstractstream.h" class LiveStream : public AbstractStream { - Q_OBJECT - public: - LiveStream(QObject *parent); + LiveStream(); virtual ~LiveStream(); void start() override; void stop(); diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 7ccb18a756..06c8e6e19c 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -10,7 +10,7 @@ #include #include -PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { +PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) { if (!connect()) { throw std::runtime_error("Failed to connect to panda"); } @@ -181,7 +181,7 @@ void OpenPandaWidget::buildConfigForm() { AbstractStream *OpenPandaWidget::open() { try { - return new PandaStream(qApp, config); + return new PandaStream(config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); return nullptr; diff --git a/openpilot/tools/cabana/streams/pandastream.h b/openpilot/tools/cabana/streams/pandastream.h index f8847f65e5..d764613fe1 100644 --- a/openpilot/tools/cabana/streams/pandastream.h +++ b/openpilot/tools/cabana/streams/pandastream.h @@ -24,9 +24,8 @@ struct PandaStreamConfig { }; class PandaStream : public LiveStream { - Q_OBJECT public: - PandaStream(QObject *parent, PandaStreamConfig config_ = {}); + PandaStream(PandaStreamConfig config_ = {}); ~PandaStream() { stop(); } inline std::string routeName() const override { return "Panda: " + config.serial; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 6d54369aef..68f4fb3056 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -12,17 +12,21 @@ #include "common/util.h" #include "tools/cabana/streams/routes.h" -ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { +ReplayStream::ReplayStream() { unsetenv("ZMQ"); setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); op_prefix = std::make_unique(); - QObject::connect(&settings, &Settings::changed, this, [this]() { + settings_connection_ = settings.changed.connect([this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); } +ReplayStream::~ReplayStream() { + cancelWaits(); +} + void ReplayStream::mergeSegments() { auto event_data = replay->getEventData(); for (const auto &[n, seg] : event_data->segments) { @@ -51,14 +55,14 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter([this](const Event *event) { return eventFilter(event); }); - // Forward replay callbacks to corresponding Qt signals. - replay->onSeeking = [this](double sec) { emit seeking(sec); }; + // replay callbacks arrive on replay threads + replay->onSeeking = [this](double sec) { postToMainThread([this, sec]() { seeking(sec); }); }; replay->onSeekedTo = [this](double sec) { - emit seekedTo(sec); + postToMainThread([this, sec]() { seekedTo(sec); }); waitForSeekFinshed(); }; - replay->onQLogLoaded = [this](std::shared_ptr qlog) { emit qLogLoaded(qlog); }; - replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); }; + replay->onQLogLoaded = [this](std::shared_ptr qlog) { postToMainThread([this, qlog]() { qLogLoaded(qlog); }); }; + replay->onSegmentsMerged = [this]() { postToMainThreadAndWait([this]() { mergeSegments(); }); }; bool success = replay->load(); if (!success) { @@ -70,18 +74,18 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d "python3 openpilot/tools/lib/auth.py\n\n" "This will grant access to routes from your comma account."; } else { - message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n" - "This is likely a private route.").arg(QString::fromStdString(route)); + message = QString("Access Denied. You do not have permission to access route:\n\n%1\n\n" + "This is likely a private route.").arg(QString::fromStdString(route)); } - QMessageBox::warning(nullptr, tr("Access Denied"), message); + QMessageBox::warning(nullptr, "Access Denied", message); } else if (replay->lastRouteError() == RouteLoadError::NetworkError) { - QMessageBox::warning(nullptr, tr("Network Error"), - tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Network Error", + QString("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); } else if (replay->lastRouteError() == RouteLoadError::FileNotFound) { - QMessageBox::warning(nullptr, tr("Route Not Found"), - tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Route Not Found", + QString("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); } else { - QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Route Load Failed", QString("Failed to load route: '%1'").arg(QString::fromStdString(route))); } } return success; @@ -102,7 +106,7 @@ bool ReplayStream::eventFilter(const Event *event) { double ts = millis_since_boot(); if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { - emit privateUpdateLastMsgsSignal(); + requestUpdateLastMessages(); prev_update_ts = ts; } return true; @@ -110,7 +114,7 @@ bool ReplayStream::eventFilter(const Event *event) { void ReplayStream::pause(bool pause) { replay->pause(pause); - emit(pause ? paused() : resume()); + pause ? paused() : resume(); } @@ -161,7 +165,7 @@ AbstractStream *OpenReplayWidget::open() { if (!is_valid_format) { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { - auto replay_stream = std::make_unique(qApp); + auto replay_stream = std::make_unique(); uint32_t flags = REPLAY_FLAG_NONE; if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; diff --git a/openpilot/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h index eecd345715..6f49502ba3 100644 --- a/openpilot/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -10,13 +10,10 @@ #include "tools/cabana/streams/abstractstream.h" #include "tools/replay/replay.h" -Q_DECLARE_METATYPE(std::shared_ptr); - class ReplayStream : public AbstractStream { - Q_OBJECT - public: - ReplayStream(QObject *parent); + ReplayStream(); + ~ReplayStream(); void start() override { replay->start(); } bool loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false); bool eventFilter(const Event *event); @@ -36,12 +33,13 @@ public: inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; -signals: - void qLogLoaded(std::shared_ptr qlog); + // invoked on the main thread + Observable> qLogLoaded; private: void mergeSegments(); std::unique_ptr replay = nullptr; + Connection settings_connection_; std::set processed_segments; std::unique_ptr op_prefix; }; diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index b616e7f242..b10f2cab90 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -16,7 +16,7 @@ #include #include -SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { +SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) { if (!available()) { throw std::runtime_error("SocketCAN not available"); } @@ -140,7 +140,7 @@ void OpenSocketCanWidget::refreshDevices() { AbstractStream *OpenSocketCanWidget::open() { try { - return new SocketCanStream(qApp, config); + return new SocketCanStream(config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); return nullptr; diff --git a/openpilot/tools/cabana/streams/socketcanstream.h b/openpilot/tools/cabana/streams/socketcanstream.h index 3c5cd184f7..78e856d506 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.h +++ b/openpilot/tools/cabana/streams/socketcanstream.h @@ -9,9 +9,8 @@ struct SocketCanStreamConfig { }; class SocketCanStream : public LiveStream { - Q_OBJECT public: - SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {}); + SocketCanStream(SocketCanStreamConfig config_ = {}); ~SocketCanStream(); static bool available(); diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index 53be1b0afa..c2ebc2e6b0 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -164,11 +164,10 @@ void test_dbc_manager() { int files_changed = 0; int signals_added = 0; int masks_updated = 0; - manager.setCallbacks({ - .signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; }, - .file_changed = [&]() { ++files_changed; }, - .mask_updated = [&]() { ++masks_updated; }, - }); + Connections connections; + connections.push_back(manager.signalAdded.connect([&](MessageId, const cabana::Signal *) { ++signals_added; })); + connections.push_back(manager.fileChanged.connect([&]() { ++files_changed; })); + connections.push_back(manager.maskUpdated.connect([&]() { ++masks_updated; })); std::string error; REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error)); diff --git a/openpilot/tools/cabana/tools/routeinfo.cc b/openpilot/tools/cabana/tools/routeinfo.cc index dc272e3d12..1037d4a206 100644 --- a/openpilot/tools/cabana/tools/routeinfo.cc +++ b/openpilot/tools/cabana/tools/routeinfo.cc @@ -6,7 +6,7 @@ #include "tools/cabana/streams/replaystream.h" RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) { - auto *replay = qobject_cast(can)->getReplay(); + auto *replay = dynamic_cast(can)->getReplay(); setWindowTitle(tr("Route: %1").arg(QString::fromStdString(replay->route().name()))); auto *table = new QTableWidget(replay->route().segments().size(), 7, this); diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 87a0f89427..9a26e5f977 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -23,6 +25,30 @@ #include #include "common/util.h" +static const std::thread::id main_thread_id = std::this_thread::get_id(); +static std::mutex main_thread_queue_mutex; +static std::vector> main_thread_queue; + +bool utils::isMainThread() { return std::this_thread::get_id() == main_thread_id; } + +void utils::runOnMainThread(std::function fn) { + if (isMainThread()) { + fn(); + } else { + std::lock_guard lk(main_thread_queue_mutex); + main_thread_queue.push_back(std::move(fn)); + } +} + +void utils::drainMainThreadQueue() { + std::vector> fns; + { + std::lock_guard lk(main_thread_queue_mutex); + fns.swap(main_thread_queue); + } + for (auto &fn : fns) fn(); +} + // SegmentTree void SegmentTree::build(const std::vector &arr) { diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index 5a3c62d118..cbcb6d14ee 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -20,9 +21,14 @@ #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +// needed by QVariant::fromValue() in the Qt views; goes away with QVariant +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + inline QColor toQColor(const CabanaColor &color) { return QColor(color.r, color.g, color.b, color.a); } @@ -132,6 +138,10 @@ public: namespace utils { +bool isMainThread(); +// inline on the main thread, queued until drainMainThreadQueue() otherwise +void runOnMainThread(std::function fn); +void drainMainThreadQueue(); QPixmap icon(const QString &id); std::string homePath(); std::filesystem::path configPath(); @@ -175,7 +185,7 @@ public: const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); setIconSize({metric, metric}); theme = settings.theme; - connect(&settings, &Settings::changed, this, &ToolButton::updateIcon); + settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); } void setIcon(const QString &icon) { icon_str = icon; @@ -184,6 +194,7 @@ public: private: void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + Connection settings_connection_; QString icon_str; int theme; }; diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index bd61573658..b1e84586a8 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -27,7 +27,7 @@ static const QColor timeline_colors[] = { }; static Replay *getReplay() { - auto stream = qobject_cast(can); + auto stream = dynamic_cast(can); return stream ? stream->getReplay() : nullptr; } @@ -42,11 +42,11 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { createPlaybackController(); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); - QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); - QObject::connect(can, &AbstractStream::msgsReceived, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::seeking, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::timeRangeChanged, this, &VideoWidget::timeRangeChanged); + connections_.push_back(can->paused.connect([this]() { updatePlayBtnState(); })); + connections_.push_back(can->resume.connect([this]() { updatePlayBtnState(); })); + connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); + connections_.push_back(can->seeking.connect([this](double) { updateState(); })); + connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { timeRangeChanged(); })); updatePlayBtnState(); setWhatsThis(tr(R"( @@ -157,14 +157,14 @@ QWidget *VideoWidget::createCameraWidget() { slider->setTimeRange(can->minSeconds(), can->maxSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(can, &AbstractStream::paused, cam_widget, qOverload<>(&StreamCameraView::update)); - QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); }); - QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); - QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); + connections_.push_back(can->paused.connect([this]() { cam_widget->update(); })); + connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { slider->update(); })); + connections_.push_back(cam_widget->clicked.connect([]() { can->pause(!can->isPaused()); })); + connections_.push_back(cam_widget->availableStreamsUpdated.connect([this](std::set streams) { vipcAvailableStreamsUpdated(streams); })); QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); }); - QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection); + connections_.push_back(static_cast(can)->qLogLoaded.connect([this](std::shared_ptr qlog) { cam_widget->parseQLog(qlog); })); slider->installEventFilter(this); return w; } diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index 09f7a9931b..1b6d81406e 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -64,6 +64,7 @@ protected: void timeRangeChanged(); void updateState(); void updatePlayBtnState(); + Connections connections_; QWidget *createCameraWidget(); void createPlaybackController(); void createSpeedDropdown(QToolBar *toolbar); From 7cc48b5bc94e197bfce1ee88d865d16488d8c286 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:00:24 -0700 Subject: [PATCH 05/34] cabana: move RoutesDialog out of streams/ (#38716) --- openpilot/tools/cabana/SConscript | 2 +- .../cabana/{streams/routes.cc => routesdialog.cc} | 12 ++++++------ .../cabana/{streams/routes.h => routesdialog.h} | 0 openpilot/tools/cabana/streams/replaystream.cc | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename openpilot/tools/cabana/{streams/routes.cc => routesdialog.cc} (94%) rename openpilot/tools/cabana/{streams/routes.h => routesdialog.h} (100%) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index bc366ab6fb..9c34de661d 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', diff --git a/openpilot/tools/cabana/streams/routes.cc b/openpilot/tools/cabana/routesdialog.cc similarity index 94% rename from openpilot/tools/cabana/streams/routes.cc rename to openpilot/tools/cabana/routesdialog.cc index b6f98da533..35e23a6eee 100644 --- a/openpilot/tools/cabana/streams/routes.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -1,4 +1,4 @@ -#include "tools/cabana/streams/routes.h" +#include "tools/cabana/routesdialog.h" #include #include @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -14,6 +13,7 @@ #include #include "json11/json11.hpp" +#include "tools/cabana/utils/util.h" #include "tools/replay/py_downloader.h" namespace { @@ -113,9 +113,9 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { // Fetch devices std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { if (!alive.expired()) parseDeviceList(r, response.first, response.second); - }, Qt::QueuedConnection); + }); }).detach(); } @@ -156,9 +156,9 @@ void RoutesDialog::fetchRoutes() { int request_id = ++fetch_id_; std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); - }, Qt::QueuedConnection); + }); }).detach(); } diff --git a/openpilot/tools/cabana/streams/routes.h b/openpilot/tools/cabana/routesdialog.h similarity index 100% rename from openpilot/tools/cabana/streams/routes.h rename to openpilot/tools/cabana/routesdialog.h diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 68f4fb3056..56e4bfa084 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -10,7 +10,7 @@ #include "common/timing.h" #include "common/util.h" -#include "tools/cabana/streams/routes.h" +#include "tools/cabana/routesdialog.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); From 9b9e3ea6048a72948922d0e58dcdbbcd876cd658 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:25:11 -0700 Subject: [PATCH 06/34] cabana: string helpers in utils return std::string (#38720) --- openpilot/tools/cabana/SConscript | 3 +- openpilot/tools/cabana/binaryview.cc | 2 +- openpilot/tools/cabana/chart/chartswidget.cc | 2 +- openpilot/tools/cabana/messageswidget.cc | 6 +- openpilot/tools/cabana/signalview.cc | 4 +- openpilot/tools/cabana/tests/test_cabana.cc | 58 ++++++++++++++++++++ openpilot/tools/cabana/utils/strings.cc | 57 +++++++++++++++++++ openpilot/tools/cabana/utils/strings.h | 33 +++++++++++ openpilot/tools/cabana/utils/util.cc | 42 -------------- openpilot/tools/cabana/utils/util.h | 15 +---- openpilot/tools/cabana/videowidget.cc | 2 +- 11 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 openpilot/tools/cabana/utils/strings.cc create mode 100644 openpilot/tools/cabana/utils/strings.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 9c34de661d..662b3952bb 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -100,7 +100,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"] cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] @@ -119,6 +119,7 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), + dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), ] dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 2f0167107c..160aead6fd 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -385,7 +385,7 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i QVariant BinaryViewModel::data(const QModelIndex &index, int role) const { auto item = (const BinaryViewModel::Item *)index.internalPointer(); - return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant(); + return role == Qt::ToolTipRole && item && !item->sigs.empty() ? QString::fromStdString(utils::signalToolTip(item->sigs.back())) : QVariant(); } // BinaryItemDelegate diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 8aa095b65f..2e3e86bab0 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -245,7 +245,7 @@ void ChartsWidget::setIsDocked(bool docked) { void ChartsWidget::updateToolBar() { title_label->setText(tr("Charts: %1").arg(charts.size())); columns_action->setText(tr("Columns: %1").arg(column_count)); - range_lb->setText(utils::formatSeconds(max_chart_range)); + range_lb->setText(QString::fromStdString(utils::formatSeconds(max_chart_range))); bool is_zoomed = can->timeRange().has_value(); range_lb_action->setVisible(!is_zoomed); diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index eea07b8711..2325bb3ae4 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -195,7 +195,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { switch (index.column()) { case Column::NAME: return item.name; case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; - case Column::ADDRESS: return toHexString(item.id.address); + case Column::ADDRESS: return QString::fromStdString(utils::toHexString(item.id.address)); case Column::NODE: return item.node; case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; @@ -288,7 +288,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, item.id.source); break; case Column::ADDRESS: - match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive); + match = QString::fromStdString(utils::toHexString(item.id.address)).contains(txt, Qt::CaseInsensitive); match = match || parseRange(txt, item.id.address, 16); break; case Column::NODE: @@ -301,7 +301,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, data.count); break; case Column::DATA: - match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive); + match = QString::fromStdString(utils::toHex(data.dat)).contains(txt, Qt::CaseInsensitive); break; } } diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 250d05e3b2..3a6d4dab25 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -153,7 +153,7 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked; if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked; } else if (role == Qt::ToolTipRole && item->type == Item::Sig) { - return (index.column() == 0) ? signalToolTip(item->sig) : QString(); + return (index.column() == 0) ? QString::fromStdString(utils::signalToolTip(item->sig)) : QString(); } } return {}; @@ -570,7 +570,7 @@ void SignalView::signalHovered(const cabana::Signal *sig) { void SignalView::updateToolBar() { signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); - sparkline_label->setText(utils::formatSeconds(settings.sparkline_range)); + sparkline_label->setText(QString::fromStdString(utils::formatSeconds(settings.sparkline_range))); } void SignalView::setSparklineRange(int value) { diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index c2ebc2e6b0..e3b8377e02 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -1,10 +1,13 @@ +#include +#include #include #include #include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/utils/strings.h" const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -185,7 +188,62 @@ void test_dbc_manager() { REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr); } +void test_format_seconds() { + REQUIRE(utils::formatSeconds(0) == "00:00"); + REQUIRE(utils::formatSeconds(59.4) == "00:59"); + REQUIRE(utils::formatSeconds(-1) == "00:00"); + REQUIRE(utils::formatSeconds(61.234, true) == "01:01.234"); + REQUIRE(utils::formatSeconds(3599.9) == "59:59"); + REQUIRE(utils::formatSeconds(3601) == "01:00:01"); + REQUIRE(utils::formatSeconds(3601.5, true) == "01:00:01.500"); + + const char *tz = getenv("TZ"); + const bool had_tz = tz != nullptr; + const std::string saved_tz = had_tz ? tz : ""; + setenv("TZ", "UTC", 1); + tzset(); + REQUIRE(utils::formatSeconds(0, false, true) == "1970-01-01 00:00:00"); + REQUIRE(utils::formatSeconds(1700000000.123, true, true) == "2023-11-14 22:13:20.123"); + if (had_tz) { + setenv("TZ", saved_tz.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); +} + +void test_to_hex() { + REQUIRE(utils::toHex({}) == ""); + REQUIRE(utils::toHex({0x00, 0x0f, 0xab, 0xff}) == "000FABFF"); + REQUIRE(utils::toHex({0x01, 0x02, 0x03}, ' ') == "01 02 03"); + + REQUIRE(utils::toHexString(0) == "0x00"); + REQUIRE(utils::toHexString(0xf) == "0x0F"); + REQUIRE(utils::toHexString(0x1ab) == "0x1AB"); + REQUIRE(utils::toHexString(0x1fffffff) == "0x1FFFFFFF"); +} + +void test_signal_tooltip() { + cabana::Signal sig{}; + sig.name = "speed"; + sig.start_bit = 3; + sig.size = 12; + sig.msb = 14; + sig.lsb = 3; + sig.is_little_endian = true; + sig.is_signed = false; + REQUIRE(utils::signalToolTip(&sig) == R"( + speed
+ Start Bit: 3 Size: 12
+ MSB: 14 LSB: 3
+ Little Endian: Y Signed: N
+ )"); +} + void test_cabana_core() { + test_format_seconds(); + test_to_hex(); + test_signal_tooltip(); test_generate_dbc(); test_comment_order(); test_preserve_original_header(); diff --git a/openpilot/tools/cabana/utils/strings.cc b/openpilot/tools/cabana/utils/strings.cc new file mode 100644 index 0000000000..5590b6078e --- /dev/null +++ b/openpilot/tools/cabana/utils/strings.cc @@ -0,0 +1,57 @@ +#include "tools/cabana/utils/strings.h" + +#include +#include +#include +#include + +#include "tools/cabana/dbc/dbc.h" + +namespace utils { + +std::string formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { + char out[80] = {}; + if (absolute_time) { + const auto ms_total = static_cast(std::llround(sec * 1000.0)); + const std::time_t secs = static_cast(ms_total / 1000); + int millis = static_cast(ms_total % 1000); + if (millis < 0) millis = -millis; + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64] = {}; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + if (!include_milliseconds) return buf; + snprintf(out, sizeof(out), "%s.%03d", buf, millis); + return out; + } + + // Relative duration (not wall-clock). + const bool show_hours = sec > 60 * 60; + int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); + const int hours = total_ms / (3600 * 1000); + const int minutes = (total_ms / (60 * 1000)) % 60; + const int seconds = (total_ms / 1000) % 60; + const int millis = total_ms % 1000; + if (show_hours && include_milliseconds) { + snprintf(out, sizeof(out), "%02d:%02d:%02d.%03d", hours, minutes, seconds, millis); + } else if (show_hours) { + snprintf(out, sizeof(out), "%02d:%02d:%02d", hours, minutes, seconds); + } else if (include_milliseconds) { + snprintf(out, sizeof(out), "%02d:%02d.%03d", minutes, seconds, millis); + } else { + snprintf(out, sizeof(out), "%02d:%02d", minutes, seconds); + } + return out; +} + +std::string signalToolTip(const cabana::Signal *sig) { + std::ostringstream s; + s << "\n " << sig->name << "
\n" + << " Start Bit: " << sig->start_bit << " Size: " << sig->size << "
\n" + << " MSB: " << sig->msb << " LSB: " << sig->lsb << "
\n" + << " Little Endian: " << (sig->is_little_endian ? "Y" : "N") + << " Signed: " << (sig->is_signed ? "Y" : "N") << "
\n "; + return s.str(); +} + +} // namespace utils diff --git a/openpilot/tools/cabana/utils/strings.h b/openpilot/tools/cabana/utils/strings.h new file mode 100644 index 0000000000..f6581c3164 --- /dev/null +++ b/openpilot/tools/cabana/utils/strings.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace cabana { class Signal; } + +namespace utils { + +std::string formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); +std::string signalToolTip(const cabana::Signal *sig); + +inline std::string toHex(const std::vector &dat, char separator = '\0') { + static const char digits[] = "0123456789ABCDEF"; + std::string hex; + hex.reserve(dat.size() * (separator ? 3 : 2)); + for (size_t i = 0; i < dat.size(); ++i) { + if (separator && i) hex += separator; + hex += digits[dat[i] >> 4]; + hex += digits[dat[i] & 0xf]; + } + return hex; +} + +inline std::string toHexString(int value) { + char buf[16] = {}; + snprintf(buf, sizeof(buf), "0x%02X", value); + return buf; +} + +} // namespace utils diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 9a26e5f977..9a44ef5054 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -442,37 +441,6 @@ void setTheme(int theme) { } } -QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { - if (absolute_time) { - const auto ms_total = static_cast(std::llround(sec * 1000.0)); - const std::time_t secs = static_cast(ms_total / 1000); - int millis = static_cast(ms_total % 1000); - if (millis < 0) millis = -millis; - std::tm tm{}; - localtime_r(&secs, &tm); - char buf[64]; - std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - if (include_milliseconds) { - return QString::asprintf("%s.%03d", buf, millis); - } - return QString::fromUtf8(buf); - } - - // Relative duration (not wall-clock). - const bool show_hours = sec > 60 * 60; - int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); - const int hours = total_ms / (3600 * 1000); - const int minutes = (total_ms / (60 * 1000)) % 60; - const int seconds = (total_ms / 1000) % 60; - const int millis = total_ms % 1000; - if (show_hours) { - return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) - : QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); - } - return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis) - : QString::asprintf("%02d:%02d", minutes, seconds); -} - } // namespace utils int num_decimals(double num) { @@ -481,16 +449,6 @@ int num_decimals(double num) { return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; } -QString signalToolTip(const cabana::Signal *sig) { - return QObject::tr(R"( - %1
- Start Bit: %2 Size: %3
- MSB: %4 LSB: %5
- Little Endian: %6 Signed: %7
- )").arg(QString::fromStdString(sig->name)).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb) - .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); -} - void sigTermHandler(int s) { std::signal(s, SIG_DFL); qApp->quit(); diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index cbcb6d14ee..f2435143c1 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -24,6 +24,7 @@ #include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +#include "tools/cabana/utils/strings.h" // needed by QVariant::fromValue() in the Qt views; goes away with QVariant Q_DECLARE_METATYPE(MessageId) @@ -149,22 +150,10 @@ bool getClipboardText(std::string *text); // false if no clipboard tool is avai bool setClipboardText(const std::string &text); bool isDarkTheme(); void setTheme(int theme); -QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { auto size = (r.size() - text.size()) / 2; p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); } -inline QString toHex(const std::vector &dat, char separator = '\0') { - static const char digits[] = "0123456789ABCDEF"; - QString hex; - hex.reserve(dat.size() * (separator ? 3 : 2)); - for (size_t i = 0; i < dat.size(); ++i) { - if (separator && i) hex += QLatin1Char(separator); - hex += QLatin1Char(digits[dat[i] >> 4]); - hex += QLatin1Char(digits[dat[i] & 0xf]); - } - return hex; -} // boundary conversions for the remaining Qt byte-array based state APIs template @@ -225,7 +214,5 @@ private: }; int num_decimals(double num); -QString signalToolTip(const cabana::Signal *sig); -inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); } void initApp(int argc, char *argv[], bool disable_hidpi = true); QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index b1e84586a8..23f60f29f1 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -204,7 +204,7 @@ void VideoWidget::timeRangeChanged() { QString VideoWidget::formatTime(double sec, bool include_milliseconds) { if (settings.absolute_time) sec += std::chrono::duration(can->beginDateTime().time_since_epoch()).count(); - return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); + return QString::fromStdString(utils::formatSeconds(sec, include_milliseconds, settings.absolute_time)); } void VideoWidget::updateState() { From 30f358eb59c191cb6ca98b4ee79e17e5b9b82b63 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:25:34 -0700 Subject: [PATCH 07/34] cabana: use std::string in RoutesDialog API results (#38717) --- openpilot/tools/cabana/routesdialog.cc | 24 ++++++++++--------- openpilot/tools/cabana/routesdialog.h | 6 ++--- .../tools/cabana/streams/replaystream.cc | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/openpilot/tools/cabana/routesdialog.cc b/openpilot/tools/cabana/routesdialog.cc index 35e23a6eee..9891104c9f 100644 --- a/openpilot/tools/cabana/routesdialog.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -58,13 +58,13 @@ int64_t parseIsoToUnixMs(const std::string &s) { return static_cast(secs) * 1000 + millis; } -QString formatUnixMs(int64_t ms) { +std::string formatUnixMs(int64_t ms) { time_t secs = static_cast(ms / 1000); std::tm tm{}; localtime_r(&secs, &tm); char buf[64]; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - return QString::fromUtf8(buf); + return buf; } } // namespace @@ -113,17 +113,18 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { // Fetch devices std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + auto response = checkApiResponse(result); + utils::runOnMainThread([this, alive, r = std::move(result), response]() { if (!alive.expired()) parseDeviceList(r, response.first, response.second); }); }).detach(); } -void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { +void RoutesDialog::parseDeviceList(const std::string &json, bool success, int error_code) { if (success) { device_list_->clear(); std::string err; - auto doc = json11::Json::parse(json.toStdString(), err); + auto doc = json11::Json::parse(json, err); if (err.empty() && doc.is_array()) { for (const auto &device : doc.array_items()) { QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); @@ -156,16 +157,17 @@ void RoutesDialog::fetchRoutes() { int request_id = ++fetch_id_; std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + auto response = checkApiResponse(result); + utils::runOnMainThread([this, alive, r = std::move(result), response, request_id]() { if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); }); }).detach(); } -void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { +void RoutesDialog::parseRouteList(const std::string &json, bool success, int error_code) { if (success) { std::string err; - auto doc = json11::Json::parse(json.toStdString(), err); + auto doc = json11::Json::parse(json, err); if (err.empty() && doc.is_array()) { for (const auto &route : doc.array_items()) { int64_t from_ms = 0, to_ms = 0; @@ -177,7 +179,7 @@ void RoutesDialog::parseRouteList(const QString &json, bool success, int error_c to_ms = static_cast(route["end_time_utc_millis"].number_value()); } const int mins = static_cast((to_ms - from_ms) / 60000); - auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins)); + auto item = new QListWidgetItem(QString::fromStdString(formatUnixMs(from_ms) + " " + std::to_string(mins) + "min")); item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); route_list_->addItem(item); } @@ -190,7 +192,7 @@ void RoutesDialog::parseRouteList(const QString &json, bool success, int error_c route_list_->setEmptyText(tr("No items")); } -QString RoutesDialog::route() { +std::string RoutesDialog::route() { auto current_item = route_list_->currentItem(); - return current_item ? current_item->data(Qt::UserRole).toString() : ""; + return current_item ? current_item->data(Qt::UserRole).toString().toStdString() : ""; } diff --git a/openpilot/tools/cabana/routesdialog.h b/openpilot/tools/cabana/routesdialog.h index 6ed145603f..44d7f068f9 100644 --- a/openpilot/tools/cabana/routesdialog.h +++ b/openpilot/tools/cabana/routesdialog.h @@ -12,11 +12,11 @@ class RoutesDialog : public QDialog { Q_OBJECT public: RoutesDialog(QWidget *parent); - QString route(); + std::string route(); protected: - void parseDeviceList(const QString &json, bool success, int error_code); - void parseRouteList(const QString &json, bool success, int error_code); + void parseDeviceList(const std::string &json, bool success, int error_code); + void parseRouteList(const std::string &json, bool success, int error_code); void fetchRoutes(); QComboBox *device_list_; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 56e4bfa084..3524c9e600 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -148,7 +148,7 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { RoutesDialog route_dlg(this); if (route_dlg.exec()) { - route_edit->setText(route_dlg.route()); + route_edit->setText(QString::fromStdString(route_dlg.route())); } }); } From 131e473f37f3f2762469f98ef11d82a238e42f6a Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:36:26 -0700 Subject: [PATCH 08/34] cabana: move stream open widgets into streamselector (#38715) --- .../tools/cabana/streams/abstractstream.h | 10 - .../tools/cabana/streams/devicestream.cc | 34 +-- openpilot/tools/cabana/streams/devicestream.h | 12 - openpilot/tools/cabana/streams/pandastream.cc | 116 -------- openpilot/tools/cabana/streams/pandastream.h | 22 -- .../tools/cabana/streams/replaystream.cc | 68 ----- openpilot/tools/cabana/streams/replaystream.h | 14 - .../tools/cabana/streams/socketcanstream.cc | 55 ---- .../tools/cabana/streams/socketcanstream.h | 16 -- openpilot/tools/cabana/streamselector.cc | 261 +++++++++++++++++- openpilot/tools/cabana/streamselector.h | 75 +++++ 11 files changed, 335 insertions(+), 348 deletions(-) diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 17d212041a..4f7c0dc3e7 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -101,16 +101,6 @@ private: std::unordered_map> masks_; }; -class AbstractOpenStreamWidget : public QWidget { - Q_OBJECT -public: - AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} - virtual AbstractStream *open() = 0; - -signals: - void enableOpenButton(bool); -}; - class DummyStream : public AbstractStream { public: std::string routeName() const override { return "No Stream"; } diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3b4b54707c..6d580cc912 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -14,12 +14,8 @@ #include "openpilot/cereal/services.h" -#include -#include +#include #include -#include - -#include "tools/cabana/utils/util.h" // DeviceStream @@ -118,31 +114,3 @@ void DeviceStream::streamThread() { handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); } } - -// OpenDeviceWidget - -OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QRadioButton *msgq = new QRadioButton(tr("MSGQ")); - QRadioButton *zmq = new QRadioButton(tr("ZMQ")); - ip_address = new QLineEdit(this); - ip_address->setPlaceholderText(tr("Enter device Ip Address")); - ip_address->setValidator(new IpAddressValidator(this)); - - group = new QButtonGroup(this); - group->addButton(msgq, 0); - group->addButton(zmq, 1); - - QFormLayout *form_layout = new QFormLayout(this); - form_layout->addRow(msgq); - form_layout->addRow(zmq, ip_address); - QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { - ip_address->setEnabled(button == zmq && checked); - }); - zmq->setChecked(true); -} - -AbstractStream *OpenDeviceWidget::open() { - QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); - bool msgq = group->checkedId() == 0; - return new DeviceStream(msgq ? "" : ip); -} diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 1aa8ce9bff..71205e5a80 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -19,15 +19,3 @@ protected: pid_t bridge_pid = -1; const QString zmq_address; }; - -class OpenDeviceWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenDeviceWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *ip_address; - QButtonGroup *group; -}; diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 06c8e6e19c..0e72443c7a 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -4,12 +4,6 @@ #include #include -#include -#include -#include -#include -#include - PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) { if (!connect()) { throw std::runtime_error("Failed to connect to panda"); @@ -77,113 +71,3 @@ void PandaStream::streamThread() { panda->send_heartbeat(false); } } - -// OpenPandaWidget - -OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - form_layout = new QFormLayout(this); - if (can && dynamic_cast(can) != nullptr) { - form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName())))); - form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); - QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); - return; - } - - QHBoxLayout *serial_layout = new QHBoxLayout(); - serial_layout->addWidget(serial_edit = new QComboBox()); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - serial_layout->addWidget(refresh); - form_layout->addRow(tr("Serial"), serial_layout); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials); - QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm); - - // Populate serials - refreshSerials(); - buildConfigForm(); -} - -void OpenPandaWidget::refreshSerials() { - serial_edit->clear(); - for (auto serial : Panda::list()) { - serial_edit->addItem(QString::fromStdString(serial)); - } -} - -void OpenPandaWidget::buildConfigForm() { - for (int i = form_layout->rowCount() - 1; i > 0; --i) { - form_layout->removeRow(i); - } - - QString serial = serial_edit->currentText(); - bool has_fd = false; - bool has_panda = !serial.isEmpty(); - if (has_panda) { - try { - Panda panda(serial.toStdString()); - has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); - } catch (const std::exception& e) { - fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); - has_panda = false; - } - } - - if (has_panda) { - config.serial = serial.toStdString(); - config.bus_config.resize(3); - for (int i = 0; i < config.bus_config.size(); i++) { - QHBoxLayout *bus_layout = new QHBoxLayout; - - // CAN Speed - bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):"))); - QComboBox *can_speed = new QComboBox; - for (int j = 0; j < std::size(speeds); j++) { - can_speed->addItem(QString::number(speeds[j])); - - if (data_speeds[j] == config.bus_config[i].can_speed_kbps) { - can_speed->setCurrentIndex(j); - } - } - QObject::connect(can_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];}); - bus_layout->addWidget(can_speed); - - // CAN-FD Speed - if (has_fd) { - QCheckBox *enable_fd = new QCheckBox("CAN-FD"); - bus_layout->addWidget(enable_fd); - bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):"))); - QComboBox *data_speed = new QComboBox; - for (int j = 0; j < std::size(data_speeds); j++) { - data_speed->addItem(QString::number(data_speeds[j])); - - if (data_speeds[j] == config.bus_config[i].data_speed_kbps) { - data_speed->setCurrentIndex(j); - } - } - - data_speed->setEnabled(false); - bus_layout->addWidget(data_speed); - - QObject::connect(data_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];}); - QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled); - QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;}); - } - - form_layout->addRow(tr("Bus %1:").arg(i), bus_layout); - } - } else { - config.serial = ""; - form_layout->addWidget(new QLabel(tr("No panda found"))); - } -} - -AbstractStream *OpenPandaWidget::open() { - try { - return new PandaStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); - return nullptr; - } -} diff --git a/openpilot/tools/cabana/streams/pandastream.h b/openpilot/tools/cabana/streams/pandastream.h index d764613fe1..760b5c7b15 100644 --- a/openpilot/tools/cabana/streams/pandastream.h +++ b/openpilot/tools/cabana/streams/pandastream.h @@ -3,15 +3,9 @@ #include #include -#include -#include - #include "tools/cabana/streams/livestream.h" #include "tools/cabana/panda.h" -const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; -const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; - struct BusConfig { int can_speed_kbps = 500; int data_speed_kbps = 2000; @@ -38,19 +32,3 @@ protected: std::unique_ptr panda; PandaStreamConfig config = {}; }; - -class OpenPandaWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenPandaWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshSerials(); - void buildConfigForm(); - - QComboBox *serial_edit; - QFormLayout *form_layout; - PandaStreamConfig config = {}; -}; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 3524c9e600..e9df400599 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,16 +1,9 @@ #include "tools/cabana/streams/replaystream.h" -#include - -#include -#include -#include #include -#include #include "common/timing.h" #include "common/util.h" -#include "tools/cabana/routesdialog.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); @@ -116,64 +109,3 @@ void ReplayStream::pause(bool pause) { replay->pause(pause); pause ? paused() : resume(); } - - -// OpenReplayWidget - -OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QGridLayout *grid_layout = new QGridLayout(this); - grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); - grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); - route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route")); - auto browse_remote_btn = new QPushButton(tr("Remote route..."), this); - grid_layout->addWidget(browse_remote_btn, 0, 2); - auto browse_local_btn = new QPushButton(tr("Local route..."), this); - grid_layout->addWidget(browse_local_btn, 0, 3); - - QHBoxLayout *camera_layout = new QHBoxLayout(); - for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) - camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); - cameras[0]->setChecked(true); - camera_layout->addStretch(1); - grid_layout->addItem(camera_layout, 1, 1); - - setMinimumWidth(550); - QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); - if (!dir.isEmpty()) { - route_edit->setText(dir); - settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); - } - }); - QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { - RoutesDialog route_dlg(this); - if (route_dlg.exec()) { - route_edit->setText(QString::fromStdString(route_dlg.route())); - } - }); -} - -AbstractStream *OpenReplayWidget::open() { - QString route = route_edit->text(); - QString data_dir; - if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { - data_dir = route.mid(0, idx + 1); - route = route.mid(idx + 1); - } - - bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0; - if (!is_valid_format) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); - } else { - auto replay_stream = std::make_unique(); - uint32_t flags = REPLAY_FLAG_NONE; - if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; - if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; - if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; - - if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { - return replay_stream.release(); - } - } - return nullptr; -} diff --git a/openpilot/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h index 6f49502ba3..aad0ac6361 100644 --- a/openpilot/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -1,10 +1,8 @@ #pragma once -#include #include #include #include -#include #include "common/prefix.h" #include "tools/cabana/streams/abstractstream.h" @@ -43,15 +41,3 @@ private: std::set processed_segments; std::unique_ptr op_prefix; }; - -class OpenReplayWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenReplayWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *route_edit; - std::vector cameras; -}; diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index b10f2cab90..f16ed36d9d 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -8,13 +8,6 @@ #include #include -#include -#include - -#include -#include -#include -#include SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) { if (!available()) { @@ -98,51 +91,3 @@ void SocketCanStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); } } - -OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addStretch(1); - - QFormLayout *form_layout = new QFormLayout(); - - QHBoxLayout *device_layout = new QHBoxLayout(); - device_edit = new QComboBox(); - device_edit->setFixedWidth(300); - device_layout->addWidget(device_edit); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setFixedWidth(100); - device_layout->addWidget(refresh); - form_layout->addRow(tr("Device"), device_layout); - main_layout->addLayout(form_layout); - - main_layout->addStretch(1); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); - QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); }); - - // Populate devices - refreshDevices(); -} - -void OpenSocketCanWidget::refreshDevices() { - device_edit->clear(); - // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) - std::error_code ec; - for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { - std::ifstream type_file(entry.path() / "type"); - int type = 0; - if (type_file >> type && type == 280) { - device_edit->addItem(QString::fromStdString(entry.path().filename().string())); - } - } -} - -AbstractStream *OpenSocketCanWidget::open() { - try { - return new SocketCanStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); - return nullptr; - } -} diff --git a/openpilot/tools/cabana/streams/socketcanstream.h b/openpilot/tools/cabana/streams/socketcanstream.h index 78e856d506..c44a498221 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.h +++ b/openpilot/tools/cabana/streams/socketcanstream.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "tools/cabana/streams/livestream.h" struct SocketCanStreamConfig { @@ -25,17 +23,3 @@ protected: SocketCanStreamConfig config = {}; int sock_fd = -1; }; - -class OpenSocketCanWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenSocketCanWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshDevices(); - - QComboBox *device_edit; - SocketCanStreamConfig config = {}; -}; diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 7e8adc568d..dae022be75 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -1,18 +1,275 @@ #include "tools/cabana/streamselector.h" #include +#include #include +#include #include +#include #include +#include +#include #include "tools/cabana/streams/devicestream.h" -#include "tools/cabana/streams/pandastream.h" #include "tools/cabana/streams/replaystream.h" +#include "tools/cabana/routesdialog.h" + +// OpenReplayWidget + +OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QGridLayout *grid_layout = new QGridLayout(this); + grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); + grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); + route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route")); + auto browse_remote_btn = new QPushButton(tr("Remote route..."), this); + grid_layout->addWidget(browse_remote_btn, 0, 2); + auto browse_local_btn = new QPushButton(tr("Local route..."), this); + grid_layout->addWidget(browse_local_btn, 0, 3); + + QHBoxLayout *camera_layout = new QHBoxLayout(); + for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) + camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); + cameras[0]->setChecked(true); + camera_layout->addStretch(1); + grid_layout->addItem(camera_layout, 1, 1); + + setMinimumWidth(550); + QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); + if (!dir.isEmpty()) { + route_edit->setText(dir); + settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); + } + }); + QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { + RoutesDialog route_dlg(this); + if (route_dlg.exec()) { + route_edit->setText(QString::fromStdString(route_dlg.route())); + } + }); +} + +AbstractStream *OpenReplayWidget::open() { + QString route = route_edit->text(); + QString data_dir; + if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { + data_dir = route.mid(0, idx + 1); + route = route.mid(idx + 1); + } + + bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0; + if (!is_valid_format) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); + } else { + auto replay_stream = std::make_unique(); + uint32_t flags = REPLAY_FLAG_NONE; + if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; + if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; + if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; + + if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { + return replay_stream.release(); + } + } + return nullptr; +} + +// OpenPandaWidget + +static const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; +static const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; + +OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + form_layout = new QFormLayout(this); + if (can && dynamic_cast(can) != nullptr) { + form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName())))); + form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); + QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); + return; + } + + QHBoxLayout *serial_layout = new QHBoxLayout(); + serial_layout->addWidget(serial_edit = new QComboBox()); + + QPushButton *refresh = new QPushButton(tr("Refresh")); + refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + serial_layout->addWidget(refresh); + form_layout->addRow(tr("Serial"), serial_layout); + + QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials); + QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm); + + // Populate serials + refreshSerials(); + buildConfigForm(); +} + +void OpenPandaWidget::refreshSerials() { + serial_edit->clear(); + for (auto serial : Panda::list()) { + serial_edit->addItem(QString::fromStdString(serial)); + } +} + +void OpenPandaWidget::buildConfigForm() { + for (int i = form_layout->rowCount() - 1; i > 0; --i) { + form_layout->removeRow(i); + } + + QString serial = serial_edit->currentText(); + bool has_fd = false; + bool has_panda = !serial.isEmpty(); + if (has_panda) { + try { + Panda panda(serial.toStdString()); + has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); + } catch (const std::exception& e) { + fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); + has_panda = false; + } + } + + if (has_panda) { + config.serial = serial.toStdString(); + config.bus_config.resize(3); + for (int i = 0; i < config.bus_config.size(); i++) { + QHBoxLayout *bus_layout = new QHBoxLayout; + + // CAN Speed + bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):"))); + QComboBox *can_speed = new QComboBox; + for (int j = 0; j < std::size(speeds); j++) { + can_speed->addItem(QString::number(speeds[j])); + + if (data_speeds[j] == config.bus_config[i].can_speed_kbps) { + can_speed->setCurrentIndex(j); + } + } + QObject::connect(can_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];}); + bus_layout->addWidget(can_speed); + + // CAN-FD Speed + if (has_fd) { + QCheckBox *enable_fd = new QCheckBox("CAN-FD"); + bus_layout->addWidget(enable_fd); + bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):"))); + QComboBox *data_speed = new QComboBox; + for (int j = 0; j < std::size(data_speeds); j++) { + data_speed->addItem(QString::number(data_speeds[j])); + + if (data_speeds[j] == config.bus_config[i].data_speed_kbps) { + data_speed->setCurrentIndex(j); + } + } + + data_speed->setEnabled(false); + bus_layout->addWidget(data_speed); + + QObject::connect(data_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];}); + QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled); + QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;}); + } + + form_layout->addRow(tr("Bus %1:").arg(i), bus_layout); + } + } else { + config.serial = ""; + form_layout->addWidget(new QLabel(tr("No panda found"))); + } +} + +AbstractStream *OpenPandaWidget::open() { + try { + return new PandaStream(config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); + return nullptr; + } +} + +// OpenDeviceWidget + +OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QRadioButton *msgq = new QRadioButton(tr("MSGQ")); + QRadioButton *zmq = new QRadioButton(tr("ZMQ")); + ip_address = new QLineEdit(this); + ip_address->setPlaceholderText(tr("Enter device Ip Address")); + ip_address->setValidator(new IpAddressValidator(this)); + + group = new QButtonGroup(this); + group->addButton(msgq, 0); + group->addButton(zmq, 1); + + QFormLayout *form_layout = new QFormLayout(this); + form_layout->addRow(msgq); + form_layout->addRow(zmq, ip_address); + QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { + ip_address->setEnabled(button == zmq && checked); + }); + zmq->setChecked(true); +} + +AbstractStream *OpenDeviceWidget::open() { + QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + bool msgq = group->checkedId() == 0; + return new DeviceStream(msgq ? "" : ip); +} + #ifdef __linux__ -#include "tools/cabana/streams/socketcanstream.h" +// OpenSocketCanWidget + +OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addStretch(1); + + QFormLayout *form_layout = new QFormLayout(); + + QHBoxLayout *device_layout = new QHBoxLayout(); + device_edit = new QComboBox(); + device_edit->setFixedWidth(300); + device_layout->addWidget(device_edit); + + QPushButton *refresh = new QPushButton(tr("Refresh")); + refresh->setFixedWidth(100); + device_layout->addWidget(refresh); + form_layout->addRow(tr("Device"), device_layout); + main_layout->addLayout(form_layout); + + main_layout->addStretch(1); + + QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); + QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); }); + + // Populate devices + refreshDevices(); +} + +void OpenSocketCanWidget::refreshDevices() { + device_edit->clear(); + // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { + std::ifstream type_file(entry.path() / "type"); + int type = 0; + if (type_file >> type && type == 280) { + device_edit->addItem(QString::fromStdString(entry.path().filename().string())); + } + } +} + +AbstractStream *OpenSocketCanWidget::open() { + try { + return new SocketCanStream(config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); + return nullptr; + } +} #endif +// StreamSelector + StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Open stream")); QVBoxLayout *layout = new QVBoxLayout(this); diff --git a/openpilot/tools/cabana/streamselector.h b/openpilot/tools/cabana/streamselector.h index 0919195e4e..1210806a3b 100644 --- a/openpilot/tools/cabana/streamselector.h +++ b/openpilot/tools/cabana/streamselector.h @@ -1,11 +1,86 @@ #pragma once +#include + +#include +#include +#include #include #include +#include #include #include #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/streams/pandastream.h" +#ifdef __linux__ +#include "tools/cabana/streams/socketcanstream.h" +#endif + +class AbstractOpenStreamWidget : public QWidget { + Q_OBJECT +public: + AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} + virtual AbstractStream *open() = 0; + +signals: + void enableOpenButton(bool); +}; + +class OpenReplayWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenReplayWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + QLineEdit *route_edit; + std::vector cameras; +}; + +class OpenPandaWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenPandaWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + void refreshSerials(); + void buildConfigForm(); + + QComboBox *serial_edit; + QFormLayout *form_layout; + PandaStreamConfig config = {}; +}; + +class OpenDeviceWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenDeviceWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + QLineEdit *ip_address; + QButtonGroup *group; +}; + +#ifdef __linux__ +// no Q_OBJECT: moc does not define __linux__ and would otherwise skip this class +class OpenSocketCanWidget : public AbstractOpenStreamWidget { +public: + OpenSocketCanWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + void refreshDevices(); + + QComboBox *device_edit; + SocketCanStreamConfig config = {}; +}; +#endif class StreamSelector : public QDialog { Q_OBJECT From 0f9c753e6ec5c7af9ea37f9020ab3cc66bed5db1 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:46:45 -0700 Subject: [PATCH 09/34] cabana: remove Qt from livestream (#38722) --- openpilot/tools/cabana/streams/livestream.cc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 05a6d44c40..71b5d9a1ec 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -21,12 +21,9 @@ struct LiveStream::Logger { localtime_r(&start_time, &local_time); std::ostringstream date; date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S"); - QString dir = QString("%1/%2--%3") - .arg(QString::fromStdString(settings.log_path)) - .arg(QString::fromStdString(date.str())) - .arg(n); - util::create_directories(dir.toStdString(), 0755); - fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); + std::string dir = settings.log_path + "/" + date.str() + "--" + std::to_string(n); + util::create_directories(dir, 0755); + fs.reset(new std::ofstream(dir + "/rlog", std::ios::binary | std::ios::out)); } auto bytes = data.asBytes(); From 6e0f4f46306d6c569d6a52a4bbbea6537354d898 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:55:28 -0700 Subject: [PATCH 10/34] cabana: split SettingsDialog out of settings (#38719) cabana: split SettingsDialog out of settings.{h,cc} --- openpilot/tools/cabana/SConscript | 2 +- openpilot/tools/cabana/mainwin.cc | 3 +- openpilot/tools/cabana/settings.cc | 89 ---------------------- openpilot/tools/cabana/settings.h | 20 ----- openpilot/tools/cabana/settingsdialog.cc | 94 ++++++++++++++++++++++++ openpilot/tools/cabana/settingsdialog.h | 21 ++++++ openpilot/tools/cabana/utils/util.h | 5 ++ 7 files changed, 123 insertions(+), 111 deletions(-) create mode 100644 openpilot/tools/cabana/settingsdialog.cc create mode 100644 openpilot/tools/cabana/settingsdialog.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 662b3952bb..75e67d99e3 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -102,7 +102,7 @@ cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc' 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] if arch != "Darwin": cabana_srcs += ['streams/socketcanstream.cc'] diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index bcfe1dde41..3ae5896a2d 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -19,6 +19,7 @@ #include "json11/json11.hpp" #include "tools/cabana/commands.h" +#include "tools/cabana/settingsdialog.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" @@ -617,7 +618,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { } void MainWindow::setOption() { - SettingsDlg dlg(this); + SettingsDialog dlg(this); dlg.exec(); } diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index cbeded0d10..bacbbd5330 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -24,19 +24,11 @@ #include #endif -#include -#include -#include -#include -#include #include #include "json11/json11.hpp" #include "tools/cabana/utils/util.h" -const int MIN_CACHE_MINIUTES = 30; -const int MAX_CACHE_MINIUTES = 120; - Settings settings; namespace { @@ -535,84 +527,3 @@ void Settings::save() { settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); }); saveSettings(stored_settings.values); } - -// SettingsDlg - -SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Settings")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - QGroupBox *groupbox = new QGroupBox("General"); - QFormLayout *form_layout = new QFormLayout(groupbox); - - form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); - theme->setToolTip(tr("You may need to restart cabana after changes theme")); - theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); - theme->setCurrentIndex(settings.theme); - - form_layout->addRow("FPS", fps = new QSpinBox(this)); - fps->setRange(10, 100); - fps->setSingleStep(10); - fps->setValue(settings.fps); - - form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); - cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); - cached_minutes->setSingleStep(1); - cached_minutes->setValue(settings.max_cached_minutes); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("New Signal Settings"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); - drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); - drag_direction->setCurrentIndex(settings.drag_direction); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("Chart"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); - chart_height->setRange(100, 500); - chart_height->setSingleStep(10); - chart_height->setValue(settings.chart_height); - main_layout->addWidget(groupbox); - - log_livestream = new QGroupBox(tr("Enable live stream logging"), this); - log_livestream->setCheckable(true); - log_livestream->setChecked(settings.log_livestream); - QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); - path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); - log_path->setReadOnly(true); - auto browse_btn = new QPushButton(tr("B&rowse...")); - path_layout->addWidget(browse_btn); - main_layout->addWidget(log_livestream); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); - setFixedSize(400, sizeHint().height()); - - QObject::connect(browse_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getExistingDirectory( - this, tr("Log File Location"), - QString::fromStdString(utils::homePath()), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (!fn.isEmpty()) { - log_path->setText(fn); - } - }); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); -} - -void SettingsDlg::save() { - if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { - // set theme before emit changed - utils::setTheme(settings.theme); - } - settings.fps = fps->value(); - settings.max_cached_minutes = cached_minutes->value(); - settings.chart_height = chart_height->value(); - settings.log_livestream = log_livestream->isChecked(); - settings.log_path = log_path->text().toStdString(); - settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - settings.changed(); - QDialog::accept(); -} diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 2c21fab419..52353dc667 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -3,12 +3,6 @@ #include #include -#include -#include -#include -#include -#include - #include "tools/cabana/core/observable.h" #include "tools/cabana/core/settings.h" @@ -26,18 +20,4 @@ public: Observable<> changed; }; -class SettingsDlg : public QDialog { -public: - SettingsDlg(QWidget *parent); - void save(); - QSpinBox *fps; - QSpinBox *cached_minutes; - QSpinBox *chart_height; - QComboBox *chart_series_type; - QComboBox *theme; - QGroupBox *log_livestream; - QLineEdit *log_path; - QComboBox *drag_direction; -}; - extern Settings settings; diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc new file mode 100644 index 0000000000..3a49de5746 --- /dev/null +++ b/openpilot/tools/cabana/settingsdialog.cc @@ -0,0 +1,94 @@ +#include "tools/cabana/settingsdialog.h" + +#include + +#include +#include +#include +#include +#include + +#include "tools/cabana/settings.h" +#include "tools/cabana/utils/util.h" + +const int MIN_CACHE_MINIUTES = 30; +const int MAX_CACHE_MINIUTES = 120; + +SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Settings")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGroupBox *groupbox = new QGroupBox("General"); + QFormLayout *form_layout = new QFormLayout(groupbox); + + form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); + theme->setToolTip(tr("You may need to restart cabana after changes theme")); + theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); + theme->setCurrentIndex(settings.theme); + + form_layout->addRow("FPS", fps = new QSpinBox(this)); + fps->setRange(10, 100); + fps->setSingleStep(10); + fps->setValue(settings.fps); + + form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); + cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); + cached_minutes->setSingleStep(1); + cached_minutes->setValue(settings.max_cached_minutes); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("New Signal Settings"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); + drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); + drag_direction->setCurrentIndex(settings.drag_direction); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("Chart"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); + chart_height->setRange(100, 500); + chart_height->setSingleStep(10); + chart_height->setValue(settings.chart_height); + main_layout->addWidget(groupbox); + + log_livestream = new QGroupBox(tr("Enable live stream logging"), this); + log_livestream->setCheckable(true); + log_livestream->setChecked(settings.log_livestream); + QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); + path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); + log_path->setReadOnly(true); + auto browse_btn = new QPushButton(tr("B&rowse...")); + path_layout->addWidget(browse_btn); + main_layout->addWidget(log_livestream); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + setFixedSize(400, sizeHint().height()); + + QObject::connect(browse_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getExistingDirectory( + this, tr("Log File Location"), + QString::fromStdString(utils::homePath()), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!fn.isEmpty()) { + log_path->setText(fn); + } + }); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::save); +} + +void SettingsDialog::save() { + if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { + // set theme before emit changed + utils::setTheme(settings.theme); + } + settings.fps = fps->value(); + settings.max_cached_minutes = cached_minutes->value(); + settings.chart_height = chart_height->value(); + settings.log_livestream = log_livestream->isChecked(); + settings.log_path = log_path->text().toStdString(); + settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); + settings.changed(); + QDialog::accept(); +} diff --git a/openpilot/tools/cabana/settingsdialog.h b/openpilot/tools/cabana/settingsdialog.h new file mode 100644 index 0000000000..c5c723c1cb --- /dev/null +++ b/openpilot/tools/cabana/settingsdialog.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include +#include + +class SettingsDialog : public QDialog { +public: + SettingsDialog(QWidget *parent); + void save(); + QSpinBox *fps; + QSpinBox *cached_minutes; + QSpinBox *chart_height; + QComboBox *chart_series_type; + QComboBox *theme; + QGroupBox *log_livestream; + QLineEdit *log_path; + QComboBox *drag_direction; +}; diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index f2435143c1..77a634fc9e 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -12,9 +12,14 @@ #include #include +#include +#include #include #include +#include +#include #include +#include #include #include #include From 46f612224cf6953a845ddc6ed7aadaa9f2128e6b Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:57:47 -0700 Subject: [PATCH 11/34] cabana: split comma API route fetching out of RoutesDialog (#38721) --- openpilot/tools/cabana/SConscript | 5 +- openpilot/tools/cabana/routes.cc | 115 ++++++++++++++++++ openpilot/tools/cabana/routes.h | 42 +++++++ openpilot/tools/cabana/routesdialog.cc | 126 +++----------------- openpilot/tools/cabana/routesdialog.h | 7 +- openpilot/tools/cabana/tests/test_cabana.cc | 62 ++++++++++ 6 files changed, 246 insertions(+), 111 deletions(-) create mode 100644 openpilot/tools/cabana/routes.cc create mode 100644 openpilot/tools/cabana/routes.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 75e67d99e3..f7db3f23b7 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', @@ -120,8 +120,9 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), + dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), ] - dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) + dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, diff --git a/openpilot/tools/cabana/routes.cc b/openpilot/tools/cabana/routes.cc new file mode 100644 index 0000000000..50a1eacf8d --- /dev/null +++ b/openpilot/tools/cabana/routes.cc @@ -0,0 +1,115 @@ +#include "tools/cabana/routes.h" + +#include +#include +#include +#include + +#include "json11/json11.hpp" +#include "tools/replay/py_downloader.h" + +namespace routes { + +std::pair checkApiResponse(const std::string &result) { + if (result.empty()) return {false, 500}; + std::string err; + auto doc = json11::Json::parse(result, err); + if (!err.empty()) return {false, 500}; + if (doc.is_object() && doc["error"].is_string()) { + return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; + } + return {true, 0}; +} + +int64_t nowUnixMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +int64_t parseIsoToUnixMs(const std::string &s) { + std::string bytes = s; + if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); + int millis = 0; + auto dot = bytes.find('.'); + if (dot != std::string::npos) { + std::string frac = bytes.substr(dot + 1); + bytes = bytes.substr(0, dot); + while (frac.size() < 3) frac.push_back('0'); + millis = std::atoi(frac.substr(0, 3).c_str()); + } + std::tm tm{}; + const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); + if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); + if (!ret) return 0; + time_t secs = timegm(&tm); + if (secs == static_cast(-1)) return 0; + return static_cast(secs) * 1000 + millis; +} + +std::string formatUnixMs(int64_t ms) { + time_t secs = static_cast(ms / 1000); + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + return buf; +} + +std::vector parseDevices(const std::string &json) { + std::vector devices; + std::string err; + auto doc = json11::Json::parse(json, err); + if (err.empty() && doc.is_array()) { + for (const auto &device : doc.array_items()) { + devices.push_back({device["dongle_id"].string_value()}); + } + } + return devices; +} + +std::vector parseRoutes(const std::string &json, bool preserved) { + std::vector items; + std::string err; + auto doc = json11::Json::parse(json, err); + if (err.empty() && doc.is_array()) { + for (const auto &route : doc.array_items()) { + RouteInfo info; + info.name = route["fullname"].string_value(); + if (preserved) { + info.start_ms = parseIsoToUnixMs(route["start_time"].string_value()); + info.end_ms = parseIsoToUnixMs(route["end_time"].string_value()); + } else { + info.start_ms = static_cast(route["start_time_utc_millis"].number_value()); + info.end_ms = static_cast(route["end_time_utc_millis"].number_value()); + } + items.push_back(std::move(info)); + } + } + return items; +} + +void fetchDevices(DevicesCallback callback) { + std::thread([callback = std::move(callback)]() { + std::string result = PyDownloader::getDevices(); + auto [success, error_code] = checkApiResponse(result); + callback(success ? parseDevices(result) : std::vector{}, success, error_code); + }).detach(); +} + +void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback) { + const bool preserved = period_days == -1; + int64_t start_ms = 0, end_ms = 0; + if (!preserved) { + end_ms = nowUnixMs(); + start_ms = end_ms - static_cast(period_days) * 24LL * 60LL * 60LL * 1000LL; + } + + std::thread([dongle_id, start_ms, end_ms, preserved, callback = std::move(callback)]() { + std::string result = PyDownloader::getDeviceRoutes(dongle_id, start_ms, end_ms, preserved); + auto [success, error_code] = checkApiResponse(result); + callback(success ? parseRoutes(result, preserved) : std::vector{}, success, error_code); + }).detach(); +} + +} // namespace routes diff --git a/openpilot/tools/cabana/routes.h b/openpilot/tools/cabana/routes.h new file mode 100644 index 0000000000..ef6fc8dee0 --- /dev/null +++ b/openpilot/tools/cabana/routes.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace routes { + +struct DeviceInfo { + std::string dongle_id; +}; + +struct RouteInfo { + std::string name; + int64_t start_ms = 0; + int64_t end_ms = 0; +}; + +using DevicesCallback = std::function devices, bool success, int error_code)>; +using RoutesCallback = std::function routes, bool success, int error_code)>; + +// Parse a PyDownloader JSON response into (success, error_code). +std::pair checkApiResponse(const std::string &result); + +int64_t nowUnixMs(); +// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. +int64_t parseIsoToUnixMs(const std::string &s); +// Local time, "%Y-%m-%d %H:%M:%S". +std::string formatUnixMs(int64_t ms); + +std::vector parseDevices(const std::string &json); +// preserved routes report ISO-8601 timestamps instead of unix millis +std::vector parseRoutes(const std::string &json, bool preserved); + +// Both fetch on a detached thread and invoke the callback from that thread. +void fetchDevices(DevicesCallback callback); +// period_days of -1 requests preserved routes +void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback); + +} // namespace routes diff --git a/openpilot/tools/cabana/routesdialog.cc b/openpilot/tools/cabana/routesdialog.cc index 9891104c9f..33b773edb9 100644 --- a/openpilot/tools/cabana/routesdialog.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -1,9 +1,6 @@ #include "tools/cabana/routesdialog.h" -#include -#include #include -#include #include #include @@ -12,62 +9,7 @@ #include #include -#include "json11/json11.hpp" #include "tools/cabana/utils/util.h" -#include "tools/replay/py_downloader.h" - -namespace { - -// Parse a PyDownloader JSON response into (success, error_code). -std::pair checkApiResponse(const std::string &result) { - if (result.empty()) return {false, 500}; - std::string err; - auto doc = json11::Json::parse(result, err); - if (!err.empty()) return {false, 500}; - if (doc.is_object() && doc["error"].is_string()) { - return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; - } - return {true, 0}; -} - -int64_t nowUnixMs() { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} - -// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. -int64_t parseIsoToUnixMs(const std::string &s) { - std::string bytes = s; - if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); - int millis = 0; - auto dot = bytes.find('.'); - if (dot != std::string::npos) { - std::string frac = bytes.substr(dot + 1); - bytes = bytes.substr(0, dot); - while (frac.size() < 3) frac.push_back('0'); - millis = std::atoi(frac.substr(0, 3).c_str()); - } - std::tm tm{}; - const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); - if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); - if (!ret) return 0; - tm.tm_isdst = -1; - time_t secs = timegm(&tm); - if (secs == static_cast(-1)) return 0; - return static_cast(secs) * 1000 + millis; -} - -std::string formatUnixMs(int64_t ms) { - time_t secs = static_cast(ms / 1000); - std::tm tm{}; - localtime_r(&secs, &tm); - char buf[64]; - std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - return buf; -} - -} // namespace // The RouteListWidget class extends QListWidget to display a custom message when empty class RouteListWidget : public QListWidget { @@ -110,26 +52,19 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - // Fetch devices - std::thread([this, alive = std::weak_ptr(alive_)]() { - std::string result = PyDownloader::getDevices(); - auto response = checkApiResponse(result); - utils::runOnMainThread([this, alive, r = std::move(result), response]() { - if (!alive.expired()) parseDeviceList(r, response.first, response.second); + routes::fetchDevices([this, alive = std::weak_ptr(alive_)](std::vector devices, bool success, int error_code) { + utils::runOnMainThread([this, alive, devices = std::move(devices), success, error_code]() { + if (!alive.expired()) setDeviceList(devices, success, error_code); }); - }).detach(); + }); } -void RoutesDialog::parseDeviceList(const std::string &json, bool success, int error_code) { +void RoutesDialog::setDeviceList(const std::vector &devices, bool success, int error_code) { if (success) { device_list_->clear(); - std::string err; - auto doc = json11::Json::parse(json, err); - if (err.empty() && doc.is_array()) { - for (const auto &device : doc.array_items()) { - QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); - device_list_->addItem(dongle_id, dongle_id); - } + for (const auto &device : devices) { + QString dongle_id = QString::fromStdString(device.dongle_id); + device_list_->addItem(dongle_id, dongle_id); } } else { QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with openpilot/tools/lib/auth.py") : tr("Network error")); @@ -144,45 +79,22 @@ void RoutesDialog::fetchRoutes() { route_list_->clear(); route_list_->setEmptyText(tr("Loading...")); - std::string did = device_list_->currentText().toStdString(); - int period = period_selector_->currentData().toInt(); - - bool preserved = (period == -1); - int64_t start_ms = 0, end_ms = 0; - if (!preserved) { - end_ms = nowUnixMs(); - start_ms = end_ms - static_cast(period) * 24LL * 60LL * 60LL * 1000LL; - } - int request_id = ++fetch_id_; - std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { - std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - auto response = checkApiResponse(result); - utils::runOnMainThread([this, alive, r = std::move(result), response, request_id]() { - if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); + auto on_routes = [this, alive = std::weak_ptr(alive_), request_id](std::vector list, bool success, int) { + utils::runOnMainThread([this, alive, list = std::move(list), success, request_id]() { + if (!alive.expired() && fetch_id_ == request_id) setRouteList(list, success); }); - }).detach(); + }; + routes::fetchRoutes(device_list_->currentText().toStdString(), period_selector_->currentData().toInt(), std::move(on_routes)); } -void RoutesDialog::parseRouteList(const std::string &json, bool success, int error_code) { +void RoutesDialog::setRouteList(const std::vector &list, bool success) { if (success) { - std::string err; - auto doc = json11::Json::parse(json, err); - if (err.empty() && doc.is_array()) { - for (const auto &route : doc.array_items()) { - int64_t from_ms = 0, to_ms = 0; - if (period_selector_->currentData().toInt() == -1) { - from_ms = parseIsoToUnixMs(route["start_time"].string_value()); - to_ms = parseIsoToUnixMs(route["end_time"].string_value()); - } else { - from_ms = static_cast(route["start_time_utc_millis"].number_value()); - to_ms = static_cast(route["end_time_utc_millis"].number_value()); - } - const int mins = static_cast((to_ms - from_ms) / 60000); - auto item = new QListWidgetItem(QString::fromStdString(formatUnixMs(from_ms) + " " + std::to_string(mins) + "min")); - item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); - route_list_->addItem(item); - } + for (const auto &route : list) { + const int mins = static_cast((route.end_ms - route.start_ms) / 60000); + auto item = new QListWidgetItem(QString::fromStdString(routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min")); + item->setData(Qt::UserRole, QString::fromStdString(route.name)); + route_list_->addItem(item); } if (route_list_->count() > 0) route_list_->setCurrentRow(0); } else { diff --git a/openpilot/tools/cabana/routesdialog.h b/openpilot/tools/cabana/routesdialog.h index 44d7f068f9..4983f1b558 100644 --- a/openpilot/tools/cabana/routesdialog.h +++ b/openpilot/tools/cabana/routesdialog.h @@ -2,10 +2,13 @@ #include #include +#include #include #include +#include "tools/cabana/routes.h" + class RouteListWidget; class RoutesDialog : public QDialog { @@ -15,8 +18,8 @@ public: std::string route(); protected: - void parseDeviceList(const std::string &json, bool success, int error_code); - void parseRouteList(const std::string &json, bool success, int error_code); + void setDeviceList(const std::vector &devices, bool success, int error_code); + void setRouteList(const std::vector &list, bool success); void fetchRoutes(); QComboBox *device_list_; diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index e3b8377e02..5c5fce9864 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -7,6 +7,7 @@ #include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/routes.h" #include "tools/cabana/utils/strings.h" const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -240,6 +241,64 @@ void test_signal_tooltip() { )"); } +void test_route_timestamps() { + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05Z") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02 03:04:05") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123Z") == 1704164645123); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.4Z") == 1704164645400); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123456Z") == 1704164645123); + REQUIRE(routes::parseIsoToUnixMs("") == 0); + REQUIRE(routes::parseIsoToUnixMs("not a timestamp") == 0); + + // formatUnixMs is local time + const char *tz = getenv("TZ"); + const std::string prev_tz = tz ? tz : ""; + setenv("TZ", "UTC", 1); + tzset(); + REQUIRE(routes::formatUnixMs(1704164645123) == "2024-01-02 03:04:05"); + if (tz) { + setenv("TZ", prev_tz.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); +} + +void test_route_api_response() { + REQUIRE(routes::checkApiResponse("") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse("not json") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse(R"({"error": "unauthorized"})") == std::make_pair(false, 401)); + REQUIRE(routes::checkApiResponse(R"({"error": "server error"})") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse("[]") == std::make_pair(true, 0)); + REQUIRE(routes::checkApiResponse(R"({"dongle_id": "aaaa"})") == std::make_pair(true, 0)); +} + +void test_route_json() { + auto devices = routes::parseDevices(R"([{"dongle_id": "aaaa"}, {"dongle_id": "bbbb"}])"); + REQUIRE(devices.size() == 2); + REQUIRE(devices[0].dongle_id == "aaaa"); + REQUIRE(devices[1].dongle_id == "bbbb"); + REQUIRE(routes::parseDevices("not json").empty()); + REQUIRE(routes::parseDevices(R"({"error": "unauthorized"})").empty()); + + auto list = routes::parseRoutes( + R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time_utc_millis": 1704164645000, "end_time_utc_millis": 1704165245000}])", false); + REQUIRE(list.size() == 1); + REQUIRE(list[0].name == "aaaa|2024-01-02--03-04-05"); + REQUIRE(list[0].start_ms == 1704164645000); + REQUIRE(list[0].end_ms == 1704165245000); + + // preserved routes report ISO-8601 timestamps + auto preserved = routes::parseRoutes( + R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time": "2024-01-02T03:04:05Z", "end_time": "2024-01-02T03:14:05Z"}])", true); + REQUIRE(preserved.size() == 1); + REQUIRE(preserved[0].start_ms == 1704164645000); + REQUIRE(preserved[0].end_ms == 1704165245000); + + REQUIRE(routes::parseRoutes("not json", false).empty()); +} + void test_cabana_core() { test_format_seconds(); test_to_hex(); @@ -251,6 +310,9 @@ void test_cabana_core() { test_parse_dbc(); test_parse_opendbc(); test_dbc_manager(); + test_route_timestamps(); + test_route_api_response(); + test_route_json(); } int main() { From 5419f57b3a63f581dde6b0485624f0a2f698d878 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:18:10 -0700 Subject: [PATCH 12/34] cabana: de-QT streams (#38718) --- openpilot/tools/cabana/cabana.cc | 3 ++- openpilot/tools/cabana/mainwin.cc | 3 +++ .../tools/cabana/streams/abstractstream.h | 1 + .../tools/cabana/streams/devicestream.cc | 26 +++++++------------ openpilot/tools/cabana/streams/devicestream.h | 7 ++--- .../tools/cabana/streams/replaystream.cc | 18 ++++++------- openpilot/tools/cabana/streamselector.cc | 5 +++- openpilot/tools/cabana/utils/util.cc | 22 ++++++++++++---- openpilot/tools/cabana/utils/util.h | 1 + 9 files changed, 50 insertions(+), 36 deletions(-) diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index e3a1850346..6cc6046bec 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -148,7 +148,7 @@ int main(int argc, char *argv[]) { if (args.msgq) { stream = new DeviceStream(); } else if (!args.zmq.empty()) { - stream = new DeviceStream(QString::fromStdString(args.zmq)); + stream = new DeviceStream(args.zmq); } else if (args.panda || !args.panda_serial.empty()) { try { stream = new PandaStream({.serial = args.panda_serial}); @@ -175,6 +175,7 @@ int main(int argc, char *argv[]) { } if (!route.isEmpty()) { auto replay_stream = std::make_unique(); + Connection err = replay_stream->error.connect([](const std::string &msg) { fprintf(stderr, "%s\n", msg.c_str()); }); if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { return 0; } diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 3ae5896a2d..939aade269 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -358,6 +358,9 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { delete video_splitter; can = stream; // take ownership + stream_connections_.push_back(can->error.connect([this](const std::string &msg) { + QMessageBox::warning(this, tr("Error"), QString::fromStdString(msg)); + })); can->start(); loadFile(dbc_file); diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 4f7c0dc3e7..b992b086fd 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -64,6 +64,7 @@ public: Observable> &> timeRangeChanged; Observable eventsMerged; Observable *, bool> msgsReceived; + Observable error; SourceSet sources; diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 6d580cc912..3a338c7f59 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -9,17 +9,16 @@ #include #include #include +#include #include #include #include "openpilot/cereal/services.h" - -#include -#include +#include "tools/cabana/utils/util.h" // DeviceStream -DeviceStream::DeviceStream(QString address) : zmq_address(address) { +DeviceStream::DeviceStream(std::string address) : zmq_address(std::move(address)) { } DeviceStream::~DeviceStream() { @@ -46,19 +45,16 @@ void DeviceStream::stopBridge() { } void DeviceStream::start() { - if (!zmq_address.isEmpty()) { + if (!zmq_address.empty()) { stopBridge(); - const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) / - "../../cereal/messaging/bridge").lexically_normal().string(); - const std::string addr = zmq_address.toStdString(); + const std::string path = (executableDir() / "../../cereal/messaging/bridge").lexically_normal().string(); const char *can_filter = "/\"can/\""; // Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec // fails, the child writes errno and the parent aborts stream start. int err_pipe[2] = {-1, -1}; if (::pipe(err_pipe) != 0) { - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + error(std::string("Failed to start bridge: ") + strerror(errno)); return; } @@ -66,7 +62,7 @@ void DeviceStream::start() { if (pid == 0) { ::close(err_pipe[0]); ::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC); - execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast(nullptr)); + execl(path.c_str(), path.c_str(), zmq_address.c_str(), can_filter, static_cast(nullptr)); const int err = errno; (void)!::write(err_pipe[1], &err, sizeof(err)); _exit(127); @@ -75,8 +71,7 @@ void DeviceStream::start() { ::close(err_pipe[1]); if (pid < 0) { ::close(err_pipe[0]); - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + error(std::string("Failed to start bridge: ") + strerror(errno)); return; } @@ -87,8 +82,7 @@ void DeviceStream::start() { // Child failed to exec; reap and surface the error. int status = 0; ::waitpid(pid, &status, 0); - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + error(std::string("Failed to start bridge: ") + strerror(exec_errno)); return; } @@ -99,7 +93,7 @@ void DeviceStream::start() { } void DeviceStream::streamThread() { - zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); + zmq_address.empty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); std::unique_ptr context(Context::create()); std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 71205e5a80..3770d952aa 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -2,14 +2,15 @@ #include "tools/cabana/streams/livestream.h" +#include #include class DeviceStream : public LiveStream { public: - DeviceStream(QString address = {}); + DeviceStream(std::string address = {}); ~DeviceStream(); inline std::string routeName() const override { - return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString()); + return "Live Streaming From " + (zmq_address.empty() ? std::string("127.0.0.1") : zmq_address); } protected: @@ -17,5 +18,5 @@ protected: void streamThread() override; void stopBridge(); pid_t bridge_pid = -1; - const QString zmq_address; + const std::string zmq_address; }; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index e9df400599..81ed34e8db 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,6 +1,6 @@ #include "tools/cabana/streams/replaystream.h" -#include +#include #include "common/timing.h" #include "common/util.h" @@ -59,27 +59,25 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d bool success = replay->load(); if (!success) { + std::string message; if (replay->lastRouteError() == RouteLoadError::Unauthorized) { auto auth_content = util::read_file(util::getenv("HOME") + "/.comma/auth.json"); - QString message; if (auth_content.empty()) { message = "Authentication Required. Please run the following command to authenticate:\n\n" "python3 openpilot/tools/lib/auth.py\n\n" "This will grant access to routes from your comma account."; } else { - message = QString("Access Denied. You do not have permission to access route:\n\n%1\n\n" - "This is likely a private route.").arg(QString::fromStdString(route)); + message = "Access Denied. You do not have permission to access route:\n\n" + route + "\n\n" + "This is likely a private route."; } - QMessageBox::warning(nullptr, "Access Denied", message); } else if (replay->lastRouteError() == RouteLoadError::NetworkError) { - QMessageBox::warning(nullptr, "Network Error", - QString("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); + message = "Unable to load the route:\n\n " + route + ".\n\nPlease check your network connection and try again."; } else if (replay->lastRouteError() == RouteLoadError::FileNotFound) { - QMessageBox::warning(nullptr, "Route Not Found", - QString("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); + message = "The specified route could not be found:\n\n " + route + ".\n\nPlease check the route name and try again."; } else { - QMessageBox::warning(nullptr, "Route Load Failed", QString("Failed to load route: '%1'").arg(QString::fromStdString(route))); + message = "Failed to load route: '" + route + "'"; } + error(message); } return success; } diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index dae022be75..8c95bf584b 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -63,6 +63,9 @@ AbstractStream *OpenReplayWidget::open() { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { auto replay_stream = std::make_unique(); + Connection err = replay_stream->error.connect([](const std::string &msg) { + QMessageBox::warning(nullptr, tr("Error"), QString::fromStdString(msg)); + }); uint32_t flags = REPLAY_FLAG_NONE; if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; @@ -211,7 +214,7 @@ OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(p } AbstractStream *OpenDeviceWidget::open() { - QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + std::string ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text().toStdString(); bool msgq = group->checkedId() == 0; return new DeviceStream(msgq ? "" : ip); } diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 9a44ef5054..d5cd77a8c8 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -16,6 +16,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #include #include @@ -454,27 +457,36 @@ void sigTermHandler(int s) { qApp->quit(); } +std::filesystem::path executableDir() { +#ifdef __APPLE__ + char buf[PATH_MAX]; + uint32_t size = sizeof(buf); + if (_NSGetExecutablePath(buf, &size) != 0) return {}; + std::error_code ec; + auto path = std::filesystem::canonical(buf, ec); + return (ec ? std::filesystem::path(buf) : path).parent_path(); +#else + return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); +#endif +} + void initApp(int argc, char *argv[], bool disable_hidpi) { // setup signal handlers to exit gracefully std::signal(SIGINT, sigTermHandler); std::signal(SIGTERM, sigTermHandler); - std::filesystem::path app_dir; #ifdef __APPLE__ // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering QApplication tmp(argc, argv); - app_dir = QCoreApplication::applicationDirPath().toStdString(); if (disable_hidpi) { qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); } -#else - app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif qputenv("QT_DBL_CLICK_DIST", "150"); // ensure the current dir matches the exectuable's directory std::error_code ec; - std::filesystem::current_path(app_dir, ec); + std::filesystem::current_path(executableDir(), ec); } // embedded at build time from the bootstrap_icons package (see SConscript) diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index 77a634fc9e..e0d13208a6 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -219,5 +219,6 @@ private: }; int num_decimals(double num); +std::filesystem::path executableDir(); void initApp(int argc, char *argv[], bool disable_hidpi = true); QPixmap bootstrapPixmap(const QString &id); From 633d17cd128690e00b5a24dfd6747141c8d34a2e Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:30:13 -0700 Subject: [PATCH 13/34] ui: fix install update button overflow (#38696) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 0f12004828..32fbc2b3a7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -184,7 +184,7 @@ class CheckUpdateButton(BigButton): class InstallUpdateButton(BigButton): def __init__(self): - super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) def _update_state(self): From 5645370f845ce70449fdf87ebfa544a2fbc36101 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:37:09 -0700 Subject: [PATCH 14/34] cabana: split utils/util into Qt-free util and qtutil (#38723) --- openpilot/tools/cabana/SConscript | 4 +- openpilot/tools/cabana/binaryview.cc | 1 + openpilot/tools/cabana/binaryview.h | 1 + openpilot/tools/cabana/cabana.cc | 10 +- openpilot/tools/cabana/chart/chart.cc | 2 +- openpilot/tools/cabana/chart/chart.h | 1 + openpilot/tools/cabana/chart/chartswidget.h | 1 + .../tools/cabana/chart/signalselector.cc | 1 + openpilot/tools/cabana/chart/sparkline.cc | 1 + openpilot/tools/cabana/chart/tiplabel.cc | 2 +- openpilot/tools/cabana/commands.cc | 1 + openpilot/tools/cabana/detailwidget.h | 2 + openpilot/tools/cabana/historylog.h | 1 + openpilot/tools/cabana/mainwin.cc | 1 + openpilot/tools/cabana/messageswidget.h | 2 + openpilot/tools/cabana/settingsdialog.cc | 2 +- openpilot/tools/cabana/signalview.cc | 3 +- .../tools/cabana/streams/abstractstream.cc | 1 + .../tools/cabana/streams/devicestream.cc | 1 + openpilot/tools/cabana/streams/livestream.cc | 1 + .../tools/cabana/streams/replaystream.cc | 1 + openpilot/tools/cabana/streamselector.cc | 1 + openpilot/tools/cabana/tools/findsignal.cc | 2 + openpilot/tools/cabana/tools/findsignal.h | 5 + openpilot/tools/cabana/utils/qtutil.cc | 236 ++++++++++ openpilot/tools/cabana/utils/qtutil.h | 139 ++++++ openpilot/tools/cabana/utils/util.cc | 410 +++++------------- openpilot/tools/cabana/utils/util.h | 212 +++------ openpilot/tools/cabana/videowidget.cc | 1 + openpilot/tools/cabana/videowidget.h | 2 +- 30 files changed, 566 insertions(+), 482 deletions(-) create mode 100644 openpilot/tools/cabana/utils/qtutil.cc create mode 100644 openpilot/tools/cabana/utils/qtutil.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index f7db3f23b7..63b725a759 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -100,7 +100,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"] cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/qtutil.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] @@ -120,6 +120,8 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), + dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'), + dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src), dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), ] dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 160aead6fd..20fd2ec1ec 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -13,6 +13,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/utils/qtutil.h" // BinaryView diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index 0b0159b422..c918ee01e7 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 6cc6046bec..b18484c556 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -12,6 +12,7 @@ #ifdef __linux__ #include "tools/cabana/streams/socketcanstream.h" #endif +#include "tools/cabana/utils/qtutil.h" namespace { @@ -134,7 +135,14 @@ int main(int argc, char *argv[]) { app.setApplicationDisplayName("Cabana"); //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui - UnixSignalHandler signalHandler; + // Marshal exit onto the GUI thread (qApp methods are not thread-safe). + UnixSignalHandler signalHandler([]() { + QMetaObject::invokeMethod(qApp, []() { + printf("\nexiting...\n"); + qApp->closeAllWindows(); + qApp->exit(); + }, Qt::QueuedConnection); + }); utils::setTheme(settings.theme); CabanaArgs args; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index beece12352..478ff3ce9d 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -248,7 +248,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * } if (!can->liveStreaming()) { - s.segment_tree.build(s.vals); + s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y(); }); } } } diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index d7227b6cf8..3188e1f401 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -10,6 +10,7 @@ #include "tools/cabana/chart/tiplabel.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" enum class SeriesType { Line = 0, diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index 4c143242b2..17e18a90d2 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -13,6 +13,7 @@ #include "tools/cabana/commands.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" const int CHART_MIN_WIDTH = 300; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index aab38f5372..90825dc402 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -8,6 +8,7 @@ #include #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { setWindowTitle(title); diff --git a/openpilot/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc index f5bef0fc2e..587a35956d 100644 --- a/openpilot/tools/cabana/chart/sparkline.cc +++ b/openpilot/tools/cabana/chart/sparkline.cc @@ -3,6 +3,7 @@ #include #include #include +#include "tools/cabana/utils/qtutil.h" void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { if (first == last || size.isEmpty()) { diff --git a/openpilot/tools/cabana/chart/tiplabel.cc b/openpilot/tools/cabana/chart/tiplabel.cc index 233fa80373..c250ebf877 100644 --- a/openpilot/tools/cabana/chart/tiplabel.cc +++ b/openpilot/tools/cabana/chart/tiplabel.cc @@ -7,7 +7,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_ShowWithoutActivating); diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index c6cdd9b1b7..40d1c5ba18 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -1,5 +1,6 @@ #include "tools/cabana/commands.h" +#include #include // UndoStack diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h index 48d320c40f..c003548da4 100644 --- a/openpilot/tools/cabana/detailwidget.h +++ b/openpilot/tools/cabana/detailwidget.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,6 +12,7 @@ #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" #include "tools/cabana/utils/elidedlabel.h" +#include "tools/cabana/utils/qtutil.h" class EditMessageDialog : public QDialog { public: diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 405fe60fbb..1b75e3b8a9 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -10,6 +10,7 @@ #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class HeaderView : public QHeaderView { public: diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 939aade269..2c19df530c 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -23,6 +23,7 @@ #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 28cab25463..7c9fc0253f 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class MessageListModel : public QAbstractTableModel { Q_OBJECT diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc index 3a49de5746..5dd8b7617c 100644 --- a/openpilot/tools/cabana/settingsdialog.cc +++ b/openpilot/tools/cabana/settingsdialog.cc @@ -9,7 +9,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_CACHE_MINIUTES = 30; const int MAX_CACHE_MINIUTES = 120; diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 3a6d4dab25..de80ce2a3f 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -11,10 +11,11 @@ #include #include #include +#include #include #include "tools/cabana/commands.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" // SignalModel diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index cc8b2f96ad..7be579269d 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/abstractstream.h" +#include #include #include diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3a338c7f59..986ca13558 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/devicestream.h" +#include #include #include #include diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 71b5d9a1ec..ec71a6794d 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -9,6 +9,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" struct LiveStream::Logger { Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {} diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 81ed34e8db..944cb10fd9 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -4,6 +4,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 8c95bf584b..31532737f3 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -14,6 +14,7 @@ #include "tools/cabana/streams/devicestream.h" #include "tools/cabana/streams/replaystream.h" #include "tools/cabana/routesdialog.h" +#include "tools/cabana/utils/qtutil.h" // OpenReplayWidget diff --git a/openpilot/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc index 4511af7c01..2e533741cc 100644 --- a/openpilot/tools/cabana/tools/findsignal.cc +++ b/openpilot/tools/cabana/tools/findsignal.cc @@ -10,6 +10,8 @@ #include #include +#include "tools/cabana/utils/qtutil.h" + // FindSignalModel QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/openpilot/tools/cabana/tools/findsignal.h b/openpilot/tools/cabana/tools/findsignal.h index 239a08c9c4..b7cae73b49 100644 --- a/openpilot/tools/cabana/tools/findsignal.h +++ b/openpilot/tools/cabana/tools/findsignal.h @@ -7,8 +7,13 @@ #include #include +#include +#include +#include #include +#include #include +#include #include #include "tools/cabana/commands.h" diff --git a/openpilot/tools/cabana/utils/qtutil.cc b/openpilot/tools/cabana/utils/qtutil.cc new file mode 100644 index 0000000000..040c90c624 --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.cc @@ -0,0 +1,236 @@ +#include "tools/cabana/utils/qtutil.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// MessageBytesDelegate + +MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) + : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { + fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); + for (int i = 0; i < 256; ++i) { + hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); + hex_text_table[i].prepare({}, fixed_font); + } + h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; +} + +QSize MessageBytesDelegate::sizeForBytes(int n) const { + int rows = multiple_lines ? std::max(1, n / 8) : 1; + return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; +} + +QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto data = index.data(BytesRole); + return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); +} + +void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); + } + + QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); + QColor highlighted_color = option.palette.color(QPalette::HighlightedText); + auto text_color = index.data(Qt::ForegroundRole).value(); + bool inactive = text_color.isValid(); + if (!inactive) { + text_color = option.palette.color(QPalette::Text); + } + auto data = index.data(BytesRole); + if (!data.isValid()) { + painter->setFont(option.font); + painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); + painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); + return; + } + + // Paint hex column + const auto &bytes = *static_cast *>(data.value()); + const auto &colors = *static_cast *>(index.data(ColorsRole).value()); + + painter->setFont(fixed_font); + const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + const QPoint pt = item_rect.topLeft(); + for (int i = 0; i < bytes.size(); ++i) { + int row = !multiple_lines ? 0 : i / 8; + int column = !multiple_lines ? i : i % 8; + QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); + + if (!inactive && i < colors.size() && colors[i].alpha() > 0) { + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(QPalette::Text)); + painter->fillRect(r, option.palette.color(QPalette::Window)); + } + painter->fillRect(r, toQColor(colors[i])); + } else { + painter->setPen(text_pen); + } + utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); + } +} + +// TabBar + +int TabBar::addTab(const QString &text) { + int index = QTabBar::addTab(text); + QToolButton *btn = new ToolButton("x", tr("Close Tab")); + int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); + int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); + btn->setFixedSize({width, height}); + setTabButton(index, QTabBar::RightSide, btn); + QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); + return index; +} + +void TabBar::closeTabClicked() { + QObject *object = sender(); + for (int i = 0; i < count(); ++i) { + if (tabButton(i, QTabBar::RightSide) == object) { + emit tabCloseRequested(i); + break; + } + } +} + +// validators + +static QValidator::State toQtState(ValidState s) { + switch (s) { + case ValidState::Acceptable: return QValidator::Acceptable; + case ValidState::Intermediate: return QValidator::Intermediate; + default: return QValidator::Invalid; + } +} + +QValidator::State NameValidator::validate(QString &input, int &pos) const { + std::string s = input.toStdString(); + auto state = validateName(s); + input = QString::fromStdString(s); + return toQtState(state); +} + +QValidator::State NodeValidator::validate(QString &input, int &pos) const { + return toQtState(validateNodes(input.toStdString())); +} + +QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { + return toQtState(validateNonWhitespace(input.toStdString())); +} + +QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { + return toQtState(validateIpAddress(input.toStdString())); +} + +QValidator::State DoubleValidator::validate(QString &input, int &pos) const { + return toQtState(validateDouble(input.toLatin1().toStdString())); +} + +namespace utils { + +bool isDarkTheme() { + QColor windowColor = QApplication::palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + +QPixmap icon(const QString &id) { + bool dark_theme = isDarkTheme(); + + QPixmap pm; + QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); + if (!QPixmapCache::find(key, &pm)) { + pm = bootstrapPixmap(id); + if (dark_theme) { + QPainter p(&pm); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(pm.rect(), QColor("#bbbbbb")); + } + QPixmapCache::insert(key, pm); + } + return pm; +} + +void setTheme(int theme) { + auto style = QApplication::style(); + if (!style) return; + + static int prev_theme = 0; + if (theme != prev_theme) { + prev_theme = theme; + QPalette new_palette; + if (theme == DARK_THEME) { + new_palette.setColor(QPalette::Window, toQColor(DarkTheme::window)); + new_palette.setColor(QPalette::WindowText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Base, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::AlternateBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipText, toQColor(DarkTheme::tooltip_text)); + new_palette.setColor(QPalette::Text, toQColor(DarkTheme::text)); + new_palette.setColor(QPalette::Button, toQColor(DarkTheme::button)); + new_palette.setColor(QPalette::ButtonText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Highlight, toQColor(DarkTheme::highlight)); + new_palette.setColor(QPalette::HighlightedText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::BrightText, toQColor(DarkTheme::bright_text)); + new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::WindowText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::Text, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Light, toQColor(DarkTheme::light)); + new_palette.setColor(QPalette::Dark, toQColor(DarkTheme::dark)); + } else { + new_palette = style->standardPalette(); + } + qApp->setPalette(new_palette); + style->polish(qApp); + for (auto w : QApplication::allWidgets()) { + w->setPalette(new_palette); + } + } +} + +} // namespace utils + +void sigTermHandler(int s) { + std::signal(s, SIG_DFL); + qApp->quit(); +} + +void initApp(int argc, char *argv[], bool disable_hidpi) { + // setup signal handlers to exit gracefully + std::signal(SIGINT, sigTermHandler); + std::signal(SIGTERM, sigTermHandler); + +#ifdef __APPLE__ + // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering + QApplication tmp(argc, argv); + if (disable_hidpi) { + qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); + } +#endif + + qputenv("QT_DBL_CLICK_DIST", "150"); + // ensure the current dir matches the exectuable's directory + std::error_code ec; + std::filesystem::current_path(executableDir(), ec); +} + +QPixmap bootstrapPixmap(const QString &id) { + QPixmap pixmap; + const std::string svg = utils::bootstrapSvg(id.toStdString()); + if (!svg.empty()) { + pixmap.loadFromData((const uchar *)svg.data(), svg.size(), "svg"); + } + return pixmap; +} diff --git a/openpilot/tools/cabana/utils/qtutil.h b/openpilot/tools/cabana/utils/qtutil.h new file mode 100644 index 0000000000..db0417ec9a --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/core/observable.h" +#include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/settings.h" +#include "tools/cabana/utils/strings.h" +#include "tools/cabana/utils/util.h" + +// needed by QVariant::fromValue() in the Qt views; goes away with QVariant +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + +inline QColor toQColor(const CabanaColor &color) { + return QColor(color.r, color.g, color.b, color.a); +} + +class LogSlider : public QSlider { + Q_OBJECT + +public: + LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : scale(factor), QSlider(orientation, parent) {} + + void setRange(double min, double max) { + scale.setRange(min, max); + QSlider::setRange(min, max); + setValue(QSlider::value()); + } + int value() const { return scale.value(QSlider::value(), minimum(), maximum()); } + void setValue(int v) { QSlider::setValue(scale.position(v, minimum(), maximum())); } + +private: + LogScale scale; +}; + +enum { + ColorsRole = Qt::UserRole + 1, + BytesRole = Qt::UserRole + 2 +}; + +class MessageBytesDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + MessageBytesDelegate(QObject *parent, bool multiple_lines = false); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool multipleLines() const { return multiple_lines; } + void setMultipleLines(bool v) { multiple_lines = v; } + QSize sizeForBytes(int n) const; + +private: + std::array hex_text_table; + QFontMetrics font_metrics; + QFont fixed_font; + QSize byte_size = {}; + bool multiple_lines = false; + int h_margin, v_margin; +}; + +// QValidator wrappers around the std::string validators in util.h +#define CABANA_VALIDATOR(Name) \ + class Name : public QValidator { \ + Q_OBJECT \ + public: \ + Name(QObject *parent = nullptr) : QValidator(parent) {} \ + QValidator::State validate(QString &input, int &pos) const override; \ + }; +CABANA_VALIDATOR(NameValidator) +CABANA_VALIDATOR(NodeValidator) +CABANA_VALIDATOR(NonWhitespaceValidator) +CABANA_VALIDATOR(IpAddressValidator) +CABANA_VALIDATOR(DoubleValidator) +#undef CABANA_VALIDATOR + +namespace utils { + +QPixmap icon(const QString &id); +bool isDarkTheme(); +void setTheme(int theme); +inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { + auto size = (r.size() - text.size()) / 2; + p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); +} +inline auto qbytes(const std::vector &dat) { + return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); +} + +} + +class ToolButton : public QToolButton { + Q_OBJECT +public: + ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { + setIcon(icon); + setToolTip(tooltip); + setAutoRaise(true); + const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + setIconSize({metric, metric}); + theme = settings.theme; + settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); + } + void setIcon(const QString &icon) { + icon_str = icon; + QToolButton::setIcon(utils::icon(icon_str)); + } + +private: + void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + Connection settings_connection_; + QString icon_str; + int theme; +}; + +class TabBar : public QTabBar { + Q_OBJECT + +public: + TabBar(QWidget *parent) : QTabBar(parent) {} + int addTab(const QString &text); + +private: + void closeTabClicked(); +}; + +void initApp(int argc, char *argv[], bool disable_hidpi = true); +QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index d5cd77a8c8..7bdf225975 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -1,18 +1,19 @@ #include "tools/cabana/utils/util.h" #include +#include #include -#include #include #include #include #include +#include #include #include -#include #include #include #include +#include #include #include #include @@ -20,11 +21,6 @@ #include #endif -#include -#include -#include -#include -#include #include "common/util.h" static const std::thread::id main_thread_id = std::this_thread::get_id(); @@ -53,22 +49,21 @@ void utils::drainMainThreadQueue() { // SegmentTree -void SegmentTree::build(const std::vector &arr) { - size = arr.size(); +void SegmentTree::build(int n, const std::function &y) { + size = n; tree.resize(4 * size); // size of the tree is 4 times the size of the array if (size > 0) { - build_tree(arr, 1, 0, size - 1); + build_tree(y, 1, 0, size - 1); } } -void SegmentTree::build_tree(const std::vector &arr, int n, int left, int right) { +void SegmentTree::build_tree(const std::function &y, int n, int left, int right) { if (left == right) { - const double y = arr[left].y(); - tree[n] = {y, y}; + tree[n] = {y(left), y(left)}; } else { const int mid = (left + right) >> 1; - build_tree(arr, 2 * n, left, mid); - build_tree(arr, 2 * n + 1, mid + 1, right); + build_tree(y, 2 * n, left, mid); + build_tree(y, 2 * n + 1, mid + 1, right); tree[n] = {std::min(tree[2 * n].first, tree[2 * n + 1].first), std::max(tree[2 * n].second, tree[2 * n + 1].second)}; } } @@ -84,119 +79,22 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in return {std::min(l.first, r.first), std::max(l.second, r.second)}; } -// MessageBytesDelegate - -MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) - : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { - fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); - for (int i = 0; i < 256; ++i) { - hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); - hex_text_table[i].prepare({}, fixed_font); - } - h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; -} - -QSize MessageBytesDelegate::sizeForBytes(int n) const { - int rows = multiple_lines ? std::max(1, n / 8) : 1; - return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; -} - -QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto data = index.data(BytesRole); - return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); -} - -void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); - } - - QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); - QColor highlighted_color = option.palette.color(QPalette::HighlightedText); - auto text_color = index.data(Qt::ForegroundRole).value(); - bool inactive = text_color.isValid(); - if (!inactive) { - text_color = option.palette.color(QPalette::Text); - } - auto data = index.data(BytesRole); - if (!data.isValid()) { - painter->setFont(option.font); - painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); - painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); - return; - } - - // Paint hex column - const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); - - painter->setFont(fixed_font); - const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - const QPoint pt = item_rect.topLeft(); - for (int i = 0; i < bytes.size(); ++i) { - int row = !multiple_lines ? 0 : i / 8; - int column = !multiple_lines ? i : i % 8; - QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); - - if (!inactive && i < colors.size() && colors[i].alpha() > 0) { - if (option.state & QStyle::State_Selected) { - painter->setPen(option.palette.color(QPalette::Text)); - painter->fillRect(r, option.palette.color(QPalette::Window)); - } - painter->fillRect(r, toQColor(colors[i])); - } else { - painter->setPen(text_pen); - } - utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); - } -} - -// TabBar - -int TabBar::addTab(const QString &text) { - int index = QTabBar::addTab(text); - QToolButton *btn = new ToolButton("x", tr("Close Tab")); - int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); - int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); - btn->setFixedSize({width, height}); - setTabButton(index, QTabBar::RightSide, btn); - QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); - return index; -} - -void TabBar::closeTabClicked() { - QObject *object = sender(); - for (int i = 0; i < count(); ++i) { - if (tabButton(i, QTabBar::RightSide) == object) { - emit tabCloseRequested(i); - break; - } - } -} - // UnixSignalHandler -UnixSignalHandler::UnixSignalHandler() { +UnixSignalHandler::UnixSignalHandler(std::function on_signal) { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { - qFatal("Couldn't create TERM socketpair"); + fprintf(stderr, "Couldn't create TERM socketpair\n"); + abort(); } - waiter = std::thread([this]() { + waiter = std::thread([this, on_signal = std::move(on_signal)]() { int tmp = 0; while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { if (errno != EINTR) return; } if (shutting_down.load()) return; - // Marshal exit onto the GUI thread (qApp methods are not thread-safe). - QMetaObject::invokeMethod(qApp, []() { - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); - }, Qt::QueuedConnection); + on_signal(); }); std::signal(SIGINT, signalHandler); @@ -216,118 +114,131 @@ void UnixSignalHandler::signalHandler(int s) { (void)!::write(sig_fd[0], &s, sizeof(s)); } -// NameValidator +// validators -NameValidator::NameValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NameValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - input.replace(' ', '_'); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid; +ValidState validateName(std::string &input) { + std::replace(input.begin(), input.end(), ' ', '_'); + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (!std::isalnum(c) && c != '_') return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// NodeValidator - -NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NodeValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateNodes(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; // Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing). bool need_word = true; - for (const QChar &c : input) { - if (c.isLetterOrNumber() || c == '_') { + for (const unsigned char c : input) { + if (std::isalnum(c) || c == '_') { need_word = false; } else if (c == ',' && !need_word) { need_word = true; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return need_word ? QValidator::Intermediate : QValidator::Acceptable; + return need_word ? ValidState::Intermediate : ValidState::Acceptable; } -// NonWhitespaceValidator - -NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (c.isSpace()) return QValidator::Invalid; +ValidState validateNonWhitespace(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (std::isspace(c)) return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// IpAddressValidator - -IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateIpAddress(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; int dots = 0; int value = 0; bool has_digit = false; - for (const QChar &c : input) { - if (c.isDigit()) { - value = has_digit ? value * 10 + c.digitValue() : c.digitValue(); - if (value > 255) return QValidator::Invalid; + for (const unsigned char c : input) { + if (std::isdigit(c)) { + value = has_digit ? value * 10 + (c - '0') : (c - '0'); + if (value > 255) return ValidState::Invalid; has_digit = true; } else if (c == '.') { - if (!has_digit || dots >= 3) return QValidator::Invalid; + if (!has_digit || dots >= 3) return ValidState::Invalid; ++dots; has_digit = false; value = 0; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate; + return (dots == 3 && has_digit) ? ValidState::Acceptable : ValidState::Intermediate; } -DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {} +ValidState validateDouble(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; -QValidator::State DoubleValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - - // Match QString::toDouble(): C locale, no hex floats / inf / nan. - const std::string bytes = input.toLatin1().toStdString(); - // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. - if (bytes.find_first_of("xXpP") != std::string::npos) { - return QValidator::Invalid; + // C locale, no hex floats / p-exponents / inf / nan (strtod accepts them, the DBC parser does not) + if (input.find_first_of("xXpP") != std::string::npos) { + return ValidState::Invalid; } - const char *start = bytes.c_str(); + const char *start = input.c_str(); char *end = nullptr; const double value = std::strtod(start, &end); if (end == start) { // Still typing a sign, decimal point, or exponent prefix. if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") { - return QValidator::Intermediate; + return ValidState::Intermediate; } - return QValidator::Invalid; + return ValidState::Invalid; } if (*end == '\0') { - // Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not). - return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid; + return std::isfinite(value) ? ValidState::Acceptable : ValidState::Invalid; } // Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1."). for (const char *p = end; *p; ++p) { const char c = *p; if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) { - return QValidator::Invalid; + return ValidState::Invalid; } } - return QValidator::Intermediate; + return ValidState::Intermediate; +} + +// embedded at build time from the bootstrap_icons package (see SConscript) +extern const unsigned char bootstrap_icons_svg[]; +extern const size_t bootstrap_icons_svg_len; + +static std::unordered_map load_bootstrap_icons() { + std::unordered_map icons; + + const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); + const std::string sym_open = " with + svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) + icons[id] = std::move(svg_str); + } + } + pos = end; + } + return icons; } namespace utils { @@ -384,77 +295,19 @@ bool setClipboardText(const std::string &text) { return false; } -bool isDarkTheme() { - QColor windowColor = QApplication::palette().color(QPalette::Window); - return windowColor.lightness() < 128; -} - -QPixmap icon(const QString &id) { - bool dark_theme = isDarkTheme(); - - QPixmap pm; - QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); - if (!QPixmapCache::find(key, &pm)) { - pm = bootstrapPixmap(id); - if (dark_theme) { - QPainter p(&pm); - p.setCompositionMode(QPainter::CompositionMode_SourceIn); - p.fillRect(pm.rect(), QColor("#bbbbbb")); - } - QPixmapCache::insert(key, pm); - } - return pm; -} - -void setTheme(int theme) { - auto style = QApplication::style(); - if (!style) return; - - static int prev_theme = 0; - if (theme != prev_theme) { - prev_theme = theme; - QPalette new_palette; - if (theme == DARK_THEME) { - // "Darcula" like dark theme - new_palette.setColor(QPalette::Window, QColor("#353535")); - new_palette.setColor(QPalette::WindowText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Base, QColor("#3c3f41")); - new_palette.setColor(QPalette::AlternateBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipText, QColor("#bbb")); - new_palette.setColor(QPalette::Text, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Button, QColor("#3c3f41")); - new_palette.setColor(QPalette::ButtonText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Highlight, QColor("#2f65ca")); - new_palette.setColor(QPalette::HighlightedText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::BrightText, QColor("#f0f0f0")); - new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::Text, QColor("#777777")); - new_palette.setColor(QPalette::Light, QColor("#777777")); - new_palette.setColor(QPalette::Dark, QColor("#353535")); - } else { - new_palette = style->standardPalette(); - } - qApp->setPalette(new_palette); - style->polish(qApp); - for (auto w : QApplication::allWidgets()) { - w->setPalette(new_palette); - } - } +std::string bootstrapSvg(const std::string &id) { + static auto icons = load_bootstrap_icons(); + auto it = icons.find(id); + return it != icons.end() ? it->second : std::string(); } } // namespace utils int num_decimals(double num) { - const QString string = QString::number(num); - auto dot_pos = string.indexOf('.'); - return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; -} - -void sigTermHandler(int s) { - std::signal(s, SIG_DFL); - qApp->quit(); + char buf[32]; + snprintf(buf, sizeof(buf), "%g", num); + const char *dot = strpbrk(buf, ".,"); // Qt sets LC_ALL from the environment so the decimal mark may be a comma + return dot ? (int)strlen(dot + 1) : 0; } std::filesystem::path executableDir() { @@ -469,70 +322,3 @@ std::filesystem::path executableDir() { return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif } - -void initApp(int argc, char *argv[], bool disable_hidpi) { - // setup signal handlers to exit gracefully - std::signal(SIGINT, sigTermHandler); - std::signal(SIGTERM, sigTermHandler); - -#ifdef __APPLE__ - // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering - QApplication tmp(argc, argv); - if (disable_hidpi) { - qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); - } -#endif - - qputenv("QT_DBL_CLICK_DIST", "150"); - // ensure the current dir matches the exectuable's directory - std::error_code ec; - std::filesystem::current_path(executableDir(), ec); -} - -// embedded at build time from the bootstrap_icons package (see SConscript) -extern const unsigned char bootstrap_icons_svg[]; -extern const size_t bootstrap_icons_svg_len; - -static std::unordered_map load_bootstrap_icons() { - std::unordered_map icons; - - const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); - const std::string sym_open = " with - svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) - icons[id] = std::move(svg_str); - } - } - pos = end; - } - return icons; -} - -QPixmap bootstrapPixmap(const QString &id) { - static auto icons = load_bootstrap_icons(); - - QPixmap pixmap; - auto it = icons.find(id.toStdString()); - if (it != icons.end()) { - pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg"); - } - return pixmap; -} diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index e0d13208a6..324c966b5a 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -10,136 +10,68 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/core/observable.h" -#include "tools/cabana/dbc/dbc.h" -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/strings.h" - -// needed by QVariant::fromValue() in the Qt views; goes away with QVariant -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -inline QColor toQColor(const CabanaColor &color) { - return QColor(color.r, color.g, color.b, color.a); -} - -class LogSlider : public QSlider { - Q_OBJECT +#include "tools/cabana/core/color.h" +class SegmentTree { public: - LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {} + SegmentTree() = default; + void build(int n, const std::function &y); + inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +private: + std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; + void build_tree(const std::function &y, int n, int left, int right); + std::vector> tree; + int size = 0; +}; + +// maps a linear slider position onto a log10 scale +class LogScale { +public: + LogScale(double factor) : factor(factor) {} void setRange(double min, double max) { log_min = factor * std::log10(min); log_max = factor * std::log10(max); - QSlider::setRange(min, max); - setValue(QSlider::value()); } - int value() const { - double v = log_min + (log_max - log_min) * ((QSlider::value() - minimum()) / double(maximum() - minimum())); + int value(int pos, int pos_min, int pos_max) const { + double v = log_min + (log_max - log_min) * ((pos - pos_min) / double(pos_max - pos_min)); return std::lround(std::pow(10, v / factor)); } - void setValue(int v) { + int position(int v, int pos_min, int pos_max) const { double log_v = std::clamp(factor * std::log10(v), log_min, log_max); - v = minimum() + (maximum() - minimum()) * ((log_v - log_min) / (log_max - log_min)); - QSlider::setValue(v); + return pos_min + (pos_max - pos_min) * ((log_v - log_min) / (log_max - log_min)); } private: double factor, log_min = 0, log_max = 1; }; -enum { - ColorsRole = Qt::UserRole + 1, - BytesRole = Qt::UserRole + 2 -}; +enum class ValidState { Invalid, Intermediate, Acceptable }; -class SegmentTree { -public: - SegmentTree() = default; - void build(const std::vector &arr); - inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +// single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_' +ValidState validateName(std::string &input); +// comma-separated identifiers: \w+(,\w+)* +ValidState validateNodes(const std::string &input); +// one or more non-whitespace characters (\S+) +ValidState validateNonWhitespace(const std::string &input); +// dotted IPv4 address (0-255 per octet) +ValidState validateIpAddress(const std::string &input); +// C-locale floating-point +ValidState validateDouble(const std::string &input); -private: - std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; - void build_tree(const std::vector &arr, int n, int left, int right); - std::vector> tree; - int size = 0; -}; - -class MessageBytesDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - MessageBytesDelegate(QObject *parent, bool multiple_lines = false); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool multipleLines() const { return multiple_lines; } - void setMultipleLines(bool v) { multiple_lines = v; } - QSize sizeForBytes(int n) const; - -private: - std::array hex_text_table; - QFontMetrics font_metrics; - QFont fixed_font; - QSize byte_size = {}; - bool multiple_lines = false; - int h_margin, v_margin; -}; - -// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'. -class NameValidator : public QValidator { - Q_OBJECT -public: - NameValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts comma-separated identifiers: \w+(,\w+)* -class NodeValidator : public QValidator { - Q_OBJECT -public: - NodeValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts one or more non-whitespace characters (\S+). -class NonWhitespaceValidator : public QValidator { - Q_OBJECT -public: - NonWhitespaceValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts a dotted IPv4 address (0-255 per octet). -class IpAddressValidator : public QValidator { - Q_OBJECT -public: - IpAddressValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// C-locale floating-point validator (matches QString::toDouble). -class DoubleValidator : public QValidator { - Q_OBJECT -public: - DoubleValidator(QObject *parent = nullptr); - QValidator::State validate(QString &input, int &pos) const override; +// "Darcula" like dark theme +struct DarkTheme { + static constexpr CabanaColor window{0x35, 0x35, 0x35}; + static constexpr CabanaColor window_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor base{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor tooltip_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor button{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor highlight{0x2f, 0x65, 0xca}; + static constexpr CabanaColor bright_text{0xf0, 0xf0, 0xf0}; + static constexpr CabanaColor disabled_text{0x77, 0x77, 0x77}; + static constexpr CabanaColor light{0x77, 0x77, 0x77}; + static constexpr CabanaColor dark{0x35, 0x35, 0x35}; }; namespace utils { @@ -148,67 +80,23 @@ bool isMainThread(); // inline on the main thread, queued until drainMainThreadQueue() otherwise void runOnMainThread(std::function fn); void drainMainThreadQueue(); -QPixmap icon(const QString &id); std::string homePath(); std::filesystem::path configPath(); bool getClipboardText(std::string *text); // false if no clipboard tool is available bool setClipboardText(const std::string &text); -bool isDarkTheme(); -void setTheme(int theme); -inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { - auto size = (r.size() - text.size()) / 2; - p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); -} +std::string bootstrapSvg(const std::string &id); // empty if unknown // boundary conversions for the remaining Qt byte-array based state APIs template std::vector toBytes(const T &dat) { return {dat.begin(), dat.end()}; } -inline auto qbytes(const std::vector &dat) { - return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); -} } -class ToolButton : public QToolButton { - Q_OBJECT -public: - ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { - setIcon(icon); - setToolTip(tooltip); - setAutoRaise(true); - const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); - setIconSize({metric, metric}); - theme = settings.theme; - settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); - } - void setIcon(const QString &icon) { - icon_str = icon; - QToolButton::setIcon(utils::icon(icon_str)); - } - -private: - void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } - Connection settings_connection_; - QString icon_str; - int theme; -}; - -class TabBar : public QTabBar { - Q_OBJECT - -public: - TabBar(QWidget *parent) : QTabBar(parent) {} - int addTab(const QString &text); - -private: - void closeTabClicked(); -}; - -// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread -// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread. +// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread. +// on_signal runs on the waiter thread; the caller marshals to the GUI thread. class UnixSignalHandler { public: - UnixSignalHandler(); + UnixSignalHandler(std::function on_signal); ~UnixSignalHandler(); static void signalHandler(int s); @@ -220,5 +108,3 @@ private: int num_decimals(double num); std::filesystem::path executableDir(); -void initApp(int argc, char *argv[], bool disable_hidpi = true); -QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 23f60f29f1..f7b1b41628 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -12,6 +12,7 @@ #include #include "tools/cabana/tools/routeinfo.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index 1b6d81406e..8416eece59 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -12,7 +12,7 @@ #include #include "tools/cabana/cameraview.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h" From 839d3f500474e7726f50cc273fe750e8a05222bd Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:06:33 -0700 Subject: [PATCH 15/34] ui: guard branch switcher before internet connected (#38692) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 32fbc2b3a7..33f6e0e4ab 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -246,6 +246,9 @@ class TargetBranchButton(BigButton): self.set_value(target) def _on_click(self): + if not ui_state.params.get("UpdaterAvailableBranches"): + gui_app.push_widget(BigDialog("", tr("Please connect to Wi-Fi to switch branches."))) + return gui_app.push_widget(BranchSelectPage(self._on_select)) def _on_select(self, branch: str): From e571e21d14dc6247177bd4713283c7bb9ef4ebdd Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:07:15 -0700 Subject: [PATCH 16/34] ui: check for update on target branch switch (#38693) --- .../selfdrive/ui/layouts/settings/software.py | 3 +++ .../ui/mici/layouts/settings/software.py | 23 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/openpilot/selfdrive/ui/layouts/settings/software.py b/openpilot/selfdrive/ui/layouts/settings/software.py index bf986e614e..0d3191aca6 100644 --- a/openpilot/selfdrive/ui/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/layouts/settings/software.py @@ -197,6 +197,9 @@ class SoftwareLayout(Widget): selection = self._branch_dialog.selection ui_state.params.put("UpdaterTargetBranch", selection, block=True) self._branch_btn.action_item.set_value(selection) + self._download_btn.action_item.set_enabled(False) + self._waiting_for_updater = True + self._waiting_start_ts = time.monotonic() subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) self._branch_dialog = None diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 33f6e0e4ab..eeb6eb9bf1 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -97,15 +97,20 @@ class CheckUpdateButton(BigButton): gui_app.push_widget(dlg) return + self._signal_updater("SIGHUP" if self.get_value() == "download update" else "SIGUSR1") + + def check_for_update(self): + self._signal_updater("SIGUSR1") + + def _signal_updater(self, sig: str): self.set_enabled(False) self._state = UpdaterState.WAITING_FOR_UPDATER + self._hide_value_t = None + self.set_value("") self.set_icon(self._txt_update_icon) def run(): - if self.get_value() == "download update": - subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True) - else: - subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + subprocess.run(f"pkill -{sig} -f openpilot.system.updated.updated", shell=True) threading.Thread(target=run, daemon=True).start() @@ -232,8 +237,9 @@ class BranchSelectPage(NavScroller): class TargetBranchButton(BigButton): - def __init__(self): + def __init__(self, check_update_btn: CheckUpdateButton): super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") + self._check_update_btn = check_update_btn self.set_click_callback(self._on_click) self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) self.set_enabled(lambda: ui_state.is_offroad()) @@ -254,7 +260,7 @@ class TargetBranchButton(BigButton): def _on_select(self, branch: str): ui_state.params.put("UpdaterTargetBranch", branch, block=True) self.set_value(branch) - subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + self._check_update_btn.check_for_update() class SoftwareLayoutMici(NavScroller): @@ -268,10 +274,11 @@ class SoftwareLayoutMici(NavScroller): gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), uninstall_openpilot_callback, exit_on_confirm=False) + check_update_btn = CheckUpdateButton() self._scroller.add_widgets([ SoftwareInfoLayoutMici(), - CheckUpdateButton(), + check_update_btn, InstallUpdateButton(), - TargetBranchButton(), + TargetBranchButton(check_update_btn), uninstall_openpilot_btn, ]) From a67cdf9a5138f2b3e0b1692fb4c92e6fa7c0fc8f Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 28 Aug 2026 15:08:18 -0700 Subject: [PATCH 17/34] ui: sync gpu loading to offroad (#38727) ui: sync gpu loading state --- openpilot/selfdrive/ui/mici/layouts/home.py | 7 ++++++- openpilot/system/ui/widgets/icon_widget.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 64f2961326..b567b405a8 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -1,4 +1,5 @@ import datetime +import math import time from openpilot.cereal import log @@ -140,6 +141,7 @@ class MiciHomeLayout(Widget): self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -151,6 +153,7 @@ class MiciHomeLayout(Widget): NetworkIcon(), self._experimental_icon, self._chestnut_icon, + self._chestnut_loading_icon, self._chestnut_failed_icon, self._body_icon, self._mic_icon, @@ -248,7 +251,9 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) + self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) + self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/system/ui/widgets/icon_widget.py b/openpilot/system/ui/widgets/icon_widget.py index bf7790b937..f6f8a47231 100644 --- a/openpilot/system/ui/widgets/icon_widget.py +++ b/openpilot/system/ui/widgets/icon_widget.py @@ -14,3 +14,6 @@ class IconWidget(Widget): def _render(self, _) -> None: color = rl.Color(255, 255, 255, int(self._opacity * 255)) rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color) + + def set_opacity(self, opacity: float) -> None: + self._opacity = opacity From 682b6a20dfe6764774a40f2ef51148c8078b7c79 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 28 Aug 2026 15:46:56 -0700 Subject: [PATCH 18/34] add chestnut offroad alerts (#38706) * system: add chestnut offroad alerts * system: refine chestnut offroad alerts * system: refine chestnut power alerts * system: confirm chestnut power recovery from PCIe * system: detect missing chestnut power from INA voltage --- openpilot/common/hardware/usb.py | 8 +- openpilot/common/params_keys.h | 6 ++ openpilot/selfdrive/modeld/helpers.py | 4 +- .../selfdrive/selfdrived/alerts_offroad.json | 24 +++++ openpilot/system/hardware/chestnut/status.py | 96 +++++++++++++++++++ openpilot/system/hardware/hardwared.py | 28 ++++-- 6 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 openpilot/system/hardware/chestnut/status.py diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py index b9f6db2757..c3c2050b6b 100644 --- a/openpilot/common/hardware/usb.py +++ b/openpilot/common/hardware/usb.py @@ -4,11 +4,17 @@ from pathlib import Path CHESTNUT_FW_VERSION = "ed4e39b7" CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) +CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN" USB_DEVICES_PATH = Path("/sys/bus/usb/devices") TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") PRIMARY_USB_CONTROLLER = "a600000.ssusb" +def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool: + ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS + return (vendor_id, product_id) in ids + + def get_usb_topology() -> set[str]: try: return set(os.listdir(USB_DEVICES_PATH)) @@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None: entry.linkErrorCount = device["linkErrorCount"] entry.usb3Lane = device.get("usb3Lane", "unknown") - if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS: + if is_chestnut_usb_id(entry.vendorId, entry.productId): chestnut_present = True device_state.chestnutPresent = chestnut_present diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 21b7463d92..ba6eae3dd5 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -92,6 +92,12 @@ inline static std::unordered_map keys = { {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 84236f3fd0..d081050055 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path from openpilot.common.file_chunker import get_manifest_path -from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH +from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' @@ -50,7 +50,7 @@ def chestnut_present() -> bool: try: usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) product = (d / "product").read_text().strip() - if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN": + if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT: return True except Exception: pass diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index b0179c0ac3..33fec6e330 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -19,6 +19,30 @@ }, "Offroad_ChestnutBranch": { "text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", + "severity": -1 + }, + "Offroad_ChestnutNotDetected": { + "text": "Chestnut not detected. Check USB and 12V connections.", + "severity": 0 + }, + "Offroad_ChestnutOverheated": { + "text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.", + "severity": 0 + }, + "Offroad_ChestnutPcieUnavailable": { + "text": "%1", + "severity": 0 + }, + "Offroad_ChestnutUncompiled": { + "text": "Chestnut model not compiled. Keep ignition on and reboot the comma.", + "severity": 0 + }, + "Offroad_ChestnutUpdateFailed": { + "text": "Chestnut update failed. Check the USB cable.", + "severity": 0 + }, + "Offroad_ChestnutUsbSlow": { + "text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.", "severity": 0 }, "Offroad_UnregisteredHardware": { diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py new file mode 100644 index 0000000000..c3321a3971 --- /dev/null +++ b/openpilot/system/hardware/chestnut/status.py @@ -0,0 +1,96 @@ +import time + +from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id +from openpilot.selfdrive.modeld.helpers import chestnut_compiled + + +CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging") +CHESTNUT_POWERED_VOLTAGE = 5000 +GPU_TEMP_LIMIT = 100. +MEMORY_TEMP_LIMIT = 95. +TEMP_HYSTERESIS = 5. + + +class ChestnutStatus: + def __init__(self): + self.started = time.monotonic() + self.offroad = True + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.overheated = False + self.usb_seen = False + self.usb_failed = False + + def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, + model_loading: bool, model_active: bool | None, state, set_alert) -> None: + detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] + devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] + firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT + + if self.offroad and not offroad: + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.usb_seen = firmware_ok + self.usb_failed = False + + self.model_loading_seen |= model_loading + self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + + if not offroad and self.usb_seen and not firmware_ok: + self.usb_failed = True + + if not offroad and state is not None: + powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE + power_lost = state.supplyFault or not powered + if self.model_attempted and power_lost and not self.power_lost: + self.power_unavailable = not self.power_seen + self.power_seen |= powered + + if not offroad and self.model_attempted and state is not None: + self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0 + self.pcie_failed |= self.link_failures >= 2 or power_lost + self.power_lost |= power_lost + + if self.pcie_failed and self.power_lost and state is not None: + self.power_restored |= not state.supplyFault and state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE + if self.usb_failed: + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + + if state is not None: + gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit + + release = branch in CHESTNUT_RELEASE_BRANCHES + missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) + slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) + set_alert("Offroad_ChestnutNotDetected", missing) + set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) + set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) + if self.power_lost: + pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else + "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else + "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") + else: + pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." + set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) + set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) + set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) + self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 64ca4415d1..11fe41400d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,16 +16,17 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR -from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state +from openpilot.common.git import get_short_branch +from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state from openpilot.common.linux import LinuxSystemStats from openpilot.system.loggerd.config import get_available_percent from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController +from openpilot.system.hardware.chestnut.status import ChestnutStatus from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -48,6 +49,11 @@ class Chestnut: self.attempts = 0 self.last_attempt = 0. self.flashed = False + self.mismatch = False + + @property + def failed(self) -> bool: + return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed def flash(self) -> None: ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION], @@ -56,9 +62,9 @@ class Chestnut: self.flashed = ret.returncode == 0 def update(self, offroad: bool, usb_state: list[dict]) -> None: - mismatch = any((d["vendorId"], d["productId"]) in CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS and - d["product"] != f"custom {CHESTNUT_FW_VERSION}-CLEAN" for d in usb_state) - if not mismatch: + self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and + d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state) + if not self.mismatch: self.flashed = False return @@ -190,7 +196,7 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() pm = messaging.PubMaster(['deviceState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates") + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") count = 0 @@ -238,7 +244,8 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() + chestnut_status = ChestnutStatus() + branch = get_short_branch() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) @@ -300,8 +307,11 @@ def hardware_thread(end_event, hw_queue) -> None: set_usb_state(msg.deviceState, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state) - set_offroad_alert_if_changed("Offroad_ChestnutBranch", msg.deviceState.chestnutPresent and not big_model_available) - + chestnut_state = sm["chestnutState"] + chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] + chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, + params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), + chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, From 7cf55c3b7a2d9bcee87821e413fa322866f64c5b Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:11:52 -0700 Subject: [PATCH 19/34] common: fix OpenpilotPrefix cleanup on macOS (#38728) The destructor built its cleanup commands as "rm %s -rf", with the flags after the operand. GNU rm permutes arguments so this works on device and in CI, but BSD rm on macOS stops option parsing at the first operand and treats "-rf" as a second filename: $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf rm: /tmp/rmtest: is a directory rm: -rf: No such file or directory exit=1 So nothing is removed, and each of the four calls prints two errors plus "system command failed (256)" from check_system. Every run of a tool that owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my machine. Pass the flags first. --- openpilot/common/prefix.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/common/prefix.h b/openpilot/common/prefix.h index 89f346b9e5..0f2c592527 100644 --- a/openpilot/common/prefix.h +++ b/openpilot/common/prefix.h @@ -27,14 +27,14 @@ public: auto param_path = Params().getParamPath(); if (util::file_exists(param_path)) { std::string real_path = util::readlink(param_path); - util::check_system(util::string_format("rm %s -rf", real_path.c_str())); + util::check_system(util::string_format("rm -rf %s", real_path.c_str())); unlink(param_path.c_str()); } if (getenv("COMMA_CACHE") == nullptr) { - util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str())); + util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str())); } - util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str())); - util::check_system(util::string_format("rm %s -rf", msgq_path.c_str())); + util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str())); + util::check_system(util::string_format("rm -rf %s", msgq_path.c_str())); unsetenv("OPENPILOT_PREFIX"); } From 0e320594844b1c1d80aada3f8c51b6e0a1c14115 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sun, 30 Aug 2026 09:32:36 -0700 Subject: [PATCH 20/34] replay: capture downloader's stderr so download progress is reported again (#38734) --- openpilot/tools/replay/py_downloader.cc | 47 +++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/openpilot/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc index a265dfd6a3..db2b2be127 100644 --- a/openpilot/tools/replay/py_downloader.cc +++ b/openpilot/tools/replay/py_downloader.cc @@ -2,8 +2,11 @@ #include #include +#include +#include #include #include +#include #include #include @@ -14,8 +17,15 @@ namespace { static std::mutex handler_mutex; static DownloadProgressHandler progress_handler = nullptr; -// Run a Python command and capture stdout. Stderr is left attached to the parent. -// Returns stdout content. If abort is signaled, kills the child process. +void reportProgress(const char *line) { + uint64_t cur = 0, total = 0; + if (sscanf(line, "PROGRESS:%llu:%llu", (unsigned long long *)&cur, (unsigned long long *)&total) != 2) return; + std::lock_guard lk(handler_mutex); + if (progress_handler && total > 0) progress_handler(cur, total, true); +} + +// Run a Python command and capture stdout. Stderr is scanned for PROGRESS lines and otherwise passed +// through to the parent's stderr. Returns stdout content. If abort is signaled, kills the child process. std::string runPython(const std::vector &args, std::atomic *abort = nullptr) { // Build argv for execvp std::vector argv; @@ -27,16 +37,22 @@ std::string runPython(const std::vector &args, std::atomic *a } argv.push_back(nullptr); - int stdout_pipe[2]; + int stdout_pipe[2], stderr_pipe[2]; if (pipe(stdout_pipe) != 0) { rWarning("py_downloader: pipe() failed"); return {}; } + if (pipe(stderr_pipe) != 0) { + rWarning("py_downloader: pipe() failed"); + close(stdout_pipe[0]); close(stdout_pipe[1]); + return {}; + } pid_t pid = fork(); if (pid < 0) { rWarning("py_downloader: fork() failed"); close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); return {}; } @@ -57,6 +73,9 @@ std::string runPython(const std::vector &args, std::atomic *a close(stdout_pipe[0]); dup2(stdout_pipe[1], STDOUT_FILENO); close(stdout_pipe[1]); + close(stderr_pipe[0]); + dup2(stderr_pipe[1], STDERR_FILENO); + close(stderr_pipe[1]); execvp("python3", const_cast(argv.data())); _exit(127); @@ -64,6 +83,27 @@ std::string runPython(const std::vector &args, std::atomic *a // Parent process close(stdout_pipe[1]); + close(stderr_pipe[1]); + + // stderr carries the progress lines, so a thread reads it while the loop below waits on stdout + std::thread stderr_thread([fd = stderr_pipe[0]]() { + FILE *f = fdopen(fd, "r"); + if (!f) { + close(fd); + return; + } + char *line = nullptr; + size_t cap = 0; + while (getline(&line, &cap, f) > 0) { + if (strncmp(line, "PROGRESS:", 9) == 0) { + reportProgress(line); + } else { + fputs(line, stderr); + } + } + free(line); + fclose(f); + }); std::string stdout_data; char buf[4096]; @@ -102,6 +142,7 @@ std::string runPython(const std::vector &args, std::atomic *a stdout_data.append(buf, n); } close(stdout_pipe[0]); + stderr_thread.join(); int status; waitpid(pid, &status, 0); From 70df7f227b8b580fd4f6afbf0cdc5e5fa9155432 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Mon, 31 Aug 2026 14:01:20 +0200 Subject: [PATCH 21/34] bump panda (new health packet) (#38736) pandad: support compact health packet --- openpilot/selfdrive/pandad/pandad.cc | 20 ++++++++++---------- panda | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpilot/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc index 78d12bd2ed..d0b65586aa 100644 --- a/openpilot/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -111,22 +111,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setUptime(health.uptime_pkt); ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt); ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt); - ps.setIgnitionLine(health.ignition_line_pkt); - ps.setIgnitionCan(health.ignition_can_pkt); - ps.setControlsAllowed(health.controls_allowed_pkt); + ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U); + ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U); + ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setPandaType(hw_type); ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt)); ps.setSafetyParam(health.safety_param_pkt); ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt)); - ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt)); - ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt)); + ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U); + ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U); ps.setAlternativeExperience(health.alternative_experience_pkt); ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); - ps.setInterruptLoad(health.interrupt_load_pkt); + ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f); ps.setFanPower(health.fan_power); - ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); + ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U); ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); @@ -184,10 +184,10 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } if (spoofing_started) { - health.ignition_line_pkt = 1; + health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE; } - bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)); + bool ignition_local = (health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U; // Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) { @@ -195,7 +195,7 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } bool power_save_desired = !ignition_local; - if (health.power_save_enabled_pkt != power_save_desired) { + if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) { panda->set_power_saving(power_save_desired); } diff --git a/panda b/panda index dd8a5b3df7..75aa44bec9 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit dd8a5b3df77706337a11555377e7180c5adc8726 +Subproject commit 75aa44bec9140849868239b1f1e3f22624adb8fe From 4adbb85742b465108ac579da680bcc633eedb2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 31 Aug 2026 09:25:32 -0700 Subject: [PATCH 22/34] BMRLNAP (#38681) --- openpilot/selfdrive/modeld/SConscript | 6 ++++-- openpilot/selfdrive/modeld/compile_modeld.py | 12 +++++++----- .../modeld/models/big_driving_supercombo.onnx | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index f046be9915..f65aebf2f9 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -24,7 +24,9 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] def estimate_pickle_max_size(onnx_size): - return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty + # QCOM programs for models with spatial recurrent features can approach 2x + # the ONNX size. Overestimating only adds an empty trailing chunk. + return 2.0 * onnx_size + 10 * 1024 * 1024 if arch == 'comma_arm64': tg_backend = 'QCOM' @@ -45,7 +47,7 @@ tg_devices = { # which device to put jit inputs to at runtime CHESTNUT = chestnut_present() if CHESTNUT: - chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' + chestnut_tg_flags = f'DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it chestnut_lock = File("models/.chestnut.lock").abspath diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index 2d27a41496..f54d09e869 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -139,16 +139,18 @@ def get_policy_npy_shapes(input_shapes): dp = input_shapes['desire_pulse'] # (1, 25, 8) tc = input_shapes['traffic_convention'] # (1, 2) at = input_shapes['action_t'] # (1, 2) - fb = input_shapes['features_buffer'] # (1, 24, 512) + fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features + feat_dim = math.prod(fb[2:]) # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now - shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} + shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)} return shapes, [math.prod(s) for s in shapes.values()] def make_input_queues(input_shapes, frame_skip, device): input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) - fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature + fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature + feat_dim = math.prod(fb[2:]) dp = input_shapes['desire_pulse'] # (1, 25, 8) shapes, sizes = get_policy_npy_shapes(input_shapes) @@ -156,7 +158,7 @@ def make_input_queues(input_shapes, frame_skip, device): # views into the packed inputs, to be refilled at runtime npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) input_queues.update({ - 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), + 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(), 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), }) @@ -211,7 +213,7 @@ def make_run_policy(model_runner, model_metadata, frame_skip): inputs = { 'img': img, 'big_img': big_img, - 'features_buffer': feat_buf, + 'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']), 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention, 'action_t': action_t, diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx index 4a04bd7833..bd92b1b876 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff -size 1757355221 +oid sha256:a086d5249fc308bb73993d1e64630c669d4c7df5bde85f42ad61902543648525 +size 765953504 From 9fa7ef3d1722020ea83868c10182812d2650a336 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:46:52 -0700 Subject: [PATCH 23/34] ui: clarify branch switcher error message (#38732) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index eeb6eb9bf1..7a3d84e1e7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -253,7 +253,7 @@ class TargetBranchButton(BigButton): def _on_click(self): if not ui_state.params.get("UpdaterAvailableBranches"): - gui_app.push_widget(BigDialog("", tr("Please connect to Wi-Fi to switch branches."))) + gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again."))) return gui_app.push_widget(BranchSelectPage(self._on_select)) From da8ce858ec5a2478f17ac2e0b94bfd478f1c7a6e Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:55:16 -0700 Subject: [PATCH 24/34] ui(mici): name updater signal constants (#38731) * mici: name updater signal constants * drop SIGNAL_ prefix * self contained --------- Co-authored-by: Shane Smiskol --- .../selfdrive/ui/mici/layouts/settings/software.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 7a3d84e1e7..539ebaca11 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -74,6 +74,10 @@ class SoftwareInfoLayoutMici(Widget): class CheckUpdateButton(BigButton): + UPDATER_PROC = "openpilot.system.updated.updated" + CHECK_FOR_UPDATE = "SIGUSR1" + DOWNLOAD_UPDATE = "SIGHUP" + def __init__(self): self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) @@ -97,10 +101,10 @@ class CheckUpdateButton(BigButton): gui_app.push_widget(dlg) return - self._signal_updater("SIGHUP" if self.get_value() == "download update" else "SIGUSR1") + self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE) def check_for_update(self): - self._signal_updater("SIGUSR1") + self._signal_updater(self.CHECK_FOR_UPDATE) def _signal_updater(self, sig: str): self.set_enabled(False) @@ -110,7 +114,7 @@ class CheckUpdateButton(BigButton): self.set_icon(self._txt_update_icon) def run(): - subprocess.run(f"pkill -{sig} -f openpilot.system.updated.updated", shell=True) + subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True) threading.Thread(target=run, daemon=True).start() From e10c0fd960016e385f1135d9af4af03b634a7ad2 Mon Sep 17 00:00:00 2001 From: XiaoXX Date: Tue, 1 Sep 2026 12:35:56 +0800 Subject: [PATCH 25/34] modem.py: accept hex chars in ICCID (#38735) E.118 specifies decimal digits, but many real SIMs carry hex characters in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs). AT+QCCID returns them verbatim, and the strict isdigit() check blanked the ICCID, leaving the modem daemon stuck in INITIALIZING forever and cellular dead. ModemManager parses ICCID as hex for the same reason. Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously stuck retrying 'identity read incomplete', now dials and passes traffic. --- openpilot/common/hardware/comma/modem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/common/hardware/comma/modem.py b/openpilot/common/hardware/comma/modem.py index 171ed8c0e4..901efae2b1 100755 --- a/openpilot/common/hardware/comma/modem.py +++ b/openpilot/common/hardware/comma/modem.py @@ -5,6 +5,7 @@ import logging import os import select import signal +import string import struct import subprocess import tempfile @@ -354,7 +355,7 @@ class Modem: imei = "" iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") - if not iccid.isdigit(): + if not all(c in string.hexdigits for c in iccid): iccid = "" imsi = first_line("AT+CIMI") From a2e422eee0e92523101329437b24bafa09538aa7 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Mon, 31 Aug 2026 22:30:28 -0700 Subject: [PATCH 26/34] TGC (#38739) * 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc * here --- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | 4 ++-- tinygrad_repo | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx index bd92b1b876..7b51732275 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a086d5249fc308bb73993d1e64630c669d4c7df5bde85f42ad61902543648525 -size 765953504 +oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123 +size 765950064 diff --git a/tinygrad_repo b/tinygrad_repo index c015351ac5..b87159cee1 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit c015351ac5c00c10c58dbdaf530ef5b2883ab948 +Subproject commit b87159cee1b137c327f901a6aef69f394aa629f6 From 7d5596d5c3527f274a3615181f52095909ffb8e9 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 05:59:02 -0700 Subject: [PATCH 27/34] monitor chestnut USB in hardwared (#38741) hardwared: monitor chestnut USB independently --- openpilot/cereal/log.capnp | 1 + openpilot/cereal/services.py | 1 + openpilot/selfdrive/modeld/modeld.py | 67 ++-------- .../system/hardware/chestnut/monitoring.py | 124 ++++++++++++++++++ openpilot/system/hardware/chestnut/status.py | 37 ++++-- openpilot/system/hardware/hardwared.py | 25 +++- 6 files changed, 180 insertions(+), 75 deletions(-) create mode 100644 openpilot/system/hardware/chestnut/monitoring.py diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fb4867b2a6..9ca1092e89 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2593,6 +2593,7 @@ struct Event { clocks @35 :Clocks; deviceState @6 :DeviceState; chestnutState @152 :ChestnutState; + chestnutGpuState @153 :ChestnutState; logMessage @18 :Text; errorLogMessage @85 :Text; diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index 08633d6975..f906e4610f 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -26,6 +26,7 @@ _services: dict[str, tuple] = { "temperatureSensor": (True, 2., 200), "deviceState": (True, 2., 1), "chestnutState": (True, 10., 10), + "chestnutGpuState": (False, 10.), "touch": (True, 20., 1), "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "controlsState": (True, 100., 10, QueueSize.MEDIUM), diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index a795f366ce..1178349c47 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,8 +6,6 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device -import usb1 -import struct import threading import time import numpy as np @@ -32,7 +30,6 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked -from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob @@ -73,45 +70,14 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. shouldStop=bool(stop)) -class ChestnutState: - # only modeld can access chestnut +class ChestnutGpuState: + # SMU metrics are only accessible from modeld. def __init__(self, pm: PubMaster, big: bool): self.pm = pm self.big = big self.valid = True self.sends = 0 self.metrics = {} - self._asm_usb = None - - def _close_asm_usb(self) -> None: - if self._asm_usb is not None: - self._asm_usb.close() - self._asm_usb = None - - def _open_asm_usb(self): - context = usb1.USBContext() - for vendor_id, product_id in CHESTNUT_USB_IDS: - if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: - return handle - context.close() - - def _read_ina(self) -> tuple[int, int, bool]: - if "AMD" in Device._opened_devices and self._asm_usb is None: - try: - raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) - return struct.unpack(' int: @@ -119,8 +85,6 @@ class ChestnutState: return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) def send(self) -> None: - msg = messaging.new_message('chestnutState') - state = msg.chestnutState self.sends += 1 if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: @@ -142,25 +106,14 @@ class ChestnutState: cloudlog.exception("chestnut state read failed") self.valid = False self.metrics.clear() + + msg = messaging.new_message('chestnutGpuState') + state = msg.chestnutGpuState if self.big: for k, v in self.metrics.items(): setattr(state, k, v) - - asm_valid = False - try: - # ASM runs on USB-C power, these still read without a gpu - state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() - asm_valid = True - except Exception: - pass - if "AMD" in Device._opened_devices: - try: - state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0] - except Exception: - pass - - msg.valid = asm_valid and (not self.big or self.valid) - self.pm.send('chestnutState', msg) + msg.valid = self.big and self.valid + self.pm.send('chestnutGpuState', msg) class FrameMeta: @@ -310,13 +263,13 @@ def main(demo=False): cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None + chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -425,7 +378,7 @@ def main(demo=False): mt1 = time.perf_counter() try: send_chestnut = (chestnut_state is not None and - run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0) model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): diff --git a/openpilot/system/hardware/chestnut/monitoring.py b/openpilot/system/hardware/chestnut/monitoring.py new file mode 100644 index 0000000000..829db99618 --- /dev/null +++ b/openpilot/system/hardware/chestnut/monitoring.py @@ -0,0 +1,124 @@ +import struct +from contextlib import suppress + +import usb1 + +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS + + +USB_TIMEOUT_MS = 100 +PCIE_LTSSM_ADDRESS = 0xB450 + + +class ChestnutUsb: + def __init__(self): + self.context: usb1.USBContext | None = None + self.handle = None + + def close(self) -> None: + handle, context = self.handle, self.context + self.handle = None + self.context = None + with suppress(Exception): + if handle is not None: + handle.close() + with suppress(Exception): + if context is not None: + context.close() + + def connect(self) -> bool: + if self.handle is not None: + return True + + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True) + if handle is not None: + self.context = context + self.handle = handle + return True + context.close() + return False + + def _read(self, request: int, value: int, length: int) -> bytes: + if self.handle is None: + raise usb1.USBErrorNoDevice + raw = bytes(self.handle.controlRead(0xC0, request, value, 0, length, timeout=USB_TIMEOUT_MS)) + if len(raw) != length: + raise ValueError(f"short chestnut USB response: {len(raw)}/{length}") + return raw + + def read_ina(self) -> tuple[int, int, bool]: + return struct.unpack(' int: + return self._read(0xE4, PCIE_LTSSM_ADDRESS, 1)[0] + + +class ChestnutMonitoring: + def __init__(self, usb: ChestnutUsb | None = None): + self.usb = usb or ChestnutUsb() + self.gpu_state = None + self.seen = False + self.enabled = False + self.usb_failed = False + + def set_enabled(self, enabled: bool) -> None: + if self.enabled == enabled: + return + self.enabled = enabled + self.usb.close() + self.usb_failed = False + + def retry(self) -> None: + self.usb_failed = False + + def model_alive(self, sm: messaging.SubMaster, now: float) -> bool: + modeld = next((p for p in sm['managerState'].processes if p.name == 'modeld'), None) + if modeld is not None and modeld.shouldBeRunning and not modeld.running: + return False + recv_time = sm.recv_time['chestnutGpuState'] + return recv_time > 0. and now - recv_time < 10. / SERVICE_LIST['chestnutGpuState'].frequency + + def update_gpu_state(self, sm: messaging.SubMaster, now: float) -> None: + if sm.updated['chestnutGpuState']: + self.gpu_state = sm['chestnutGpuState'] if sm.valid['chestnutGpuState'] else None + elif not self.model_alive(sm, now): + self.gpu_state = None + + def update(self, sm: messaging.SubMaster, now: float, model_loading: bool = False): + self.update_gpu_state(sm, now) + return self.build_message(model_loading) + + def build_message(self, model_loading: bool = False): + if not self.enabled: + return None + + msg = messaging.new_message('chestnutState') + if self.gpu_state is not None: + msg.chestnutState = self.gpu_state + state = msg.chestnutState + + if self.usb_failed: + return msg if self.seen else None + + try: + if not self.usb.connect(): + self.usb_failed = True + return msg if self.seen else None + + self.seen = True + voltage, current, fault = self.usb.read_ina() + pcie_ltssm = self.usb.read_pcie_ltssm() + state.supplyVoltage = voltage + state.supplyCurrent = current + state.supplyFault = fault + state.pcieLtssm = pcie_ltssm + msg.valid = True + except Exception as e: + if not model_loading or not isinstance(e, usb1.USBErrorTimeout): + self.usb.close() + self.usb_failed = True + return msg diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index c3321a3971..34456b2c11 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -21,14 +21,13 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 - self.model_loading_seen = False self.model_attempted = False self.overheated = False self.usb_seen = False self.usb_failed = False def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, - model_loading: bool, model_active: bool | None, state, set_alert) -> None: + model_active: bool | None, state, usb_failed: bool, set_alert) -> None: detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT @@ -40,16 +39,15 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 - self.model_loading_seen = False self.model_attempted = False self.usb_seen = firmware_ok self.usb_failed = False - self.model_loading_seen |= model_loading - self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + self.model_attempted |= model_active is not None - if not offroad and self.usb_seen and not firmware_ok: - self.usb_failed = True + if not offroad: + self.usb_seen |= firmware_ok + self.usb_failed = not offroad and self.usb_seen and (not firmware_ok or usb_failed) if not offroad and state is not None: powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE @@ -80,17 +78,28 @@ class ChestnutStatus: release = branch in CHESTNUT_RELEASE_BRANCHES missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 - set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) - set_alert("Offroad_ChestnutNotDetected", missing) - set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) - set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) + update_failed = offroad and firmware_failed + compiled = firmware_ok and chestnut_compiled() + uncompiled = offroad and firmware_ok and not compiled + if self.power_lost: pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") else: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." - set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) - set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) - set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) + + alerts = ( + ("Offroad_ChestnutNotDetected", missing, None), + ("Offroad_ChestnutUpdateFailed", update_failed, None), + ("Offroad_ChestnutUncompiled", uncompiled, None), + ("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert), + ("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None), + ("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None), + ) + active_alert = next((name for name, active, _ in alerts if active), None) + + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1 and not missing) + for name, _, extra_text in alerts: + set_alert(name, name == active_alert, extra_text) self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 11fe41400d..c7d11fa5b0 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -27,6 +27,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController from openpilot.system.hardware.chestnut.status import ChestnutStatus +from openpilot.system.hardware.chestnut.monitoring import ChestnutMonitoring from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -195,8 +196,9 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() - pm = messaging.PubMaster(['deviceState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") + pm = messaging.PubMaster(['deviceState', 'chestnutState']) + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", + "chestnutState", "chestnutGpuState", "managerState"], poll="pandaStates") count = 0 @@ -244,7 +246,9 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() + chestnut_monitoring = ChestnutMonitoring() chestnut_status = ChestnutStatus() + model_loading = params.get_bool("ChestnutLoading") branch = get_short_branch() while not end_event.is_set(): @@ -276,6 +280,8 @@ def hardware_thread(end_event, hw_queue) -> None: # Run at 2Hz, plus either edge of ignition ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: + if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: + pm.send('chestnutState', chestnut_msg) continue msg = messaging.new_message('deviceState', valid=True) @@ -309,9 +315,11 @@ def hardware_thread(end_event, hw_queue) -> None: chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut_state = sm["chestnutState"] chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] + model_loading = params.get_bool("ChestnutLoading") + model_active = params.get("ChestnutActive") chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, - params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), - chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) + model_active, chestnut_state if chestnut_valid else None, chestnut_monitoring.usb_failed, + set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, @@ -419,6 +427,15 @@ def hardware_thread(end_event, hw_queue) -> None: if off_ts is None: off_ts = time.monotonic() + chestnut_usb_ready = any(is_chestnut_usb_id(d["vendorId"], d["productId"]) and d["product"] == CHESTNUT_USB_PRODUCT + for d in last_hw_state.usb_state) + flash_active = chestnut.thread is not None and chestnut.thread.is_alive() + chestnut_monitoring.set_enabled(started_ts is not None and (chestnut_usb_ready or chestnut_monitoring.seen) and not flash_active) + if chestnut_usb_ready and chestnut_monitoring.usb_failed: + chestnut_monitoring.retry() + if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: + pm.send('chestnutState', chestnut_msg) + # Offroad power monitoring voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage power_monitor.calculate(voltage, onroad_conditions["ignition"]) From 06af2abe67db217f96e6152ebf4f018fa1cb4bca Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 07:20:56 -0700 Subject: [PATCH 28/34] modeld: wait for stable chestnut (#38742) modeld: wait for stable chestnut --- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/modeld/helpers.py | 6 ++++++ openpilot/selfdrive/modeld/modeld.py | 24 +++++++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index ba6eae3dd5..07a2283ece 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -135,5 +135,6 @@ inline static std::unordered_map keys = { {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index d081050055..23eb6cbd21 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -11,6 +11,8 @@ from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' +CHESTNUT_POWERED_VOLTAGE = 5000 +CHESTNUT_PCIE_READY = 0x78 def get_tg_input_devices(process_name: str, chestnut: bool): @@ -58,3 +60,7 @@ def chestnut_present() -> bool: def chestnut_compiled() -> bool: return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() + + +def chestnut_ready(state) -> bool: + return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 1178349c47..c31418ee0d 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -31,7 +31,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -205,12 +205,25 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - CHESTNUT = chestnut_present() and chestnut_compiled() + chestnut_available = chestnut_present() and chestnut_compiled() + CHESTNUT = False + if chestnut_available: + poller = messaging.Poller() + sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True) + deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency + while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.: + if not poller.poll(round(remaining * 1000)): + break + msg = messaging.recv_one_or_none(sock) + CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState) if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() params.put_bool("ChestnutLoading", CHESTNUT) - params.remove("ChestnutActive") + if chestnut_available and not CHESTNUT: + params.put_bool("ChestnutActive", False) + else: + params.remove("ChestnutActive") config_realtime_process(7, 54) @@ -254,7 +267,11 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model + if model is None: + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", model is not None) + if model is not None: + params.remove("ChestnutModelError") small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: @@ -385,6 +402,7 @@ def main(demo=False): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", False) model = small_model if chestnut_state is not None: From c9f1602040149be793ea3d3e17a5fd8d915408c8 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 11:13:30 -0700 Subject: [PATCH 29/34] Revert "monitor chestnut USB in hardwared (#38741)" (#38744) This reverts commit 7d5596d5c3527f274a3615181f52095909ffb8e9. --- openpilot/cereal/log.capnp | 1 - openpilot/cereal/services.py | 1 - openpilot/selfdrive/modeld/modeld.py | 67 ++++++++-- .../system/hardware/chestnut/monitoring.py | 124 ------------------ openpilot/system/hardware/chestnut/status.py | 37 ++---- openpilot/system/hardware/hardwared.py | 25 +--- 6 files changed, 75 insertions(+), 180 deletions(-) delete mode 100644 openpilot/system/hardware/chestnut/monitoring.py diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 9ca1092e89..fb4867b2a6 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2593,7 +2593,6 @@ struct Event { clocks @35 :Clocks; deviceState @6 :DeviceState; chestnutState @152 :ChestnutState; - chestnutGpuState @153 :ChestnutState; logMessage @18 :Text; errorLogMessage @85 :Text; diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index f906e4610f..08633d6975 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -26,7 +26,6 @@ _services: dict[str, tuple] = { "temperatureSensor": (True, 2., 200), "deviceState": (True, 2., 1), "chestnutState": (True, 10., 10), - "chestnutGpuState": (False, 10.), "touch": (True, 20., 1), "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "controlsState": (True, 100., 10, QueueSize.MEDIUM), diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index c31418ee0d..de66decf5c 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,6 +6,8 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device +import usb1 +import struct import threading import time import numpy as np @@ -30,6 +32,7 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob @@ -70,14 +73,45 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. shouldStop=bool(stop)) -class ChestnutGpuState: - # SMU metrics are only accessible from modeld. +class ChestnutState: + # only modeld can access chestnut def __init__(self, pm: PubMaster, big: bool): self.pm = pm self.big = big self.valid = True self.sends = 0 self.metrics = {} + self._asm_usb = None + + def _close_asm_usb(self) -> None: + if self._asm_usb is not None: + self._asm_usb.close() + self._asm_usb = None + + def _open_asm_usb(self): + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: + return handle + context.close() + + def _read_ina(self) -> tuple[int, int, bool]: + if "AMD" in Device._opened_devices and self._asm_usb is None: + try: + raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) + return struct.unpack(' int: @@ -85,6 +119,8 @@ class ChestnutGpuState: return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) def send(self) -> None: + msg = messaging.new_message('chestnutState') + state = msg.chestnutState self.sends += 1 if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: @@ -106,14 +142,25 @@ class ChestnutGpuState: cloudlog.exception("chestnut state read failed") self.valid = False self.metrics.clear() - - msg = messaging.new_message('chestnutGpuState') - state = msg.chestnutGpuState if self.big: for k, v in self.metrics.items(): setattr(state, k, v) - msg.valid = self.big and self.valid - self.pm.send('chestnutGpuState', msg) + + asm_valid = False + try: + # ASM runs on USB-C power, these still read without a gpu + state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() + asm_valid = True + except Exception: + pass + if "AMD" in Device._opened_devices: + try: + state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0] + except Exception: + pass + + msg.valid = asm_valid and (not self.big or self.valid) + self.pm.send('chestnutState', msg) class FrameMeta: @@ -280,13 +327,13 @@ def main(demo=False): cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -395,7 +442,7 @@ def main(demo=False): mt1 = time.perf_counter() try: send_chestnut = (chestnut_state is not None and - run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0) + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): diff --git a/openpilot/system/hardware/chestnut/monitoring.py b/openpilot/system/hardware/chestnut/monitoring.py deleted file mode 100644 index 829db99618..0000000000 --- a/openpilot/system/hardware/chestnut/monitoring.py +++ /dev/null @@ -1,124 +0,0 @@ -import struct -from contextlib import suppress - -import usb1 - -import openpilot.cereal.messaging as messaging -from openpilot.cereal.services import SERVICE_LIST -from openpilot.common.hardware.usb import CHESTNUT_USB_IDS - - -USB_TIMEOUT_MS = 100 -PCIE_LTSSM_ADDRESS = 0xB450 - - -class ChestnutUsb: - def __init__(self): - self.context: usb1.USBContext | None = None - self.handle = None - - def close(self) -> None: - handle, context = self.handle, self.context - self.handle = None - self.context = None - with suppress(Exception): - if handle is not None: - handle.close() - with suppress(Exception): - if context is not None: - context.close() - - def connect(self) -> bool: - if self.handle is not None: - return True - - context = usb1.USBContext() - for vendor_id, product_id in CHESTNUT_USB_IDS: - handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True) - if handle is not None: - self.context = context - self.handle = handle - return True - context.close() - return False - - def _read(self, request: int, value: int, length: int) -> bytes: - if self.handle is None: - raise usb1.USBErrorNoDevice - raw = bytes(self.handle.controlRead(0xC0, request, value, 0, length, timeout=USB_TIMEOUT_MS)) - if len(raw) != length: - raise ValueError(f"short chestnut USB response: {len(raw)}/{length}") - return raw - - def read_ina(self) -> tuple[int, int, bool]: - return struct.unpack(' int: - return self._read(0xE4, PCIE_LTSSM_ADDRESS, 1)[0] - - -class ChestnutMonitoring: - def __init__(self, usb: ChestnutUsb | None = None): - self.usb = usb or ChestnutUsb() - self.gpu_state = None - self.seen = False - self.enabled = False - self.usb_failed = False - - def set_enabled(self, enabled: bool) -> None: - if self.enabled == enabled: - return - self.enabled = enabled - self.usb.close() - self.usb_failed = False - - def retry(self) -> None: - self.usb_failed = False - - def model_alive(self, sm: messaging.SubMaster, now: float) -> bool: - modeld = next((p for p in sm['managerState'].processes if p.name == 'modeld'), None) - if modeld is not None and modeld.shouldBeRunning and not modeld.running: - return False - recv_time = sm.recv_time['chestnutGpuState'] - return recv_time > 0. and now - recv_time < 10. / SERVICE_LIST['chestnutGpuState'].frequency - - def update_gpu_state(self, sm: messaging.SubMaster, now: float) -> None: - if sm.updated['chestnutGpuState']: - self.gpu_state = sm['chestnutGpuState'] if sm.valid['chestnutGpuState'] else None - elif not self.model_alive(sm, now): - self.gpu_state = None - - def update(self, sm: messaging.SubMaster, now: float, model_loading: bool = False): - self.update_gpu_state(sm, now) - return self.build_message(model_loading) - - def build_message(self, model_loading: bool = False): - if not self.enabled: - return None - - msg = messaging.new_message('chestnutState') - if self.gpu_state is not None: - msg.chestnutState = self.gpu_state - state = msg.chestnutState - - if self.usb_failed: - return msg if self.seen else None - - try: - if not self.usb.connect(): - self.usb_failed = True - return msg if self.seen else None - - self.seen = True - voltage, current, fault = self.usb.read_ina() - pcie_ltssm = self.usb.read_pcie_ltssm() - state.supplyVoltage = voltage - state.supplyCurrent = current - state.supplyFault = fault - state.pcieLtssm = pcie_ltssm - msg.valid = True - except Exception as e: - if not model_loading or not isinstance(e, usb1.USBErrorTimeout): - self.usb.close() - self.usb_failed = True - return msg diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index 34456b2c11..c3321a3971 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -21,13 +21,14 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 + self.model_loading_seen = False self.model_attempted = False self.overheated = False self.usb_seen = False self.usb_failed = False def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, - model_active: bool | None, state, usb_failed: bool, set_alert) -> None: + model_loading: bool, model_active: bool | None, state, set_alert) -> None: detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT @@ -39,15 +40,16 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 + self.model_loading_seen = False self.model_attempted = False self.usb_seen = firmware_ok self.usb_failed = False - self.model_attempted |= model_active is not None + self.model_loading_seen |= model_loading + self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None - if not offroad: - self.usb_seen |= firmware_ok - self.usb_failed = not offroad and self.usb_seen and (not firmware_ok or usb_failed) + if not offroad and self.usb_seen and not firmware_ok: + self.usb_failed = True if not offroad and state is not None: powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE @@ -78,28 +80,17 @@ class ChestnutStatus: release = branch in CHESTNUT_RELEASE_BRANCHES missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 - update_failed = offroad and firmware_failed - compiled = firmware_ok and chestnut_compiled() - uncompiled = offroad and firmware_ok and not compiled - + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) + set_alert("Offroad_ChestnutNotDetected", missing) + set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) + set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) if self.power_lost: pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") else: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." - - alerts = ( - ("Offroad_ChestnutNotDetected", missing, None), - ("Offroad_ChestnutUpdateFailed", update_failed, None), - ("Offroad_ChestnutUncompiled", uncompiled, None), - ("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert), - ("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None), - ("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None), - ) - active_alert = next((name for name, active, _ in alerts if active), None) - - set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1 and not missing) - for name, _, extra_text in alerts: - set_alert(name, name == active_alert, extra_text) + set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) + set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) + set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index c7d11fa5b0..11fe41400d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -27,7 +27,6 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController from openpilot.system.hardware.chestnut.status import ChestnutStatus -from openpilot.system.hardware.chestnut.monitoring import ChestnutMonitoring from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -196,9 +195,8 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() - pm = messaging.PubMaster(['deviceState', 'chestnutState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", - "chestnutState", "chestnutGpuState", "managerState"], poll="pandaStates") + pm = messaging.PubMaster(['deviceState']) + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") count = 0 @@ -246,9 +244,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - chestnut_monitoring = ChestnutMonitoring() chestnut_status = ChestnutStatus() - model_loading = params.get_bool("ChestnutLoading") branch = get_short_branch() while not end_event.is_set(): @@ -280,8 +276,6 @@ def hardware_thread(end_event, hw_queue) -> None: # Run at 2Hz, plus either edge of ignition ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: - if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: - pm.send('chestnutState', chestnut_msg) continue msg = messaging.new_message('deviceState', valid=True) @@ -315,11 +309,9 @@ def hardware_thread(end_event, hw_queue) -> None: chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut_state = sm["chestnutState"] chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] - model_loading = params.get_bool("ChestnutLoading") - model_active = params.get("ChestnutActive") chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, - model_active, chestnut_state if chestnut_valid else None, chestnut_monitoring.usb_failed, - set_offroad_alert_if_changed) + params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), + chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, @@ -427,15 +419,6 @@ def hardware_thread(end_event, hw_queue) -> None: if off_ts is None: off_ts = time.monotonic() - chestnut_usb_ready = any(is_chestnut_usb_id(d["vendorId"], d["productId"]) and d["product"] == CHESTNUT_USB_PRODUCT - for d in last_hw_state.usb_state) - flash_active = chestnut.thread is not None and chestnut.thread.is_alive() - chestnut_monitoring.set_enabled(started_ts is not None and (chestnut_usb_ready or chestnut_monitoring.seen) and not flash_active) - if chestnut_usb_ready and chestnut_monitoring.usb_failed: - chestnut_monitoring.retry() - if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: - pm.send('chestnutState', chestnut_msg) - # Offroad power monitoring voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage power_monitor.calculate(voltage, onroad_conditions["ignition"]) From cb85ac1f0e78425752cf744b2ca2cd268720a337 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 1 Sep 2026 13:59:55 -0700 Subject: [PATCH 30/34] amd warp (#38684) * modeld: fuse warp and policy TinyJit * bump tg * fix? * this simple trick... * debug 1 * bump tg * pack all * wips * fix * BIG_INTO_SMALL remove * slower --- openpilot/selfdrive/modeld/SConscript | 24 ++- openpilot/selfdrive/modeld/compile_modeld.py | 147 ++++++++++-------- openpilot/selfdrive/modeld/modeld.py | 43 ++--- .../test/process_replay/model_replay.py | 10 +- 4 files changed, 108 insertions(+), 116 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index f65aebf2f9..5a99aa890c 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -10,11 +10,6 @@ from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path -CAMERA_CONFIGS = [ - (_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208 - (_os_fisheye.width, _os_fisheye.height), # mici: 1344x760 -] - Import('env', 'arch') chunker_file = File("#openpilot/common/file_chunker.py") lenv = env.Clone() @@ -29,17 +24,17 @@ def estimate_pickle_max_size(onnx_size): return 2.0 * onnx_size + 10 * 1024 * 1024 if arch == 'comma_arm64': + from openpilot.common.hardware import HARDWARE + camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye + camera_configs = [(camera.width, camera.height)] tg_backend = 'QCOM' tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1' else: + camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)] tg_backend = 'CPU' tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM' tg_devices = { # which device to put jit inputs to at runtime - 'openpilot.selfdrive.modeld.modeld': { - 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, - 'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} - }, 'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'default': {'DEV': tg_backend} }, @@ -47,7 +42,7 @@ tg_devices = { # which device to put jit inputs to at runtime CHESTNUT = chestnut_present() if CHESTNUT: - chestnut_tg_flags = f'DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' + chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it chestnut_lock = File("models/.chestnut.lock").abspath @@ -77,10 +72,9 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ for chestnut in [False, True] if CHESTNUT else [False]: target_pkl_path = File(modeld_pkl_path(chestnut)).abspath - # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut - file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) + file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) - camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) + camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs) # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' @@ -108,7 +102,7 @@ for chestnut in [False, True] if CHESTNUT else [False]: actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] node = lenv.Command( chunk_targets, - tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], + tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file], actions, ) if chestnut: @@ -122,7 +116,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file dm_w, dm_h = DM_INPUT_SIZE compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")] -for cam_w, cam_h in CAMERA_CONFIGS: +for cam_w, cam_h in camera_configs: dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py ' f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} ' diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index f54d09e869..d851c3cc86 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -37,17 +37,12 @@ from tinygrad.engine.jit import TinyJit NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) -WARP_INPUTS = ['tfm', 'big_tfm'] -POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] - -UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) -UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) - -WARP_DEV = os.getenv('WARP_DEV') +MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] -def make_random_images(keys, shape, device=None): - return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys} +def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int: + # Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation. + return stride * (y_height + uv_height) def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None): @@ -99,7 +94,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): def frame_prepare_tinygrad(input_frame, M_inv): # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling - M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV) + M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT) # deinterleave NV12 UV plane (UVUV... -> separate U, V) uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) with Context(SPLIT_REDUCEOP=0): @@ -118,23 +113,6 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): return frame_prepare_tinygrad -def make_warp_input_queues(vision_input_shapes, frame_skip, device): - img = vision_input_shapes['img'] # (1, 12, 128, 256) - n_frames = img[1] // 6 - img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) - - npy = { - 'tfm': np.zeros((3, 3), dtype=np.float32), - 'big_tfm': np.zeros((3, 3), dtype=np.float32), - } - input_queues = { - 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - **{k: Tensor(v, device='NPY').realize() for k, v in npy.items()}, - } - return input_queues, npy - - def get_policy_npy_shapes(input_shapes): dp = input_shapes['desire_pulse'] # (1, 25, 8) tc = input_shapes['traffic_convention'] # (1, 2) @@ -146,23 +124,32 @@ def get_policy_npy_shapes(input_shapes): return shapes, [math.prod(s) for s in shapes.values()] -def make_input_queues(input_shapes, frame_skip, device): - input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) - +def make_input_queues(input_shapes, frame_skip, device, frame_copy_size): + img = input_shapes['img'] # (1, 12, 128, 256) fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature feat_dim = math.prod(fb[2:]) dp = input_shapes['desire_pulse'] # (1, 25, 8) + n_frames = img[1] // 6 + img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) - shapes, sizes = get_policy_npy_shapes(input_shapes) - packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) + policy_shapes, _ = get_policy_npy_shapes(input_shapes) + shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes + sizes = [math.prod(s) for s in shapes.values()] + packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize + packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8) + packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32) + frames = packed_input[packed_npy_size:] + frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]} # views into the packed inputs, to be refilled at runtime - npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) - input_queues.update({ + npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)} + input_queues = { + 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(), 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), - 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), - }) - return input_queues, npy + 'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(), + } + return input_queues, npy, frame_views def shift_and_sample(buf, new_val, sample_fn): @@ -178,13 +165,15 @@ def sample_desire(buf, frame_skip): return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0) -def make_warp(nv12, model_w, model_h, frame_skip): +def make_warp(nv12, model_w, model_h): frame_prepare = make_frame_prepare(nv12, model_w, model_h) def warp(tfm, big_tfm, frame, big_frame): - tfm = tfm.to(WARP_DEV) - big_tfm = big_tfm.to(WARP_DEV) - Tensor.realize(tfm, big_tfm) + tfm = tfm.to(Device.DEFAULT) + big_tfm = big_tfm.to(Device.DEFAULT) + frame = frame.to(Device.DEFAULT) + big_frame = big_frame.to(Device.DEFAULT) + Tensor.realize(tfm, big_tfm, frame, big_frame) warped_frame = frame_prepare(frame, tfm).unsqueeze(0) warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) @@ -197,10 +186,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip): sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) + model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()} def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT) - warped = warped.to(Device.DEFAULT) Tensor.realize(packed_npy_inputs, warped) img = shift_and_sample(img_q, warped[0:1], sample_skip_fn) @@ -218,28 +207,45 @@ def make_run_policy(model_runner, model_metadata, frame_skip): 'traffic_convention': traffic_convention, 'action_t': action_t, } + inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()} out = next(iter(model_runner(inputs).values())).cast('float32') return out, return run_policy -def compile_jit(jit, make_random_inputs, input_keys, make_queues): - SEED = 42 - def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): - input_queues, npy = make_queues(Device.DEFAULT) - rng = np.random.default_rng(seed) - Tensor.manual_seed(seed) +def make_run_model(warp, run_policy, model_metadata, frame_copy_size): + _, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) + packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize - testing = test_val is not None or test_buffers is not None - n_runs = 1 if testing else 3 + def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): + packed_input = packed_npy_inputs.to(Device.DEFAULT) + Tensor.realize(packed_input) + packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32') + frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size] + big_frame = packed_input[packed_npy_size + frame_copy_size:] + tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)]) + warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame) + return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs) + return run_model + + +def compile_jit(jit, input_keys, make_queues, benchmark_runs): + if benchmark_runs < 1: + raise ValueError("benchmark_runs must be at least 1") + + SEED = 42 + def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True): + input_queues, npy, frame_views = make_queues(Device.DEFAULT) + rng = np.random.default_rng(seed) for i in range(n_runs): for v in npy.values(): v[:] = rng.standard_normal(v.shape).astype(v.dtype) + for v in frame_views.values(): + v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8) Device.default.synchronize() - random_inputs = make_random_inputs() st = time.perf_counter() - outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs) + outs = fn(**{k: input_queues[k] for k in input_keys}) mt = time.perf_counter() Device.default.synchronize() et = time.perf_counter() @@ -258,14 +264,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues): return val, buffers print('capture + replay') - test_val, test_buffers = random_inputs_run(jit, SEED) - print('pickle round trip') + test_val, test_buffers = random_inputs_run(jit, SEED, 3) + print(f'pickle round trip ({benchmark_runs} runs per seed)') with tempfile.TemporaryFile(dir=".") as f: dump_oob(jit, f) f.seek(0) - jit = load_oob(f) - random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True) - random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False) + loaded_jit = load_oob(f) + random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True) + random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False) + # Keep the original so per-resolution JITs share model weight buffers in the final pickle. return jit @@ -294,27 +301,31 @@ if __name__ == "__main__": p.add_argument('--onnx', required=True) p.add_argument('--output', required=True) p.add_argument('--frame-skip', type=int, required=True) + p.add_argument('--benchmark-runs', type=int, default=1, + help='timed loaded-JIT runs for each correctness seed') args = p.parse_args() model_path = read_file_chunked_to_disk(args.onnx) model_w, model_h = args.model_size model_runner = OnnxRunner(model_path) - out = {'metadata': make_metadata_dict(model_path)} + out = { + 'metadata': make_metadata_dict(model_path), + 'input_devices': {'model': Device.DEFAULT}, + 'run_model': {}, + } - run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) - - make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip) - make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV) - out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, - make_policy_queues) + run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip) for cam_w, cam_h in args.camera_resolutions: nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) - make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) - warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) - make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip) - out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height) + make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip, + frame_copy_size=frame_copy_size) + warp = make_warp(nv12, model_w, model_h) + run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True) + out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues, + args.benchmark_runs) with open(args.output, "wb") as f: dump_oob(out, f) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index de66decf5c..040c6b980f 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -4,7 +4,6 @@ import ctypes from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom -from tinygrad.tensor import Tensor from tinygrad.device import Device import usb1 import struct @@ -29,14 +28,13 @@ from openpilot.common.transformations.model import get_warp_matrix from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser -from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS +from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob -PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') LAT_SMOOTH_SECONDS = 0.0 @@ -177,9 +175,9 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, cam_w: int, cam_h: int, chestnut: bool): - input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) - self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut))) + input_devices = jits['input_devices'] + self.model_device = input_devices['model'] metadata = jits['metadata'] self.input_shapes = metadata['input_shapes'] self.vision_input_names = [k for k in self.input_shapes if 'img' in k] @@ -189,13 +187,11 @@ class ModelState: self.chestnut = chestnut self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ - self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) - self.full_frames: dict[str, Tensor] = {} - self._blob_cache: dict[tuple[str, int], Tensor] = {} + self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3]) + self.input_queues, self.npy, self.frame_views = make_input_queues( + self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size) self.parser = Parser() - self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')} - self.run_policy = jits['run_policy'] - self.warp = jits[(cam_w,cam_h)] + self.run_model = jits['run_model'][(cam_w,cam_h)] def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} @@ -203,14 +199,8 @@ class ModelState: def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: - for key in bufs.keys(): - ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data - yuv_size = self.frame_buf_params[key][3] - # There is a ringbuffer of imgs, just cache tensors pointing to all of them - cache_key = (key, ptr) - if cache_key not in self._blob_cache: - self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV) - self.full_frames[key] = self._blob_cache[cache_key] + for key, buf in bufs.items(): + np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size)) # Model decides when action is completed, so desire input is just a pulse triggered on rising edge inputs['desire_pulse'][0] = 0 @@ -221,11 +211,7 @@ class ModelState: self.npy['tfm'][:,:] = transforms['img'][:,:] self.npy['big_tfm'][:,:] = transforms['big_img'][:,:] - warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']) - - outs, = self.run_policy( - **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped - ) + outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS}) if after_enqueue is not None: after_enqueue() model_output = outs.numpy()[0] @@ -239,14 +225,13 @@ class ModelState: return outputs_dict def warmup(self) -> None: - dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names} + dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names} eye = np.eye(3, dtype=np.float32) dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2} self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()}) - self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.input_queues, self.npy, self.frame_views = make_input_queues( + self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size) self.prev_desire[:] = 0 - self.full_frames.clear() - self._blob_cache.clear() def main(demo=False): diff --git a/openpilot/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py index ba610c6a2b..927c9b38f1 100755 --- a/openpilot/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master" GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN) EXEC_TIMINGS = [ - # model, instant max, average max - ("modelV2", 0.05, 0.028), - ("driverStateV2", 0.05, 0.018), + # model, instant max, average max, chestnut average max + ("modelV2", 0.05, 0.03, 0.05), + ("driverStateV2", 0.05, 0.018, 0.018), ] def get_log_fn(test_route, ref="master"): @@ -169,11 +169,13 @@ def model_replay(lr, frs): dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs) msgs = modeld_msgs + dmonitoringmodeld_msgs + chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2") header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result'] rows = [] timings_ok = True - for (s, instant_max, avg_max) in EXEC_TIMINGS: + for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS: + avg_max = chestnut_avg_max if chestnut else avg_max ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s] # TODO some init can happen in first iteration ts = ts[1:] From 36561258facd79875b58567f8b72ef751810ae3a Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 15:45:57 -0700 Subject: [PATCH 31/34] ui: show usb connection (#38745) * ui: show USB status * ui: resize USB icon * ui: classify USB device once * ui: debounce USB disconnect --- openpilot/selfdrive/assets/icons_mici/usb.png | 3 +++ openpilot/selfdrive/ui/mici/layouts/home.py | 6 ++++- openpilot/selfdrive/ui/ui_state.py | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/usb.png diff --git a/openpilot/selfdrive/assets/icons_mici/usb.png b/openpilot/selfdrive/assets/icons_mici/usb.png new file mode 100644 index 0000000000..2f3afb0de9 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/usb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500 +size 1544 diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index b567b405a8..30d4a19451 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -140,6 +140,7 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) + self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40)) self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) @@ -152,6 +153,7 @@ class MiciHomeLayout(Widget): IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), self._experimental_icon, + self._usb_icon, self._chestnut_icon, self._chestnut_loading_icon, self._chestnut_failed_icon, @@ -251,7 +253,9 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) + self._usb_icon.set_visible(ui_state.usb_connected and ui_state.usb_unknown) + self._chestnut_icon.set_visible(not ui_state.usb_unknown and (ui_state.usb_connected or + ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index c169cbeaff..ae1fad0b66 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,6 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC +from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int from openpilot.selfdrive.modeld.helpers import chestnut_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 @@ -90,6 +91,10 @@ class UIState: self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None self.chestnut_loading: bool = False + self.usb_connected: bool = False + self.usb_connected_ts: float | None = None + self.usb_disconnected_ts: float | None = None + self.usb_unknown: bool = False self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False @@ -246,6 +251,23 @@ class UIState: self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive") self.chestnut_loading = self.params.get_bool("ChestnutLoading") + now = time.monotonic() + if read_int(TYPEC_CC_ORIENTATION_PATH) != 0: + self.usb_disconnected_ts = None + if not self.usb_connected: + self.usb_connected = True + self.usb_connected_ts = now + self.usb_unknown = False + elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.: + self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state()) + self.usb_connected_ts = None + elif self.usb_connected: + if self.usb_disconnected_ts is None: + self.usb_disconnected_ts = now + elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME: + self.usb_connected = False + self.usb_connected_ts = None + self.usb_unknown = False class Device: From 79658800ce142a84009e83a7f98e1c5d3415ceee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 1 Sep 2026 17:15:13 -0700 Subject: [PATCH 32/34] cereal: log big model in drivingModelData (#38747) --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/modeld/fill_model_msg.py | 1 + openpilot/selfdrive/test/process_replay/migration.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fb4867b2a6..177a220451 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -1005,6 +1005,7 @@ struct DrivingModelData { frameIdExtra @1 :UInt32; frameDropPerc @6 :Float32; modelExecutionTime @7 :Float32; + big @8 :Bool; action @2 :ModelDataV2.Action; diff --git a/openpilot/selfdrive/modeld/fill_model_msg.py b/openpilot/selfdrive/modeld/fill_model_msg.py index 558f881b37..055c182f6c 100644 --- a/openpilot/selfdrive/modeld/fill_model_msg.py +++ b/openpilot/selfdrive/modeld/fill_model_msg.py @@ -63,6 +63,7 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn driving_model_data.frameIdExtra = modelV2.frameIdExtra driving_model_data.frameDropPerc = modelV2.frameDropPerc driving_model_data.modelExecutionTime = modelV2.modelExecutionTime + driving_model_data.big = modelV2.big driving_model_data.action = modelV2.action driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index 2fb8932abc..ee1fb3b943 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -149,7 +149,7 @@ def migrate_drivingModelData(msgs): add_ops = [] for _, msg in msgs: dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime) - for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]: + for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]: setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field)) for meta_field in ["laneChangeState", "laneChangeState"]: setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field)) From 8b88f7dd6e0fe50985e27676370dc71145679d23 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 18:32:39 -0700 Subject: [PATCH 33/34] ui: show one GPU status (#38748) ui: show one GPU status icon --- openpilot/selfdrive/ui/mici/layouts/home.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 30d4a19451..553fa37f8c 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -252,13 +252,17 @@ class MiciHomeLayout(Widget): self._version_commit_label.render() # ***** Center-aligned bottom section icons ***** + usb_connected = ui_state.usb_connected + usb_unknown = ui_state.usb_unknown + chestnut_state = ui_state.chestnut_state self._experimental_icon.set_visible(ui_state.experimental_mode) - self._usb_icon.set_visible(ui_state.usb_connected and ui_state.usb_unknown) - self._chestnut_icon.set_visible(not ui_state.usb_unknown and (ui_state.usb_connected or - ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) - self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) + self._usb_icon.set_visible(usb_connected and usb_unknown) + self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in + (ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and + (usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) + self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING) self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) - self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) + self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) From 6249f4d5b0e63c05f08bce12ca3afebda9f764a3 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 18:32:59 -0700 Subject: [PATCH 34/34] AGNOS 19.7 (#38750) --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 094622005a..4fcb243f12 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.6" + export AGNOS_VERSION="19.7" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index ae1cbbcd38..4a48016ed2 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz", - "hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", - "hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", + "url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz", + "hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", + "hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", "size": 46897152, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e" + "ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz", - "hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b", - "hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", + "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz", + "hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744", + "hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468", + "ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448", "alt": { - "hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", - "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img", + "hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", + "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img", "size": 4718592000 } } -] +] \ No newline at end of file