mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
229 lines
12 KiB
Python
229 lines
12 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from opendbc.car.structs import car
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.selfdrive.car.card import Car
|
|
from openpilot.selfdrive.car.tests.publication_fixture import initialize_publication_sources
|
|
from openpilot.selfdrive.ui.soundd import Soundd
|
|
from openpilot.starpilot.aol.intent import AOL_TOGGLE, AolCardIntent, AolSettings
|
|
from openpilot.starpilot.aol.runtime import decide_axes
|
|
from openpilot.starpilot.aol.wire import INTENT_SERVICE, IntentState, decode_intent, encode_intent
|
|
from openpilot.starpilot.audio.axis_alerts import AudibleAlert, AxisAlerts
|
|
|
|
|
|
class AudioFixture:
|
|
def __init__(self):
|
|
self.sm = messaging.SubMaster(['selfdriveState', 'aolAxisState', INTENT_SERVICE])
|
|
self.sound = Soundd.__new__(Soundd)
|
|
self.sound.axis_alerts = AxisAlerts()
|
|
self.sound.update_alert = Mock()
|
|
self.now = 1_000_000_000
|
|
self.sequence = 0
|
|
|
|
def publish(self, armed=False, lateral=False, longitudinal=False, native=AudibleAlert.none,
|
|
*, age_ns=0, valid=True, session='card', sequence=None, acknowledged=True, payload=None,
|
|
intent_message=None, enabled=None, pause_longitudinal=False, pause_lateral=False,
|
|
active=False, state_age_ns=0, state_valid=True):
|
|
self.now += 50_000_000
|
|
self.sequence += 1
|
|
stamp = self.now - age_ns
|
|
if intent_message is None:
|
|
intent_message = messaging.new_message(INTENT_SERVICE, 0, valid=valid)
|
|
intent_message.logMonoTime = stamp
|
|
intent_message.aolIntentWire = payload if payload is not None else encode_intent(IntentState(
|
|
session, self.sequence if sequence is None else sequence, stamp, stamp, stamp + 200_000_000,
|
|
False, pause_lateral, pause_longitudinal, True, lateralArmed=armed))
|
|
else:
|
|
self.now = intent_message.logMonoTime + 1_000_000
|
|
stamp = self.now
|
|
message = messaging.new_message('aolAxisState', valid=True)
|
|
message.logMonoTime = stamp
|
|
axis = message.aolAxisState
|
|
axis.sessionId = 'selfdrived'
|
|
axis.sequence = self.sequence
|
|
axis.qualified = True
|
|
axis.nativeAcknowledged = acknowledged
|
|
axis.lateralActive, axis.longitudinalActive = lateral, longitudinal
|
|
axis.desiredLateral, axis.desiredLongitudinal = lateral, longitudinal
|
|
axis.observedMonoTime = stamp
|
|
axis.validUntilMonoTime = stamp + 30_000_000
|
|
state = messaging.new_message('selfdriveState', valid=state_valid)
|
|
state.logMonoTime = self.now - state_age_ns
|
|
state.selfdriveState.alertSound = native
|
|
state.selfdriveState.enabled = longitudinal if enabled is None else enabled
|
|
state.selfdriveState.active = active
|
|
self.sm.update_msgs(self.now / 1e9, [message.as_reader(), state.as_reader(), intent_message.as_reader()])
|
|
with patch('openpilot.selfdrive.ui.soundd.time.monotonic_ns', return_value=self.now):
|
|
self.sound.get_audible_alert(self.sm)
|
|
return self.sound.update_alert.call_args.args[0]
|
|
|
|
|
|
@pytest.fixture
|
|
def audio():
|
|
with OpenpilotPrefix():
|
|
yield AudioFixture()
|
|
|
|
|
|
def test_park_lkas_producer_wire_and_sound(audio):
|
|
owner = AolCardIntent(AolSettings(True, 0.0, AOL_TOGGLE, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
|
|
published = {}
|
|
fields = SimpleNamespace(sm=SimpleNamespace(frame=1, all_checks=lambda _: True),
|
|
pm=SimpleNamespace(send=lambda service, message: published.__setitem__(service, message)),
|
|
CP=car.CarParams.new_message(), car_params_published=True,
|
|
slc_replay=False, curve_replay=False, conditional_replay=False,
|
|
last_actuators_output=car.CarControl.Actuators.new_message(), can_rcv_cum_timeout_counter=0,
|
|
rk=SimpleNamespace(remaining=0), aol_replay=True, aol_qualified=True, aol_sequence=0,
|
|
slc_producer_session='card', aol_card_intent=owner, v_cruise_helper=SimpleNamespace(slc_cruise_change=None, toyota=None))
|
|
card = Car.__new__(Car)
|
|
card.__dict__.update(vars(fields))
|
|
initialize_publication_sources(card)
|
|
cs = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.park, standstill=True)
|
|
for pressed, gear, expected in ((None, 'park', AudibleAlert.none), (True, 'park', AudibleAlert.engage),
|
|
(True, 'park', AudibleAlert.none), (False, 'park', AudibleAlert.none),
|
|
(None, 'drive', AudibleAlert.none), (None, 'park', AudibleAlert.none),
|
|
(True, 'park', AudibleAlert.disengage)):
|
|
cs.gearShifter = gear
|
|
cs.buttonEvents = [] if pressed is None else [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=pressed)]
|
|
owner.update(cs)
|
|
Car.state_publish(card, cs, None)
|
|
decoded = decode_intent(published[INTENT_SERVICE].aolIntentWire)
|
|
assert decoded is not None
|
|
assert decoded.lateralArmed == owner.allowed_latch
|
|
assert decoded.allowedLatch == (owner.allowed_latch and gear == 'drive')
|
|
assert audio.publish(intent_message=published[INTENT_SERVICE]) == expected
|
|
|
|
|
|
def test_lateral_activation_without_latch_change_is_silent(audio):
|
|
assert audio.publish() == AudibleAlert.none
|
|
assert audio.publish(armed=True) == AudibleAlert.engage
|
|
for lateral in (True, False, True, False):
|
|
assert audio.publish(armed=True, lateral=lateral) == AudibleAlert.none
|
|
assert audio.publish() == AudibleAlert.disengage
|
|
|
|
|
|
def test_native_latch_revocation_and_one_lkas_rearm_sound(audio):
|
|
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
|
|
state = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.drive)
|
|
owner.update(state, now_ns=100)
|
|
assert audio.publish(armed=owner.allowed_latch) == AudibleAlert.none
|
|
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
|
|
owner.update(state, now_ns=200)
|
|
assert audio.publish(armed=owner.allowed_latch) == AudibleAlert.engage
|
|
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=False)]
|
|
owner.update(state, now_ns=300)
|
|
audio.publish(armed=owner.allowed_latch)
|
|
state.buttonEvents = []
|
|
owner.update(state, now_ns=400, native_rejection_ns=350)
|
|
assert audio.publish(armed=owner.allowed_latch) == AudibleAlert.disengage
|
|
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
|
|
owner.update(state, now_ns=500, native_rejection_ns=350)
|
|
assert owner.allowed_latch
|
|
assert audio.publish(armed=owner.allowed_latch) == AudibleAlert.engage
|
|
|
|
|
|
def test_longitudinal_transitions_without_aol(audio):
|
|
assert audio.publish() == AudibleAlert.none
|
|
assert audio.publish(longitudinal=True) == AudibleAlert.engage
|
|
assert audio.publish(longitudinal=True) == AudibleAlert.none
|
|
assert audio.publish() == AudibleAlert.disengage
|
|
|
|
|
|
@pytest.mark.parametrize('native', [AudibleAlert.warningImmediate, AudibleAlert.warningSoft, AudibleAlert.refuse,
|
|
AudibleAlert.prompt, AudibleAlert.engage, AudibleAlert.disengage])
|
|
def test_native_alert_wins_without_queued_chime(audio, native):
|
|
audio.publish()
|
|
assert audio.publish(armed=True, native=native) == native
|
|
assert audio.publish(armed=True) == AudibleAlert.none
|
|
|
|
|
|
def test_native_then_axis_ack_is_one_engagement(audio):
|
|
audio.publish()
|
|
assert audio.publish(native=AudibleAlert.engage) == AudibleAlert.engage
|
|
assert audio.publish(longitudinal=True) == AudibleAlert.none
|
|
assert audio.publish(armed=True, longitudinal=True) == AudibleAlert.none
|
|
|
|
|
|
@pytest.mark.parametrize('invalid', [{'age_ns': 31_000_000}, {'age_ns': -1}, {'valid': False}, {'payload': b'invalid'}])
|
|
def test_invalid_latch_is_silent_and_recovery_does_not_replay(audio, invalid):
|
|
audio.publish()
|
|
assert audio.publish(armed=True, **invalid) == AudibleAlert.none
|
|
assert audio.publish(armed=True) == AudibleAlert.none
|
|
assert audio.publish() == AudibleAlert.disengage
|
|
|
|
|
|
def test_startup_new_session_and_old_sequence_do_not_chime(audio):
|
|
assert audio.publish(armed=True) == AudibleAlert.none
|
|
assert audio.publish(armed=False, session='new-card') == AudibleAlert.none
|
|
assert audio.publish(armed=True, session='new-card', sequence=1) == AudibleAlert.none
|
|
assert audio.publish(armed=True, session='new-card') == AudibleAlert.engage
|
|
|
|
|
|
def test_runtime_native_pending_permission_and_loss(audio):
|
|
audio.publish()
|
|
cs = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.drive, vEgo=20.0)
|
|
for requested, allowed, expected in ((False, False, AudibleAlert.none), (True, True, AudibleAlert.engage),
|
|
(True, False, AudibleAlert.disengage)):
|
|
native = SimpleNamespace(requestedLateral=False, requestedLongitudinal=requested,
|
|
lateralAllowed=False, longitudinalAllowed=allowed)
|
|
result = decide_axes(standard_lateral=False, standard_longitudinal=True,
|
|
intent=SimpleNamespace(allowedLatch=False, pauseLateral=False, pauseLongitudinal=False),
|
|
native=native, car_state=cs, initialized=True, model_ready=True,
|
|
no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=0.0)
|
|
assert result.native_acknowledged
|
|
assert audio.publish(longitudinal=result.longitudinal_active, acknowledged=result.native_acknowledged) == expected
|
|
|
|
|
|
def test_actuator_ack_does_not_own_engagement_sound(audio):
|
|
audio.publish()
|
|
assert audio.publish(longitudinal=True, enabled=False, acknowledged=False) == AudibleAlert.none
|
|
|
|
|
|
def test_mismatched_payload_timestamp_is_silent(audio):
|
|
audio.publish()
|
|
stamp = audio.now + 50_000_000
|
|
payload = encode_intent(IntentState('card', 2, stamp - 1, stamp, stamp + 200_000_000,
|
|
False, False, False, True, lateralArmed=True))
|
|
assert audio.publish(armed=True, payload=payload) == AudibleAlert.none
|
|
|
|
|
|
def test_native_only_subscriber_retains_alert(audio):
|
|
sm = messaging.SubMaster(['selfdriveState'])
|
|
state = messaging.new_message('selfdriveState', valid=True)
|
|
state.selfdriveState.alertSound = AudibleAlert.engage
|
|
sm.update_msgs(audio.now / 1e9, [state.as_reader()])
|
|
audio.sound.get_audible_alert(sm)
|
|
audio.sound.update_alert.assert_called_once_with(AudibleAlert.engage)
|
|
|
|
|
|
def test_output_drop_and_recovery_do_not_sound_when_engagement_is_unchanged(audio):
|
|
audio.publish(enabled=False)
|
|
assert audio.publish(enabled=True, longitudinal=True) == AudibleAlert.engage
|
|
for active, acknowledged in ((False, True), (True, True), (False, False), (True, True)):
|
|
assert audio.publish(enabled=True, longitudinal=active, acknowledged=acknowledged) == AudibleAlert.none
|
|
assert audio.publish(enabled=False) == AudibleAlert.disengage
|
|
|
|
|
|
def test_explicit_longitudinal_pause_and_resume_sound_once(audio):
|
|
audio.publish(enabled=True, longitudinal=True)
|
|
assert audio.publish(enabled=True, pause_longitudinal=True) == AudibleAlert.disengage
|
|
assert audio.publish(enabled=True, pause_longitudinal=True) == AudibleAlert.none
|
|
assert audio.publish(enabled=True, longitudinal=True) == AudibleAlert.engage
|
|
|
|
|
|
@pytest.mark.parametrize('invalid', [{'state_age_ns': 101_000_000}, {'state_age_ns': -1}, {'state_valid': False}])
|
|
def test_standard_lateral_stale_state_and_recovery_do_not_replay(audio, invalid):
|
|
audio.publish(active=True, enabled=True)
|
|
assert audio.publish(active=True, enabled=True, **invalid) == AudibleAlert.none
|
|
assert audio.publish(active=True, enabled=True) == AudibleAlert.none
|
|
assert audio.publish(active=True, enabled=True, pause_lateral=True) == AudibleAlert.disengage
|
|
|
|
|
|
def test_explicit_lateral_pause_remains_known_without_standard_state(audio):
|
|
audio.publish(active=True, enabled=True)
|
|
assert audio.publish(active=True, enabled=True, pause_lateral=True, state_valid=False) == AudibleAlert.disengage
|
|
assert audio.publish(active=True, enabled=True, pause_lateral=True) == AudibleAlert.none
|