Stopped Timer / Driver Cam on reverse / Camera view

This commit is contained in:
firestar5683
2026-04-06 01:18:05 -05:00
parent 1d49528e37
commit cda677c4dc
4 changed files with 216 additions and 15 deletions
+44
View File
@@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
from openpilot.system.ui.widgets import Widget
@@ -14,6 +15,7 @@ from openpilot.system.ui.lib.application import gui_app
ONROAD_DELAY = 2.5 # seconds
REVERSE_DRIVER_CAMERA_DELAY = 0.5 # seconds
class MainState(IntEnum):
@@ -31,6 +33,8 @@ class MiciMainLayout(Widget):
self._prev_onroad = False
self._prev_standstill = False
self._onroad_time_delay: float | None = None
self._reverse_started_time: float | None = None
self._reverse_driver_camera: DriverCameraDialog | None = None
self._setup = False
# Initialize widgets
@@ -61,6 +65,7 @@ class MiciMainLayout(Widget):
# Set callbacks
self._setup_callbacks()
gui_app.set_modal_overlay_tick(self._modal_overlay_tick)
# Skip onboarding on desktop; keep normal flow on device.
self._onboarding_window = None
@@ -84,6 +89,8 @@ class MiciMainLayout(Widget):
if self._current_mode is None:
self._set_mode(MainState.MAIN)
self._update_reverse_driver_camera()
if not self._setup:
if self._alerts_layout.active_alerts() > 0:
self._scroller.scroll_to(self._alerts_layout.rect.x)
@@ -108,6 +115,8 @@ class MiciMainLayout(Widget):
self._current_mode = mode
def _handle_transitions(self):
self._update_reverse_driver_camera()
if ui_state.started != self._prev_onroad:
self._prev_onroad = ui_state.started
@@ -150,3 +159,38 @@ class MiciMainLayout(Widget):
user_bookmark = messaging.new_message('bookmarkButton')
user_bookmark.valid = True
self._pm.send('bookmarkButton', user_bookmark)
def _modal_overlay_tick(self):
self._update_reverse_driver_camera()
def _update_reverse_driver_camera(self):
current_overlay = gui_app._modal_overlay.overlay
reverse_overlay = self._reverse_driver_camera
should_show = ui_state.started and ui_state.params.get_bool("DriverCamera") and self._onroad_layout.is_in_reverse()
if not should_show:
self._reverse_started_time = None
if current_overlay is reverse_overlay:
gui_app.set_modal_overlay(None)
self._reverse_driver_camera = None
return
now = rl.get_time()
if self._reverse_started_time is None:
self._reverse_started_time = now
return
if now - self._reverse_started_time < REVERSE_DRIVER_CAMERA_DELAY:
return
if current_overlay is reverse_overlay:
return
if current_overlay is not None:
return
if reverse_overlay is None:
self._reverse_driver_camera = DriverCameraDialog(no_escape=True)
reverse_overlay = self._reverse_driver_camera
gui_app.set_modal_overlay(reverse_overlay)
+145 -15
View File
@@ -11,7 +11,13 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color, get_experimental_mode_banner_text
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import (
ENGAGED_COLOR,
EXPERIMENTAL_COLOR,
TRAFFIC_COLOR,
get_border_color,
get_experimental_mode_banner_text,
)
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -25,16 +31,22 @@ OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
DRIVER_CAM = VisionStreamType.VISION_STREAM_DRIVER
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
CAMERA_VIEW_AUTO = 0
CAMERA_VIEW_DRIVER = 1
CAMERA_VIEW_STANDARD = 2
CAMERA_VIEW_WIDE = 3
class BookmarkState(IntEnum):
HIDDEN = 0
DRAGGING = 1
TRIGGERED = 2
WIDE_CAM_MAX_SPEED = 5.0 # m/s (10 mph)
ROAD_CAM_MIN_SPEED = 10 # m/s (25 mph)
WIDE_CAM_MAX_SPEED = 10.0 # m/s
ROAD_CAM_MIN_SPEED = 15.0 # m/s
CAM_Y_OFFSET = 20
@@ -273,6 +285,100 @@ class MinSteerSpeedBanner(Widget):
self._label.render(text_rect)
class StandstillTimerOverlay:
def __init__(self):
self._last_started_frame = -1
self._standstill_duration = 0
self._standstill_started_at: float | None = None
self._font_bold = gui_app.font(FontWeight.BOLD)
self._font_medium = gui_app.font(FontWeight.MEDIUM)
def _reset(self):
self._standstill_duration = 0
self._standstill_started_at = None
@staticmethod
def _blend_colors(start: rl.Color, end: rl.Color, transition: float) -> rl.Color:
transition = float(np.clip(transition, 0.0, 1.0))
return rl.Color(
int(start.r + transition * (end.r - start.r)),
int(start.g + transition * (end.g - start.g)),
int(start.b + transition * (end.b - start.b)),
255,
)
def _get_duration_color(self) -> rl.Color:
if self._standstill_duration < 60:
return ENGAGED_COLOR
if self._standstill_duration < 150:
transition = (self._standstill_duration - 60) / 90.0
return self._blend_colors(ENGAGED_COLOR, EXPERIMENTAL_COLOR, transition)
if self._standstill_duration < 300:
transition = (self._standstill_duration - 150) / 150.0
return self._blend_colors(EXPERIMENTAL_COLOR, TRAFFIC_COLOR, transition)
return TRAFFIC_COLOR
def _update_state(self, in_reverse: bool) -> None:
if not ui_state.started:
self._last_started_frame = -1
self._reset()
return
if ui_state.started_frame != self._last_started_frame:
self._last_started_frame = ui_state.started_frame
self._reset()
if ui_state.sm.recv_frame["carState"] < ui_state.started_frame:
self._reset()
return
if in_reverse or not ui_state.params.get_bool("StoppedTimer"):
self._reset()
return
if not ui_state.sm["carState"].standstill:
self._reset()
return
now = time.monotonic()
if self._standstill_started_at is None:
self._standstill_started_at = now
self._standstill_duration = 0
return
if now - ui_state.started_time < 60.0:
self._standstill_duration = 0
return
self._standstill_duration = int(now - self._standstill_started_at)
@staticmethod
def _format_duration_text(total_seconds: int) -> tuple[str, str]:
minutes = total_seconds // 60
seconds = total_seconds % 60
minute_text = f"{minutes} minute" if minutes == 1 else f"{minutes} minutes"
second_text = f"{seconds} second" if seconds == 1 else f"{seconds} seconds"
return minute_text, second_text
def _draw_centered_text(self, rect: rl.Rectangle, text: str, y: float, font: rl.Font, font_size: int, color: rl.Color) -> None:
text_size = rl.measure_text_ex(font, text, font_size, 0)
text_pos = rl.Vector2(rect.x + rect.width / 2 - text_size.x / 2, rect.y + y - text_size.y / 2)
shadow_pos = rl.Vector2(text_pos.x + 2, text_pos.y + 2)
rl.draw_text_ex(font, text, shadow_pos, font_size, 0, rl.Color(0, 0, 0, 170))
rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
def render(self, rect: rl.Rectangle, in_reverse: bool) -> bool:
self._update_state(in_reverse)
if self._standstill_duration == 0:
return False
minute_text, second_text = self._format_duration_text(self._standstill_duration)
duration_color = self._get_duration_color()
self._draw_centered_text(rect, minute_text, 210, self._font_bold, 176, duration_color)
self._draw_centered_text(rect, second_text, 290, self._font_medium, 66, rl.Color(255, 255, 255, 242))
return True
class AugmentedRoadView(CameraView):
def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
super().__init__("camerad", stream_type)
@@ -300,6 +406,7 @@ class AugmentedRoadView(CameraView):
self._confidence_ball = ConfidenceBall()
self._experimental_mode_banner = ExperimentalModeBanner()
self._min_steer_speed_banner = MinSteerSpeedBanner()
self._standstill_timer = StandstillTimerOverlay()
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
@@ -377,36 +484,42 @@ class AugmentedRoadView(CameraView):
return
in_reverse = self._is_in_reverse()
is_driver_stream = self.stream_type == DRIVER_CAM
self._hud_renderer.prepare(self._content_rect)
# Draw all UI overlays
if not in_reverse:
if not in_reverse and not is_driver_stream:
self._model_renderer.render(self._content_rect)
# Fade out bottom of overlays for looks
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE)
if not in_reverse:
if not in_reverse and not is_driver_stream:
self._hud_renderer.render_background()
alert_to_render, not_animating_out = self._alert_renderer.will_render()
should_draw_dmoji = (not in_reverse) and (not self._hud_renderer.drawing_top_icons()) and ui_state.is_onroad()
should_draw_dmoji = ui_state.is_onroad() and (
is_driver_stream or ((not in_reverse) and (not self._hud_renderer.drawing_top_icons()))
)
self._driver_state_renderer.set_should_draw(should_draw_dmoji)
self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10)
if not in_reverse:
self._driver_state_renderer.render()
self._hud_renderer.set_can_draw_top_icons((not in_reverse) and (alert_to_render is None))
self._hud_renderer.set_wheel_critical_icon((not in_reverse) and alert_to_render is not None and not not_animating_out and
self._hud_renderer.set_can_draw_top_icons((not in_reverse) and (not is_driver_stream) and (alert_to_render is None))
self._hud_renderer.set_wheel_critical_icon((not in_reverse) and (not is_driver_stream) and alert_to_render is not None and not not_animating_out and
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
# TODO: have alert renderer draw offroad mici label below
if ui_state.started:
self._alert_renderer.render(self._content_rect)
if not in_reverse:
if not in_reverse and not is_driver_stream:
self._hud_renderer.render_foreground()
if (not in_reverse) and alert_to_render is None:
if (not in_reverse) and (not is_driver_stream) and alert_to_render is None:
self._experimental_mode_banner.render(self._content_rect)
if not in_reverse:
rendered_standstill_timer = False
if not in_reverse and not is_driver_stream:
rendered_standstill_timer = self._standstill_timer.render(self._content_rect, in_reverse)
if not in_reverse and not is_driver_stream and not rendered_standstill_timer:
self._min_steer_speed_banner.render(self._content_rect)
# End clipping region
@@ -414,8 +527,9 @@ class AugmentedRoadView(CameraView):
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
if not in_reverse:
if not in_reverse and not is_driver_stream:
self._confidence_ball.render(self.rect)
if not in_reverse:
self._draw_border()
self._bookmark_icon.render(self.rect)
@@ -454,16 +568,29 @@ class AugmentedRoadView(CameraView):
return str(gear).lower().endswith("reverse")
def is_in_reverse(self) -> bool:
return self._is_in_reverse()
def _switch_stream_if_needed(self, sm):
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
camera_view = ui_state.params.get_int("CameraView", return_default=True, default=CAMERA_VIEW_WIDE)
if camera_view not in (CAMERA_VIEW_AUTO, CAMERA_VIEW_DRIVER, CAMERA_VIEW_STANDARD, CAMERA_VIEW_WIDE):
camera_view = CAMERA_VIEW_WIDE
if camera_view == CAMERA_VIEW_DRIVER:
target = DRIVER_CAM
elif camera_view == CAMERA_VIEW_STANDARD:
target = ROAD_CAM
elif camera_view == CAMERA_VIEW_WIDE:
target = WIDE_CAM
elif sm['selfdriveState'].experimentalMode 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 = ROAD_CAM
else:
# Hysteresis zone - keep current stream
target = self.stream_type
# Hysteresis zone - keep the current road camera selection.
target = WIDE_CAM if self.stream_type == WIDE_CAM else ROAD_CAM
else:
target = ROAD_CAM
@@ -494,6 +621,9 @@ class AugmentedRoadView(CameraView):
self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
if self.stream_type == DRIVER_CAM:
return CameraView._calc_frame_matrix(self, rect)
# Get camera configuration
# TODO: cache with vEgo?
calib_time = ui_state.sm.recv_frame['liveCalibration']
+1
View File
@@ -377,6 +377,7 @@ class CameraView(Widget):
self.client = self._target_client
self._stream_type = self._target_stream_type
self._texture_needs_update = True
self._enhance_driver_val[0] = 1 if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0
# Reset state
self._target_client = None
@@ -1753,6 +1753,32 @@
"ui_type": "toggle",
"is_parent_toggle": true
},
{
"key": "CameraView",
"label": "Camera View",
"description": "Choose which camera feed the driving screen uses: Auto, Driver, Standard, or Wide.",
"data_type": "int",
"ui_type": "dropdown",
"options": [
{
"value": 0,
"label": "Auto"
},
{
"value": 1,
"label": "Driver"
},
{
"value": 2,
"label": "Standard"
},
{
"value": 3,
"label": "Wide"
}
],
"parent_key": "QOLVisuals"
},
{
"key": "DriverCamera",
"label": "Show Driver Camera When In Reverse",