IQ.Pilot Release Commit @ 2f37564

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-27 01:30:19 -05:00
parent 6a4096e52e
commit 15c14e1369
236 changed files with 2475 additions and 990 deletions

View File

@@ -9,7 +9,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
if gui_app.iqpilot_ui():
from openpilot.system.ui.iqpilot.widgets.list_view import toggle_item
from openpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
# Description constants
DESCRIPTIONS = {

View File

@@ -21,7 +21,7 @@ from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller_tici import Scroller
if gui_app.iqpilot_ui():
from openpilot.system.ui.iqpilot.widgets.list_view import button_item
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
# Description constants
DESCRIPTIONS = {

View File

@@ -12,7 +12,7 @@ from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller_tici import Scroller
if gui_app.iqpilot_ui():
from openpilot.system.ui.iqpilot.widgets.list_view import button_item
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
# TODO: remove this. updater fails to respond on startup if time is not correct
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
@@ -237,7 +237,7 @@ class SoftwareLayout(Widget):
def _on_auth_branch(self):
# Collect username then token; store encrypted and signal the updater to
# re-check (refreshing the available-branch list for private repos).
from openpilot.system.ui.iqpilot.widgets.list_view import open_text_prompt
from openpilot.system.ui.iqwidgets.widgets.list_view import open_text_prompt
from openpilot.common import git_creds
creds = git_creds.get_credentials()

View File

@@ -10,8 +10,8 @@ from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
if gui_app.iqpilot_ui():
from openpilot.system.ui.iqpilot.widgets.list_view import toggle_item
from openpilot.system.ui.iqpilot.widgets.list_view import multiple_button_item
from openpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
from openpilot.system.ui.iqwidgets.widgets.list_view import multiple_button_item
from openpilot.iqpilot.ui.layouts.settings.iq_dynamic import IQDynamicLayout
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants

View File

@@ -544,10 +544,10 @@ class SettingsHubLayout(Widget):
def _toggle_offroad_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Enter Always Offroad Mode")))
gui_app.set_modal_overlay(alert_dialog(tr("Disengage before forcing offroad")))
return
active = ui_state.params.get_bool("IQAlwaysOffroad")
msg = tr("Are you sure you want to exit Always Offroad mode?") if active else tr("Are you sure you want to enter Always Offroad mode?")
msg = tr("Leave forced-offroad mode now?") if active else tr("Switch the device into forced-offroad mode?")
def _confirm(result: int):
if result == DialogResult.CONFIRM and not ui_state.engaged:

View File

@@ -200,7 +200,7 @@ class TrainingGuideDMTutorial(Widget):
looking_center = False
# stay at 100% once reached
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
if (dm_state.visionPolicyState.faceDetected and looking_center) or self._progress.x > 0.99:
slow = self._progress.x < 0.25
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
self._progress.x += 1.0 / (duration * gui_app.target_fps)

View File

@@ -27,7 +27,7 @@ class DisplayLayoutMici(NavScroller):
self._force_mici = BigParamControl("force mici UI", "ForceSmallUI")
self._display_bright = MappedParamToggle("display brightness", "Brightness",
_DISPLAY_BRIGHT_OPTIONS, _DISPLAY_BRIGHT_VALUES)
self._onroad_bright = MappedParamToggle("onroad brightness", "OnroadScreenOffBrightness",
self._onroad_bright = MappedParamToggle("driving brightness", "OnroadScreenOffBrightness",
_ONROAD_BRIGHT_OPTIONS, _ONROAD_BRIGHT_VALUES)
self._delay = MappedParamToggle("brightness delay", "OnroadScreenOffTimer",
_DELAY_OPTIONS, _DELAY_VALUES)

View File

@@ -10,7 +10,7 @@ import pyray as rl
from cereal import custom
from openpilot.system.ui.iqpilot.widgets.helpers.glyphs import draw_star
from openpilot.system.ui.iqwidgets.widgets.helpers.glyphs import draw_star
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
@@ -96,10 +96,10 @@ class ModelsLayoutMici(NavScroller):
self._redownload_icon = gui_app.texture("icons_mici/settings/device/update.png", 56, 56, keep_aspect_ratio=True)
self._reset_icon = gui_app.texture("icons_mici/wheel.png", 56, 56)
self._current = BigButton("current model")
self._current = BigButton("active model")
self._current.set_click_callback(self._show_folders)
self._cancel = BigButton("cancel download")
self._cancel = BigButton("stop download")
self._cancel.set_click_callback(self._cancel_model_request)
self._cancel.set_visible(self._is_downloading)
@@ -107,17 +107,17 @@ class ModelsLayoutMici(NavScroller):
self._redownload.set_click_callback(self._confirm_redownload_model)
self._redownload.set_enabled(self._can_redownload)
self._refresh = BigButton("refresh model list")
self._refresh = BigButton("reload model list")
self._refresh.set_click_callback(lambda: ui_state.params.put("ModelManager_LastSyncTime", 0))
self._supercombo = GreyBigButton("driving model")
self._supercombo = GreyBigButton("combined model")
self._supercombo.set_visible(False)
self._vision = GreyBigButton("vision model")
self._vision = GreyBigButton("vision weights")
self._vision.set_visible(False)
self._policy = GreyBigButton("policy model")
self._policy = GreyBigButton("policy weights")
self._policy.set_visible(False)
self._clear = BigButton("clear model cache")
self._clear = BigButton("purge model cache")
self._clear.set_click_callback(self._confirm_clear_cache)
self._clear.set_enabled(lambda: ui_state.is_offroad())
@@ -125,7 +125,7 @@ class ModelsLayoutMici(NavScroller):
self._sw_delay = MappedParamToggle("manual delay offset", "IQSoftwareSteerDelay", _DELAY_OPTIONS, _DELAY_VALUES)
self._sw_delay.set_visible(lambda: not self._steer_delay._checked)
self._lane_turn = BigParamControl("use lane turn desires", "IQLaneTurnDesire")
self._lane_turn = BigParamControl("low-speed turn planning", "IQLaneTurnDesire")
self._lane_speed = MappedParamToggle("lane turn speed", "IQLaneTurnValue", _LANE_TURN_OPTIONS, _LANE_TURN_VALUES)
self._lane_speed.set_visible(lambda: self._lane_turn._checked)

View File

@@ -99,6 +99,32 @@ class ForgetButton(Widget):
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
class DisconnectButton(Widget):
MARGIN = 12
RADIUS = 42
# the only round button art is the destructive red one, and dropping a connection is not destructive
BG = rl.Color(56, 56, 61, 255)
BG_PRESSED = rl.Color(84, 84, 90, 255)
def __init__(self, disconnect_network: Callable):
super().__init__()
self._disconnect_network = disconnect_network
self._slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 38, 38)
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg = BigConfirmationDialog("slide to\ndisconnect", gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 54, 54),
self._disconnect_network)
gui_app.push_widget(dlg)
def _render(self, _):
center = rl.Vector2(self._rect.x + self._rect.width / 2, self._rect.y + self._rect.height / 2)
rl.draw_circle_v(center, self.RADIUS, self.BG_PRESSED if self.is_pressed else self.BG)
rl.draw_texture_ex(self._slash_txt, (center.x - self._slash_txt.width / 2, center.y - self._slash_txt.height / 2),
0, 1.0, rl.WHITE)
class WifiButton(BigButton):
LABEL_PADDING = 98
LABEL_WIDTH = 402 - 98 - 28
@@ -111,9 +137,11 @@ class WifiButton(BigButton):
self._connecting_ssid = connecting_ssid
self._wifi_icon = WifiIcon(network)
self._forget_btn = ForgetButton(self._forget_network)
self._disconnect_btn = DisconnectButton(self._disconnect_network)
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
self._network_missing = False
self._network_forgetting = False
self._network_disconnecting = False
self._wrong_password = False
@property
@@ -142,9 +170,18 @@ class WifiButton(BigButton):
self._network_forgetting = True
self._wifi_manager.forget_connection(self._network.ssid)
def _disconnect_network(self):
if self._network_disconnecting:
return
self._network_disconnecting = True
self._wifi_manager.disconnect_connection(self._network.ssid)
def on_forgotten(self):
self._network_forgetting = False
def on_disconnected(self):
self._network_disconnecting = False
def set_network_missing(self, missing: bool):
self._network_missing = missing
self._wifi_icon.set_network_missing(missing)
@@ -171,31 +208,44 @@ class WifiButton(BigButton):
@property
def _show_forget_btn(self) -> bool:
if self._is_tethering or self._network_forgetting:
if self._is_tethering or self._network_forgetting or self._show_disconnect_btn:
return False
return (self._is_saved and not self._wrong_password) or self._is_connecting
@property
def _show_disconnect_btn(self) -> bool:
# 402 units of row cannot hold both buttons plus the status word, so the slot is contextual:
# disconnect while connected, forget once it is only saved
if self._is_tethering or self._network_forgetting or self._network_disconnecting:
return False
return self._is_connected
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect):
return
if self._show_disconnect_btn and rl.check_collision_point_rec(mouse_pos, self._disconnect_btn.rect):
return
super()._handle_mouse_release(mouse_pos)
def _get_label_font_size(self):
return 48
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed)
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed and not self._disconnect_btn.is_pressed)
self._forget_btn.set_touch_valid_callback(touch_callback)
self._disconnect_btn.set_touch_valid_callback(touch_callback)
def _update_state(self):
super()._update_state()
if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting,
self._network.security_type == SecurityType.UNSUPPORTED)):
self._network_disconnecting, self._network.security_type == SecurityType.UNSUPPORTED)):
self.set_enabled(False)
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.585)))
self._sub_label.set_font_weight(FontWeight.ROMAN)
if self._network_forgetting:
self.set_value("forgetting...")
elif self._network_disconnecting:
self.set_value("disconnecting...")
elif self._is_connecting:
self.set_value("starting..." if self._is_tethering else "connecting...")
elif self._is_connected:
@@ -219,9 +269,10 @@ class WifiButton(BigButton):
if self.value:
sub_label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING
sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0)
sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0) \
- (self._disconnect_btn.rect.width if self._show_disconnect_btn else 0)
sub_label_height = self._sub_label.get_content_height(sub_label_w)
if self._is_connected and not self._network_forgetting:
if self._is_connected and not self._network_forgetting and not self._network_disconnecting:
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
rl.draw_texture_ex(self._check_txt, rl.Vector2(sub_label_x, check_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
sub_label_x += self._check_txt.width + 14
@@ -230,11 +281,19 @@ class WifiButton(BigButton):
self._wifi_icon.render(rl.Rectangle(self._rect.x + 30, btn_y + 30, self._wifi_icon.rect.width, self._wifi_icon.rect.height))
btn_right = self._rect.x + self._rect.width
if self._show_forget_btn:
self._forget_btn.render(rl.Rectangle(
self._rect.x + self._rect.width - self._forget_btn.rect.width,
btn_right - self._forget_btn.rect.width,
btn_y + self._rect.height - self._forget_btn.rect.height,
self._forget_btn.rect.width, self._forget_btn.rect.height))
btn_right -= self._forget_btn.rect.width
if self._show_disconnect_btn:
self._disconnect_btn.render(rl.Rectangle(
btn_right - self._disconnect_btn.rect.width,
btn_y + self._rect.height - self._disconnect_btn.rect.height,
self._disconnect_btn.rect.width, self._disconnect_btn.rect.height))
class ScanningButton(BigButton):
@@ -298,6 +357,9 @@ class WifiUIMici(NavScroller):
def _on_disconnected(self):
self._connecting = None
for btn in self._scroller.items:
if isinstance(btn, WifiButton):
btn.on_disconnected()
def _on_forgotten(self, ssid=None):
self._connecting = None

View File

@@ -47,7 +47,7 @@ class SabSettingsPanel(NavScroller):
self._main_cruise = BigParamControl("Availability While Cruise Changes", "AolMainCruiseAllowed")
self._brake = SabBrakeToggle()
self._mode = MappedParamToggle("Brake Response Mode", "AolSteeringMode",
["remain active", "standby", "disengage"], [0, 1, 2])
["stay engaged", "standby", "disengage"], [0, 1, 2])
self._scroller.add_widgets([self._main_cruise, self._brake, self._mode])
def show_event(self):
@@ -67,7 +67,7 @@ class LaneChangePanel(NavScroller):
def __init__(self):
super().__init__()
self._timer = MappedParamToggle("Auto Lane Change", "IQLaneChangeTimer",
["off", "nudge", "nudgeless", "0.5 s", "1 s", "2 s", "3 s"],
["off", "nudge", "no nudge", "0.5 s", "1 s", "2 s", "3 s"],
[-1, 0, 1, 2, 3, 4, 5])
self._bsm_delay = BigParamControl("Delay with Blind Spot", "IQLaneChangeBsmDelay")
self._continuous = BigParamControl("Continuous Changes", "LaneChangeContinuous")
@@ -87,7 +87,7 @@ class LaneChangePanel(NavScroller):
class SteeringLayoutMici(NavScroller):
_AOL_MODES = ["remain active", "standby", "disengage"]
_AOL_MODES = ["stay engaged", "standby", "disengage"]
def __init__(self):
super().__init__()

View File

@@ -73,7 +73,7 @@ class VehicleLayoutMici(NavScroller):
toggle_callback=self._on_toyota_long)
self._hyundai_tuning = MappedParamToggle("hyundai long. tuning", "HyundaiLongitudinalTuning",
["off", "dynamic", "predictive"], [0, 1, 2])
self._subaru_snag = BigParamControl("stop and go (beta)", "SubaruStopAndGo")
self._subaru_snag = BigParamControl("creep from standstill (beta)", "SubaruStopAndGo")
self._subaru_manual = BigParamControl("stop and go manual brake", "SubaruStopAndGoManualParkingBrake")
self._vw_pq_hca = BigParamControl("PQ HCA status 7 mode", "pqhca5or7Toggle")
self._vw_lateral = BigParamControl("lateral when cruise faulted", "AllowLateralWhenLongUnavailable")

