diff --git a/.gitattributes b/.gitattributes index 5cb404146d..62e66beb12 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,7 @@ *.ttf filter=lfs diff=lfs merge=lfs -text *.otf filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text +openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text diff --git a/openpilot/selfdrive/assets/sounds/milestone.wav b/openpilot/selfdrive/assets/sounds/milestone.wav new file mode 100644 index 0000000000..91ec34c91d Binary files /dev/null and b/openpilot/selfdrive/assets/sounds/milestone.wav differ diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index 7b96366894..cb116469d7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -1,5 +1,8 @@ +import os + import pyray as rl import openpilot.cereal.messaging as messaging +from openpilot.common.hardware import PC 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 @@ -61,7 +64,8 @@ class MiciMainLayout(Scroller): # Start onboarding if terms or training not completed, make sure to push after self self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self)) - if not self._onboarding_window.completed: + skip_onboarding_for_local_prototype = PC and os.getenv("SP_MILESTONE_PROTOTYPE") == "1" + if not self._onboarding_window.completed and not skip_onboarding_for_local_prototype: gui_app.push_widget(self._onboarding_window) # initialize correct onroad layout diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index e37e9b7ea9..149b136cce 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -19,10 +19,15 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum +MILESTONE_PROTOTYPE_ENABLED = gui_app.sunnypilot_ui() + if gui_app.sunnypilot_ui(): from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus + if MILESTONE_PROTOTYPE_ENABLED: + from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_celebration_prototype import MilestoneCelebrationPrototype + OpState = log.SelfdriveState.OpenpilotState CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD @@ -156,6 +161,7 @@ class AugmentedRoadView(CameraView): self._alert_renderer = AlertRenderer() self._driver_state_renderer = DriverStateRenderer() self._confidence_ball = ConfidenceBall() + self._milestone_celebration = MilestoneCelebrationPrototype() if MILESTONE_PROTOTYPE_ENABLED else None self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, @@ -221,6 +227,9 @@ class AugmentedRoadView(CameraView): # 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 self._milestone_celebration is not None: + self._milestone_celebration.render(self._content_rect) + alert_to_render, not_animating_out = self._alert_renderer.will_render() # Hide DMoji when disengaged unless AlwaysOnDM is enabled @@ -247,6 +256,8 @@ class AugmentedRoadView(CameraView): self._confidence_ball.render(self.rect) self._bookmark_icon.render(self.rect) + if self._milestone_celebration is not None: + self._milestone_celebration.capture_screenshot() def _switch_stream_if_needed(self, sm): if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: diff --git a/openpilot/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py index a61e26b0b6..4740e248db 100644 --- a/openpilot/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -15,6 +15,7 @@ from openpilot.system import micd from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.selfdrive.ui.quiet_mode import QuietMode +from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_tracker_prototype import PerDriveMilestoneTracker, assist_category SAMPLE_RATE = 48000 SAMPLE_BUFFER = 4096 # (approx 100ms) @@ -53,6 +54,7 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME), AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME), + AudibleAlert.complete: ("milestone.wav", 1, MAX_VOLUME), AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME), AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME), @@ -85,6 +87,8 @@ class Soundd(QuietMode): self.selfdrive_timeout_alert = False self.pending_stop = False + self.milestone_tracker = PerDriveMilestoneTracker() + self._started_prev = False self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) @@ -164,6 +168,24 @@ class Soundd(QuietMode): self.update_alert(AudibleAlert.none) self.selfdrive_timeout_alert = False + def update_milestone_alert(self, sm): + started = sm['deviceState'].started + if started != self._started_prev: + self.milestone_tracker.reset() + self._started_prev = started + + if not started: + return + + car_control = sm['carControl'] + milestones = self.milestone_tracker.update( + sm.logMonoTime['carState'], + sm['carState'].vEgo, + assist_category(car_control.latActive, car_control.longActive), + ) + if milestones and self.current_alert == AudibleAlert.none and not self.enabled: + self.update_alert(AudibleAlert.complete) + def calculate_volume(self, weighted_db): volume = ((weighted_db - AMBIENT_DB) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME return math.pow(VOLUME_BASE, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1)) @@ -180,7 +202,7 @@ class Soundd(QuietMode): import sounddevice as sd micd.patch_sounddevice(sd) - sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure']) + sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure', 'deviceState', 'carState', 'carControl']) with self.get_stream(sd) as stream: rk = Ratekeeper(20) @@ -198,6 +220,7 @@ class Soundd(QuietMode): self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.get_audible_alert(sm) + self.update_milestone_alert(sm) # Ramp up immediate warning sound over 4s if self.current_alert == AudibleAlert.warningImmediate: diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_celebration_prototype.py b/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_celebration_prototype.py new file mode 100644 index 0000000000..e500063362 --- /dev/null +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_celebration_prototype.py @@ -0,0 +1,181 @@ +"""PROTOTYPE: Tesla-style milestone celebration over the on-road view. + +Question: does a brief, full-screen confetti overlay feel at home on comma four? +This deliberately keeps all state in memory and retriggers once on every drive. +""" + +import math +import os +import random +import time +from collections import deque +from dataclasses import dataclass + +import pyray as rl + +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_tracker_prototype import ( + AssistCategory, + DistanceMilestone, + PerDriveMilestoneTracker, + assist_category, +) +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +CELEBRATION_DURATION = 4.5 +PARTICLE_COUNT = 150 + +CONFETTI_COLORS = ( + rl.Color(255, 55, 95, 255), + rl.Color(255, 183, 3, 255), + rl.Color(48, 209, 88, 255), + rl.Color(36, 179, 255, 255), + rl.Color(112, 72, 232, 255), + rl.Color(255, 45, 196, 255), +) + + +@dataclass(frozen=True) +class ConfettiParticle: + x: float + y: float + width: float + height: float + speed: float + drift: float + angle: float + spin: float + phase: float + color: rl.Color + + +class MilestoneCelebrationPrototype(Widget): + """Throwaway visual spike enabled on the sunnypilot comma four UI.""" + + def __init__(self): + super().__init__() + self._drive_started_time = -1.0 + self._celebration_started_time: float | None = None + self._current_milestone: DistanceMilestone | None = None + self._pending_milestones: deque[DistanceMilestone] = deque() + self._tracker = PerDriveMilestoneTracker() + self._screenshot_taken = False + self._screenshot_ready = False + self._particles = self._make_particles() + + @staticmethod + def _make_particles() -> list[ConfettiParticle]: + rng = random.Random(20260828) + return [ + ConfettiParticle( + x=rng.random(), + y=rng.uniform(-0.25, 0.95), + width=rng.uniform(10, 24), + height=rng.uniform(24, 58), + speed=rng.uniform(0.12, 0.34), + drift=rng.uniform(-0.035, 0.035), + angle=rng.uniform(0, 360), + spin=rng.uniform(-150, 150), + phase=rng.uniform(0, math.tau), + color=CONFETTI_COLORS[rng.randrange(len(CONFETTI_COLORS))], + ) + for _ in range(PARTICLE_COUNT) + ] + + def _render(self, rect: rl.Rectangle, /) -> None: + now = time.monotonic() + if ui_state.started_time != self._drive_started_time: + # Reset on every off-road -> on-road transition so the spike can be tested on every drive. + self._drive_started_time = ui_state.started_time + self._celebration_started_time = None + self._current_milestone = None + self._pending_milestones.clear() + self._tracker.reset() + self._screenshot_taken = False + self._screenshot_ready = False + + car_control = ui_state.sm["carControl"] + category = assist_category(car_control.latActive, car_control.longActive) + + self._pending_milestones.extend(self._tracker.update( + ui_state.sm.logMonoTime["carState"], + ui_state.sm["carState"].vEgo, + category, + )) + + if self._current_milestone is None and self._pending_milestones: + self._current_milestone = self._pending_milestones.popleft() + self._celebration_started_time = now + + if self._celebration_started_time is None or self._current_milestone is None: + return + + elapsed = now - self._celebration_started_time + if elapsed >= CELEBRATION_DURATION: + self._celebration_started_time = None + self._current_milestone = None + return + + alpha = min(1.0, elapsed / 0.2, (CELEBRATION_DURATION - elapsed) / 0.8) + self._draw_confetti(rect, elapsed, alpha) + self._draw_milestone_card(rect, elapsed, alpha, self._current_milestone) + self._screenshot_ready = elapsed >= 1.0 + + def capture_screenshot(self) -> None: + screenshot_path = os.getenv("SP_MILESTONE_SCREENSHOT") + if screenshot_path and self._screenshot_ready and not self._screenshot_taken: + rl.rl_draw_render_batch_active() + rl.take_screenshot(screenshot_path) + self._screenshot_taken = True + + def _draw_confetti(self, rect: rl.Rectangle, elapsed: float, alpha: float) -> None: + travel_height = rect.height * 1.45 + compact = rect.height <= 300 + particle_scale = rect.height / 1080.0 + particles = self._particles[:100] if compact else self._particles + for particle in particles: + x = rect.x + rect.width * (particle.x + particle.drift * elapsed + 0.012 * math.sin(elapsed * 3 + particle.phase)) + y = rect.y - rect.height * 0.2 + (particle.y * travel_height + particle.speed * rect.height * elapsed) % travel_height + flip = 0.2 + 0.8 * abs(math.sin(elapsed * 5 + particle.phase)) + particle_rect = rl.Rectangle(x, y, particle.width * particle_scale * flip, particle.height * particle_scale) + origin = rl.Vector2(particle_rect.width / 2, particle_rect.height / 2) + color = rl.Color(particle.color.r, particle.color.g, particle.color.b, int(245 * alpha)) + rl.draw_rectangle_pro(particle_rect, origin, particle.angle + particle.spin * elapsed, color) + + @staticmethod + def _draw_milestone_card(rect: rl.Rectangle, elapsed: float, alpha: float, milestone: DistanceMilestone) -> None: + compact = rect.height <= 300 + scale = rect.height / (240.0 if compact else 1080.0) + card_width = (210 if compact else 590) * scale + card_height = (82 if compact else 230) * scale + pulse = 1.0 + 0.025 * math.sin(min(elapsed, 0.6) / 0.6 * math.pi) + card_width *= pulse + card_height *= pulse + card = rl.Rectangle( + rect.x + (rect.width - card_width) / 2, + rect.y + (rect.height - card_height) / 2, + card_width, + card_height, + ) + + rl.draw_rectangle_rounded(card, 0.20, 16, rl.Color(25, 31, 42, int(225 * alpha))) + rl.draw_rectangle_rounded_lines_ex(card, 0.20, 16, max(1, int(2 * scale)), rl.Color(255, 255, 255, int(90 * alpha))) + + number_font = gui_app.font(FontWeight.BOLD) + label_font = gui_app.font(FontWeight.MEDIUM) + number_size = int((35 if compact else 98) * scale) + label_size = int((14 if compact else 42) * scale) + number = f"{milestone.distance_miles:.1f} mi" + label = "FULL ASSIST MILESTONE" if milestone.category == AssistCategory.FULL_ASSIST else "MADS MILESTONE" + + number_bounds = measure_text_cached(number_font, number, number_size) + label_bounds = measure_text_cached(label_font, label, label_size) + number_y = 9 if compact else 35 + label_y = 55 if compact else 145 + number_pos = rl.Vector2(card.x + (card.width - number_bounds.x) / 2, card.y + number_y * scale) + label_pos = rl.Vector2(card.x + (card.width - label_bounds.x) / 2, card.y + label_y * scale) + rl.draw_text_ex(number_font, number, number_pos, number_size, 0, rl.Color(255, 255, 255, int(255 * alpha))) + rl.draw_text_ex(label_font, label, label_pos, label_size, 2 * scale, rl.Color(220, 226, 235, int(230 * alpha))) diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_tracker_prototype.py b/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_tracker_prototype.py new file mode 100644 index 0000000000..4eecef836a --- /dev/null +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/milestone_tracker_prototype.py @@ -0,0 +1,70 @@ +"""PROTOTYPE: per-drive assisted-distance milestone tracking.""" + +from dataclasses import dataclass +from enum import StrEnum + + +METERS_PER_MILE = 1609.344 +TEST_MILESTONE_MILES = 0.5 +MAX_SAMPLE_INTERVAL_SECONDS = 0.5 + + +class AssistCategory(StrEnum): + MADS = "mads" + FULL_ASSIST = "full_assist" + + +def assist_category(lat_active: bool, long_active: bool) -> AssistCategory | None: + if not lat_active: + return None + return AssistCategory.FULL_ASSIST if long_active else AssistCategory.MADS + + +@dataclass(frozen=True) +class DistanceMilestone: + category: AssistCategory + distance_meters: float + + @property + def distance_miles(self) -> float: + return self.distance_meters / METERS_PER_MILE + + +class PerDriveMilestoneTracker: + def __init__(self, milestone_meters: float = TEST_MILESTONE_MILES * METERS_PER_MILE): + if milestone_meters <= 0: + raise ValueError("milestone distance must be positive") + self.milestone_meters = milestone_meters + self.reset() + + def reset(self) -> None: + self._distance_meters = dict.fromkeys(AssistCategory, 0.0) + self._next_milestone_meters = dict.fromkeys(AssistCategory, self.milestone_meters) + self._last_timestamp_ns: int | None = None + self._last_speed_mps = 0.0 + self._last_category: AssistCategory | None = None + + def distance_meters(self, category: AssistCategory) -> float: + return self._distance_meters[category] + + def update(self, timestamp_ns: int, speed_mps: float, category: AssistCategory | None) -> list[DistanceMilestone]: + milestones: list[DistanceMilestone] = [] + speed_mps = max(0.0, speed_mps) + + if self._last_timestamp_ns is not None and timestamp_ns != self._last_timestamp_ns: + dt = (timestamp_ns - self._last_timestamp_ns) / 1e9 + if 0 < dt <= MAX_SAMPLE_INTERVAL_SECONDS and self._last_category is not None: + delta_meters = (self._last_speed_mps + speed_mps) / 2.0 * dt + active_category = self._last_category + self._distance_meters[active_category] += delta_meters + + next_milestone = self._next_milestone_meters[active_category] + while self._distance_meters[active_category] >= next_milestone: + milestones.append(DistanceMilestone(active_category, next_milestone)) + next_milestone += self.milestone_meters + self._next_milestone_meters[active_category] = next_milestone + + self._last_timestamp_ns = timestamp_ns + self._last_speed_mps = speed_mps + self._last_category = category + return milestones diff --git a/openpilot/selfdrive/ui/tests/generate_milestone_sound.py b/openpilot/selfdrive/ui/tests/generate_milestone_sound.py new file mode 100644 index 0000000000..5d7fc7b86c --- /dev/null +++ b/openpilot/selfdrive/ui/tests/generate_milestone_sound.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Generate the temporary milestone celebration chime.""" + +import math +import wave +from array import array +from pathlib import Path + + +SAMPLE_RATE = 48_000 +DURATION_SECONDS = 0.82 +NOTES = ( + (0.00, 523.25), + (0.11, 659.25), + (0.22, 783.99), +) + + +def note_sample(age: float, frequency: float) -> float: + if not 0 <= age <= 0.58: + return 0.0 + attack = min(age / 0.008, 1.0) + release = min((0.58 - age) / 0.15, 1.0) + envelope = attack * release * math.exp(-3.8 * age) + tone = math.sin(math.tau * frequency * age) + 0.16 * math.sin(math.tau * frequency * 2 * age) + return envelope * tone + + +def main() -> None: + output = Path(__file__).parents[4] / "openpilot/selfdrive/assets/sounds/milestone.wav" + samples = array('h') + for frame in range(round(SAMPLE_RATE * DURATION_SECONDS)): + t = frame / SAMPLE_RATE + value = 0.38 * sum(note_sample(t - start, frequency) for start, frequency in NOTES) + samples.append(round(max(-1.0, min(1.0, value)) * 32767)) + + with wave.open(str(output), "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(SAMPLE_RATE) + wav.writeframes(samples.tobytes()) + + +if __name__ == "__main__": + main() diff --git a/openpilot/selfdrive/ui/tests/run_milestone_celebration_prototype.sh b/openpilot/selfdrive/ui/tests/run_milestone_celebration_prototype.sh new file mode 100644 index 0000000000..b0868400db --- /dev/null +++ b/openpilot/selfdrive/ui/tests/run_milestone_celebration_prototype.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# PROTOTYPE: launch the demo replay and comma four milestone celebration together. +set -e + +prototype_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" +prototype_replay_pid="" + +cleanup_prototype() { + if [[ -n "$prototype_replay_pid" ]]; then + kill "$prototype_replay_pid" 2>/dev/null || true + wait "$prototype_replay_pid" 2>/dev/null || true + fi +} +trap cleanup_prototype EXIT INT TERM + +export PATH="$prototype_root/.venv/bin:$PATH" +export SP_MILESTONE_PROTOTYPE=1 + +"$prototype_root/openpilot/tools/replay/replay" --demo & +prototype_replay_pid=$! + +"$prototype_root/.venv/bin/python" "$prototype_root/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py" diff --git a/openpilot/selfdrive/ui/tests/test_milestone_tracker_prototype.py b/openpilot/selfdrive/ui/tests/test_milestone_tracker_prototype.py new file mode 100644 index 0000000000..9cd18c019c --- /dev/null +++ b/openpilot/selfdrive/ui/tests/test_milestone_tracker_prototype.py @@ -0,0 +1,55 @@ +import unittest + +from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_tracker_prototype import ( + AssistCategory, + PerDriveMilestoneTracker, + assist_category, +) + + +class TestPerDriveMilestoneTracker(unittest.TestCase): + def test_classifies_actual_actuation(self): + self.assertIsNone(assist_category(False, False)) + self.assertIsNone(assist_category(False, True)) + self.assertEqual(assist_category(True, False), AssistCategory.MADS) + self.assertEqual(assist_category(True, True), AssistCategory.FULL_ASSIST) + + def test_tracks_categories_and_emits_repeated_milestones(self): + tracker = PerDriveMilestoneTracker(milestone_meters=10.0) + + self.assertEqual(tracker.update(0, 10.0, AssistCategory.MADS), []) + self.assertEqual(tracker.update(500_000_000, 10.0, AssistCategory.MADS), []) + milestones = tracker.update(1_000_000_000, 10.0, AssistCategory.FULL_ASSIST) + self.assertEqual([(m.category, m.distance_meters) for m in milestones], [(AssistCategory.MADS, 10.0)]) + + self.assertEqual(tracker.update(1_500_000_000, 10.0, AssistCategory.FULL_ASSIST), []) + milestones = tracker.update(2_000_000_000, 10.0, AssistCategory.FULL_ASSIST) + self.assertEqual([(m.category, m.distance_meters) for m in milestones], [(AssistCategory.FULL_ASSIST, 10.0)]) + + self.assertEqual(tracker.update(2_500_000_000, 10.0, AssistCategory.FULL_ASSIST), []) + milestones = tracker.update(3_000_000_000, 10.0, AssistCategory.FULL_ASSIST) + self.assertEqual([(m.category, m.distance_meters) for m in milestones], [(AssistCategory.FULL_ASSIST, 20.0)]) + + def test_does_not_count_unassisted_time_or_timestamp_gaps(self): + tracker = PerDriveMilestoneTracker(milestone_meters=10.0) + + tracker.update(0, 20.0, None) + tracker.update(500_000_000, 20.0, AssistCategory.MADS) + self.assertEqual(tracker.distance_meters(AssistCategory.MADS), 0.0) + + tracker.update(2_000_000_000, 20.0, AssistCategory.MADS) + self.assertEqual(tracker.distance_meters(AssistCategory.MADS), 0.0) + + def test_reset_clears_per_drive_distance_and_sampling_state(self): + tracker = PerDriveMilestoneTracker(milestone_meters=10.0) + tracker.update(0, 10.0, AssistCategory.MADS) + tracker.update(500_000_000, 10.0, AssistCategory.MADS) + + tracker.reset() + + self.assertEqual(tracker.distance_meters(AssistCategory.MADS), 0.0) + self.assertEqual(tracker.update(1_000_000_000, 10.0, AssistCategory.MADS), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/ui/tests/test_soundd.py b/openpilot/selfdrive/ui/tests/test_soundd.py index 3416a22c98..38e7721202 100644 --- a/openpilot/selfdrive/ui/tests/test_soundd.py +++ b/openpilot/selfdrive/ui/tests/test_soundd.py @@ -1,15 +1,42 @@ import threading import time +from types import SimpleNamespace from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log, messaging from openpilot.cereal.messaging import SubMaster, PubMaster -from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert +from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, Soundd, check_selfdrive_timeout_alert +from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_tracker_prototype import PerDriveMilestoneTracker AudibleAlert = log.SelfdriveState.AudibleAlert class TestSoundd(OpenpilotTestCase): + def test_milestone_chime_uses_real_assisted_distance(self): + soundd = Soundd() + soundd.milestone_tracker = PerDriveMilestoneTracker(milestone_meters=10.0) + + class SubMasterStub: + def __init__(self): + self.logMonoTime = {'carState': 0} + self.data = { + 'deviceState': SimpleNamespace(started=True), + 'carState': SimpleNamespace(vEgo=10.0), + 'carControl': SimpleNamespace(latActive=True, longActive=True), + } + + def __getitem__(self, service): + return self.data[service] + + sm = SubMasterStub() + soundd.update_milestone_alert(sm) + sm.logMonoTime['carState'] = 500_000_000 + soundd.update_milestone_alert(sm) + sm.logMonoTime['carState'] = 1_000_000_000 + soundd.update_milestone_alert(sm) + + assert soundd.current_alert == AudibleAlert.complete + def test_check_selfdrive_timeout_alert(self, mocker): sm = SubMaster(['selfdriveState', 'selfdriveStateSP']) pm = PubMaster(['selfdriveState', 'selfdriveStateSP'])