mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-03 16:53:44 +08:00
Prototype assisted driving milestones
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)))
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user