View File

@@ -10,7 +10,7 @@ class VisualsLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._blind_spot = BigParamControl("Blind Spot Warnings", "BlindSpot")
self._steering_arc = BigParamControl("Steering Arc", "TorqueBar")
self._steering_arc = BigParamControl("Steering Effort Arc", "TorqueBar")
self._road_name = BigParamControl("Road Name", "RoadNameToggle")
self._turn_signals = BigParamControl("Turn Signals", "ShowTurnSignals")
self._accel_bar = BigParamControl("Acceleration Bar", "RocketFuel")

View File

@@ -1,20 +1,14 @@
import pyray as rl
from cereal import log, messaging
from cereal import car, log, messaging
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.selfdrived.events import EVENTS, ET
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.label import gui_label
EventName = log.OnroadEvent.EventName
EVENT_TO_INT = EventName.schema.enumerants
class DriverCameraView(CameraView):
def _calc_frame_matrix(self, rect: rl.Rectangle):
base = super()._calc_frame_matrix(rect)
@@ -116,10 +110,14 @@ class DriverCameraDialog(NavWidget):
return
msg = messaging.new_message('selfdriveState')
if dm_state is not None and len(dm_state.events):
event_name = EVENT_TO_INT[dm_state.events[0].name]
if event_name is not None and event_name in EVENTS and ET.PERMANENT in EVENTS[event_name]:
msg.selfdriveState.alertSound = EVENTS[event_name][ET.PERMANENT].audible_alert
if dm_state is not None:
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
alert_sounds = {
'one': AudibleAlert.preAlert,
'two': AudibleAlert.promptDistracted,
'three': AudibleAlert.warningImmediate,
}
msg.selfdriveState.alertSound = alert_sounds.get(str(dm_state.alertLevel), AudibleAlert.none)
self._pm.send('selfdriveState', msg)
def _render_dm_alerts(self, rect: rl.Rectangle):
@@ -127,29 +125,30 @@ class DriverCameraDialog(NavWidget):
dm_state = ui_state.sm["driverMonitoringState"]
self._publish_alert_sound(dm_state)
is_vision = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
f"Awareness: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, f"Awareness: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
if not dm_state.events:
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
return
# Show first event (only one should be active at a time)
event_name_str = str(dm_state.events[0].name).split('.')[-1]
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
@@ -166,7 +165,7 @@ class DriverCameraDialog(NavWidget):
def _draw_face_detection(self, rect: rl.Rectangle):
dm_state = ui_state.sm["driverMonitoringState"]
driver_data = self.driver_state_renderer.get_driver_data()
if not dm_state.faceDetected:
if not dm_state.visionPolicyState.faceDetected:
return
# Get face position and orientation

