from openpilot.starpilot.navigation.wire import navigation_envelope from types import SimpleNamespace as NS import pytest from openpilot.cereal import messaging, log from openpilot.common.constants import CV from openpilot.starpilot.navigation.intent import current_instruction, turn_desire, cruise_ceiling, turn_speed NOW = 20_000_000_000 @pytest.fixture def sm(): class SM(dict): valid = dict.fromkeys(('starpilotNavigation', 'deviceState', 'carState', 'carControl'), True) alive = dict(valid) logMonoTime = dict.fromkeys(valid, NOW) instruction = NS(maneuverType='turn', maneuverModifier='right', distanceMeters=30.) nav = NS(enabled=True, controlValid=True, status='guiding', sessionId='producer', revision='one', frameMonoTime=NOW, startedMonoTime=10_000_000_000, locationMonoTime=NOW, instruction=instruction, nextManeuver=instruction) nav.version = 2 return SM(starpilotNavigation=NS(navigation=nav), deviceState=NS(started=True, startedMonoTime=10_000_000_000), carState=NS(canValid=True, canTimeout=False, gearShifter='drive', vEgo=5., standstill=False, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False, gasPressed=False, brakePressed=False), carControl=NS(latActive=True, longActive=True)) def test_signal_turn_and_existing_desire_preserved(sm): assert turn_desire(sm, NOW, log.Desire.none, supported=True) == log.Desire.turnRight assert turn_desire(sm, NOW, log.Desire.laneChangeLeft, supported=True) == log.Desire.laneChangeLeft assert turn_desire(sm, NOW, log.Desire.none, supported=False) == log.Desire.none sm['carState'].rightBlinker = False assert turn_desire(sm, NOW, log.Desire.none, supported=True) == log.Desire.none @pytest.mark.parametrize('field,value', [('standstill', True), ('rightBlindspot', True), ('leftBlinker', True), ('vEgo', 14.), ('gearShifter', 'reverse'), ('canValid', False), ('canTimeout', True)]) def test_no_turn_without_vehicle_admission(sm, field, value): setattr(sm['carState'], field, value) assert turn_desire(sm, NOW, log.Desire.none, supported=True) == log.Desire.none @pytest.mark.parametrize('field,value', [('controlValid', False), ('enabled', False), ('frameMonoTime', NOW-3_000_000_000), ('locationMonoTime', NOW+1), ('startedMonoTime', 1), ('sessionId', '')]) def test_stale_previous_drive_and_disabled_nav_are_inert(sm, field, value): setattr(sm['starpilotNavigation'].navigation, field, value) assert current_instruction(sm, NOW) is None def test_original_turn_ceiling_and_override(sm): cp = NS(openpilotLongitudinalControl=True, passive=False, dashcamOnly=False, minSteerSpeed=0.) expected = ((14 * CV.MPH_TO_MS) ** 2 + 2 * .45 * (30 - 8)) ** .5 assert cruise_ceiling(sm, cp, NOW, 20.) == pytest.approx(expected) sm['carState'].gasPressed = True assert cruise_ceiling(sm, cp, NOW, 20.) is None sm['carState'].gasPressed = False cp.openpilotLongitudinalControl = False assert cruise_ceiling(sm, cp, NOW, 20.) is None @pytest.mark.parametrize('kind,modifier,mph', [('turn', 'left', 14), ('turn', 'sharpRight', 10), ('turn', 'uturn', 5), ('roundabout', 'right', 12), ('fork', 'right', None)]) def test_turn_target_matches_dom_table(kind, modifier, mph): result = turn_speed(NS(maneuverType=kind, maneuverModifier=modifier, distanceMeters=8.), 0.) assert result == pytest.approx(mph * CV.MPH_TO_MS) if mph is not None else result is None def test_new_event_roundtrip_keeps_route_and_control_evidence(): message = messaging.new_message('starpilotNavigation') message.starpilotNavigation = navigation_envelope({'sessionId': 'owner', 'frameMonoTime': NOW, 'startedMonoTime': 10_000_000_000, 'enabled': True, 'revision': 'one', 'controlValid': True, 'status': 'guiding', 'instruction': {'text': 'Turn right', 'maneuverType': 'turn', 'maneuverModifier': 'right', 'distanceMeters': 30.}, 'route': [{'latitude': 1., 'longitude': 2.}], 'locationMonoTime': NOW}) with log.Event.from_bytes(message.to_bytes()) as received: assert received.which() == 'starpilotNavigation' assert received.starpilotNavigation.navigation.route[0].latitude == 1. assert received.starpilotNavigation.navigation.controlValid def test_native_planner_navigation_cap_and_default_equivalence(): import numpy as np from unittest.mock import patch from opendbc.car.hyundai.interface import CarInterface from opendbc.car.hyundai.values import CAR from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner from openpilot.starpilot.longitudinal.tests.test_conditional_handoff import Frame, NOW as FRAME_NOW, DRIVE cp = CarInterface.get_non_essential_params(CAR.HYUNDAI_IONIQ_6) cp.openpilotLongitudinalControl = True baseline, absent, guided = (LongitudinalPlanner(cp, init_v=20.) for _ in range(3)) changed = False for tick in range(40): frame = Frame() now = FRAME_NOW + tick * 50_000_000 frame['carState'].vEgo = 20. frame['carState'].gearShifter = 'drive' frame.logMonoTime = dict.fromkeys(frame, now) with patch('openpilot.selfdrive.controls.lib.longitudinal_planner.navigation_ceiling', return_value=None): baseline.update(frame, now_ns=now) absent.update(frame, now_ns=now) assert baseline.output_a_target == absent.output_a_target np.testing.assert_array_equal(baseline.mpc.params, absent.mpc.params) nav = messaging.new_message('starpilotNavigation') nav.starpilotNavigation = navigation_envelope({'enabled': True, 'controlValid': True, 'status': 'guiding', 'sessionId': 'test', 'revision': 'one', 'frameMonoTime': now, 'startedMonoTime': DRIVE, 'locationMonoTime': now, 'instruction': {'maneuverType': 'turn', 'maneuverModifier': 'right', 'distanceMeters': 20.}}) frame['starpilotNavigation'] = nav.starpilotNavigation frame.logMonoTime['starpilotNavigation'] = now frame.valid['starpilotNavigation'] = frame.alive['starpilotNavigation'] = True guided.update(frame, now_ns=now) assert guided.output_a_target <= baseline.output_a_target changed |= guided.output_a_target < baseline.output_a_target assert changed def test_turn_stop_hold_reuses_planner_stop_until_standstill(sm): from openpilot.starpilot.navigation.intent import TurnIntent owner = TurnIntent() sm['longitudinalPlan'] = NS(modelMonoTime=NOW, shouldStop=False) sm.logMonoTime['longitudinalPlan'] = NOW sm.valid['longitudinalPlan'] = sm.alive['longitudinalPlan'] = True def sample(model_stop=False): return owner.select(sm, NOW, log.Desire.none, supported=True, model_stop=model_stop) assert sample() == log.Desire.turnRight sm['longitudinalPlan'].shouldStop = True assert sample() == log.Desire.none sm['longitudinalPlan'].shouldStop = False sm['starpilotNavigation'].navigation.revision = 'favorite-added' assert sample() == log.Desire.none sm['carState'].standstill = True assert sample() == log.Desire.none sm['carState'].standstill = False assert sample() == log.Desire.turnRight assert sample(model_stop=True) == log.Desire.none sm.valid['longitudinalPlan'] = False assert sample() == log.Desire.none def test_approaching_turn_does_not_start_or_replace_lane_change(sm): from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.starpilot.navigation.intent import matching_turn_signal from openpilot.starpilot.lateral.lane_change_preferences import LaneChangePolicy cs = sm['carState'] cs.steeringPressed, cs.steeringTorque = True, -1. cs.vEgo = 21. * CV.MPH_TO_MS # Above the current turn-guidance lane-change floor. helper = DesireHelper(LaneChangePolicy(minimum_speed_mps=0.)) turn = matching_turn_signal(sm, NOW, supported=True) assert turn helper.update(cs, True, 1., navigation_turn=turn) assert helper.lane_change_state == log.LaneChangeState.off helper.lane_change_state = log.LaneChangeState.preLaneChange helper.update(cs, True, 1., navigation_turn=turn) assert helper.lane_change_state == log.LaneChangeState.off helper.lane_change_state = log.LaneChangeState.laneChangeStarting helper.lane_change_direction = log.LaneChangeDirection.right helper.update(cs, True, 1., navigation_turn=turn) assert helper.lane_change_state == log.LaneChangeState.laneChangeStarting assert helper.desire == log.Desire.laneChangeRight