From 9cc734a7b5d83342ba89f4be644a852e8f40b447 Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 31 Jul 2026 21:42:46 +0900 Subject: [PATCH 1/4] Refactor cluster screen mode 2 to render system-debug screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced mode 2’s standalone system-debug view with the mode-0 default system screen from commit `c0a6773f`, restoring its navigation/disconnected-system panel behavior and fallback logic. Updated labels, CLI aliases, and tests for clarity, ensuring functionality and configuration consistency. Documentation and code now fully align with the revised behavior. --- openpilot/selfdrive/carrot/cluster/README.md | 24 ++++-- .../carrot/cluster/cluster_renderer.py | 9 +- openpilot/selfdrive/carrot/cluster/main.py | 2 +- .../carrot/tests/test_cluster_config.py | 8 ++ .../carrot/tests/test_cluster_trip_report.py | 85 ++++++++++++++++++- openpilot/selfdrive/carrot_settings.json | 6 +- 6 files changed, 114 insertions(+), 20 deletions(-) diff --git a/openpilot/selfdrive/carrot/cluster/README.md b/openpilot/selfdrive/carrot/cluster/README.md index 2146c4e1cd..3a49f59d85 100644 --- a/openpilot/selfdrive/carrot/cluster/README.md +++ b/openpilot/selfdrive/carrot/cluster/README.md @@ -451,12 +451,14 @@ intermediate copy. They also report the availability of `async_pbo` and `auto` falls back to ffmpeg, the run uses the software RGBA pipe. Changing this setting while the HUD is running makes the current HUD process exit so `cluster_autorun` can relaunch it with the new encoder choice. -`ClusterHudScreenMode` controls optional debug views: `0` default, `1` shows -the live debug panel with grouped `LIVE DELAY`, `LIVE TORQUE`, `STEERING`, and -`LATERAL PLAN` rows, `2` shows the system information panel with memory and CPU -core usage, `3` shows a large debug graph selected by `ShowPlotMode` with the -driving scene disabled, and `4` -shows the same graph in the information panel while keeping the driving scene. +`ClusterHudScreenMode` controls the right-side content: `0` is the default mode +that switches between navigation and the driving report, `1` shows the live +debug panel with grouped `LIVE DELAY`, `LIVE TORQUE`, `STEERING`, and +`LATERAL PLAN` rows, `2` is the system-debug slot rendering commit +`c0a6773f794a5e4e86aeca8e14515232abc26b1b`'s mode-0 default system screen, +`3` shows a large debug graph selected by `ShowPlotMode` with the driving scene +disabled, and `4` shows the same graph in the information panel while keeping +the driving scene. `5` shows the driving report in the information panel while keeping the driving scene. The report uses a large trip/event summary card and a separate system-load card with four 2-by-2 circular gauges. A lower target plots stored calibration pitch @@ -482,9 +484,13 @@ other presentation with `cluster_replay_usb.py ROUTE --trip-report --language en --imperial`. In default screen mode (`0`), the trip report is shown while no live navigation is being received and the navigation panel returns automatically when reception -starts. Mode 5 keeps the branch, network address, and frame-rate status in the -lower-left camera area while omitting the lower-right core-usage text that would -overlap the report. In road-camera view, ungrouped radar detections are projected +starts. System-debug mode (`2`) reproduces the reference commit's mode-0 system +screen and does not use the current automatic report fallback. It keeps the +navigation/disconnected-system panel whenever a navigation dashboard exists, +and falls back to the route overlay only when no navigation panel source exists. +Mode `5` keeps the branch, network address, and +frame-rate status in the lower-left camera area while omitting the lower-right +core-usage text that would overlap the report. In road-camera view, ungrouped radar detections are projected as small transparent rounded source-colored markers, while detected vehicles are enclosed by larger transparent rounded frames using their existing detection colors. Vehicle frames use a single low-segment outline, ignore noisy diff --git a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py index 427d4943d5..1032d8e7ea 100644 --- a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py +++ b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py @@ -3536,10 +3536,6 @@ def _draw_hud(self, state: ClusterUiState, signal_lights: tuple[bool, bool] | No profile_stage = self._profile_start() self._draw_live_debug_panel(state) self._profile_add("hud.live_debug", profile_stage) - if screen_mode == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM: - profile_stage = self._profile_start() - self._draw_system_stats_panel(state) - self._profile_add("hud.system_stats", profile_stage) if screen_mode == CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT: profile_stage = self._profile_start() self._draw_debug_plot( @@ -3564,7 +3560,6 @@ def _draw_hud(self, state: ClusterUiState, signal_lights: tuple[bool, bool] | No self._profile_add("hud.navi_live", profile_stage) if screen_mode not in ( CLUSTER_SCREEN_MODE_DEBUG, - CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, CLUSTER_SCREEN_MODE_DEBUG_GRAPH, CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT, CLUSTER_SCREEN_MODE_TRIP_REPORT, @@ -3650,6 +3645,10 @@ def _draw_driving_hud_content( rl.rl_pop_matrix() def _effective_screen_mode(self, state: ClusterUiState) -> int: + if self.screen_mode == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM: + # Screen mode 2 renders the reference commit's mode-0 default + # system screen without inheriting the current report fallback. + return CLUSTER_SCREEN_MODE_DEFAULT if self.screen_mode != CLUSTER_SCREEN_MODE_DEFAULT: return self.screen_mode dashboard = getattr(state, "navi_dashboard", None) diff --git a/openpilot/selfdrive/carrot/cluster/main.py b/openpilot/selfdrive/carrot/cluster/main.py index 752f64df01..ec57e83fad 100644 --- a/openpilot/selfdrive/carrot/cluster/main.py +++ b/openpilot/selfdrive/carrot/cluster/main.py @@ -2318,7 +2318,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--screen-mode", default=None, - help=f"HUD screen override such as default, trip-report, or navi. Default reads {CLUSTER_SCREEN_MODE_PARAM}.", + help=f"HUD screen override: default, debug-system, trip-report, or navi; default reads {CLUSTER_SCREEN_MODE_PARAM}.", ) parser.add_argument( "--cluster-hud-mode", diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_config.py b/openpilot/selfdrive/carrot/tests/test_cluster_config.py index 16231d2b24..15854ce834 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_config.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_config.py @@ -8,6 +8,7 @@ from cluster_config import ( CLUSTER_PANEL_LAYOUT_DRIVING_LEFT, CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT, + CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, CLUSTER_SCREEN_MODE_TRIP_REPORT, normalize_cluster_live_fps, normalize_cluster_panel_layout, @@ -44,6 +45,13 @@ def test_cluster_screen_mode_five_selects_trip_report(): assert normalize_cluster_screen_mode("navi-debug") == 0 +def test_cluster_screen_mode_two_selects_debug_system(): + assert CLUSTER_SCREEN_MODE_DEBUG_SYSTEM == 2 + assert normalize_cluster_screen_mode(2) == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM + assert normalize_cluster_screen_mode("system") == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM + assert normalize_cluster_screen_mode("debug_system") == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM + + def test_cluster_panel_layout_accepts_named_and_numeric_sides(): assert normalize_cluster_panel_layout(0) == CLUSTER_PANEL_LAYOUT_DRIVING_LEFT assert normalize_cluster_panel_layout("driving-left") == CLUSTER_PANEL_LAYOUT_DRIVING_LEFT diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py index f9d7c43bf5..793289c745 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py @@ -8,10 +8,12 @@ CLUSTER_DIR = Path(__file__).resolve().parents[1] / "cluster" sys.path.insert(0, str(CLUSTER_DIR)) +import cluster_renderer from cluster_config import ( CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT, CLUSTER_SCREEN_MODE_DEFAULT, CLUSTER_SCREEN_MODE_DEBUG_GRAPH, + CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, CLUSTER_SCREEN_MODE_TRIP_REPORT, normalize_cluster_screen_mode, ) @@ -116,13 +118,92 @@ def test_default_screen_uses_trip_report_until_navigation_is_received(): ) assert renderer._effective_screen_mode(connected) == CLUSTER_SCREEN_MODE_DEFAULT - legacy_navigation = SimpleNamespace( + external_navigation = SimpleNamespace( camera_view_mode=0, external_nav_active=True, navi_live=None, navi_dashboard=None, ) - assert renderer._effective_screen_mode(legacy_navigation) == CLUSTER_SCREEN_MODE_DEFAULT + assert renderer._effective_screen_mode(external_navigation) == CLUSTER_SCREEN_MODE_DEFAULT + + +def test_mode_two_preserves_the_reference_default_system_screen_contract(): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.height = 480 + renderer.screen_mode = CLUSTER_SCREEN_MODE_DEBUG_SYSTEM + disconnected = SimpleNamespace( + camera_view_mode=0, + external_nav_active=False, + navi_live=None, + navi_dashboard=SimpleNamespace(connected=False), + ) + + assert renderer._effective_screen_mode(disconnected) == CLUSTER_SCREEN_MODE_DEFAULT + assert renderer._world_view_shift_x(disconnected) == NAVI_WORLD_VIEW_SHIFT_X + + no_navigation_source = SimpleNamespace( + camera_view_mode=0, + external_nav_active=False, + navi_live=None, + navi_dashboard=None, + ) + assert renderer._effective_screen_mode(no_navigation_source) == CLUSTER_SCREEN_MODE_DEFAULT + assert renderer._world_view_shift_x(no_navigation_source) == 0.0 + + +def test_mode_two_dispatches_reference_default_system_content(monkeypatch): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.height = 480 + renderer.screen_mode = CLUSTER_SCREEN_MODE_DEBUG_SYSTEM + calls = [] + route_overlay = object() + + monkeypatch.setattr(cluster_renderer.rl, "rl_push_matrix", lambda: None) + monkeypatch.setattr(cluster_renderer.rl, "rl_scalef", lambda *_args: None) + monkeypatch.setattr(cluster_renderer.rl, "rl_pop_matrix", lambda: None) + monkeypatch.setattr(renderer, "_profile_start", lambda: 0.0) + monkeypatch.setattr(renderer, "_profile_add", lambda *_args: None) + monkeypatch.setattr( + renderer, + "_draw_driving_hud_content", + lambda _state, mode, *_signals: calls.append(("driving", mode)), + ) + monkeypatch.setattr(renderer, "_draw_navi_live_panel", lambda _state: calls.append(("navi", None))) + monkeypatch.setattr(renderer, "_draw_route_overlay", lambda overlay: calls.append(("route", overlay))) + monkeypatch.setattr( + renderer, + "_draw_status_footer", + lambda _state, **kwargs: calls.append(("footer", kwargs.get("include_core_usage"))), + ) + + no_navigation_source = SimpleNamespace( + navi_debug=None, + navi_live=None, + navi_dashboard=None, + route_overlay=route_overlay, + ) + renderer._draw_hud(no_navigation_source, (False, False)) + assert calls == [ + ("driving", CLUSTER_SCREEN_MODE_DEFAULT), + ("route", route_overlay), + ("footer", True), + ] + + calls.clear() + disconnected_dashboard = SimpleNamespace( + navi_debug=None, + navi_live=None, + navi_dashboard=SimpleNamespace(connected=False), + route_overlay=route_overlay, + ) + renderer._draw_hud(disconnected_dashboard, (False, False)) + assert calls == [ + ("driving", CLUSTER_SCREEN_MODE_DEFAULT), + ("navi", None), + ("footer", True), + ] def test_trip_report_footer_keeps_left_status_without_right_core_usage(monkeypatch): diff --git a/openpilot/selfdrive/carrot_settings.json b/openpilot/selfdrive/carrot_settings.json index c60c196b70..d7209aef29 100644 --- a/openpilot/selfdrive/carrot_settings.json +++ b/openpilot/selfdrive/carrot_settings.json @@ -3358,13 +3358,13 @@ "group": "외부 HUD", "name": "ClusterHudScreenMode", "title": "외부 HUD 화면 모드(0)", - "descr": "0: 기본\n1: 디버그\n2: 디버그(시스템)\n3: 디버그(그래프1)\n4: 디버그(그래프2)\n5: 주행 리포트", + "descr": "0: 기본(내비/주행 리포트)\n1: 디버그\n2: 디버그(시스템)\n3: 디버그(그래프1)\n4: 디버그(그래프2)\n5: 주행 리포트", "egroup": "CLUSTER HUD", "etitle": "External HUD Screen Mode(0)", - "edescr": "0: Default\n1: Debug\n2: Debug (system)\n3: Debug (graph1)\n4: Debug (graph2)\n5: Driving Report", + "edescr": "0: Default (navigation/driving report)\n1: Debug\n2: Debug (system)\n3: Debug (graph1)\n4: Debug (graph2)\n5: Driving Report", "cgroup": "外部 HUD", "ctitle": "外部 HUD 画面模式(0)", - "cdescr": "0: 默认\n1: 调试\n2: 调试(系统)\n3: 调试(图表1)\n4: 调试(图表2)\n5: 驾驶报告", + "cdescr": "0: 默认(导航/驾驶报告)\n1: 调试\n2: 调试(系统)\n3: 调试(图表1)\n4: 调试(图表2)\n5: 驾驶报告", "min": 0, "max": 5, "default": 0, From de5a5312d950668b612c063019ef8361a3ec1e44 Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 31 Jul 2026 23:11:43 +0900 Subject: [PATCH 2/4] Add fullscreen 3D mode for Cluster HUD Added `ClusterHudScreenMode=-1` to enable fullscreen 3D mode on the cluster HUD, removing information panels and expanding the 3D scene across the entire display (1920x480). Mode `-1` also aligns HUD widgets precisely while maintaining existing behaviors in road-camera mode. Includes tests, localized labels, and documentation updates, with no impact on Android, APK, or network transport. --- openpilot/selfdrive/carrot/cluster/README.md | 17 ++- .../carrot/cluster/cluster_config.py | 6 + .../carrot/cluster/cluster_renderer.py | 103 ++++++++++++--- openpilot/selfdrive/carrot/cluster/main.py | 2 +- .../server/tests/test_settings_schema.py | 5 + .../carrot/tests/test_cluster_config.py | 8 ++ .../carrot/tests/test_cluster_trip_report.py | 120 ++++++++++++++++++ openpilot/selfdrive/carrot_settings.json | 8 +- 8 files changed, 241 insertions(+), 28 deletions(-) diff --git a/openpilot/selfdrive/carrot/cluster/README.md b/openpilot/selfdrive/carrot/cluster/README.md index 3a49f59d85..5180e87740 100644 --- a/openpilot/selfdrive/carrot/cluster/README.md +++ b/openpilot/selfdrive/carrot/cluster/README.md @@ -451,10 +451,13 @@ intermediate copy. They also report the availability of `async_pbo` and `auto` falls back to ffmpeg, the run uses the software RGBA pipe. Changing this setting while the HUD is running makes the current HUD process exit so `cluster_autorun` can relaunch it with the new encoder choice. -`ClusterHudScreenMode` controls the right-side content: `0` is the default mode -that switches between navigation and the driving report, `1` shows the live -debug panel with grouped `LIVE DELAY`, `LIVE TORQUE`, `STEERING`, and -`LATERAL PLAN` rows, `2` is the system-debug slot rendering commit +`ClusterHudScreenMode` controls the right-side content. `-1` uses the entire +display for either 3D camera view, suppresses information panels, and balances +the clock, side gauges, TPMS, traffic image, turn signals, and status footer +across the full width. In road-camera view it behaves exactly like mode `0`. +Mode `0` is the default mode that switches between navigation and the driving +report, `1` shows the live debug panel with grouped `LIVE DELAY`, `LIVE TORQUE`, +`STEERING`, and `LATERAL PLAN` rows, `2` is the system-debug slot rendering commit `c0a6773f794a5e4e86aeca8e14515232abc26b1b`'s mode-0 default system screen, `3` shows a large debug graph selected by `ShowPlotMode` with the driving scene disabled, and `4` shows the same graph in the information panel while keeping @@ -488,6 +491,12 @@ starts. System-debug mode (`2`) reproduces the reference commit's mode-0 system screen and does not use the current automatic report fallback. It keeps the navigation/disconnected-system panel whenever a navigation dashboard exists, and falls back to the route overlay only when no navigation panel source exists. +Fullscreen-3D mode (`-1`) never reserves a navigation/report panel in either 3D +camera view, even when Navi data is available. Switching to road-camera view +re-enables the complete mode-0 panel selection and panel-layout behavior. +Left-edge HUD items keep their normal margins, right-edge gauges and TPMS keep +their small-3D-view right margins at the physical display edge, and the clock, +world, and turn signals use the full-display center axis. Mode `5` keeps the branch, network address, and frame-rate status in the lower-left camera area while omitting the lower-right core-usage text that would overlap the report. In road-camera view, ungrouped radar detections are projected diff --git a/openpilot/selfdrive/carrot/cluster/cluster_config.py b/openpilot/selfdrive/carrot/cluster/cluster_config.py index 801bb02f08..802e906b8e 100644 --- a/openpilot/selfdrive/carrot/cluster/cluster_config.py +++ b/openpilot/selfdrive/carrot/cluster/cluster_config.py @@ -76,6 +76,7 @@ class ClusterTheme: CLUSTER_PANEL_LAYOUT_DRIVING_LEFT = 0 CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT = 1 CLUSTER_PANEL_LAYOUT_PARAM = "ClusterHudPanelLayout" +CLUSTER_SCREEN_MODE_FULLSCREEN_3D = -1 CLUSTER_SCREEN_MODE_DEFAULT = 0 CLUSTER_SCREEN_MODE_DEBUG = 1 CLUSTER_SCREEN_MODE_DEBUG_SYSTEM = 2 @@ -385,6 +386,10 @@ def normalize_cluster_screen_mode(value: object) -> int: if isinstance(value, str): normalized = value.strip().lower() aliases = { + "3d-fullscreen": CLUSTER_SCREEN_MODE_FULLSCREEN_3D, + "3d_fullscreen": CLUSTER_SCREEN_MODE_FULLSCREEN_3D, + "fullscreen-3d": CLUSTER_SCREEN_MODE_FULLSCREEN_3D, + "fullscreen_3d": CLUSTER_SCREEN_MODE_FULLSCREEN_3D, "default": CLUSTER_SCREEN_MODE_DEFAULT, "debug": CLUSTER_SCREEN_MODE_DEBUG, "system": CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, @@ -418,6 +423,7 @@ def normalize_cluster_screen_mode(value: object) -> int: except (TypeError, ValueError): return CLUSTER_SCREEN_MODE_DEFAULT if mode in ( + CLUSTER_SCREEN_MODE_FULLSCREEN_3D, CLUSTER_SCREEN_MODE_DEFAULT, CLUSTER_SCREEN_MODE_DEBUG, CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, diff --git a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py index 1032d8e7ea..1a1c92bdb6 100644 --- a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py +++ b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py @@ -45,6 +45,7 @@ CLUSTER_SCREEN_MODE_DEBUG, CLUSTER_SCREEN_MODE_DEBUG_GRAPH, CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT, + CLUSTER_SCREEN_MODE_FULLSCREEN_3D, CLUSTER_SCREEN_MODE_NAVI, CLUSTER_SCREEN_MODE_TRIP_REPORT, CLUSTER_SCREEN_MODE_DEFAULT, @@ -290,6 +291,11 @@ NAVI_LIVE_PANEL_Y = 1 NAVI_LIVE_PANEL_H = DESIGN_HEIGHT - 2 NAVI_WORLD_VIEW_SHIFT_X = (DESIGN_WIDTH - NAVI_LIVE_PANEL_X) * 0.5 +# Preserve each right-side widget's distance from the driving-view edge when +# expanding the 1124 px driving region to the full 1920 px display. +FULLSCREEN_3D_SIDE_WIDGET_OFFSET_X = DESIGN_WIDTH - CAMERA_BACKGROUND_W +FULLSCREEN_3D_CLOCK_CENTER_X = DESIGN_WIDTH * 0.5 +FULLSCREEN_3D_TRAFFIC_PANEL_RIGHT = DESIGN_WIDTH - 28.0 NAVI_MAP_BACKGROUND = (0, 0, 0, 255) TPMS_STATUS_CENTER_X = NAVI_LIVE_PANEL_X - 62.0 TPMS_STATUS_VALUE_CENTER_Y = 429.5 @@ -302,6 +308,12 @@ TPMS_STATUS_WHEEL_H = 30.0 TPMS_STATUS_ICON_W = 104.0 TPMS_STATUS_ICON_H = 78.0 +FULLSCREEN_3D_CORE_USAGE_RIGHT_X = ( + TPMS_STATUS_CENTER_X + + FULLSCREEN_3D_SIDE_WIDGET_OFFSET_X + - TPMS_STATUS_ICON_W * 0.5 + - 18.0 +) TPMS_STATUS_FONT_SIZE = 21.0 NAVI_LIVE_ICON_X = NAVI_LIVE_PANEL_X + 72 NAVI_LIVE_ICON_Y = NAVI_LIVE_PANEL_Y + 99 @@ -984,6 +996,32 @@ def _information_panel_offset_design_x(self) -> float: def _information_panel_x(self, x: float) -> float: return x + self._information_panel_offset_design_x() + def _driving_hud_offset_design_x(self, screen_mode: int) -> float: + if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + return 0.0 + return self._driving_panel_offset_design_x() + + @staticmethod + def _side_widget_offset_design_x(screen_mode: int) -> float: + return FULLSCREEN_3D_SIDE_WIDGET_OFFSET_X if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D else 0.0 + + @staticmethod + def _center_clock_x(screen_mode: int) -> float: + return FULLSCREEN_3D_CLOCK_CENTER_X if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D else TOP_CLOCK_CENTER_X + + def _traffic_panel_right(self, screen_mode: int) -> float: + if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + return FULLSCREEN_3D_TRAFFIC_PANEL_RIGHT + right = NAVI_TRAFFIC_PANEL_RIGHT + if self._panel_swap_active(): + right -= NAVI_WORLD_VIEW_SHIFT_X + return right + + def _core_usage_right_x(self, screen_mode: int) -> float: + if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + return FULLSCREEN_3D_CORE_USAGE_RIGHT_X + return DESIGN_WIDTH + self._information_panel_offset_design_x() + def set_target_fps(self, target_fps: int) -> None: self.target_fps = max(0, int(target_fps)) if self._window_open: @@ -2805,6 +2843,8 @@ def _turn_signal_center_x_offset(self, state: ClusterUiState, side: str) -> floa signal_center_x = ( LANE_TURN_SIGNAL_LEFT_CENTER_X if side == "left" else LANE_TURN_SIGNAL_RIGHT_CENTER_X ) + if self._effective_screen_mode(state) == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + return 0.0 if state.camera_view_mode == CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA: camera_signal_center_x = CAMERA_BACKGROUND_X + signal_center_x * CAMERA_BACKGROUND_W / DESIGN_WIDTH return camera_signal_center_x - signal_center_x @@ -3532,6 +3572,9 @@ def _draw_hud(self, state: ClusterUiState, signal_lights: tuple[bool, bool] | No left_signal_lit, right_signal_lit, ) + if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + self._draw_status_footer(state) + return if screen_mode == CLUSTER_SCREEN_MODE_DEBUG: profile_stage = self._profile_start() self._draw_live_debug_panel(state) @@ -3583,18 +3626,27 @@ def _draw_driving_hud_content( left_signal_lit: bool, right_signal_lit: bool, ) -> None: - offset_x = self._driving_panel_offset_design_x() + offset_x = self._driving_hud_offset_design_x(screen_mode) + side_widget_offset_x = self._side_widget_offset_design_x(screen_mode) rl.rl_push_matrix() if abs(offset_x) > 0.001: rl.rl_translatef(offset_x, 0.0, 0.0) try: profile_stage = self._profile_start() - self._draw_speed_block(state) + self._draw_speed_block(state, tpms_offset_x=side_widget_offset_x) self._profile_add("hud.speed_block", profile_stage) - profile_stage = self._profile_start() - self._draw_accel_block(state) - self._profile_add("hud.accel_block", profile_stage) - self._draw_steering_output_block(state) + side_widgets_translated = abs(side_widget_offset_x) > 0.001 + if side_widgets_translated: + rl.rl_push_matrix() + rl.rl_translatef(side_widget_offset_x, 0.0, 0.0) + try: + profile_stage = self._profile_start() + self._draw_accel_block(state) + self._profile_add("hud.accel_block", profile_stage) + self._draw_steering_output_block(state) + finally: + if side_widgets_translated: + rl.rl_pop_matrix() profile_stage = self._profile_start() self._draw_turn_signal( "left", @@ -3628,9 +3680,7 @@ def _draw_driving_hud_content( and screen_mode != CLUSTER_SCREEN_MODE_TRIP_REPORT ): profile_stage = self._profile_start() - traffic_panel_right = NAVI_TRAFFIC_PANEL_RIGHT - if self._panel_swap_active(): - traffic_panel_right -= NAVI_WORLD_VIEW_SHIFT_X + traffic_panel_right = self._traffic_panel_right(screen_mode) self._draw_navi_media( traffic_frame, rl.Rectangle(traffic_panel_right - 230.0, NAVI_TRAFFIC_PANEL_Y, 230.0, 98.0), @@ -3639,18 +3689,24 @@ def _draw_driving_hud_content( ) self._profile_add("hud.navi_traffic", profile_stage) profile_stage = self._profile_start() - self._draw_center_clock(state) + self._draw_center_clock(state, center_x=self._center_clock_x(screen_mode)) self._profile_add("hud.center_clock", profile_stage) finally: rl.rl_pop_matrix() def _effective_screen_mode(self, state: ClusterUiState) -> int: - if self.screen_mode == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM: + requested_screen_mode = getattr(self, "screen_mode", CLUSTER_SCREEN_MODE_DEFAULT) + if ( + requested_screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D + and getattr(state, "camera_view_mode", 0) == CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA + ): + requested_screen_mode = CLUSTER_SCREEN_MODE_DEFAULT + if requested_screen_mode == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM: # Screen mode 2 renders the reference commit's mode-0 default # system screen without inheriting the current report fallback. return CLUSTER_SCREEN_MODE_DEFAULT - if self.screen_mode != CLUSTER_SCREEN_MODE_DEFAULT: - return self.screen_mode + if requested_screen_mode != CLUSTER_SCREEN_MODE_DEFAULT: + return requested_screen_mode dashboard = getattr(state, "navi_dashboard", None) dashboard_connected = bool( dashboard is not None and dashboard.connected @@ -3673,7 +3729,8 @@ def _draw_status_footer( include_core_usage: bool = True, ) -> None: profile_stage = self._profile_start() - offset_x = self._driving_panel_offset_design_x() + screen_mode = self._effective_screen_mode(state) + offset_x = self._driving_hud_offset_design_x(screen_mode) if abs(offset_x) > 0.001: rl.rl_push_matrix() try: @@ -3688,7 +3745,7 @@ def _draw_status_footer( profile_stage = self._profile_start() self._draw_cluster_core_usage( state.cluster_core_usage_text, - right_x=DESIGN_WIDTH + self._information_panel_offset_design_x(), + right_x=self._core_usage_right_x(screen_mode), ) self._profile_add("hud.cluster_core_usage", profile_stage) @@ -3781,11 +3838,11 @@ def _format_time(seconds: float) -> str: secs = total % 60 return f"{minutes:d}:{secs:02d}" - def _draw_center_clock(self, state: ClusterUiState) -> None: + def _draw_center_clock(self, state: ClusterUiState, *, center_x: float = TOP_CLOCK_CENTER_X) -> None: if state.center_clock_text: self._draw_text_with_stroke( state.center_clock_text, - TOP_CLOCK_CENTER_X, + center_x, TOP_STATUS_CENTER_Y, 48, WHITE, @@ -6487,7 +6544,7 @@ def _draw_lfa_status_icon(self, state: ClusterUiState, bottom_y: float) -> None: rl_color(outline, 210), ) - def _draw_speed_block(self, state: ClusterUiState) -> None: + def _draw_speed_block(self, state: ClusterUiState, *, tpms_offset_x: float = 0.0) -> None: theme = self._current_theme() display_speed_kph = state.display_speed_kph if state.display_speed_kph is not None else state.speed_kph speed_value = int(round(display_speed(clamp(display_speed_kph, 0.0, MAX_SPEED_KPH), self.is_metric))) @@ -6522,7 +6579,15 @@ def _draw_speed_block(self, state: ClusterUiState) -> None: self._draw_cruise_gap_badge(state.cruise_gap) self._draw_speed_gear_badge(state) self._draw_ev_mode_indicator(state) - self._draw_tpms_status(state) + tpms_translated = abs(tpms_offset_x) > 0.001 + if tpms_translated: + rl.rl_push_matrix() + rl.rl_translatef(tpms_offset_x, 0.0, 0.0) + try: + self._draw_tpms_status(state) + finally: + if tpms_translated: + rl.rl_pop_matrix() if self._cruise_set_visible(state) and state.cruise_override_kph is not None: override_color = ( diff --git a/openpilot/selfdrive/carrot/cluster/main.py b/openpilot/selfdrive/carrot/cluster/main.py index ec57e83fad..ebecb94040 100644 --- a/openpilot/selfdrive/carrot/cluster/main.py +++ b/openpilot/selfdrive/carrot/cluster/main.py @@ -2318,7 +2318,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--screen-mode", default=None, - help=f"HUD screen override: default, debug-system, trip-report, or navi; default reads {CLUSTER_SCREEN_MODE_PARAM}.", + help=f"HUD screen override: 3d-fullscreen, default, debug-system, trip-report, or navi; default reads {CLUSTER_SCREEN_MODE_PARAM}.", ) parser.add_argument( "--cluster-hud-mode", diff --git a/openpilot/selfdrive/carrot/server/tests/test_settings_schema.py b/openpilot/selfdrive/carrot/server/tests/test_settings_schema.py index 93c0ada481..dc5dcf91c9 100644 --- a/openpilot/selfdrive/carrot/server/tests/test_settings_schema.py +++ b/openpilot/selfdrive/carrot/server/tests/test_settings_schema.py @@ -70,6 +70,11 @@ def test_external_hud_brightness_and_orientation_use_catalog_controls(settings, "Driving left / info right", "Info left / driving right", ] + screen_mode = by_name["ClusterHudScreenMode"] + assert (screen_mode["min"], screen_mode["max"], screen_mode["default"]) == (-1, 5, 0) + assert screen_mode["descr"].startswith("-1: 3D 전체화면\n0: 기본(내비/주행 리포트)") + assert screen_mode["edescr"].startswith("-1: 3D fullscreen\n0: Default (navigation/driving report)") + assert screen_mode["cdescr"].startswith("-1: 3D全屏\n0: 默认(导航/驾驶报告)") display = next(category for category in settings["menu"] if category["id"] == "DISPLAY") hud = next(group for group in display["groups"] if group["id"] == "DISP_HUD") diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_config.py b/openpilot/selfdrive/carrot/tests/test_cluster_config.py index 15854ce834..1355a5bf3f 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_config.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_config.py @@ -9,6 +9,7 @@ CLUSTER_PANEL_LAYOUT_DRIVING_LEFT, CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT, CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, + CLUSTER_SCREEN_MODE_FULLSCREEN_3D, CLUSTER_SCREEN_MODE_TRIP_REPORT, normalize_cluster_live_fps, normalize_cluster_panel_layout, @@ -52,6 +53,13 @@ def test_cluster_screen_mode_two_selects_debug_system(): assert normalize_cluster_screen_mode("debug_system") == CLUSTER_SCREEN_MODE_DEBUG_SYSTEM +def test_cluster_screen_mode_minus_one_selects_fullscreen_3d(): + assert CLUSTER_SCREEN_MODE_FULLSCREEN_3D == -1 + assert normalize_cluster_screen_mode(-1) == CLUSTER_SCREEN_MODE_FULLSCREEN_3D + assert normalize_cluster_screen_mode("3d-fullscreen") == CLUSTER_SCREEN_MODE_FULLSCREEN_3D + assert normalize_cluster_screen_mode("fullscreen_3d") == CLUSTER_SCREEN_MODE_FULLSCREEN_3D + + def test_cluster_panel_layout_accepts_named_and_numeric_sides(): assert normalize_cluster_panel_layout(0) == CLUSTER_PANEL_LAYOUT_DRIVING_LEFT assert normalize_cluster_panel_layout("driving-left") == CLUSTER_PANEL_LAYOUT_DRIVING_LEFT diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py index 793289c745..22f17440c8 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py @@ -10,10 +10,12 @@ import cluster_renderer from cluster_config import ( + CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA, CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT, CLUSTER_SCREEN_MODE_DEFAULT, CLUSTER_SCREEN_MODE_DEBUG_GRAPH, CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, + CLUSTER_SCREEN_MODE_FULLSCREEN_3D, CLUSTER_SCREEN_MODE_TRIP_REPORT, normalize_cluster_screen_mode, ) @@ -206,6 +208,124 @@ def test_mode_two_dispatches_reference_default_system_content(monkeypatch): ] +def test_fullscreen_3d_mode_falls_back_to_mode_zero_outside_3d_views(): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.screen_mode = CLUSTER_SCREEN_MODE_FULLSCREEN_3D + + for camera_view_mode in (0, 1): + state = SimpleNamespace(camera_view_mode=camera_view_mode) + assert renderer._effective_screen_mode(state) == CLUSTER_SCREEN_MODE_FULLSCREEN_3D + assert renderer._world_view_shift_x(state) == 0.0 + assert renderer._turn_signal_center_x_offset(state, "left") == 0.0 + assert renderer._turn_signal_center_x_offset(state, "right") == 0.0 + + no_navigation = SimpleNamespace( + camera_view_mode=CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA, + external_nav_active=False, + navi_live=None, + navi_dashboard=None, + ) + assert renderer._effective_screen_mode(no_navigation) == CLUSTER_SCREEN_MODE_TRIP_REPORT + + connected_navigation = SimpleNamespace( + camera_view_mode=CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA, + external_nav_active=False, + navi_live=None, + navi_dashboard=SimpleNamespace(connected=True), + ) + assert renderer._effective_screen_mode(connected_navigation) == CLUSTER_SCREEN_MODE_DEFAULT + + +def test_fullscreen_3d_uses_full_width_hud_layout_even_when_panels_are_swapped(): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.height = 480 + renderer.screen_mode = CLUSTER_SCREEN_MODE_FULLSCREEN_3D + renderer.panel_layout = CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT + + fullscreen_offset_x = renderer._side_widget_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) + assert renderer._driving_hud_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 0.0 + assert fullscreen_offset_x == 796.0 + assert renderer._center_clock_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 960.0 + assert renderer._traffic_panel_right(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 1892.0 + assert renderer._core_usage_right_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 1788.0 + assert ( + cluster_renderer.DESIGN_WIDTH + - (cluster_renderer.SIDE_GAUGE_LEFT_CENTER_X + fullscreen_offset_x) + == cluster_renderer.CAMERA_BACKGROUND_W - cluster_renderer.SIDE_GAUGE_LEFT_CENTER_X + ) + assert ( + cluster_renderer.DESIGN_WIDTH + - (cluster_renderer.TPMS_STATUS_CENTER_X + fullscreen_offset_x) + == cluster_renderer.CAMERA_BACKGROUND_W - cluster_renderer.TPMS_STATUS_CENTER_X + ) + + road_camera_rect = renderer._camera_overlay_content_rect() + assert renderer._driving_hud_offset_design_x(CLUSTER_SCREEN_MODE_TRIP_REPORT) == 792.0 + assert road_camera_rect.x == 792.0 + assert road_camera_rect.width == CAMERA_BACKGROUND_W + + +def test_fullscreen_3d_repositions_hud_widgets_and_suppresses_information_panels(monkeypatch): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.height = 480 + renderer.screen_mode = CLUSTER_SCREEN_MODE_FULLSCREEN_3D + renderer.panel_layout = CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT + calls = [] + translations = [] + + monkeypatch.setattr(cluster_renderer.rl, "rl_push_matrix", lambda: None) + monkeypatch.setattr(cluster_renderer.rl, "rl_scalef", lambda *_args: None) + monkeypatch.setattr(cluster_renderer.rl, "rl_pop_matrix", lambda: None) + monkeypatch.setattr(cluster_renderer.rl, "rl_translatef", lambda *args: translations.append(args)) + monkeypatch.setattr(renderer, "_profile_start", lambda: 0.0) + monkeypatch.setattr(renderer, "_profile_add", lambda *_args: None) + monkeypatch.setattr( + renderer, + "_draw_speed_block", + lambda _state, **kwargs: calls.append(("speed", kwargs["tpms_offset_x"])), + ) + monkeypatch.setattr(renderer, "_draw_accel_block", lambda _state: calls.append(("accel", None))) + monkeypatch.setattr(renderer, "_draw_steering_output_block", lambda _state: calls.append(("steer", None))) + monkeypatch.setattr(renderer, "_draw_turn_signal", lambda *_args, **_kwargs: None) + monkeypatch.setattr(renderer, "_draw_drive_status", lambda _state: None) + monkeypatch.setattr( + renderer, + "_draw_center_clock", + lambda _state, **kwargs: calls.append(("clock", kwargs["center_x"])), + ) + + hud_state = SimpleNamespace(navi_dashboard=None, debug_ui_visible=False) + renderer._draw_driving_hud_content( + hud_state, + CLUSTER_SCREEN_MODE_FULLSCREEN_3D, + False, + False, + ) + assert calls == [("speed", 796), ("accel", None), ("steer", None), ("clock", 960)] + assert translations == [(796, 0.0, 0.0)] + + calls.clear() + state = SimpleNamespace( + camera_view_mode=0, + navi_debug=object(), + navi_live=None, + navi_dashboard=None, + ) + monkeypatch.setattr( + renderer, + "_draw_driving_hud_content", + lambda _state, mode, *_signals: calls.append(("driving", mode)), + ) + monkeypatch.setattr(renderer, "_draw_status_footer", lambda _state, **_kwargs: calls.append(("footer", None))) + monkeypatch.setattr(renderer, "_draw_navi_debug_panel", lambda _state: calls.append(("navi-debug", None))) + monkeypatch.setattr(renderer, "_draw_route_overlay", lambda _overlay: calls.append(("route", None))) + renderer._draw_hud(state, (False, False)) + assert calls == [("driving", CLUSTER_SCREEN_MODE_FULLSCREEN_3D), ("footer", None)] + + def test_trip_report_footer_keeps_left_status_without_right_core_usage(monkeypatch): renderer = object.__new__(ClusterUiRenderer) calls = [] diff --git a/openpilot/selfdrive/carrot_settings.json b/openpilot/selfdrive/carrot_settings.json index d7209aef29..6c15114117 100644 --- a/openpilot/selfdrive/carrot_settings.json +++ b/openpilot/selfdrive/carrot_settings.json @@ -3358,14 +3358,14 @@ "group": "외부 HUD", "name": "ClusterHudScreenMode", "title": "외부 HUD 화면 모드(0)", - "descr": "0: 기본(내비/주행 리포트)\n1: 디버그\n2: 디버그(시스템)\n3: 디버그(그래프1)\n4: 디버그(그래프2)\n5: 주행 리포트", + "descr": "-1: 3D 전체화면\n0: 기본(내비/주행 리포트)\n1: 디버그\n2: 디버그(시스템)\n3: 디버그(그래프1)\n4: 디버그(그래프2)\n5: 주행 리포트", "egroup": "CLUSTER HUD", "etitle": "External HUD Screen Mode(0)", - "edescr": "0: Default (navigation/driving report)\n1: Debug\n2: Debug (system)\n3: Debug (graph1)\n4: Debug (graph2)\n5: Driving Report", + "edescr": "-1: 3D fullscreen\n0: Default (navigation/driving report)\n1: Debug\n2: Debug (system)\n3: Debug (graph1)\n4: Debug (graph2)\n5: Driving Report", "cgroup": "外部 HUD", "ctitle": "外部 HUD 画面模式(0)", - "cdescr": "0: 默认(导航/驾驶报告)\n1: 调试\n2: 调试(系统)\n3: 调试(图表1)\n4: 调试(图表2)\n5: 驾驶报告", - "min": 0, + "cdescr": "-1: 3D全屏\n0: 默认(导航/驾驶报告)\n1: 调试\n2: 调试(系统)\n3: 调试(图表1)\n4: 调试(图表2)\n5: 驾驶报告", + "min": -1, "max": 5, "default": 0, "unit": 1, From 04c66e6292644e6cdeb3523eeae9fef404dd8bcb Mon Sep 17 00:00:00 2001 From: thftgr Date: Fri, 31 Jul 2026 23:57:50 +0900 Subject: [PATCH 3/4] Add fullscreen 3D mode for Cluster HUD Added `ClusterHudScreenMode=-1` to enable fullscreen 3D mode on the cluster HUD, removing information panels and expanding the 3D scene across the entire display (1920x480). Mode `-1` also aligns HUD widgets precisely while maintaining existing behaviors in road-camera mode. Includes tests, localized labels, and documentation updates, with no impact on Android, APK, or network transport. --- docs/user/en/settings.md | 10 ++++++ docs/user/ko/settings.md | 10 ++++++ openpilot/selfdrive/carrot/cluster/README.md | 5 ++- .../carrot/cluster/cluster_renderer.py | 35 +++++++++++++++---- .../carrot/tests/test_cluster_trip_report.py | 26 +++++++++++++- .../docs/wiki_settings/tests/test_ci_check.py | 2 +- .../docs/wiki_settings/tests/test_generate.py | 8 ++--- 7 files changed, 82 insertions(+), 14 deletions(-) diff --git a/docs/user/en/settings.md b/docs/user/en/settings.md index 7af6f2224a..94f7fa7e76 100644 --- a/docs/user/en/settings.md +++ b/docs/user/en/settings.md @@ -233,6 +233,16 @@ An APN label remaining in the `ShowRouteInfo` description refers to route-input While the external HUD is connected over USB, the on-device driving view on both regular C3/C3X hardware and mici switches to a black background and skips camera-video and model-path rendering. Speed, speed limit, driver state, alerts, and the driving-state border remain visible on the device, and the camera view returns automatically when the external HUD disconnects. `camerad` and model input continue running; only duplicate rendering on the device display is reduced. +The final `ClusterHudScreenMode` layout is: + +- `-1` uses the full width only in 3D camera views `0` and `1`, with no information panel or world shift. Left-side HUD items retain their margins, right-side gauges and TPMS align to the physical right edge, and the clock, world, and turn signals use the full-display center axis. In road-camera view `2`, it behaves exactly like mode `0`, including automatic navigation/report selection and `ClusterHudPanelLayout`. +- `0` is the default screen. It shows navigation while live navigation is received and automatically shows the driving report otherwise. +- `1` is the general live-debug panel for grouped delay, torque, steering, and lateral-plan state. +- `2` is the reference system screen. It does not inherit mode `0`'s automatic report fallback: it keeps navigation or `NAVI DISCONNECTED` when navigation state exists and falls back to the route overlay only when no navigation source exists. +- `3` disables the driving scene and shows the `ShowPlotMode` graph at large size. +- `4` keeps the driving scene and shows the same graph in the information panel. The acceleration, steering, fuel, and DEF gauges sit immediately to the graph's left with an 18 px gap instead of near the display center, and follow the graph when the panel sides are swapped. TPMS remains with the driving view. +- `5` always shows the driving report. + `ClusterHudScreenMode=5` shows a live driving report in the information panel. In default screen mode (`0`), the same report is shown automatically while no live navigation is being received, and the navigation panel returns when reception starts. Its large card summarizes driving time, distance, average and maximum speed, the automated-driving ratio, maximum acceleration/deceleration, and hard acceleration/braking/corner counts. The small card presents CPU load, temperature, memory, and disk use as a 2×2 set of circular gauges. Its lower target plots the stored device pitch (P) vertically and yaw (Y) horizontally relative to the calibrated center while retaining the numeric angles. The driving area retains the branch, network address, and frame-rate status; the core-usage text is omitted when it would overlap the report. In road-camera view, detected vehicles are enclosed by transparent rounded frames whose border retains the existing detection color; ungrouped radar detections use smaller transparent rounded markers in their source color. Vehicle frames use one lightweight outline, and frames that would be partially projected at the screen edge or stretched by a noisy radar heading are omitted. The external HUD follows the device `LanguageSetting` and updates driving-report, driving-mode, and navigation status labels live in Korean (`ko`) or English (`en`). Other language values, including Chinese, fall back to English. With `IsMetric` enabled, vehicle/cruise/limit speeds, navigation, radar labels, and the driving report use `km/h`, `m`, and `km`; with it disabled they are converted to `mph`, `ft`, and `mi`. Acceleration and temperature remain `m/s²` and `°C`. Both settings are polled about once per second and do not require a HUD restart. diff --git a/docs/user/ko/settings.md b/docs/user/ko/settings.md index 4344076cf6..2fa6a16b51 100644 --- a/docs/user/ko/settings.md +++ b/docs/user/ko/settings.md @@ -244,6 +244,16 @@ Carrot Web 설정 화면에서는 다음 기능을 사용할 수 있습니다. 외부 HUD가 USB로 연결된 동안 일반 C3/C3X와 mici의 본체 주행 화면은 검은 배경으로 전환하고 카메라 영상과 모델 경로 렌더링을 생략합니다. 속도·제한속도·운전자 상태·경고·주행 상태 테두리 등 본체 HUD는 계속 표시되며, 외부 HUD 연결이 끊기면 카메라 화면이 자동으로 복귀합니다. `camerad`와 모델 입력은 계속 동작하고 본체 화면의 중복 렌더링만 줄입니다. +`ClusterHudScreenMode`의 최종 화면 구성은 다음과 같습니다. + +- `-1`은 카메라 뷰 `0`, `1`의 3D 화면에서만 정보 패널과 월드 이동을 제거하고 전체 폭을 사용합니다. 왼쪽 HUD는 기존 여백을 유지하고, 오른쪽 게이지·TPMS는 물리 화면 오른쪽 여백에 맞추며, 시계·월드·방향지시등은 전체 화면 중앙축을 사용합니다. 로드카메라 뷰 `2`에서는 내비 유무에 따른 자동 주행 리포트와 `ClusterHudPanelLayout`까지 `0`과 완전히 동일하게 동작합니다. +- `0`은 기본 화면입니다. 내비가 수신되면 내비 패널을, 수신되지 않으면 주행 리포트를 자동으로 표시합니다. +- `1`은 라이브 지연·토크·조향·횡방향 계획을 묶은 일반 디버그 패널입니다. +- `2`는 기준 시스템 화면입니다. 현재 `0`의 자동 주행 리포트 전환은 사용하지 않고, 내비 상태가 있으면 내비 또는 `NAVI DISCONNECTED` 패널을 유지하며 내비 소스가 없을 때만 경로 오버레이로 폴백합니다. +- `3`은 주행 장면을 끄고 `ShowPlotMode` 그래프를 크게 표시합니다. +- `4`는 주행 장면을 유지한 채 정보 패널에 같은 그래프를 표시합니다. 가속·조향·연료·요소수 게이지는 중앙이 아니라 그래프 바로 왼쪽에 18px 간격으로 붙고, 좌우 패널을 바꿔도 그래프와 함께 이동합니다. TPMS는 주행 화면 쪽 위치를 유지합니다. +- `5`는 주행 리포트를 항상 표시합니다. + `ClusterHudScreenMode=5`는 정보 패널에 실시간 주행 리포트를 표시합니다. 기본 화면 모드(`0`)에서도 실시간 내비가 수신되지 않는 동안에는 이 리포트가 자동으로 표시되고, 수신이 시작되면 내비 패널로 돌아갑니다. 넓은 카드에는 주행 시간·거리·평균/최고 속도·자동주행 비율·최대 가감속·급가감속/급코너 횟수를 큰 글씨로 표시합니다. 작은 카드는 CPU·온도·메모리·디스크 사용량을 2×2 원형 게이지로 표시하고, 하단 타깃은 저장된 디바이스 피치(P)와 요(Y)를 정상 중심점 대비 상하·좌우 방향으로 시각화하면서 각도 숫자도 함께 표시합니다. 주행 화면 아래에는 브랜치·네트워크 주소·프레임률 상태를 계속 표시하고, 리포트와 겹치는 코어 사용량 문구만 생략합니다. 로드카메라 화면에서는 감지 차량을 내부가 투명한 라운드 사각 프레임으로 감싸고 기존 감지 색상을 테두리에 적용합니다. 그룹화되지 않은 레이더 감지점은 같은 방식의 작은 소스 색상 표식으로 표시합니다. 차량 프레임은 가벼운 단일 테두리로 그리고, 화면 경계에서 일부만 투영되거나 레이더 방위값 때문에 비정상적으로 길어지는 프레임은 표시하지 않습니다. 외부 HUD는 기기의 `LanguageSetting`을 따라 주행 리포트, 주행 모드와 내비 상태 문구를 한글(`ko`) 또는 영어(`en`)로 실시간 전환합니다. 중국어를 포함한 그 밖의 언어값은 영문으로 표시합니다. `IsMetric`이 켜져 있으면 현재/설정/제한 속도, 내비, 레이더 라벨과 주행 리포트를 `km/h`, `m`, `km`로 표시하고, 꺼져 있으면 `mph`, `ft`, `mi`로 변환합니다. 가속도와 온도는 각각 `m/s²`, `°C`를 유지합니다. 두 설정은 약 1초마다 확인하므로 HUD를 다시 시작하지 않아도 적용됩니다. diff --git a/openpilot/selfdrive/carrot/cluster/README.md b/openpilot/selfdrive/carrot/cluster/README.md index 5180e87740..61cc35a3f9 100644 --- a/openpilot/selfdrive/carrot/cluster/README.md +++ b/openpilot/selfdrive/carrot/cluster/README.md @@ -461,7 +461,10 @@ report, `1` shows the live debug panel with grouped `LIVE DELAY`, `LIVE TORQUE`, `c0a6773f794a5e4e86aeca8e14515232abc26b1b`'s mode-0 default system screen, `3` shows a large debug graph selected by `ShowPlotMode` with the driving scene disabled, and `4` shows the same graph in the information panel while keeping -the driving scene. +the driving scene. Mode `4` keeps the acceleration, steering, fuel, and DEF +gauges immediately to the left of the graph instead of near the center of the +driving view; the gauge block follows the graph when the panel layout is +swapped, while TPMS remains with the driving view. `5` shows the driving report in the information panel while keeping the driving scene. The report uses a large trip/event summary card and a separate system-load card with four 2-by-2 circular gauges. A lower target plots stored calibration pitch diff --git a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py index 1a1c92bdb6..fd7a0daa90 100644 --- a/openpilot/selfdrive/carrot/cluster/cluster_renderer.py +++ b/openpilot/selfdrive/carrot/cluster/cluster_renderer.py @@ -368,6 +368,7 @@ DEBUG_PLOT_RIGHT_Y = DEBUG_PLOT_MARGIN DEBUG_PLOT_RIGHT_W = SYSTEM_PANEL_W DEBUG_PLOT_RIGHT_H = DESIGN_HEIGHT - DEBUG_PLOT_MARGIN * 2.0 +DEBUG_PLOT_SIDE_GAUGE_GAP = DEBUG_PLOT_MARGIN GIT_STATUS_MARGIN = 2 GIT_STATUS_BOTTOM_MARGIN = 12 GIT_STATUS_DOT_RADIUS = 7 @@ -1002,9 +1003,28 @@ def _driving_hud_offset_design_x(self, screen_mode: int) -> float: return self._driving_panel_offset_design_x() @staticmethod - def _side_widget_offset_design_x(screen_mode: int) -> float: + def _tpms_offset_design_x(screen_mode: int) -> float: return FULLSCREEN_3D_SIDE_WIDGET_OFFSET_X if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D else 0.0 + def _side_gauge_offset_design_x(self, screen_mode: int) -> float: + if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D: + return FULLSCREEN_3D_SIDE_WIDGET_OFFSET_X + if screen_mode != CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT: + return 0.0 + + plot_x = self._information_panel_x(DEBUG_PLOT_RIGHT_X) + desired_left_center_x = ( + plot_x + - DEBUG_PLOT_SIDE_GAUGE_GAP + - SIDE_GAUGE_WIDTH * 0.5 + - SIDE_GAUGE_COLUMN_GAP + ) + return ( + desired_left_center_x + - self._driving_hud_offset_design_x(screen_mode) + - SIDE_GAUGE_LEFT_CENTER_X + ) + @staticmethod def _center_clock_x(screen_mode: int) -> float: return FULLSCREEN_3D_CLOCK_CENTER_X if screen_mode == CLUSTER_SCREEN_MODE_FULLSCREEN_3D else TOP_CLOCK_CENTER_X @@ -3627,25 +3647,26 @@ def _draw_driving_hud_content( right_signal_lit: bool, ) -> None: offset_x = self._driving_hud_offset_design_x(screen_mode) - side_widget_offset_x = self._side_widget_offset_design_x(screen_mode) + tpms_offset_x = self._tpms_offset_design_x(screen_mode) + side_gauge_offset_x = self._side_gauge_offset_design_x(screen_mode) rl.rl_push_matrix() if abs(offset_x) > 0.001: rl.rl_translatef(offset_x, 0.0, 0.0) try: profile_stage = self._profile_start() - self._draw_speed_block(state, tpms_offset_x=side_widget_offset_x) + self._draw_speed_block(state, tpms_offset_x=tpms_offset_x) self._profile_add("hud.speed_block", profile_stage) - side_widgets_translated = abs(side_widget_offset_x) > 0.001 - if side_widgets_translated: + side_gauges_translated = abs(side_gauge_offset_x) > 0.001 + if side_gauges_translated: rl.rl_push_matrix() - rl.rl_translatef(side_widget_offset_x, 0.0, 0.0) + rl.rl_translatef(side_gauge_offset_x, 0.0, 0.0) try: profile_stage = self._profile_start() self._draw_accel_block(state) self._profile_add("hud.accel_block", profile_stage) self._draw_steering_output_block(state) finally: - if side_widgets_translated: + if side_gauges_translated: rl.rl_pop_matrix() profile_stage = self._profile_start() self._draw_turn_signal( diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py index 22f17440c8..9fa167ba99 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py @@ -11,9 +11,11 @@ import cluster_renderer from cluster_config import ( CLUSTER_CAMERA_VIEW_MODE_ROAD_CAMERA, + CLUSTER_PANEL_LAYOUT_DRIVING_LEFT, CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT, CLUSTER_SCREEN_MODE_DEFAULT, CLUSTER_SCREEN_MODE_DEBUG_GRAPH, + CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT, CLUSTER_SCREEN_MODE_DEBUG_SYSTEM, CLUSTER_SCREEN_MODE_FULLSCREEN_3D, CLUSTER_SCREEN_MODE_TRIP_REPORT, @@ -244,9 +246,10 @@ def test_fullscreen_3d_uses_full_width_hud_layout_even_when_panels_are_swapped() renderer.screen_mode = CLUSTER_SCREEN_MODE_FULLSCREEN_3D renderer.panel_layout = CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT - fullscreen_offset_x = renderer._side_widget_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) + fullscreen_offset_x = renderer._side_gauge_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) assert renderer._driving_hud_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 0.0 assert fullscreen_offset_x == 796.0 + assert renderer._tpms_offset_design_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == fullscreen_offset_x assert renderer._center_clock_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 960.0 assert renderer._traffic_panel_right(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 1892.0 assert renderer._core_usage_right_x(CLUSTER_SCREEN_MODE_FULLSCREEN_3D) == 1788.0 @@ -267,6 +270,27 @@ def test_fullscreen_3d_uses_full_width_hud_layout_even_when_panels_are_swapped() assert road_camera_rect.width == CAMERA_BACKGROUND_W +def test_graph_right_mode_places_side_gauges_next_to_graph_in_both_panel_layouts(): + renderer = object.__new__(ClusterUiRenderer) + renderer.width = 1920 + renderer.height = 480 + renderer.screen_mode = CLUSTER_SCREEN_MODE_DEBUG_GRAPH_RIGHT + + for panel_layout in (CLUSTER_PANEL_LAYOUT_DRIVING_LEFT, CLUSTER_PANEL_LAYOUT_DRIVING_RIGHT): + renderer.panel_layout = panel_layout + driving_offset_x = renderer._driving_hud_offset_design_x(renderer.screen_mode) + gauge_offset_x = renderer._side_gauge_offset_design_x(renderer.screen_mode) + plot_x = renderer._information_panel_x(cluster_renderer.DEBUG_PLOT_RIGHT_X) + first_center_x = cluster_renderer.SIDE_GAUGE_LEFT_CENTER_X + driving_offset_x + gauge_offset_x + second_center_x = first_center_x + cluster_renderer.SIDE_GAUGE_COLUMN_GAP + gauge_half_width = cluster_renderer.SIDE_GAUGE_WIDTH * 0.5 + graph_gap = cluster_renderer.DEBUG_PLOT_SIDE_GAUGE_GAP + + assert second_center_x + gauge_half_width + graph_gap == plot_x + + assert renderer._tpms_offset_design_x(renderer.screen_mode) == 0.0 + + def test_fullscreen_3d_repositions_hud_widgets_and_suppresses_information_panels(monkeypatch): renderer = object.__new__(ClusterUiRenderer) renderer.width = 1920 diff --git a/tools/docs/wiki_settings/tests/test_ci_check.py b/tools/docs/wiki_settings/tests/test_ci_check.py index db332022bb..e6cd122697 100644 --- a/tools/docs/wiki_settings/tests/test_ci_check.py +++ b/tools/docs/wiki_settings/tests/test_ci_check.py @@ -47,7 +47,7 @@ def test_read_only_check_writes_reports_without_touching_wiki(self): self.assertTrue((output / "summary.md").is_file()) self.assertTrue((output / "pages.diff").is_file()) stored = json.loads((output / "summary.json").read_text(encoding="utf-8")) - self.assertEqual(stored["result"]["settings"], 169) + self.assertEqual(stored["result"]["settings"], 170) self.assertEqual(stored["validationIssues"], []) self.assertIn("Settings-Catalog.md", (output / "pages.diff").read_text(encoding="utf-8")) diff --git a/tools/docs/wiki_settings/tests/test_generate.py b/tools/docs/wiki_settings/tests/test_generate.py index 5ab6d8edfc..119c6ef17d 100644 --- a/tools/docs/wiki_settings/tests/test_generate.py +++ b/tools/docs/wiki_settings/tests/test_generate.py @@ -85,16 +85,16 @@ def test_current_catalog_generates_all_settings_and_valid_markdown(self): catalog_commit=COMMIT, generated_at=STAMP, ) - self.assertEqual(len(result.generated_settings), 169) - self.assertEqual(result.index["review"], {"current": 0, "needs_review": 169}) + self.assertEqual(len(result.generated_settings), 170) + self.assertEqual(result.index["review"], {"current": 0, "needs_review": 170}) self.assertEqual(result.index["locales"], ["ko", "en", "zh"]) - self.assertEqual(len(result.pages), (169 * 3) + 2) + self.assertEqual(len(result.pages), (170 * 3) + 2) setting_pages = { name: text for name, text in result.pages.items() if GENERATOR.GENERATED_PAGE_RE.fullmatch(name) } - self.assertEqual(len(setting_pages), 169 * 3) + self.assertEqual(len(setting_pages), 170 * 3) self.assertTrue(all(text.count("