View File

@@ -6,12 +6,12 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
AlertSize = log.SelfdriveState.AlertSize
DEBUG = False
ACTIVE_ACCENT = rl.Color(0x0C, 0x94, 0x96, 0xFF)
CONE_COLOR_ORANGE = (255, 115, 0)
LOOKING_CENTER_THRESHOLD_UPPER = math.radians(6)
LOOKING_CENTER_THRESHOLD_LOWER = math.radians(3)
@@ -21,6 +21,7 @@ class DriverStateRenderer(Widget):
BASE_SIZE = 60
LINES_ANGLE_INCREMENT = 5
LINES_STALE_ANGLES = 3.0 # seconds
AWARENESS_UNFULL_PERCENT = 95
def __init__(self, lines: bool = False, inset: bool = False):
super().__init__()
@@ -35,11 +36,15 @@ class DriverStateRenderer(Widget):
self._is_active = False
self._is_rhd = False
self._face_detected = False
self._face_pitch = 0.
self._face_yaw = 0.
self._should_draw = False
self._force_active = False
self._looking_center = False
self._awareness_unfull = False
self._fade_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
self._color_fade_filter = FirstOrderFilter(1.0, 0.05, 1 / gui_app.target_fps)
self._pitch_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps, initialized=False)
self._yaw_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps, initialized=False)
self._rotation_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps, initialized=False)
@@ -95,6 +100,7 @@ class DriverStateRenderer(Widget):
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
if self.effective_active:
active_amount = self._color_fade_filter.update(0.0 if self._awareness_unfull else 1.0)
source_rect = rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height)
dest_rect = rl.Rectangle(
self._rect.x + self._rect.width / 2,
@@ -104,13 +110,16 @@ class DriverStateRenderer(Widget):
)
if not self._lines:
r = int(round(ACTIVE_ACCENT.r * active_amount + CONE_COLOR_ORANGE[0] * (1 - active_amount)))
g = int(round(ACTIVE_ACCENT.g * active_amount + CONE_COLOR_ORANGE[1] * (1 - active_amount)))
b = int(round(ACTIVE_ACCENT.b * active_amount + CONE_COLOR_ORANGE[2] * (1 - active_amount)))
rl.draw_texture_pro(
self._dm_cone,
source_rect,
dest_rect,
rl.Vector2(dest_rect.width / 2, dest_rect.height / 2),
self._rotation_filter.x - 90,
rl.Color(ACTIVE_ACCENT.r, ACTIVE_ACCENT.g, ACTIVE_ACCENT.b, int(255 * self._fade_filter.x)),
rl.Color(r, g, b, int(255 * self._fade_filter.x)),
)
else:
@@ -150,9 +159,12 @@ class DriverStateRenderer(Widget):
sm = ui_state.sm
dm_state = sm["driverMonitoringState"]
self._is_active = dm_state.isActiveMode
self._is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
self._is_rhd = dm_state.isRHD
self._face_detected = dm_state.faceDetected
self._face_detected = dm_state.visionPolicyState.faceDetected
self._awareness_unfull = self.effective_active and dm_state.visionPolicyState.awarenessPercent < self.AWARENESS_UNFULL_PERCENT
self._face_pitch = dm_state.visionPolicyState.pose.pitch + math.radians(6)
self._face_yaw = -dm_state.visionPolicyState.pose.yaw
driverstate = sm["driverStateV2"]
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
@@ -160,24 +172,9 @@ class DriverStateRenderer(Widget):
def _update_state(self):
# Get monitoring state
driver_data = self.get_driver_data()
driver_orient = driver_data.faceOrientation
if len(driver_orient) != 3:
return
# Calibrate orientation so looking straight ahead at the road (instead of at the device) reads
# (0, 0), using live calibration. Makes the cone point in the correct direction. (stock PR #37149)
sm = ui_state.sm
if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
cal_rpy = sm['liveCalibration'].rpyCalib
else:
cal_rpy = [0.0, 0.0, 0.0]
_, pitch, yaw = face_orientation_from_net(driver_orient, driver_data.facePosition, cal_rpy)
yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
pitch = self._pitch_filter.update(pitch)
yaw = self._yaw_filter.update(yaw)
_ = self.get_driver_data()
pitch = self._pitch_filter.update(self._face_pitch)
yaw = self._yaw_filter.update(self._face_yaw)
# hysteresis on looking center
if abs(pitch) < LOOKING_CENTER_THRESHOLD_LOWER and abs(yaw) < LOOKING_CENTER_THRESHOLD_LOWER:
@@ -198,9 +195,8 @@ class DriverStateRenderer(Widget):
rl.draw_circle(int(pitch_x), 100, 5, rl.GREEN)
rl.draw_circle(int(yaw_x), 120, 5, rl.GREEN)
# filter head rotation, handling wrap-around (bias pitch up since calib/DM pose isn't exact,
# and halve yaw sensitivity)
rotation = math.degrees(math.atan2((pitch + math.radians(6)) * 2, yaw))
# filter head rotation, handling wrap-around
rotation = math.degrees(math.atan2(pitch * 2, yaw))
angle_diff = rotation - self._rotation_filter.x
angle_diff = ((angle_diff + 180) % 360) - 180
self._rotation_filter.update(self._rotation_filter.x + angle_diff)

