diff --git a/docs/user/en/settings.md b/docs/user/en/settings.md index 7af6f2224a..f691e858e3 100644 --- a/docs/user/en/settings.md +++ b/docs/user/en/settings.md @@ -97,13 +97,13 @@ Ignoring `x0.01`, `x0.001`, `cm`, `km/h`, or `%` can make a value appear one hun ## Settings map -The current `carrot_settings.json` contains **169 parameters**. Every entry is assigned to one of these menus: +The current `carrot_settings.json` contains **171 parameters**. Every entry is assigned to one of these menus: | Category | Count | Groups | |---|---:|---| | Driving control | 107 | Startup and auto, buttons and presets, steering, speed and deceleration, cruise and following gap | | Vehicle and hardware | 16 | Hyundai/Kia, CAN FD/HDA, radar, driver monitoring, vehicle assistance, device hardware | -| Display | 36 | Information, path, brightness/on-road view, external HUD | +| Display | 37 | Information, path, brightness/on-road view, external HUD | | System | 11 | Recording/power, network/map, sound, software | ## Driving control @@ -216,22 +216,34 @@ See [Radar tracks and corner radar](radar.md) before changing radar modes. ## Display -Display contains 36 settings. Most on-road display settings are easy to reverse; external-HUD settings include hardware and performance choices. +Display contains 37 settings. Most on-road display settings are easy to reverse; external-HUD settings include hardware and performance choices. | Group | Parameters | Purpose | |---|---|---| | Information | `ShowDebugUI`, `ShowTpms`, `ShowDateTime`, `ShowPathEnd`, `ShowDeviceState`, `ShowLaneInfo`, `ShowRadarInfo`, `ShowRouteInfo`, `ShowPlotMode` | Debug, tire, time, lane, radar, and route information | | Path | `ShowPathMode`, `ShowPathColor`, `ShowPathColorCruiseOff`, `ShowPathModeLane`, `ShowPathColorLane` | Path shape and color by driving state | -| Brightness/on-road view | `ShowCustomBrightness`, `ShowModelView` | Brightness and camera/model composition | +| Brightness/on-road view | `ShowCustomBrightness`, `ShowModelView`, `ShowCameraWithCluster` | Brightness, camera/model composition, and the on-device camera while the external HUD is connected | | External HUD | `ClusterHud`, `ClusterHudBrightness`, `ClusterHudOrientation`, and related `ClusterHud*` settings | Supported TURZX HUD layout, live brightness, screen rotation, camera, radar, encoder, and performance options | An APN label remaining in the `ShowRouteInfo` description refers to route-input state. It is not an indication that CarrotMan or CarrotLink is supported. +`ShowCameraWithCluster=0` keeps the existing default: while the external HUD is connected, the on-device camera is hidden. Set it to `1` to show the on-device camera video. + `ClusterHudBrightness=0` follows camera exposure automatically; values `1` through `100` select fixed brightness. `ClusterHudOrientation` supports only `0` (0 degrees) and `2` (180 degrees); values `1` and `3` are ignored. The running TURZX process checks both stored settings every 100 ms. Brightness applies live; a managed H.264 orientation change automatically restarts the HUD and applies it through the capture-compatible stream setup. `ClusterHudPanelLayout=0` places the driving view selected by `ClusterHudCameraViewMode` on the left and the information panel selected by the screen, debug, and navigation state on the right. `1` swaps them, placing information on the left and the driving view on the right. The running HUD applies the setting within about one second without a restart. Modes without two side regions, such as the full-screen graph and full-screen navigation, are unchanged. `ClusterHudDebug` forces always-on output and optional debug UI or navigation input; any resulting debug or navigation information panel follows the selected layout. -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. +While the external HUD is connected over USB, `ShowCameraWithCluster=0` switches the on-device driving view on both regular C3/C3X hardware and mici to a black background and skips camera-video and model-path rendering. Setting it to `1` removes that connection-specific suppression and uses the normal on-device camera and on-road rendering path. A live change takes effect within about five seconds. Speed, speed limit, driver state, alerts, and the driving-state border remain visible with either value, and the option has no display effect after the external HUD disconnects. With value `0`, `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. diff --git a/docs/user/ko/settings.md b/docs/user/ko/settings.md index 4344076cf6..fc86b7f0a5 100644 --- a/docs/user/ko/settings.md +++ b/docs/user/ko/settings.md @@ -97,13 +97,13 @@ Carrot Web 설정 화면에서는 다음 기능을 사용할 수 있습니다. ## 전체 설정 지도 -현재 `carrot-wip`의 `carrot_settings.json`에는 **169개 파라미터**가 있으며, 모든 항목이 아래 메뉴에 연결되어 있습니다. +현재 `carrot-wip`의 `carrot_settings.json`에는 **171개 파라미터**가 있으며, 모든 항목이 아래 메뉴에 연결되어 있습니다. | 대분류 | 항목 수 | 중분류 | |---|---:|---| | 주행 제어 | 107 | 시작·오토, 버튼·프리셋, 차량 조향, 속도·감속, 크루즈·차간 | | 차량·하드웨어 | 16 | 현대·기아, CANFD·HDA, 레이더, 운전자 모니터링, 차량 보조, 기기 하드웨어 | -| 화면 표시 | 36 | 정보 표시, 경로 표시, 밝기·주행화면, 외부 HUD | +| 화면 표시 | 37 | 정보 표시, 경로 표시, 밝기·주행화면, 외부 HUD | | 시스템 | 11 | 녹화·전원, 네트워크·지도, 사운드, 소프트웨어 | ## 주행 제어 @@ -222,13 +222,13 @@ Carrot Web 설정 화면에서는 다음 기능을 사용할 수 있습니다. ## 화면 표시 -화면 표시에는 36개 항목이 있습니다. 일반 화면 항목은 비교적 되돌리기 쉽지만, 외부 HUD는 별도 하드웨어와 성능 설정을 포함합니다. +화면 표시에는 37개 항목이 있습니다. 일반 화면 항목은 비교적 되돌리기 쉽지만, 외부 HUD는 별도 하드웨어와 성능 설정을 포함합니다. | 중분류 | 파라미터 | 용도 | |---|---|---| | 정보 표시 | `ShowDebugUI`, `ShowTpms`, `ShowDateTime`, `ShowPathEnd`, `ShowDeviceState`, `ShowLaneInfo`, `ShowRadarInfo`, `ShowRouteInfo`, `ShowPlotMode` | 주행 화면의 디버그, 타이어, 시간, 차선, 레이더와 경로 정보 | | 경로 표시 | `ShowPathMode`, `ShowPathColor`, `ShowPathColorCruiseOff`, `ShowPathModeLane`, `ShowPathColorLane` | 레인리스·레인모드·크루즈 OFF 상태의 경로 모양과 색상 | -| 밝기·주행화면 | `ShowCustomBrightness`, `ShowModelView` | 주행 중 밝기와 카메라·모델 표시 조합 | +| 밝기·주행화면 | `ShowCustomBrightness`, `ShowModelView`, `ShowCameraWithCluster` | 주행 중 밝기, 카메라·모델 표시 조합과 외부 HUD 연결 중 본체 카메라 표시 | | 외부 HUD·기본 | `ClusterHud`, `ClusterHudBrightness`, `ClusterHudOrientation`, `ClusterHudMirror`, `ClusterHudTheme`, `ClusterNaviMapTheme`, `ClusterNaviMapType`, `ClusterNaviMapFps` | TURZX 외부 HUD, 밝기, 화면 회전, 미러링과 지도 테마 | | 외부 HUD·화면·카메라 | `ClusterHudEncoder`, `ClusterHudLiveFps`, `ClusterHudScreenMode`, `ClusterHudPanelLayout`, `ClusterHudCameraViewMode` | 인코더, 전송 FPS와 화면·카메라·좌우 패널 구성 | | 외부 HUD·레이더 표시 | `ClusterHudRadarInfo`, `ClusterHudRadarDisplay`, `ClusterHudRadarSourceColor` | 외부 HUD의 레이더 정보와 색상 | @@ -236,13 +236,23 @@ Carrot Web 설정 화면에서는 다음 기능을 사용할 수 있습니다. `ShowRouteInfo` 설명에 남아 있는 APN 표기는 경로 정보 입력 상태를 뜻합니다. 이를 CarrotMan 또는 CarrotLink 지원 안내로 해석하면 안 됩니다. -`ShowCustomBrightness=0`은 주변 밝기에 따른 자동 조절이고, `ShowModelView`는 카메라와 모델 표시 조합을 선택합니다. `ClusterHud` 계열은 지원되는 외부 HUD를 연결한 경우에만 사용하세요. +`ShowCustomBrightness=0`은 주변 밝기에 따른 자동 조절이고, `ShowModelView`는 카메라와 모델 표시 조합을 선택합니다. `ShowCameraWithCluster=0`은 외부 HUD 연결 중 본체 카메라를 숨기는 기존 기본 동작이고, `1`은 본체 카메라 영상을 표시합니다. `ClusterHud` 계열은 지원되는 외부 HUD를 연결한 경우에만 사용하세요. `ClusterHudBrightness=0`은 카메라 노출값을 따르는 자동 밝기이고, `1~100`은 고정 밝기입니다. `ClusterHudOrientation`은 `0`(0도)과 `2`(180도)만 지원하며 `1`, `3`은 무시합니다. 실행 중인 TURZX 프로세스는 두 저장값을 100ms마다 확인합니다. 밝기는 실행 중 적용되고, 관리형 H.264의 회전값이 바뀌면 HUD가 자동 재시작되어 캡처와 동일한 스트림 설정 절차로 적용됩니다. `ClusterHudPanelLayout=0`은 `ClusterHudCameraViewMode`가 선택한 주행 화면을 왼쪽에, 화면 모드·디버그·내비 상태에 따라 선택되는 정보 패널을 오른쪽에 배치합니다. `1`은 두 영역을 서로 바꿔 정보 패널을 왼쪽에, 주행 화면을 오른쪽에 표시합니다. 실행 중 약 1초 안에 적용되며 재시작하지 않습니다. 전체화면 그래프와 전체화면 내비처럼 좌우 영역이 없는 모드는 바뀌지 않습니다. `ClusterHudDebug`은 HUD 상시 출력과 디버그 UI·내비 입력을 강제로 켜는 설정이며, 그 결과 표시되는 디버그·내비 정보 패널도 선택한 좌우 배치를 따릅니다. -외부 HUD가 USB로 연결된 동안 일반 C3/C3X와 mici의 본체 주행 화면은 검은 배경으로 전환하고 카메라 영상과 모델 경로 렌더링을 생략합니다. 속도·제한속도·운전자 상태·경고·주행 상태 테두리 등 본체 HUD는 계속 표시되며, 외부 HUD 연결이 끊기면 카메라 화면이 자동으로 복귀합니다. `camerad`와 모델 입력은 계속 동작하고 본체 화면의 중복 렌더링만 줄입니다. +외부 HUD가 USB로 연결된 동안 `ShowCameraWithCluster=0`이면 일반 C3/C3X와 mici의 본체 주행 화면은 검은 배경으로 전환하고 카메라 영상과 모델 경로 렌더링을 생략합니다. `1`이면 이 연결 전용 억제를 해제해 본체 카메라 영상과 기존 주행화면 렌더 경로를 사용합니다. 값은 주행 중에도 최대 약 5초 안에 반영됩니다. 어느 값에서도 속도·제한속도·운전자 상태·경고·주행 상태 테두리 등 본체 HUD는 계속 표시되며, 외부 HUD 연결이 끊기면 이 옵션은 화면에 영향을 주지 않습니다. `0`에서도 `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)를 정상 중심점 대비 상하·좌우 방향으로 시각화하면서 각도 숫자도 함께 표시합니다. 주행 화면 아래에는 브랜치·네트워크 주소·프레임률 상태를 계속 표시하고, 리포트와 겹치는 코어 사용량 문구만 생략합니다. 로드카메라 화면에서는 감지 차량을 내부가 투명한 라운드 사각 프레임으로 감싸고 기존 감지 색상을 테두리에 적용합니다. 그룹화되지 않은 레이더 감지점은 같은 방식의 작은 소스 색상 표식으로 표시합니다. 차량 프레임은 가벼운 단일 테두리로 그리고, 화면 경계에서 일부만 투영되거나 레이더 방위값 때문에 비정상적으로 길어지는 프레임은 표시하지 않습니다. diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 78bf311c47..a6235b6616 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -179,6 +179,7 @@ inline static std::unordered_map keys = { {"CarrotTireTrajectory", {PERSISTENT, INT, "0"}}, {"ShowCustomBrightness", {PERSISTENT, INT, "100"}}, {"ShowModelView", {PERSISTENT, INT, "0"}}, + {"ShowCameraWithCluster", {PERSISTENT, INT, "0"}}, {"ClusterHud", {PERSISTENT, INT, "0"}}, {"ClusterHudConnected", {CLEAR_ON_MANAGER_START, BOOL}}, {"ClusterHudDebug", {PERSISTENT, INT, "0"}}, diff --git a/openpilot/selfdrive/carrot/cluster/README.md b/openpilot/selfdrive/carrot/cluster/README.md index 2146c4e1cd..61cc35a3f9 100644 --- a/openpilot/selfdrive/carrot/cluster/README.md +++ b/openpilot/selfdrive/carrot/cluster/README.md @@ -451,12 +451,20 @@ 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. `-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 +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 @@ -482,9 +490,19 @@ 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. +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 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_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 427d4943d5..fd7a0daa90 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 @@ -356,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 @@ -984,6 +997,51 @@ 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 _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 + + 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 +2863,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,14 +3592,13 @@ 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) 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 +3623,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, @@ -3588,18 +3646,28 @@ 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) + 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) + self._draw_speed_block(state, tpms_offset_x=tpms_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_gauges_translated = abs(side_gauge_offset_x) > 0.001 + if side_gauges_translated: + rl.rl_push_matrix() + 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_gauges_translated: + rl.rl_pop_matrix() profile_stage = self._profile_start() self._draw_turn_signal( "left", @@ -3633,9 +3701,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), @@ -3644,14 +3710,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_DEFAULT: - return self.screen_mode + 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 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 @@ -3674,7 +3750,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: @@ -3689,7 +3766,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) @@ -3782,11 +3859,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, @@ -6488,7 +6565,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))) @@ -6523,7 +6600,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 752f64df01..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 such as default, 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..1cbaa4ed72 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") @@ -94,6 +99,26 @@ def test_external_hud_brightness_and_orientation_use_catalog_controls(settings, assert '{"ClusterHudPanelLayout", {PERSISTENT, INT, "0"}}' in params_keys +def test_cluster_camera_preference_is_in_brightness_and_view(settings, params): + by_name = {p["name"]: p for p in params} + camera = by_name["ShowCameraWithCluster"] + assert (camera["min"], camera["max"], camera["default"], camera["unit"]) == (0, 1, 0, 1) + assert camera["descr"] == "0: 카메라 미표시(기본)\n1: 카메라 영상 표시" + assert camera["edescr"] == "0: Hide camera (default)\n1: Show camera video" + assert camera["cdescr"] == "0: 不显示摄像头(默认)\n1: 显示摄像头画面" + + display = next(category for category in settings["menu"] if category["id"] == "DISPLAY") + brightness = next(group for group in display["groups"] if group["id"] == "DISP_BRIGHT") + assert brightness["params"] == [ + "ShowCustomBrightness", + "ShowModelView", + "ShowCameraWithCluster", + ] + + params_keys = PARAMS_KEYS_PATH.read_text(encoding="utf-8") + assert '{"ShowCameraWithCluster", {PERSISTENT, INT, "0"}}' in params_keys + + def test_carrot_radar_mode_replaces_removed_model_mode(settings, params): by_name = {p["name"]: p for p in params} assert "RadarLeadModelMode" not in by_name diff --git a/openpilot/selfdrive/carrot/tests/test_cluster_config.py b/openpilot/selfdrive/carrot/tests/test_cluster_config.py index 16231d2b24..1355a5bf3f 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_config.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_config.py @@ -8,6 +8,8 @@ from cluster_config import ( 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, @@ -44,6 +46,20 @@ 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_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 f9d7c43bf5..9fa167ba99 100644 --- a/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py +++ b/openpilot/selfdrive/carrot/tests/test_cluster_trip_report.py @@ -8,10 +8,16 @@ CLUSTER_DIR = Path(__file__).resolve().parents[1] / "cluster" sys.path.insert(0, str(CLUSTER_DIR)) +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, normalize_cluster_screen_mode, ) @@ -116,13 +122,232 @@ 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_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_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 + 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_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 + 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): diff --git a/openpilot/selfdrive/carrot_settings.json b/openpilot/selfdrive/carrot_settings.json index c60c196b70..e905983865 100644 --- a/openpilot/selfdrive/carrot_settings.json +++ b/openpilot/selfdrive/carrot_settings.json @@ -480,7 +480,8 @@ "zh": "亮度与视图", "params": [ "ShowCustomBrightness", - "ShowModelView" + "ShowModelView", + "ShowCameraWithCluster" ] }, { @@ -3103,6 +3104,22 @@ "default": 0, "unit": 1 }, + { + "group": "화면", + "name": "ShowCameraWithCluster", + "title": "외부 HUD 연결 시 본체 카메라(0)", + "descr": "0: 카메라 미표시(기본)\n1: 카메라 영상 표시", + "egroup": "DISPLAY", + "etitle": "On-device Camera with External HUD(0)", + "edescr": "0: Hide camera (default)\n1: Show camera video", + "cgroup": "显示", + "ctitle": "外部 HUD 连接时本机摄像头(0)", + "cdescr": "0: 不显示摄像头(默认)\n1: 显示摄像头画面", + "min": 0, + "max": 1, + "default": 0, + "unit": 1 + }, { "group": "외부 HUD", "name": "ClusterHud", @@ -3358,14 +3375,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\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, diff --git a/openpilot/selfdrive/ui/layouts/main.py b/openpilot/selfdrive/ui/layouts/main.py index 16b5d9eed2..1c8815dfb5 100644 --- a/openpilot/selfdrive/ui/layouts/main.py +++ b/openpilot/selfdrive/ui/layouts/main.py @@ -100,7 +100,10 @@ def _render(self, _): self._handle_onroad_transition() if ui_state.started: cluster_hud_connected = ui_state.params.get_bool("ClusterHudConnected") - self._layouts[MainState.ONROAD].set_cluster_hud_connected(cluster_hud_connected) + self._layouts[MainState.ONROAD].set_cluster_hud_connected( + cluster_hud_connected, + ui_state.show_camera_with_cluster, + ) self._render_main_content() self._handle_carrot_record_cmd(ui_state.sm) diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index 1456fe0264..e5ebf7b846 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -165,7 +165,10 @@ def _handle_transitions(self): if ui_state.started: show_plot_mode = ui_state.params.get_int("ShowPlotMode") cluster_hud_connected = ui_state.params.get_bool("ClusterHudConnected") - self._onroad_layout.set_cluster_hud_connected(cluster_hud_connected) + self._onroad_layout.set_cluster_hud_connected( + cluster_hud_connected, + ui_state.show_camera_with_cluster, + ) effective_plot_mode = 0 if cluster_hud_connected else show_plot_mode if effective_plot_mode != self._show_plot_mode: self._show_plot_mode = effective_plot_mode diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 08ab193391..31e59d22a3 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -155,7 +155,7 @@ def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = Visio self._cached_matrix: np.ndarray | None = None self._content_rect = rl.Rectangle() self._last_click_time = 0.0 - self._cluster_hud_connected = False + self._suppress_camera_for_cluster = False # Bookmark icon with swipe gesture self._bookmark_icon = BookmarkIcon(bookmark_callback) @@ -193,8 +193,8 @@ def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" return self._bookmark_icon.is_swiping_left() - def set_cluster_hud_connected(self, connected: bool) -> None: - self._cluster_hud_connected = connected + def set_cluster_hud_connected(self, connected: bool, show_camera: bool = False) -> None: + self._suppress_camera_for_cluster = connected and not show_camera def _update_state(self): super()._update_state() @@ -220,7 +220,7 @@ def _render(self, _): # alert/extras처럼 흩어진 구간은 누적(+=). scissor begin/end는 raylib 배치 flush # 지점이라 특정 구간에 귀속시키지 않는다 — total과 구간 합의 차이(미귀속)로 남는다. cam_ms = model_ms = ds_ms = hud_ms = alert_ms = extras_ms = 0.0 - if not self._cluster_hud_connected: + if not self._suppress_camera_for_cluster: self._switch_stream_if_needed(ui_state.sm) self._update_calibration() @@ -243,20 +243,20 @@ def _render(self, _): road_view_mode = self._road_view_mode() _t = time.monotonic() - if self._cluster_hud_connected or road_view_mode in (2, 3): + if self._suppress_camera_for_cluster or road_view_mode in (2, 3): rl.draw_rectangle_rec(self._content_rect, rl.BLACK) else: # Render the base camera view super()._render(self._content_rect) cam_ms = (time.monotonic() - _t) * 1000.0 - if not self._cluster_hud_connected and road_view_mode in (0, 2): + if not self._suppress_camera_for_cluster and road_view_mode in (0, 2): # Draw all UI overlays _t = time.monotonic() self._model_renderer.render(self._content_rect) model_ms = (time.monotonic() - _t) * 1000.0 - if not self._cluster_hud_connected: + if not self._suppress_camera_for_cluster: # Fade out bottom of overlays for looks _t = time.monotonic() rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE) diff --git a/openpilot/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py index 2811b3200d..b3f11ebec0 100644 --- a/openpilot/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -45,7 +45,7 @@ def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREA self._matrix_cache_key = (0, 0.0, 0.0, stream_type) self._cached_matrix: np.ndarray | None = None self._content_rect = rl.Rectangle() - self._cluster_hud_connected = False + self._suppress_camera_for_cluster = False self.model_renderer = ModelRenderer() self._hud_renderer = HudRenderer() @@ -71,8 +71,8 @@ def _refresh_plot_mode(self, now: float) -> None: except Exception: self._plot_mode = 0 - def set_cluster_hud_connected(self, connected: bool) -> None: - self._cluster_hud_connected = connected + def set_cluster_hud_connected(self, connected: bool, show_camera: bool = False) -> None: + self._suppress_camera_for_cluster = connected and not show_camera def _render(self, rect): # plotMode 갱신(가끔 파일 읽기)은 drawTime 창 밖에서 — total을 오염시키지 않는다 @@ -86,7 +86,7 @@ def _render(self, rect): # total과 구간 합의 차이(미귀속)로 남는다. extras = carrot 테두리(+텍스트) cam_ms = model_ms = ds_ms = hud_ms = alert_ms = extras_ms = 0.0 - if not self._cluster_hud_connected: + if not self._suppress_camera_for_cluster: self._switch_stream_if_needed(ui_state.sm) self._update_calibration() @@ -108,14 +108,14 @@ def _render(self, rect): ) _t = time.monotonic() - if self._cluster_hud_connected: + if self._suppress_camera_for_cluster: rl.draw_rectangle_rec(self._content_rect, rl.BLACK) else: # Render the base camera view super()._render(rect) cam_ms = (time.monotonic() - _t) * 1000.0 - if not self._cluster_hud_connected: + if not self._suppress_camera_for_cluster: # Draw the model overlay only with the camera view _t = time.monotonic() self.model_renderer.render(self._content_rect) diff --git a/openpilot/selfdrive/ui/tests/test_cluster_hud_camera_suppression.py b/openpilot/selfdrive/ui/tests/test_cluster_hud_camera_suppression.py index 0f7e2e4805..4b9e6a9210 100644 --- a/openpilot/selfdrive/ui/tests/test_cluster_hud_camera_suppression.py +++ b/openpilot/selfdrive/ui/tests/test_cluster_hud_camera_suppression.py @@ -5,6 +5,9 @@ UI_DIR = Path(__file__).parents[1] MAIN_LAYOUT_PATH = UI_DIR / "layouts" / "main.py" ROAD_VIEW_PATH = UI_DIR / "onroad" / "augmented_road_view.py" +MICI_MAIN_LAYOUT_PATH = UI_DIR / "mici" / "layouts" / "main.py" +MICI_ROAD_VIEW_PATH = UI_DIR / "mici" / "onroad" / "augmented_road_view.py" +UI_STATE_PATH = UI_DIR / "ui_state.py" def _method(path: Path, class_name: str, method_name: str) -> ast.FunctionDef: @@ -13,18 +16,18 @@ def _method(path: Path, class_name: str, method_name: str) -> ast.FunctionDef: return next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == method_name) -def _is_cluster_connected(node: ast.expr, *, negated: bool) -> bool: +def _is_cluster_camera_suppressed(node: ast.expr, *, negated: bool) -> bool: if negated: return ( isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not) - and _is_cluster_connected(node.operand, negated=False) + and _is_cluster_camera_suppressed(node.operand, negated=False) ) return ( isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == "self" - and node.attr == "_cluster_hud_connected" + and node.attr == "_suppress_camera_for_cluster" ) @@ -37,8 +40,26 @@ def _calls(node: ast.AST, attr: str) -> bool: ) -def test_main_layout_forwards_external_hud_connection_to_road_view(): - render = _method(MAIN_LAYOUT_PATH, "MainLayout", "_render") +def _call(node: ast.AST, attr: str) -> ast.Call: + return next( + child for child in ast.walk(node) + if isinstance(child, ast.Call) + and isinstance(child.func, ast.Attribute) + and child.func.attr == attr + ) + + +def _is_attr(node: ast.expr, owner: str, attr: str) -> bool: + return ( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id == owner + and node.attr == attr + ) + + +def _assert_main_layout_forwards_camera_preference(path: Path, class_name: str, method_name: str) -> None: + render = _method(path, class_name, method_name) connected_guards = [ node for node in ast.walk(render) if isinstance(node, ast.If) @@ -50,18 +71,59 @@ def test_main_layout_forwards_external_hud_connection_to_road_view(): assert connected_guards guard = connected_guards[0] assert _calls(guard, "get_bool") - assert _calls(guard, "set_cluster_hud_connected") + setter = _call(guard, "set_cluster_hud_connected") + assert len(setter.args) == 2 + assert _is_attr(setter.args[1], "ui_state", "show_camera_with_cluster") + + +def test_main_layouts_forward_external_hud_connection_and_camera_preference(): + _assert_main_layout_forwards_camera_preference(MAIN_LAYOUT_PATH, "MainLayout", "_render") + _assert_main_layout_forwards_camera_preference(MICI_MAIN_LAYOUT_PATH, "MiciMainLayout", "_handle_transitions") + + +def test_ui_state_caches_camera_preference_as_exact_enabled_value(): + update_params = _method(UI_STATE_PATH, "UIState", "update_params") + assignment = next( + node for node in ast.walk(update_params) + if isinstance(node, ast.Assign) + and any(_is_attr(target, "self", "show_camera_with_cluster") for target in node.targets) + ) + assert isinstance(assignment.value, ast.Compare) + assert isinstance(assignment.value.left, ast.Call) + assert isinstance(assignment.value.left.func, ast.Attribute) + assert assignment.value.left.func.attr == "get_int" + assert assignment.value.left.args[0].value == "ShowCameraWithCluster" + assert isinstance(assignment.value.ops[0], ast.Eq) + assert assignment.value.comparators[0].value == 1 + + +def test_camera_preference_only_bypasses_connected_render_suppression(): + for path in (ROAD_VIEW_PATH, MICI_ROAD_VIEW_PATH): + setter = _method(path, "AugmentedRoadView", "set_cluster_hud_connected") + assignment = next( + node for node in setter.body + if isinstance(node, ast.Assign) + and any(_is_attr(target, "self", "_suppress_camera_for_cluster") for target in node.targets) + ) + assert isinstance(assignment.value, ast.BoolOp) + assert isinstance(assignment.value.op, ast.And) + assert isinstance(assignment.value.values[0], ast.Name) + assert assignment.value.values[0].id == "connected" + assert isinstance(assignment.value.values[1], ast.UnaryOp) + assert isinstance(assignment.value.values[1].op, ast.Not) + assert isinstance(assignment.value.values[1].operand, ast.Name) + assert assignment.value.values[1].operand.id == "show_camera" def test_external_hud_skips_camera_and_model_but_retains_device_hud(): render = _method(ROAD_VIEW_PATH, "AugmentedRoadView", "_render") connected = [ node for node in ast.walk(render) - if isinstance(node, ast.If) and _is_cluster_connected(node.test, negated=False) + if isinstance(node, ast.If) and _is_cluster_camera_suppressed(node.test, negated=False) ] disconnected = [ node for node in ast.walk(render) - if isinstance(node, ast.If) and _is_cluster_connected(node.test, negated=True) + if isinstance(node, ast.If) and _is_cluster_camera_suppressed(node.test, negated=True) ] assert any(_calls(node, "draw_rectangle_rec") and _calls(ast.Module(body=node.orelse), "_render") diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index adcd0ef636..beba13c855 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -102,6 +102,7 @@ def _initialize(self): # Brightness and UI options self.show_brightness_ratio: float = 1.0 self.show_model_view: int = 0 + self.show_camera_with_cluster: bool = False self.update_params() @@ -207,6 +208,7 @@ def update_params(self) -> None: self.show_radar_info = self.params.get_int("ShowRadarInfo") self.show_brightness_ratio: float = self.params.get_int("ShowCustomBrightness") / 100.0 self.show_model_view = self.params.get_int("ShowModelView") + self.show_camera_with_cluster = self.params.get_int("ShowCameraWithCluster") == 1 self._param_update_time = time.monotonic() diff --git a/tools/docs/wiki_settings/tests/test_ci_check.py b/tools/docs/wiki_settings/tests/test_ci_check.py index db332022bb..2741409a50 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"], 171) 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..ecd45d17eb 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), 171) + self.assertEqual(result.index["review"], {"current": 0, "needs_review": 171}) self.assertEqual(result.index["locales"], ["ko", "en", "zh"]) - self.assertEqual(len(result.pages), (169 * 3) + 2) + self.assertEqual(len(result.pages), (171 * 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), 171 * 3) self.assertTrue(all(text.count("