ui: move road camera view selection to visual settings

This commit is contained in:
Comma Device
2026-10-23 23:13:14 +08:00
parent 6dfa2c35d8
commit 609f72c6af
9 changed files with 146 additions and 136 deletions
Binary file not shown.
+1
View File
@@ -242,6 +242,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// sunnypilot model params
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"RoadCameraViewMode", {PERSISTENT | BACKUP, INT, "0"}},
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
@@ -37,8 +37,24 @@ BORDER_COLORS = {
WIDE_CAM_MAX_SPEED = 5.0 # m/s (11 mph)
ROAD_CAM_MIN_SPEED = 10.0 # m/s (22 mph)
INF_POINT = np.array([1000.0, 0.0, 0.0])
CAMERA_TOGGLE_SIZE = 104
CAMERA_TOGGLE_MARGIN = 18
ROAD_CAMERA_VIEW_AUTO = 0
ROAD_CAMERA_VIEW_WIDE = 1
ROAD_CAMERA_VIEW_STANDARD = 2
def camera_stream_target(mode: int, current_stream: VisionStreamType, v_ego: float,
openpilot_longitudinal_control: bool, available_streams: list[VisionStreamType]) -> VisionStreamType:
if mode == ROAD_CAMERA_VIEW_WIDE and WIDE_CAM in available_streams:
return WIDE_CAM
if mode == ROAD_CAMERA_VIEW_STANDARD:
return NARROW_ROAD_CAM
if openpilot_longitudinal_control and WIDE_CAM in available_streams:
if v_ego < WIDE_CAM_MAX_SPEED:
return WIDE_CAM
if v_ego > ROAD_CAM_MIN_SPEED:
return NARROW_ROAD_CAM
return current_stream
return NARROW_ROAD_CAM
class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
@@ -54,10 +70,6 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
self._matrix_cache_key = (0, 0.0, 0.0, stream_type)
self._cached_matrix: np.ndarray | None = None
self._content_rect = rl.Rectangle()
self._manual_stream_type: VisionStreamType | None = None
self._camera_toggle_rect = rl.Rectangle()
self._camera_toggle_flash_until = 0.0
self._camera_toggle_icon = gui_app.texture("icons/eye_open.png", 58, 32)
self.model_renderer = ModelRenderer()
self._hud_renderer = HudRenderer()
@@ -110,7 +122,6 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
))
self.alert_renderer.render(self._content_rect)
self.driver_state_renderer.render(self._content_rect)
self._draw_camera_toggle(self._content_rect)
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
@@ -121,10 +132,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
# Draw colored border based on driving state
self._draw_border(rect)
def _handle_mouse_press(self, mouse_pos):
if self._camera_toggle_rect.width and rl.check_collision_point_rec(mouse_pos, self._camera_toggle_rect):
self._toggle_camera_stream()
return
def _handle_mouse_press(self, _):
if self._click_callback is not None:
self._click_callback()
@@ -141,77 +149,20 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
def _switch_stream_if_needed(self, sm):
if self._manual_stream_type in self.available_streams:
target = self._manual_stream_type
elif sm['carParams'].openpilotLongitudinalControl and WIDE_CAM in self.available_streams:
v_ego = sm['carState'].vEgo
if v_ego < WIDE_CAM_MAX_SPEED:
target = WIDE_CAM
elif v_ego > ROAD_CAM_MIN_SPEED:
target = NARROW_ROAD_CAM
else:
# Hysteresis zone - keep current stream
target = self.stream_type
else:
target = NARROW_ROAD_CAM
try:
mode = int(ui_state.params.get("RoadCameraViewMode", return_default=True))
except Exception:
mode = ROAD_CAMERA_VIEW_AUTO
target = camera_stream_target(
mode,
self.stream_type,
sm['carState'].vEgo,
sm['carParams'].openpilotLongitudinalControl,
self.available_streams,
)
if self.stream_type != target:
self.switch_stream(target)
def _toggle_camera_stream(self):
if WIDE_CAM not in self.available_streams:
return
current = self._target_stream_type or self._stream_type
target = NARROW_ROAD_CAM if current == WIDE_CAM else WIDE_CAM
self._manual_stream_type = target
self._camera_toggle_flash_until = rl.get_time() + 0.2
self.switch_stream(target)
def _draw_camera_toggle(self, rect: rl.Rectangle):
if WIDE_CAM not in self.available_streams:
self._camera_toggle_rect = rl.Rectangle()
return
self._camera_toggle_rect = rl.Rectangle(
rect.x + rect.width - CAMERA_TOGGLE_SIZE - CAMERA_TOGGLE_MARGIN,
rect.y + CAMERA_TOGGLE_MARGIN,
CAMERA_TOGGLE_SIZE,
CAMERA_TOGGLE_SIZE,
)
pressed = rl.get_time() < self._camera_toggle_flash_until
current = self._target_stream_type or self._stream_type
is_wide = current == WIDE_CAM
center = rl.Vector2(
self._camera_toggle_rect.x + self._camera_toggle_rect.width / 2,
self._camera_toggle_rect.y + self._camera_toggle_rect.height / 2,
)
radius = self._camera_toggle_rect.width / 2
bg = rl.Color(35, 83, 61, 215) if pressed else rl.Color(4, 10, 14, 150)
border = rl.Color(155, 230, 185, 245) if pressed else rl.Color(255, 255, 255, 70)
rl.draw_circle_v(center, radius, bg)
rl.draw_circle_lines_v(center, radius - 1, border)
icon_pos = rl.Vector2(center.x - self._camera_toggle_icon.width / 2,
center.y - self._camera_toggle_icon.height / 2)
rl.draw_texture_ex(self._camera_toggle_icon, icon_pos, 0.0, 1.0, rl.Color(255, 255, 255, 238))
inset = 17 if is_wide else 25
left = self._camera_toggle_rect.x + inset
right = self._camera_toggle_rect.x + self._camera_toggle_rect.width - inset
top = self._camera_toggle_rect.y + inset
bottom = self._camera_toggle_rect.y + self._camera_toggle_rect.height - inset
corner = 9
accent = rl.Color(155, 230, 185, 220) if is_wide else rl.Color(255, 255, 255, 180)
rl.draw_line_ex(rl.Vector2(left, top), rl.Vector2(left + corner, top), 3, accent)
rl.draw_line_ex(rl.Vector2(left, top), rl.Vector2(left, top + corner), 3, accent)
rl.draw_line_ex(rl.Vector2(right, top), rl.Vector2(right - corner, top), 3, accent)
rl.draw_line_ex(rl.Vector2(right, top), rl.Vector2(right, top + corner), 3, accent)
rl.draw_line_ex(rl.Vector2(left, bottom), rl.Vector2(left + corner, bottom), 3, accent)
rl.draw_line_ex(rl.Vector2(left, bottom), rl.Vector2(left, bottom - corner), 3, accent)
rl.draw_line_ex(rl.Vector2(right, bottom), rl.Vector2(right - corner, bottom), 3, accent)
rl.draw_line_ex(rl.Vector2(right, bottom), rl.Vector2(right, bottom - corner), 3, accent)
def _update_calibration(self):
# Update device camera if not already set
sm = ui_state.sm
@@ -17,6 +17,8 @@ CHEVRON_INFO_DESCRIPTION = {
"disabled": tr_noop("This feature requires sunnypilot longitudinal control to be available.")
}
CAMERA_VIEW_DESCRIPTION = tr_noop("Automatic switches between wide and standard views based on vehicle speed. Wide always uses the wide road camera; Standard always uses the narrow road camera.") # noqa: E501
class VisualsLayout(Widget):
def __init__(self):
@@ -110,6 +112,13 @@ class VisualsLayout(Widget):
)
self._toggles[param] = toggle
self._camera_view = multiple_button_item_sp(
title=lambda: tr("Road Camera View"),
description=lambda: tr(CAMERA_VIEW_DESCRIPTION),
buttons=[lambda: tr("Auto"), lambda: tr("Wide"), lambda: tr("Standard")],
callback=self._on_camera_view_changed,
inline=False,
)
self._chevron_info = multiple_button_item_sp(
title=lambda: tr("Display Metrics Below Chevron"),
description="",
@@ -126,7 +135,7 @@ class VisualsLayout(Widget):
inline=False
)
items = list(self._toggles.values()) + [
items = [self._camera_view] + list(self._toggles.values()) + [
self._chevron_info,
self._dev_ui_info,
]
@@ -139,6 +148,11 @@ class VisualsLayout(Widget):
self._toggles[param].action_item.set_state(self._params.get_bool(param))
self._dev_ui_info.action_item.set_selected_button(ui_state.params.get("DevUIInfo", return_default=True))
try:
camera_view_mode = ui_state.params.get("RoadCameraViewMode", return_default=True)
except Exception:
camera_view_mode = 0
self._camera_view.action_item.set_selected_button(camera_view_mode)
if ui_state.has_longitudinal_control:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"]))
@@ -149,6 +163,14 @@ class VisualsLayout(Widget):
self._chevron_info.action_item.set_enabled(False)
ui_state.params.put("ChevronInfo", 0)
def _on_camera_view_changed(self, index: int):
if index not in (0, 1, 2):
return
try:
ui_state.params.put("RoadCameraViewMode", index, block=True)
except Exception:
pass
def _render(self, rect):
self._scroller.render(rect)
@@ -1,56 +0,0 @@
import unittest
from types import SimpleNamespace
from unittest.mock import Mock
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView, NARROW_ROAD_CAM, WIDE_CAM
class TestView(AugmentedRoadView):
def __del__(self):
pass
class TestOnroadCameraToggle(unittest.TestCase):
def _view(self):
view = object.__new__(TestView)
view.available_streams = [NARROW_ROAD_CAM, WIDE_CAM]
view._stream_type = NARROW_ROAD_CAM
view._target_stream_type = None
view._manual_stream_type = None
view._camera_toggle_flash_until = 0.0
view.switch_stream = Mock()
return view
def test_toggle_between_wide_and_narrow(self):
view = self._view()
view._toggle_camera_stream()
self.assertEqual(view._manual_stream_type, WIDE_CAM)
view.switch_stream.assert_called_once_with(WIDE_CAM)
view._stream_type = WIDE_CAM
view.switch_stream.reset_mock()
view._toggle_camera_stream()
self.assertEqual(view._manual_stream_type, NARROW_ROAD_CAM)
view.switch_stream.assert_called_once_with(NARROW_ROAD_CAM)
def test_toggle_requires_wide_stream(self):
view = self._view()
view.available_streams = [NARROW_ROAD_CAM]
view._toggle_camera_stream()
self.assertIsNone(view._manual_stream_type)
view.switch_stream.assert_not_called()
def test_manual_view_overrides_speed_auto_switch(self):
view = self._view()
view._stream_type = NARROW_ROAD_CAM
view._manual_stream_type = WIDE_CAM
sm = {
'carParams': SimpleNamespace(openpilotLongitudinalControl=False),
'carState': SimpleNamespace(vEgo=30.0),
}
view._switch_stream_if_needed(sm)
view.switch_stream.assert_called_once_with(WIDE_CAM)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,41 @@
import unittest
from openpilot.selfdrive.ui.onroad.augmented_road_view import (
NARROW_ROAD_CAM,
ROAD_CAMERA_VIEW_AUTO,
ROAD_CAMERA_VIEW_STANDARD,
ROAD_CAMERA_VIEW_WIDE,
WIDE_CAM,
camera_stream_target,
)
class TestRoadCameraViewMode(unittest.TestCase):
def test_auto_switches_by_speed(self):
available = [NARROW_ROAD_CAM, WIDE_CAM]
self.assertEqual(camera_stream_target(ROAD_CAMERA_VIEW_AUTO, NARROW_ROAD_CAM, 2.0, True, available), WIDE_CAM)
self.assertEqual(camera_stream_target(ROAD_CAMERA_VIEW_AUTO, WIDE_CAM, 12.0, True, available), NARROW_ROAD_CAM)
self.assertEqual(camera_stream_target(ROAD_CAMERA_VIEW_AUTO, WIDE_CAM, 7.5, True, available), WIDE_CAM)
self.assertEqual(camera_stream_target(ROAD_CAMERA_VIEW_AUTO, NARROW_ROAD_CAM, 7.5, True, available), NARROW_ROAD_CAM)
def test_wide_mode_overrides_speed(self):
self.assertEqual(
camera_stream_target(ROAD_CAMERA_VIEW_WIDE, NARROW_ROAD_CAM, 30.0, False, [NARROW_ROAD_CAM, WIDE_CAM]),
WIDE_CAM,
)
def test_standard_mode_overrides_speed(self):
self.assertEqual(
camera_stream_target(ROAD_CAMERA_VIEW_STANDARD, WIDE_CAM, 0.0, True, [NARROW_ROAD_CAM, WIDE_CAM]),
NARROW_ROAD_CAM,
)
def test_missing_wide_stream_falls_back_to_standard(self):
self.assertEqual(
camera_stream_target(ROAD_CAMERA_VIEW_WIDE, NARROW_ROAD_CAM, 0.0, True, [NARROW_ROAD_CAM]),
NARROW_ROAD_CAM,
)
if __name__ == '__main__':
unittest.main()
@@ -434,6 +434,7 @@ msgid "Aggressive"
msgstr ""
#: openpilot/selfdrive/ui/layouts/settings/toggles.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Standard"
msgstr ""
@@ -1064,6 +1065,10 @@ msgstr ""
msgid "Neural Network Lateral Control (NNLC)"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Automatic switches between wide and standard views based on vehicle speed. Wide always uses the wide road camera; Standard always uses the narrow road camera."
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "This feature requires sunnypilot longitudinal control to be available."
msgstr ""
@@ -1140,6 +1145,10 @@ msgstr ""
msgid "Real-time Acceleration Bar"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Road Camera View"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Display Metrics Below Chevron"
msgstr ""
@@ -1152,6 +1161,14 @@ msgstr ""
msgid "Display real-time parameters and metrics from various sources."
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Auto"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Wide"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py
@@ -192,6 +192,10 @@ msgstr ""
msgid "Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Auto"
msgstr "Auto"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
msgid "Auto (Dark)"
msgstr ""
@@ -208,6 +212,10 @@ msgstr ""
msgid "Auto Lane Change: Delay with Blind Spot"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Automatic switches between wide and standard views based on vehicle speed. Wide always uses the wide road camera; Standard always uses the narrow road camera."
msgstr "Automatic switches between wide and standard views based on vehicle speed. Wide always uses the wide road camera; Standard always uses the narrow road camera."
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py
msgid "Avoid EPS Lockout"
msgstr ""
@@ -1611,6 +1619,10 @@ msgstr ""
msgid "Right & Bottom"
msgstr ""
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Road Camera View"
msgstr "Road Camera View"
#: openpilot/selfdrive/ui/layouts/settings/software.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py
@@ -1796,6 +1808,7 @@ msgid "Speedometer: Hide from Onroad Screen"
msgstr ""
#: openpilot/selfdrive/ui/layouts/settings/toggles.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Standard"
msgstr "Standard"
@@ -2200,6 +2213,10 @@ msgstr "Wi-Fi"
msgid "Wi-Fi Network Metered"
msgstr "Wi-Fi Network Metered"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Wide"
msgstr "Wide"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/developer.py
msgid "Would you like to delete this log?"
msgstr ""
@@ -192,6 +192,10 @@ msgstr "辅助"
msgid "Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."
msgstr "辅助:操作 +/- 按钮时,根据当前道路限速调整车辆巡航速度。"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Auto"
msgstr "自动"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
msgid "Auto (Dark)"
msgstr "自动(暗)"
@@ -208,6 +212,10 @@ msgstr "转向灯自动变道"
msgid "Auto Lane Change: Delay with Blind Spot"
msgstr "自动变道:盲区延迟"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Automatic switches between wide and standard views based on vehicle speed. Wide always uses the wide road camera; Standard always uses the narrow road camera."
msgstr "自动模式会根据车速在广角和标准视野之间切换。广角模式固定使用广角相机;标准模式固定使用窄角相机。"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py
msgid "Avoid EPS Lockout"
msgstr "避免EPS锁定"
@@ -1613,6 +1621,10 @@ msgstr "右侧"
msgid "Right & Bottom"
msgstr "右侧和底部"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Road Camera View"
msgstr "道路相机视野"
#: openpilot/selfdrive/ui/layouts/settings/software.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py
@@ -1798,6 +1810,7 @@ msgid "Speedometer: Hide from Onroad Screen"
msgstr "速度表:从行车屏幕隐藏"
#: openpilot/selfdrive/ui/layouts/settings/toggles.py
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Standard"
msgstr "标准"
@@ -2205,6 +2218,10 @@ msgstr "Wi‑Fi"
msgid "Wi-Fi Network Metered"
msgstr "Wi‑Fi 计量网络"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py
msgid "Wide"
msgstr "广角"
#: openpilot/selfdrive/ui/sunnypilot/layouts/settings/developer.py
msgid "Would you like to delete this log?"
msgstr "您要删除此日志吗?"