View File

@@ -114,7 +114,7 @@ class DriverStateRenderer(Widget):
# Get monitoring state
dm_state = sm["driverMonitoringState"]
self.is_active = dm_state.isActiveMode
self.is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
self.is_rhd = dm_state.isRHD
# Update fade state (smoother transition between active/inactive)

View File

@@ -52,6 +52,22 @@ class LeadVehicle:
fill_alpha: int = 0
@dataclass
class VisionDot:
x: float
y: float
tx: float
ty: float
radius: float
tradius: float
alpha: float = 0.0
talpha: float = 1.0
rgb: tuple[int, int, int] | None = None
_VD_EASE = 0.4
class ModelRenderer(Widget, IQModelRenderer):
def __init__(self):
Widget.__init__(self)
@@ -66,14 +82,14 @@ class ModelRenderer(Widget, IQModelRenderer):
self._road_edge_stds = np.zeros(2, dtype=np.float32)
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
self._track_dots: list[LeadVehicle] = []
self._vision_dots: list[LeadVehicle] = []
self._sign_dots: list[tuple[float, float, float, rl.Color]] = []
self._vision_dots: list[VisionDot] = []
self._vt_frame = -1
self._frame_transform: np.ndarray | None = None
self._frame_transform_wide = False
self._path_offset_z = HEIGHT_INIT[0]
self._counter = -1
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
self._ambient_dots = ui_state.params.get_bool("AmbientTrackDots")
self._ambient_dots = bool(ui_state.params.get("AmbientTrackDots", return_default=True))
# Initialize ModelPoints objects
self._path = ModelPoints()
self._lane_lines = [ModelPoints() for _ in range(4)]
@@ -127,7 +143,7 @@ class ModelRenderer(Widget, IQModelRenderer):
if self._counter % 60 == 0:
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
self._ambient_dots = ui_state.params.get_bool("AmbientTrackDots")
self._ambient_dots = bool(ui_state.params.get("AmbientTrackDots", return_default=True))
self._counter += 1
if sm.updated['carParams']:
@@ -164,7 +180,6 @@ class ModelRenderer(Widget, IQModelRenderer):
if self._ambient_dots:
self._update_vision_dots(sm)
self._draw_vision_dots()
self._draw_sign_dots()
if render_lead_indicator and radar_state:
if self._ambient_dots:
@@ -230,20 +245,34 @@ class ModelRenderer(Widget, IQModelRenderer):
self._track_dots.append(LeadVehicle(center=(float(x), float(y)), radius=float(radius), sz=float(sz)))
def _update_vision_dots(self, sm):
self._vision_dots = []
if (self._frame_transform is None or self._frame_transform_wide or
not sm.alive['iqVehicleTracks'] or not sm.valid['iqVehicleTracks']):
return
# iqVehicleTracks arrives at a few Hz; the dots are eased toward the latest
# detection every render frame so they glide instead of teleporting.
hidden = (self._frame_transform is None or
not sm.alive['iqVehicleTracks'] or not sm.valid['iqVehicleTracks'])
if not hidden:
vt = sm['iqVehicleTracks']
hidden = bool(vt.wide) != self._frame_transform_wide or vt.frameWidth == 0 or vt.frameHeight == 0
self._sign_dots = []
vt = sm['iqVehicleTracks']
if hidden:
for d in self._vision_dots:
d.talpha = 0.0
elif vt.frameId != self._vt_frame:
self._vt_frame = vt.frameId
self._retarget_vision_dots(vt)
for d in self._vision_dots:
d.x += (d.tx - d.x) * _VD_EASE
d.y += (d.ty - d.y) * _VD_EASE
d.radius += (d.tradius - d.radius) * _VD_EASE
d.alpha += (d.talpha - d.alpha) * _VD_EASE
self._vision_dots = [d for d in self._vision_dots if d.alpha > 0.02 or d.talpha > 0.0]
def _retarget_vision_dots(self, vt):
fw, fh = vt.frameWidth, vt.frameHeight
if fw == 0 or fh == 0:
return
m = self._frame_transform
occupied = [d.center for d in self._lead_vehicles + self._track_dots if d.center is not None]
m = self._frame_transform
targets = []
for t in vt.tracks:
is_vehicle = t.label in VEHICLE_TRACK_LABELS
sign_color = SIGN_TRACK_COLORS.get(t.label)
@@ -256,18 +285,34 @@ class ModelRenderer(Widget, IQModelRenderer):
if not (self._rect.x <= x <= self._rect.x + self._rect.width and
self._rect.y <= y <= self._rect.y + self._rect.height):
continue
box_h = (t.y2 - t.y1) * fh * m[1, 1]
radius = float(np.clip(box_h * 0.35, 14.0, 40.0))
if sign_color is not None:
self._sign_dots.append((float(x), float(y), min(radius, 22.0), sign_color))
continue
targets.append((x, y, min(radius, 22.0), (sign_color.r, sign_color.g, sign_color.b)))
elif not any((x - ox) ** 2 + (y - oy) ** 2 < (radius * 2.2) ** 2 for ox, oy in occupied):
targets.append((x, y, radius, None))
if any((x - ox) ** 2 + (y - oy) ** 2 < (radius * 2.2) ** 2 for ox, oy in occupied):
continue
dots = self._vision_dots
used = [False] * len(dots)
for tx, ty, tr, rgb in targets:
best, best_d2 = -1, 1e18
for i, d in enumerate(dots):
if used[i] or (d.rgb is None) != (rgb is None):
continue
d2 = (d.x - tx) ** 2 + (d.y - ty) ** 2
if d2 < best_d2:
best, best_d2 = i, d2
if best >= 0 and best_d2 <= (max(tr, dots[best].radius) * 3.0) ** 2:
d = dots[best]
used[best] = True
d.tx, d.ty, d.tradius, d.talpha, d.rgb = tx, ty, tr, 1.0, rgb
else:
dots.append(VisionDot(x=tx, y=ty, tx=tx, ty=ty, radius=tr, tradius=tr, alpha=0.0, talpha=1.0, rgb=rgb))
used.append(True)
self._vision_dots.append(LeadVehicle(center=(float(x), float(y)), radius=radius, sz=radius))
for i, d in enumerate(dots):
if not used[i]:
d.talpha = 0.0
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
@@ -411,17 +456,17 @@ class ModelRenderer(Widget, IQModelRenderer):
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
def _draw_vision_dots(self):
src = rl.Rectangle(0, 0, self._lead_orb.width, self._lead_orb.height)
for dot in self._vision_dots:
cx, cy = dot.center
r = dot.radius
dest = rl.Rectangle(cx, cy, r * 2.0, r * 2.0)
rl.draw_texture_pro(self._lead_orb, src, dest, rl.Vector2(r, r), 0.0, rl.Color(255, 255, 255, 90))
def _draw_sign_dots(self):
for x, y, r, color in self._sign_dots:
rl.draw_circle(int(x), int(y), r, color)
rl.draw_circle_lines(int(x), int(y), r, rl.Color(255, 255, 255, 160))
a = dot.alpha
if a <= 0.02:
continue
x, y, r = int(dot.x), int(dot.y), dot.radius
if dot.rgb is None:
# soft teal glow, brighter in the center fading to transparent at the edge
rl.draw_circle_gradient(x, y, r, rl.Color(120, 235, 225, int(165 * a)), rl.Color(0, 150, 150, 0))
else:
rl.draw_circle_gradient(x, y, r, rl.Color(dot.rgb[0], dot.rgb[1], dot.rgb[2], int(210 * a)),
rl.Color(dot.rgb[0], dot.rgb[1], dot.rgb[2], 0))
def _draw_track_dots(self):
src = rl.Rectangle(0, 0, self._lead_orb.width, self._lead_orb.height)

