IQ.Pilot Release Commit @ 2f37564
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user