Files
StarPilot/openpilot/starpilot/audio/tests/test_availability_warning_delivery.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

129 lines
7.1 KiB
Python

import unittest
import numpy as np
from types import SimpleNamespace as N
from unittest.mock import patch
from openpilot.cereal import messaging, log
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager
from openpilot.selfdrive.selfdrived.events import Alert, ET, Priority, VisualAlert
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.selfdrive.ui.soundd import Soundd, AudibleAlert
from openpilot.starpilot.audio.axis_alerts import AxisAlerts
from openpilot.starpilot.audio.availability_warning import AvailabilityWarningDelivery
class TestAvailabilityWarningDelivery(unittest.TestCase):
def fixture_attr(self, obj, name, value):
replacement = patch.object(obj, name, value, create=True)
replacement.start()
self.addCleanup(replacement.stop)
def publisher(self, pm):
sd = SelfdriveD.__new__(SelfdriveD)
sd.enabled = sd.active = False
self.fixture_attr(sd, 'state_machine', N(state=log.SelfdriveState.OpenpilotState.disabled))
self.fixture_attr(sd, 'events', N(contains=lambda _: False))
sd.experimental_mode = False
sd.personality = log.LongitudinalPersonality.standard
sd.AM = AlertManager()
sd.pm = pm
sd.conditional_replay = False
sd.aol_session_id = 'delivery-session'
self.fixture_attr(sd, 'sm', {'deviceState': N(started=True, startedMonoTime=1)})
sd.availability_warning_delivery = AvailabilityWarningDelivery()
return sd
def issue(self, sd, now):
alert = Alert('Steering Paused', 'Communication Issue', log.SelfdriveState.AlertStatus.normal,
log.SelfdriveState.AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.)
alert.alert_type, alert.event_type = 'aolAvailability/warning', ET.WARNING
sd.AM.add_many(10, [alert])
sd.AM.process_alerts(10, set())
sd.availability_warning_delivery.observe(session=sd.aol_session_id, drive=1, now_ns=now,
selected=True, requested_until_ns=now + 400_000_000)
def sound(self):
sound = Soundd.__new__(Soundd)
sound.axis_alerts = AxisAlerts()
sound.current_alert = sound.current_sound = AudibleAlert.none
sound.current_sound_frame = 0
sound.pending_stop = False
sound.selfdrive_timeout_alert = False
sound.loaded_sounds = {AudibleAlert.prompt: np.full(16, 0.25, dtype=np.float32),
AudibleAlert.none: np.zeros(16, dtype=np.float32)}
sound.current_volume = 1.0
sound.saved_volumes = {}
self.fixture_attr(sound, 'pack_loader', N(is_builtin=lambda _: False))
return sound
def test_actual_serialized_publisher_latest_conflation_delivers_after_five_ms_recovery(self):
with OpenpilotPrefix():
pm = messaging.PubMaster(['selfdriveState'])
sm = messaging.SubMaster(['selfdriveState']) # production conflate=True
sd = self.publisher(pm)
now = 10_000_000_000
self.issue(sd, now)
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
sd.publish_selfdriveState(N(), axis_published=True)
sd.AM.process_alerts(11, {ET.WARNING})
for delta in (5_000_000, 10_000_000, 20_000_000, 30_000_000, 40_000_000):
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now + delta):
sd.publish_selfdriveState(N(), axis_published=True)
sm.update(1000) # first soundd read after all producer publications
self.assertTrue(sm.updated['selfdriveState'])
self.assertEqual(sm['selfdriveState'].alertType, '')
self.assertEqual(str(sm['selfdriveState'].alertSound), 'prompt')
self.assertFalse(sm['selfdriveState'].enabled or sm['selfdriveState'].active)
sound = self.sound()
with patch('openpilot.selfdrive.ui.soundd.time.monotonic_ns', return_value=now + 50_000_000):
sound.get_audible_alert(sm) # actual 20 Hz first receive boundary, no audio device
self.assertEqual(sound.current_alert, AudibleAlert.prompt)
self.assertEqual(sound.current_sound_frame, 0)
pcm = sound.get_sound_data(16)
self.assertTrue(np.all(pcm > 0))
self.assertEqual(sound.current_sound_frame, 16)
with patch('openpilot.selfdrive.ui.soundd.time.monotonic_ns', return_value=now + 100_000_000):
sound.get_audible_alert(sm)
self.assertEqual(sound.current_sound_frame, 16) # repeated prompt cannot restart the sample
self.assertTrue(np.all(sound.get_sound_data(16) == 0))
def test_deadline_does_not_extend_on_healthy_frames(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=True, requested_until_ns=400_000_100)
d.observe(session='s', drive=1, now_ns=300_000_100, selected=False, requested_until_ns=999_000_100)
self.assertTrue(d.pending(session='s', drive=1, now_ns=400_000_100, audible_none=True, priority=0, low_priority=1))
self.assertFalse(d.pending(session='s', drive=1, now_ns=400_000_101, audible_none=True, priority=0, low_priority=1))
def test_never_issued_stays_quiet(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=False, requested_until_ns=400_000_100)
self.assertFalse(d.pending(session='s', drive=1, now_ns=100, audible_none=True, priority=0, low_priority=1))
def test_urgent_and_existing_audible_preempt(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=True, requested_until_ns=400_000_100)
self.assertFalse(d.pending(session='s', drive=1, now_ns=101, audible_none=True, priority=2, low_priority=1))
self.assertFalse(d.pending(session='s', drive=1, now_ns=101, audible_none=False, priority=0, low_priority=1))
def test_context_changes_between_observation_and_publication_deny(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=True, requested_until_ns=400_000_100)
for session,drive in [('new',1),('s',2),('s',0),(None,1)]:
with self.subTest(session=session,drive=drive):
self.assertFalse(d.pending(session=session, drive=drive, now_ns=101, audible_none=True, priority=0, low_priority=1))
def test_clock_rollback_and_offroad_reset(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=True, requested_until_ns=400_000_100)
d.observe(session='s', drive=1, now_ns=99, selected=False, requested_until_ns=400_000_100)
self.assertFalse(d.pending(session='s', drive=1, now_ns=101, audible_none=True, priority=0, low_priority=1))
d.observe(session='s', drive=1, now_ns=102, selected=True, requested_until_ns=400_000_102)
d.observe(session='s', drive=0, now_ns=103, selected=False, requested_until_ns=400_000_102)
self.assertFalse(d.pending(session='s', drive=1, now_ns=104, audible_none=True, priority=0, low_priority=1))
def test_new_episode_after_deadline_can_issue(self):
d = AvailabilityWarningDelivery()
d.observe(session='s', drive=1, now_ns=100, selected=True, requested_until_ns=400_000_100)
d.observe(session='s', drive=1, now_ns=500_000_100, selected=True, requested_until_ns=900_000_100)
self.assertTrue(d.pending(session='s', drive=1, now_ns=550_000_100, audible_none=True, priority=0, low_priority=1))