View File

@@ -45,26 +45,28 @@ sound_list_iq: dict[int, tuple[str, int | None, float]] = {
AudibleAlertIQ.promptSingleHigh: ("prompt_single_high.wav", 1, MAX_VOLUME),
}
sound_list: dict[int, tuple[str, int | None, float]] = {
# AudibleAlert, file name, play count (none for infinite)
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME),
AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME),
AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME),
AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME),
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
**sound_list_iq,
}
if HARDWARE.get_device_type() in ("tizi", "tici"):
sound_list.update({
def get_sound_list(device_type: str) -> dict[int, tuple[str, int | None, float]]:
sounds = {
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
})
AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME),
AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME),
AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME),
AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME),
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
**sound_list_iq,
}
if device_type in ("tizi", "tici"):
sounds.update({
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
})
return sounds
sound_list = get_sound_list(HARDWARE.get_device_type())
def check_selfdrive_timeout_alert(sm):
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']

View File

@@ -2,13 +2,19 @@ from cereal import car
from cereal import messaging
from cereal.messaging import SubMaster, PubMaster
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
from openpilot.selfdrive.ui.soundd import get_sound_list
import pytest
import time
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
class TestSoundd:
@pytest.mark.parametrize("device_type", ["mici", "tici", "tizi"])
def test_prompt_distracted_sound(self, device_type):
assert get_sound_list(device_type)[AudibleAlert.promptDistracted][0] == "prompt_distracted.wav"
def test_check_selfdrive_timeout_alert(self):
sm = SubMaster(['selfdriveState'])
pm = PubMaster(['selfdriveState'])
@@ -32,4 +38,3 @@ class TestSoundd:
assert check_selfdrive_timeout_alert(sm)
# TODO: add test with micd for checking that soundd actually outputs sounds