From 46596218ba38c7f47b14ed30bccb2f26499b2da0 Mon Sep 17 00:00:00 2001 From: AngusBell97 <124716116+AngusBell97@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:17:29 +0100 Subject: [PATCH] Add an optional GPU-model-ready chime (cherry picked from commit 0a212734c17c815a1d66d0482d562c5152eccc0f) --- common/params_keys.h | 1 + selfdrive/assets/sounds/model_ready.wav | Bin 0 -> 38444 bytes selfdrive/ui/soundd.py | 36 +++++++++ .../ui/tests/test_gpu_model_ready_sound.py | 70 ++++++++++++++++++ .../common/assets/device_settings_layout.json | 8 ++ starpilot/common/gpu_model_ready_sound.py | 20 +++++ 6 files changed, 135 insertions(+) create mode 100644 selfdrive/assets/sounds/model_ready.wav create mode 100644 selfdrive/ui/tests/test_gpu_model_ready_sound.py create mode 100644 starpilot/common/gpu_model_ready_sound.py diff --git a/common/params_keys.h b/common/params_keys.h index dcd46dbd6a..4a9fd1c775 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -352,6 +352,7 @@ inline static std::unordered_map keys = { {"DrivingModelVersion", {PERSISTENT, STRING, "v15", "v15", 1}}, {"DynamicPathWidth", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}}, {"DynamicPedalsOnUI", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}}, + {"GpuModelReadySound", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}}, {"EngageVolume", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}}, {"EVTuning", {PERSISTENT, BOOL, "0", "0", 3}}, {"Fahrenheit", {PERSISTENT, BOOL, "0", "0", 3}}, diff --git a/selfdrive/assets/sounds/model_ready.wav b/selfdrive/assets/sounds/model_ready.wav new file mode 100644 index 0000000000000000000000000000000000000000..b0592f6573618d425836f98384f45eec371d8756 GIT binary patch literal 38444 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 len(loaded): + self.current_alert = AudibleAlert.none + self.current_sound_frame = 0 + now = time.monotonic() + if now - self.model_ready_last_check >= 0.25: + self.model_ready_last_check = now + try: + self.model_ready_pending = self.model_ready_chime.update( + active=self.model_ready_params.get_bool("UsbGpuActive"), + loading=self.model_ready_params.get_bool("UsbGpuLoading"), + onroad=self.model_ready_params.get_bool("IsOnroad"), + enabled=self.model_ready_params.get_bool("GpuModelReadySound"), now=now) + except Exception: + self.model_ready_chime.consume() + self.model_ready_pending = False + # Run after stock/custom alert selection: warnings and timeout alerts always win. + if (self.model_ready_pending and self.current_alert == AudibleAlert.none + and not self.selfdrive_timeout_alert and sm.valid["selfdriveState"] and sm.alive["selfdriveState"]): + self.model_ready_chime.consume() + self.model_ready_pending = False + self.current_alert_type = "" + self.update_alert(GPU_MODEL_READY_ALERT) + def get_volume_override(self): if self.current_alert_type.startswith("belowSteerSpeed/"): return self.starpilot_toggles.below_steer_speed_volume / 100.0 @@ -391,6 +426,7 @@ class Soundd: self.current_volume = 0.0 self.get_audible_alert(sm) + self.update_model_ready_sound(sm) if self.current_alert != AudibleAlert.none: v_ego = max(float(getattr(sm["starpilotSelfdriveState"], "vEgo", 0.0)), 0.0) diff --git a/selfdrive/ui/tests/test_gpu_model_ready_sound.py b/selfdrive/ui/tests/test_gpu_model_ready_sound.py new file mode 100644 index 0000000000..1911ea5ab3 --- /dev/null +++ b/selfdrive/ui/tests/test_gpu_model_ready_sound.py @@ -0,0 +1,70 @@ +import ast +from pathlib import Path +from types import SimpleNamespace +import wave + +import pytest + +from openpilot.starpilot.common.gpu_model_ready_sound import GpuModelReadyChime + + +def update(chime, now=0, **kwargs): + return chime.update(**dict(active=False, loading=False, onroad=True, enabled=True, now=now) | kwargs) + + +def test_ready_once_after_success_not_during_loading_or_failure(): + chime = GpuModelReadyChime() + assert not update(chime, loading=True) + assert not update(chime, 1, active=True, loading=True) + assert update(chime, 2, active=True) + chime.consume() + assert not update(chime, 3, active=True) + assert not update(chime, 4, loading=True) + assert not update(chime, 5) + + +def test_restart_with_already_ready_model_does_not_chime(): + assert not update(GpuModelReadyChime(), active=True) + + +@pytest.mark.parametrize("cancel", [dict(enabled=False), dict(onroad=False), dict(active=False), dict(loading=True)]) +def test_cancel_pending_on_disabled_offroad_fallback_or_reload(cancel): + chime = GpuModelReadyChime() + update(chime) + assert update(chime, 1, active=True) + assert not update(chime, 2, **(dict(active=True) | cancel)) + + +def test_pending_notification_expires_without_late_chime(): + chime = GpuModelReadyChime() + update(chime) + assert update(chime, 1, active=True) + assert not update(chime, 6, active=True) + assert not update(chime, 7, active=True) + + +def test_enabling_option_after_load_does_not_replay(): + chime = GpuModelReadyChime() + update(chime, enabled=False) + assert not update(chime, 1, active=True, enabled=False) + assert not update(chime, 2, active=True, enabled=True) + + +@pytest.mark.parametrize("alert,timeout,valid,alive,expected", [(1,False,True,True,False),(0,True,True,True,False),(0,False,False,True,False),(0,False,True,False,False),(0,False,True,True,True)]) +def test_soundd_never_replaces_existing_or_timeout_alerts(alert,timeout,valid,alive,expected): + source=(Path(__file__).parents[1]/'soundd.py').read_text() + cls=next(n for n in ast.parse(source).body if isinstance(n,ast.ClassDef) and n.name=='Soundd') + method=next(n for n in cls.body if isinstance(n,ast.FunctionDef) and n.name=='update_model_ready_sound') + scope={'time':SimpleNamespace(monotonic=lambda:1.1),'AudibleAlert':SimpleNamespace(none=0),'GPU_MODEL_READY_ALERT':2000} + exec(compile(ast.Module(body=[method],type_ignores=[]),'','exec'),scope) + calls=[] + sound=SimpleNamespace(model_ready_last_check=1.0,model_ready_pending=True,model_ready_chime=GpuModelReadyChime(),current_alert=alert,selfdrive_timeout_alert=timeout,update_alert=calls.append) + sm=SimpleNamespace(valid={'selfdriveState':valid},alive={'selfdriveState':alive}) + scope['update_model_ready_sound'](sound,sm) + assert calls == ([2000] if expected else []) + + +def test_notification_asset_matches_soundd_audio_format(): + with wave.open(str(Path(__file__).parents[2]/'assets/sounds/model_ready.wav')) as audio: + assert (audio.getnchannels(),audio.getsampwidth(),audio.getframerate()) == (1,2,48000) + assert 0 < audio.getnframes() < 48000 diff --git a/starpilot/common/assets/device_settings_layout.json b/starpilot/common/assets/device_settings_layout.json index 27ddec6a37..a1fba4454c 100644 --- a/starpilot/common/assets/device_settings_layout.json +++ b/starpilot/common/assets/device_settings_layout.json @@ -3355,6 +3355,14 @@ "data_type": "bool", "ui_type": "toggle", "settings_tier": "simple" + }, + { + "key": "GpuModelReadySound", + "label": "GPU Model Ready Sound", + "description": "Play a short chime when the external GPU model finishes loading. Driving alerts take priority.", + "data_type": "bool", + "ui_type": "toggle", + "settings_tier": "simple" } ] }, diff --git a/starpilot/common/gpu_model_ready_sound.py b/starpilot/common/gpu_model_ready_sound.py new file mode 100644 index 0000000000..3e4d8803d4 --- /dev/null +++ b/starpilot/common/gpu_model_ready_sound.py @@ -0,0 +1,20 @@ +"""Optional notification state; never supersedes driving alerts or starts inference.""" + + +class GpuModelReadyChime: + def __init__(self): + self.ready = None + self.pending_until = None + + def update(self, *, active, loading, onroad, enabled, now): + ready = bool(onroad and active and not loading) + # A soundd restart with an already-loaded model is not a loading event. + if self.ready is False and ready and enabled: + self.pending_until = now + 5.0 + self.ready = ready + if not enabled or not ready or (self.pending_until is not None and now >= self.pending_until): + self.pending_until = None + return self.pending_until is not None + + def consume(self): + self.pending_until = None