"""Native parser packets through typed SLC state and the opt-in planner ceiling.""" import math import json import tempfile import time import unittest from dataclasses import replace from collections import deque from contextlib import ExitStack from itertools import product from types import SimpleNamespace from typing import cast from unittest import mock from opendbc.can import CANPacker, CANParser from opendbc.car.dashboard_speed_limit import Tracker, Status, ford_sign, honda_sign, hyundai_canfd_sign, parser_expiry, toyota_sign from opendbc.car.honda.interface import CarInterface from opendbc.car.honda.values import CAR from opendbc.car.hyundai.carstate import CarState as HyundaiCarState from opendbc.car.hyundai.interface import CarInterface as HyundaiCarInterface from opendbc.car.hyundai.values import CAR as HyundaiCAR, HyundaiFlags from opendbc.car import Bus from opendbc.car.honda.carstate import CarState as HondaCarState from opendbc.car.toyota.carstate import CarState as ToyotaCarState from opendbc.car.toyota.interface import CarInterface as ToyotaCarInterface from opendbc.car.toyota.values import CAR as ToyotaCAR from opendbc.car.ford.carstate import CarState as FordCarState from opendbc.car.ford.interface import CarInterface as FordCarInterface from opendbc.car.ford.values import CAR as FordCAR from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner from openpilot.starpilot.longitudinal.tests.test_cruise_ceiling import messages from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime from openpilot.starpilot.speed_limits.runtime_settings import parse from openpilot.starpilot.speed_limits.runtime_settings import read_params from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.starpilot.speed_limits.selection import Source from openpilot.starpilot.speed_limits.acceptance import ObservationKind from openpilot.starpilot.speed_limits import acceptance as acc, history from openpilot.starpilot.speed_limits import physical_actions from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation from opendbc.car.structs import car class ReplaySM: def __init__(self, data, timestamp): self.data = data self.valid = dict.fromkeys(data, True) self.updated = dict.fromkeys(data, True) self.alive = dict.fromkeys(data, True) self.logMonoTime = dict.fromkeys(data, timestamp) def __getitem__(self, name): return self.data[name] def advance(self, timestamp): self.logMonoTime = dict.fromkeys(self.data, timestamp) self.data['slcDashboardObservation'].carStateLogMonoTime = timestamp def update(self, timeout: int = 100) -> None: pass class NativeSourceTests(unittest.TestCase): def test_brand_carstate_trackers_receive_real_packets(self): cases = ( (CarInterface, CAR.HONDA_CIVIC_BOSCH, HondaCarState, 'CAMERA_MESSAGES', {'SPEED_LIMIT_SIGN': 104}, {'SPEED_LIMIT_SIGN': 1}, 17.8816), (ToyotaCarInterface, ToyotaCAR.TOYOTA_RAV4, ToyotaCarState, 'RSA1', {'TSGN1': 36, 'SPDVAL1': 45}, {'TSGN1': 35, 'SPDVAL1': 45}, 20.1168), (FordCarInterface, FordCAR.FORD_ESCAPE_MK4, FordCarState, 'Traffic_RecognitnData', {'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 2}, {'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 0}, 20.1168), (HyundaiCarInterface, HyundaiCAR.HYUNDAI_IONIQ_6, HyundaiCarState, 'FR_CMR_02_100ms', {'ISLW_SysSta': 0, 'ISLW_SpdCluMainDis': 72}, {'ISLW_SysSta': 1, 'ISLW_SpdCluMainDis': 72}, 20.0), ) for interface, fingerprint, state_type, message, signals, unsupported, expected in cases: with self.subTest(fingerprint=fingerprint): cp = interface.get_non_essential_params(fingerprint) state = state_type(cp) parsers = state.get_can_parsers(cp) subscribed = [(bus, parser) for bus, parser in parsers.items() if (definition := parser.dbc.name_to_msg.get(message)) is not None and definition.address in parser.message_states] self.assertEqual(len(subscribed), 1) _, parser = subscribed[0] packer = CANPacker(parser.dbc.name) for i in range(6): timestamp = 1_000_000_000 + i * 300_000_000 parser.update([timestamp, [packer.make_can_msg(message, parser.bus, {key: float(value) for key, value in signals.items()})]]) state.update(parsers) observation = state.dashboard_limit.observation self.assertEqual(observation.status, Status.VALID) self.assertAlmostEqual(observation.speed_mps, expected) self.assertEqual(observation.observed_ns, timestamp) self.assertGreater(observation.valid_until_ns, timestamp) parser.update([timestamp + 300_000_000, [packer.make_can_msg(message, parser.bus, {key: float(value) for key, value in unsupported.items()})]]) state.update(parsers) self.assertEqual(state.dashboard_limit.observation.status, Status.UNKNOWN) self.assertEqual(state.dashboard_limit.observation.speed_mps, 0.0) def test_real_dbc_packets_and_parser_expiry(self): cases = ( ('honda_civic_hatchback_ex_2017_can_generated', 'CAMERA_MESSAGES', {'SPEED_LIMIT_SIGN': 104}, 'SPEED_LIMIT_SIGN', lambda: honda_sign(104), 17.8816), ('toyota_nodsu_pt_generated', 'RSA1', {'TSGN1': 36, 'SPDVAL1': 45}, 'TSGN1', lambda: toyota_sign(36, 45), 20.1168), ('ford_lincoln_base_pt', 'Traffic_RecognitnData', {'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 2}, 'TsrVLim1MsgTxt_D_Rq', lambda: ford_sign(45, 2), 20.1168), ('hyundai_canfd_generated', 'FR_CMR_02_100ms', {'ISLW_SysSta': 0, 'ISLW_SpdCluMainDis': 72}, 'ISLW_SpdCluMainDis', lambda: hyundai_canfd_sign(72, 0, True), 20.0), ) for dbc, message, signals, signal, decode, expected_mps in cases: with self.subTest(message=message): packer = CANPacker(dbc) parser = CANParser(dbc, [(message, math.nan)], 0) self.assertEqual(parser_expiry(parser, message, signal), (0, 0)) for i in range(6): timestamp = 1_000_000_000 + i * 300_000_000 parser.update([timestamp, [packer.make_can_msg(message, 0, {key: float(value) for key, value in signals.items()})]]) observed, expiry = parser_expiry(parser, message, signal) self.assertEqual(observed, timestamp) self.assertGreater(expiry, observed) self.assertEqual(parser.vl[message][signal], signals[signal]) kind, value = decode() self.assertIs(kind, Status.VALID) self.assertAlmostEqual(value, expected_mps) def test_invalid_sign_does_not_preserve_previous_limit_and_reconnects(self): tracker = Tracker() first = tracker.update(100, *honda_sign(104), valid_until_ns=200) self.assertEqual(first.status, Status.VALID) unknown = tracker.update(150, *honda_sign(1), valid_until_ns=250) self.assertEqual(unknown.status, Status.UNKNOWN) self.assertEqual(unknown.speed_mps, 0) again = tracker.update(300, *honda_sign(104), valid_until_ns=400) self.assertEqual(again.status, Status.VALID) self.assertGreater(again.episode, first.episode) self.assertEqual(tracker.update(350, Status.VALID, math.inf, valid_until_ns=450), again) self.assertEqual(hyundai_canfd_sign(0, 0, True), (Status.ABSENT, 0.0)) self.assertEqual(hyundai_canfd_sign(253, 0, True), (Status.ABSENT, 0.0)) self.assertEqual(hyundai_canfd_sign(72, 1, True), (Status.UNKNOWN, 0.0)) def test_hyundai_optional_sign_subscription_follows_source_bus(self): cp = HyundaiCarInterface.get_non_essential_params(HyundaiCAR.HYUNDAI_IONIQ_6) for hda2, expected_bus in ((False, Bus.cam), (True, Bus.pt)): with self.subTest(hda2=hda2): if hda2: cp.flags = int(cp.flags | HyundaiFlags.CANFD_LKA_STEER_MSG) parsers = HyundaiCarState(cp).get_can_parsers(cp) for bus, parser in parsers.items(): message = parser.dbc.name_to_msg['FR_CMR_02_100ms'] subscribed = message.address in parser.message_states self.assertEqual(subscribed, bus == expected_bus) if subscribed: self.assertTrue(parser.message_states[message.address].ignore_alive) class RuntimeReplayTests(unittest.TestCase): def test_card_reserves_optional_slc_before_saved_admission(self): from opendbc.car.gm.values import CAR as GM_CAR from opendbc.car.gm.tests.test_bolt_cc import params as bolt_params from openpilot.starpilot.aol.tests.test_gm import TestGmAol from openpilot.cereal import messaging for active in (False, True): with self.subTest(active=active), OpenpilotPrefix(), mock.patch.dict('os.environ', { 'SLC_REPLAY_RUNTIME': '0', 'AOL_REPLAY_RUNTIME': '0', 'CONDITIONAL_MODE_REPLAY_RUNTIME': '0', 'CURVE_REPLAY_RUNTIME': '0', }): messaging.reset_context() params = Params() params.put_bool('SpeedLimitController', False, block=True) params.put_bool('ShowSpeedLimits', False, block=True) params.put('SLCPriority1', 'Dashboard', block=True) params.put_bool('DisableOpenpilotLongitudinal', not active, block=True) process = TestGmAol.card(bolt_params(GM_CAR.CHEVROLET_BOLT_CC_2018_2021, alpha=True, present=True), params) self.assertTrue(process.slc_transport_available) self.assertFalse(process.slc_replay) self.assertFalse(process.slc_control_enabled) self.assertIn('slcState', process.sm.data) self.assertIn('slcState', process.sm.ignore_alive) self.assertIn('slcState', process.sm.ignore_valid) self.assertIn('slcState', process.sm.ignore_average_freq) self.assertIsNotNone(process.slc_command_sock) self.assertIn('slcDashboardObservation', process.pm.sock) shared_producer = process.slc_producer_session params.put_bool('SpeedLimitController', True, block=True) process.slc_requested_configuration = process.read_slc_configuration() process.refresh_slc_configuration(1_000_000_000) self.assertTrue(process.slc_replay) self.assertEqual(process.slc_control_enabled, active) self.assertEqual(process.CP.openpilotLongitudinalControl, active) self.assertTrue(params.get_bool('SpeedLimitController')) process.slc_commands.append(object()) process.slc_command_session = 'retired-planner-session' process.slc_last_command_id = 4 params.put_bool('SpeedLimitController', False, block=True) params.put_bool('ShowSpeedLimits', True, block=True) process.slc_requested_configuration = process.read_slc_configuration() process.refresh_slc_configuration(2_000_000_000) self.assertTrue(process.slc_replay) self.assertFalse(process.slc_control_enabled) self.assertFalse(process.slc_commands) self.assertEqual(process.slc_command_session, '') self.assertEqual(process.slc_last_command_id, 0) self.assertEqual(process.slc_producer_session, shared_producer) params.put('SLCPriority1', 'broken source', block=True) process.slc_requested_configuration = process.read_slc_configuration() process.refresh_slc_configuration(3_000_000_000) self.assertFalse(process.slc_replay) self.assertFalse(process.slc_control_enabled) def test_isolated_ipc_submaster_handles_independent_card_and_model_cadences(self): from openpilot.cereal import messaging with OpenpilotPrefix(): messaging.reset_context() pm = messaging.PubMaster(['carState', 'slcDashboardObservation', 'slcState', 'modelV2']) sm = messaging.SubMaster(['carState', 'slcDashboardObservation', 'slcState', 'modelV2'], poll='modelV2') base_ns = time.monotonic_ns() - 50_000_000 def publish(service, stamp_ns): message = messaging.new_message(service) message.valid = True message.logMonoTime = stamp_ns pm.send(service, message) return message car = messaging.new_message('carState') car.valid = True car.logMonoTime = base_ns + 10_000_000 pm.send('carState', car) source = messaging.new_message('slcDashboardObservation') source.valid = True source.logMonoTime = base_ns source.slcDashboardObservation.producerSessionId = 'ipc-card' source.slcDashboardObservation.carStateLogMonoTime = base_ns source.slcDashboardObservation.status = 'valid' source.slcDashboardObservation.speedMps = 25.0 source.slcDashboardObservation.observedMonoTime = base_ns - 20_000_000 source.slcDashboardObservation.validUntilMonoTime = base_ns + 1_000_000_000 source.slcDashboardObservation.episode = 1 pm.send('slcDashboardObservation', source) state = messaging.new_message('slcState') state.valid = True state.logMonoTime = base_ns + 5_000_000 state.slcState.sessionId = 'ipc-drive' state.slcState.frameMonoTime = base_ns + 5_000_000 state.slcState.enabled = True state.slcState.observationKind = 'valid' state.slcState.source = 'dashboard' state.slcState.speedLimit = 25.0 state.slcState.hasPending = True state.slcState.pendingSpeedLimit = 25.0 state.slcState.decisionId = 1 state.slcState.presentationId = 1 state.slcState.sourceProducerSessionId = 'ipc-card' state.slcState.sourceEpisode = 1 state.slcState.sourceObservedMonoTime = base_ns - 20_000_000 state.slcState.sourceValidUntilMonoTime = base_ns + 1_000_000_000 pm.send('slcState', state) publish('modelV2', base_ns + 5_000_000) sm.update(100) now_ns = time.monotonic_ns() self.assertGreater(sm.logMonoTime['carState'], sm.logMonoTime['modelV2']) self.assertEqual(sm.logMonoTime['carState'] - sm.logMonoTime['slcDashboardObservation'], 10_000_000) self.assertTrue(physical_actions.pending_confirmation( sm, now_ns, long_active=True)) runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ipc-drive') self.assertEqual(runtime._observations(sm, self.cp, now_ns)[Source.DASHBOARD].kind, ObservationKind.VALID) newer = messaging.new_message('carState') newer.valid = True newer.logMonoTime = base_ns + 35_000_000 pm.send('carState', newer) publish('modelV2', base_ns + 40_000_000) sm.update(100) self.assertNotEqual(runtime._observations(sm, self.cp, time.monotonic_ns())[Source.DASHBOARD].kind, ObservationKind.VALID) # Card runs at 100 Hz while the planner state runs at 20 Hz. A new TSR # packet can outrun the planner; the fresh presented decision still owns confirmation. local = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=base_ns - 20_000_000, valid_until_ns=base_ns + 1_000_000_000, episode=1) qualified = 0 for tick in range(20): if tick in (7, 17): local.observed_ns = time.monotonic_ns() local.episode += 1 if tick % 5 == 0: stamp = time.monotonic_ns() cadence_state = messaging.new_message('slcState') cadence_state.valid = True cadence_state.logMonoTime = stamp snapshot = cadence_state.slcState snapshot.frameMonoTime = stamp snapshot.sessionId = 'ipc-drive' snapshot.enabled = True snapshot.observationKind = 'valid' snapshot.source = 'dashboard' snapshot.speedLimit = 25.0 snapshot.hasPending = True snapshot.pendingSpeedLimit = 25.0 snapshot.decisionId = 1 snapshot.presentationId = 1 snapshot.sourceProducerSessionId = 'ipc-card' snapshot.sourceEpisode = local.episode snapshot.sourceObservedMonoTime = local.observed_ns snapshot.sourceValidUntilMonoTime = local.valid_until_ns pm.send('slcState', cadence_state) sm.update(0) pending = physical_actions.pending_confirmation(sm, time.monotonic_ns(), long_active=True) qualified += pending is not None time.sleep(0.01) self.assertGreaterEqual(qualified, 12) def test_card_consumes_physical_press_through_release_without_replay(self): helper = VCruiseHelper(car.CarParams(pcmCruise=False)) helper.v_cruise_kph = 70.0 pending = SlcPendingConfirmation('drive', 1, 1) press = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)]) held = car.CarState(cruiseState={'available': True}) release = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)]) helper.update_v_cruise(press, True, False, pending) self.assertEqual(helper.slc_consumed_button.pending, pending) self.assertEqual(helper.v_cruise_kph, 70.0) for _ in range(55): helper.update_v_cruise(held, True, False) self.assertEqual(helper.v_cruise_kph, 70.0) self.assertIsNone(helper.slc_consumed_button) helper.update_v_cruise(release, True, False) self.assertEqual(helper.v_cruise_kph, 70.0) self.assertFalse(helper.slc_suppressed_buttons) helper.update_v_cruise(press, True, False) helper.update_v_cruise(release, True, False) self.assertGreater(helper.v_cruise_kph, 70.0) helper.v_cruise_kph = 70.0 helper.update_v_cruise(press, True, False, pending) helper.update_v_cruise(held, False, False) for _ in range(55): helper.update_v_cruise(held, True, False) self.assertEqual(helper.v_cruise_kph, 70.0) helper.update_v_cruise(release, True, False) self.assertEqual(helper.v_cruise_kph, 70.0) self.assertFalse(helper.slc_suppressed_buttons) def test_physical_accept_creates_bounded_card_command_and_receipt(self): settings = parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCConfirmationHigher': True, 'SLCPriority1': 'Dashboard'}) runtime = Runtime(settings, session_id='physical-drive') self.sm['carState'].vCruise = 60.0 self.sm['carState'].vCruiseCluster = 60.0 first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertTrue(first.message.slcState.hasPending) self.sm.advance(2_010_000_000) initial = runtime.step(self.sm, self.cp, now_ns=2_010_000_000, request=Action(runtime.session_id, 1, first.message.slcState.decisionId, first.message.slcState.presentationId, 'accept')) self.assertTrue(initial.message.slcState.hasAccepted) source = self.sm['slcDashboardObservation'] source.speedMps = 30.0 source.observedMonoTime = 2_040_000_000 source.episode = 2 self.sm.advance(2_050_000_000) state = runtime.step(self.sm, self.cp, now_ns=2_050_000_000).message.slcState self.assertTrue(state.hasPending) self.assertEqual(state.sourceProducerSessionId, 'card-producer') self.sm.advance(2_100_000_000) accepted = runtime.step(self.sm, self.cp, now_ns=2_100_000_000, cruise_event=CruiseEvent(1, 2_100_000_000, 0.0, 0.0, 'accel', 'card-producer', 2_100_000_000, 'confirmationAccept', runtime.session_id, state.decisionId, state.presentationId)) self.assertEqual(accepted.action_result.status, 'resolved') self.assertTrue(accepted.message.slcState.hasAccepted) self.assertFalse(accepted.message.slcState.hasPending) self.assertEqual(accepted.message.slcState.actionStatus, 'accept') self.assertEqual(runtime.ledger.transactions, ()) self.assertIsNotNone(accepted.command) command = accepted.command.slcCruiseCommand self.assertLessEqual(command.expiresMonoTime - command.issuedMonoTime, physical_actions.COMMAND_MAX_AGE_NS) card_sm = ReplaySM({'slcState': accepted.message.slcState}, 2_100_000_000) observation = SimpleNamespace(status=Status.VALID, speed_mps=30.0, observed_ns=2_040_000_000, valid_until_ns=9_000_000_000, episode=2) current = physical_actions.state_current(card_sm, observation, 'card-producer', 2_100_000_000) self.assertIsNotNone(current) self.assertTrue(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 65.0 / 3.6, long_active=True, button_event=False)) self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=True)) self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=False, button_event=False)) command.commandId += 1 self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) command.commandId -= 1 command.presentationId += 1 self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) command.presentationId -= 1 command.sourceEpisode = 99 self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) command.sourceEpisode = 2 command.expiresMonoTime = 2_099_000_000 self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) command.expiresMonoTime = 2_200_000_000 command.targetMps = math.nan self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer', 2_100_000_000, 60.0 / 3.6, long_active=True, button_event=False)) command.targetMps = 30.0 self.assertIsNone(physical_actions.state_current(card_sm, observation, 'card-producer', 2_210_000_000)) # The latest conflated CarState can already contain a newer driver choice # when the historical card-owned application receipt reaches plannerd. self.sm.advance(2_160_000_000) self.sm['carState'].vCruise = 115.0 self.sm['carState'].vCruiseCluster = 115.0 def receipt(event_id, observed_ns, previous_mps, selected_mps): return CruiseEvent(event_id, observed_ns, previous_mps, selected_mps, 'unknown', 'card-producer', 2_150_000_000, 'commandApplied', runtime.session_id, state.decisionId, state.presentationId, command.commandId) wrong_time = runtime.step(self.sm, self.cp, now_ns=2_160_000_000, cruise_event=receipt(2, 2_090_000_000, 60.0 / 3.6, 30.0)) self.assertEqual(wrong_time.message.slcState.commandStatus, 'issued') wrong_previous = runtime.step(self.sm, self.cp, now_ns=2_160_000_000, cruise_event=receipt(3, 2_150_000_000, 20.0, 30.0)) self.assertEqual(wrong_previous.message.slcState.commandStatus, 'issued') wrong_target = runtime.step(self.sm, self.cp, now_ns=2_160_000_000, cruise_event=receipt(4, 2_150_000_000, 60.0 / 3.6, 20.0)) self.assertEqual(wrong_target.message.slcState.commandStatus, 'issued') applied = runtime.step(self.sm, self.cp, now_ns=2_160_000_000, cruise_event=receipt(5, 2_150_000_000, 60.0 / 3.6, 30.0)) self.assertEqual(applied.message.slcState.commandStatus, 'applied') self.sm.advance(2_170_000_000) newer_driver = runtime.step(self.sm, self.cp, now_ns=2_170_000_000, cruise_event=CruiseEvent(6, 2_160_000_000, 30.0, 115.0 / 3.6, 'accel', 'card-producer', 2_160_000_000)) self.assertEqual(newer_driver.message.slcState.commandStatus, 'applied') def test_first_action_zero_can_issue_and_apply_command(self): settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationHigher': True}) runtime = Runtime(settings, session_id='first-action') self.sm['carState'].vCruise = 60.0 self.sm['carState'].vCruiseCluster = 60.0 pending = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState self.assertTrue(pending.hasPending) self.sm.advance(2_050_000_000) accepted = runtime.step(self.sm, self.cp, now_ns=2_050_000_000, cruise_event=CruiseEvent(1, 2_050_000_000, 0.0, 0.0, 'accel', 'card-producer', 2_050_000_000, 'confirmationAccept', runtime.session_id, pending.decisionId, pending.presentationId)) self.assertIsNotNone(accepted.command) self.assertEqual(accepted.command.slcCruiseCommand.actionId, 0) self.assertEqual(accepted.message.slcState.commandId, accepted.command.slcCruiseCommand.commandId) card_sm = ReplaySM({'slcState': accepted.message.slcState}, 2_050_000_000) observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=1_900_000_000, valid_until_ns=9_000_000_000, episode=1) self.assertTrue(physical_actions.command_applicable( accepted.command.slcCruiseCommand, card_sm['slcState'], observation, 'card-producer', 2_050_000_000, 60.0 / 3.6, long_active=True, button_event=False)) def test_late_bounded_card_receipt_supersedes_clock_expiry(self): settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationHigher': True}) runtime = Runtime(settings, session_id='late-receipt') self.sm['carState'].vCruise = 60.0 self.sm['carState'].vCruiseCluster = 60.0 pending = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState self.sm.advance(2_010_000_000) accepted = runtime.step(self.sm, self.cp, now_ns=2_010_000_000, cruise_event=CruiseEvent(1, 2_010_000_000, 0.0, 0.0, 'accel', 'card-producer', 2_010_000_000, 'confirmationAccept', runtime.session_id, pending.decisionId, pending.presentationId)) self.assertIsNotNone(accepted.command) command = accepted.command.slcCruiseCommand after_expiry = int(command.expiresMonoTime) + 10_000_000 actual_application = int(command.expiresMonoTime) - 10_000_000 self.sm.advance(after_expiry) result = runtime.step(self.sm, self.cp, now_ns=after_expiry, cruise_event=CruiseEvent(2, actual_application, 60.0 / 3.6, 25.0, 'unknown', 'card-producer', actual_application, 'commandApplied', runtime.session_id, pending.decisionId, pending.presentationId, command.commandId)) self.assertEqual(result.message.slcState.commandStatus, 'applied') def test_physical_decel_rejects_without_selected_speed_command(self): settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationLower': True}) runtime = Runtime(settings, session_id='physical-reject') first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertTrue(first.message.slcState.hasPending) self.sm.advance(2_010_000_000) first = runtime.step(self.sm, self.cp, now_ns=2_010_000_000, request=Action(runtime.session_id, 1, first.message.slcState.decisionId, first.message.slcState.presentationId, 'accept')) self.assertTrue(first.message.slcState.hasAccepted) source = self.sm['slcDashboardObservation'] source.speedMps = 15.0 source.observedMonoTime = 2_040_000_000 source.episode = 2 self.sm.advance(2_050_000_000) pending = runtime.step(self.sm, self.cp, now_ns=2_050_000_000).message.slcState self.assertTrue(pending.hasPending) self.sm.advance(2_100_000_000) rejected = runtime.step(self.sm, self.cp, now_ns=2_100_000_000, cruise_event=CruiseEvent(1, 2_100_000_000, 0.0, 0.0, 'decel', 'card-producer', 2_100_000_000, 'confirmationReject', runtime.session_id, pending.decisionId, pending.presentationId)) self.assertEqual(rejected.message.slcState.actionStatus, 'reject') self.assertEqual(rejected.result.acceptance.basis, 'driver_reject') self.assertEqual(rejected.result.acceptance.state.accepted, first.result.acceptance.state.accepted) self.assertFalse(rejected.message.slcState.hasPending) self.assertIsNone(rejected.command) self.assertEqual(runtime.ledger.transactions, ()) def test_identified_ui_adopt_uses_same_card_owned_command_path(self): runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ui-adopt-drive') self.sm['carState'].vCruise = 60.0 self.sm['carState'].vCruiseCluster = 60.0 shown = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState self.assertTrue(shown.hasAccepted) self.sm.advance(2_050_000_000) adopted = runtime.step(self.sm, self.cp, now_ns=2_050_000_000, request=Action(runtime.session_id, 1, 0, shown.presentationId, 'adopt')) self.assertEqual(adopted.message.slcState.actionStatus, 'adopt') self.assertIsNotNone(adopted.command) self.assertEqual(adopted.command.slcCruiseCommand.presentationId, shown.presentationId) self.assertEqual(adopted.command.slcCruiseCommand.actionId, 0) self.assertEqual(adopted.command.slcCruiseCommand.decisionId, 0) card_sm = ReplaySM({'slcState': adopted.message.slcState}, 2_050_000_000) observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=1_900_000_000, valid_until_ns=9_000_000_000, episode=1) self.assertTrue(physical_actions.command_applicable( adopted.command.slcCruiseCommand, card_sm['slcState'], observation, 'card-producer', 2_050_000_000, 60.0 / 3.6, long_active=True, button_event=False)) def test_native_card_applies_one_shot_command_only_with_current_state(self): from openpilot.cereal import messaging from openpilot.selfdrive.car import card now_ns = time.monotonic_ns() observation = SimpleNamespace(status=Status.VALID, speed_mps=30.0, observed_ns=now_ns - 20_000_000, valid_until_ns=now_ns + 1_000_000_000, episode=4) state = messaging.new_message('slcState') state.valid = True state.logMonoTime = now_ns - 10_000_000 state.slcState.frameMonoTime = state.logMonoTime state.slcState.sessionId = 'card-drive' state.slcState.enabled = True state.slcState.source = 'dashboard' state.slcState.observationKind = 'valid' state.slcState.speedLimit = 30.0 state.slcState.hasAccepted = True state.slcState.acceptedSpeedLimit = 30.0 state.slcState.commandId = 1 state.slcState.presentationId = 1 state.slcState.sourceProducerSessionId = 'card-producer' state.slcState.sourceEpisode = 4 state.slcState.sourceObservedMonoTime = observation.observed_ns state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns command = messaging.new_message('slcCruiseCommand') command.valid = True command.slcCruiseCommand.kind = 'adoptAcceptedLimit' command.slcCruiseCommand.source = 'dashboard' command.slcCruiseCommand.sessionId = 'card-drive' command.slcCruiseCommand.commandId = 1 command.slcCruiseCommand.actionId = 1 command.slcCruiseCommand.presentationId = 1 command.slcCruiseCommand.sourceProducerSessionId = 'card-producer' command.slcCruiseCommand.sourceEpisode = 4 command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns command.slcCruiseCommand.issuedMonoTime = now_ns - 10_000_000 command.slcCruiseCommand.expiresMonoTime = now_ns + 80_000_000 command.slcCruiseCommand.expectedSelectedMps = 60.0 / 3.6 command.slcCruiseCommand.targetMps = 30.0 fake = card.Car.__new__(card.Car) fake.conditional_replay = False fake.switchback_button_tracker = None fake.switchback_settings_owner = None from openpilot.starpilot.controllers.wheel_actions import WheelPublisher fake.params = Params() from openpilot.starpilot.vehicle_startup import VehicleStartupOwner fake.vehicle_startup = VehicleStartupOwner() fake.wheel_publisher = WheelPublisher(fake.params) fake.car_gps_publisher = card.CarGpsPublisher() fake.pm = mock.Mock() fake.ioniq6_long_prearmed = False fake.slc_replay = True fake.slc_control_enabled = True fake.slc_source_floor_ns = 0 fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'})) fake.slc_requested_configuration = fake.slc_configuration fake.aol_card_intent = None fake.slc_producer_session = 'card-producer' fake.slc_command_sock = object() fake.slc_commands = deque(maxlen=8) fake.slc_command_session = '' fake.slc_last_command_id = 0 fake.slc_receipts = [] fake.can_sock = object() fake.can_rcv_cum_timeout_counter = 0 fake.is_metric = False fake.CP = car.CarParams(openpilotLongitudinalControl=True, pcmCruise=False) fake.CC_prev = car.CarControl(enabled=True) fake.v_cruise_helper = VCruiseHelper(fake.CP) fake.v_cruise_helper.v_cruise_kph = 60.0 fake.sm = cast(messaging.SubMaster, ReplaySM( {'carControl': car.CarControl(enabled=True, longActive=True), 'slcState': state.slcState, 'deviceState': SimpleNamespace(startedMonoTime=1)}, now_ns - 10_000_000)) self.enterContext(mock.patch.object(fake, 'CI', SimpleNamespace( CS=SimpleNamespace(dashboard_limit=SimpleNamespace(observation=observation)), update=lambda packets: car.CarState(canValid=True, cruiseState={'available': True})), create=True)) self.enterContext(mock.patch.object(fake, 'RI', SimpleNamespace(update=lambda packets: None), create=True)) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None, command.as_reader(), None])): selected, _ = fake.state_update() self.assertGreater(selected.vCruise, 60.0) self.assertEqual(fake.slc_receipts[0][0], 'commandApplied') first_selected = selected.vCruise selected_again, _ = fake.state_update() self.assertEqual(selected_again.vCruise, first_selected) self.assertEqual(fake.slc_receipts, []) fake.CC_prev.enabled = False fake.CS_prev = car.CarState(vEgo=20.0) fake.experimental_mode = False fake.initial_cruise_speed = 40 fake.slc_last_command_id = 0 fake.slc_command_session = '' fake.v_cruise_helper.v_cruise_kph = 60.0 with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): edge, _ = fake.state_update() self.assertNotEqual(edge.vCruise, first_selected) self.assertEqual(fake.slc_receipts[0][0], 'commandRejected') # The command socket can beat the conflated slcState socket by one card tick. fake.CC_prev.enabled = True fake.slc_last_command_id = 0 fake.slc_command_session = 'card-drive' fake.v_cruise_helper.v_cruise_kph = 60.0 fake.slc_commands.clear() stale_snapshot = messaging.new_message('slcState') stale_snapshot.slcState = state.slcState stale_snapshot.slcState.commandId = 0 fake.sm.data['slcState'] = stale_snapshot.slcState fake.sm.logMonoTime['slcState'] = command.slcCruiseCommand.issuedMonoTime - 1 with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): waiting, _ = fake.state_update() self.assertEqual(waiting.vCruise, 60.0) self.assertEqual(fake.slc_receipts, []) self.assertEqual(len(fake.slc_commands), 1) fake.sm.data['slcState'] = state.slcState fake.sm.logMonoTime['slcState'] = state.logMonoTime with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): caught_up, _ = fake.state_update() self.assertGreater(caught_up.vCruise, 60.0) self.assertEqual(fake.slc_receipts[0][0], 'commandApplied') # A saved admission replacement retires only SLC work, not shared producers # or the physical suppression needed to consume a confirmation release. fake.v_cruise_helper.v_cruise_kph = 60.0 press = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)]) release = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)]) fake.v_cruise_helper.update_v_cruise(press, True, False, SlcPendingConfirmation('card-drive', 1, 1)) self.assertTrue(fake.v_cruise_helper.slc_suppressed_buttons) fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'})) fake.slc_requested_configuration = (False, parse({'ShowSpeedLimits': False})) fake.slc_commands.append(command.as_reader()) floor = time.monotonic_ns() fake.refresh_slc_configuration(floor) self.assertFalse(fake.slc_commands) self.assertEqual(fake.slc_producer_session, 'card-producer') self.assertTrue(fake.v_cruise_helper.slc_suppressed_buttons) fake.v_cruise_helper.update_v_cruise(release, True, False) self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons) self.assertEqual(fake.v_cruise_helper.v_cruise_kph, 60.0) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): disabled, _ = fake.state_update() self.assertEqual(disabled.vCruise, 60.0) self.assertFalse(fake.slc_commands) self.assertEqual(fake.slc_receipts, []) fake.slc_requested_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'})) fake.refresh_slc_configuration(floor + 1) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): retired, _ = fake.state_update() self.assertEqual(retired.vCruise, 60.0) self.assertFalse(fake.slc_commands) fresh_ns = floor + 30_000_000 observation.observed_ns = floor + 10_000_000 observation.valid_until_ns = floor + 100_000_000 state.slcState.frameMonoTime = floor + 20_000_000 state.slcState.sessionId = 'new-card-drive' state.slcState.commandId = 2 state.slcState.sourceObservedMonoTime = observation.observed_ns state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns command.slcCruiseCommand.sessionId = 'new-card-drive' command.slcCruiseCommand.commandId = 2 command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns command.slcCruiseCommand.issuedMonoTime = floor + 20_000_000 command.slcCruiseCommand.expiresMonoTime = floor + 90_000_000 fake.sm.data['slcState'] = state.slcState fake.sm.logMonoTime['slcState'] = floor + 20_000_000 fake.sm.logMonoTime['carControl'] = fresh_ns with (mock.patch.object(card.time, 'monotonic_ns', return_value=fresh_ns), mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): resumed, _ = fake.state_update() self.assertGreater(resumed.vCruise, 60.0) self.assertEqual(fake.slc_receipts[0][0], 'commandApplied') # A replay Card observes recorded CAN time, not the host process clock. replay = card.Car.__new__(card.Car) replay.__dict__.update(fake.__dict__) replay.slc_source_floor_ns = 0 replay.slc_configuration = (False, parse({'ShowSpeedLimits': False})) replay.slc_requested_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'})) replay.slc_commands = deque(maxlen=8) replay.v_cruise_helper.v_cruise_kph = 60.0 recorded_ns, host_ns = 2_000_000_000, 90_000_000_000 packet = messaging.new_message('can', 1) packet.logMonoTime = recorded_ns packet.can[0] = {'address': 0x100, 'dat': b'\x00' * 8, 'src': 0} with (mock.patch.object(card, 'REPLAY', True), mock.patch.object(card.time, 'monotonic_ns', return_value=host_ns), mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[packet.to_bytes()]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): replay.state_update() self.assertEqual(replay.can_log_mono_time, recorded_ns) self.assertEqual(replay.slc_source_floor_ns, recorded_ns) self.assertLess(replay.slc_source_floor_ns, host_ns) observation.observed_ns = recorded_ns + 10_000_000 observation.valid_until_ns = recorded_ns + 100_000_000 state.slcState.frameMonoTime = recorded_ns + 20_000_000 state.slcState.sourceObservedMonoTime = observation.observed_ns state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns command.slcCruiseCommand.issuedMonoTime = recorded_ns + 20_000_000 command.slcCruiseCommand.expiresMonoTime = recorded_ns + 90_000_000 replay.sm.logMonoTime['slcState'] = recorded_ns + 20_000_000 replay.sm.logMonoTime['carControl'] = recorded_ns + 30_000_000 packet.logMonoTime = recorded_ns + 30_000_000 packet.clear_write_flag() with (mock.patch.object(card, 'REPLAY', True), mock.patch.object(card.time, 'monotonic_ns', return_value=host_ns), mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[packet.to_bytes()]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])): replayed, _ = replay.state_update() self.assertGreater(replayed.vCruise, 60.0) self.assertEqual(replay.slc_receipts[0][0], 'commandApplied') self.assertEqual(replay.slc_producer_session, 'card-producer') def _card_with_slc_state(self, pending, observation, now_ns): from openpilot.selfdrive.car import card fake = card.Car.__new__(card.Car) fake.conditional_replay = False fake.switchback_button_tracker = None fake.switchback_settings_owner = None from openpilot.starpilot.controllers.wheel_actions import WheelPublisher fake.params = Params() from openpilot.starpilot.vehicle_startup import VehicleStartupOwner fake.vehicle_startup = VehicleStartupOwner() fake.wheel_publisher = WheelPublisher(fake.params) fake.car_gps_publisher = card.CarGpsPublisher() fake.pm = mock.Mock() fake.ioniq6_long_prearmed = False fake.slc_replay = True fake.slc_control_enabled = True fake.slc_source_floor_ns = 0 fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'})) fake.slc_requested_configuration = fake.slc_configuration fake.aol_card_intent = None fake.slc_producer_session = 'card-producer' fake.slc_command_sock = object() fake.slc_commands = deque(maxlen=8) fake.slc_command_session = '' fake.slc_last_command_id = 0 fake.slc_receipts = [] fake.can_sock = object() fake.can_rcv_cum_timeout_counter = 0 fake.is_metric = False fake.CP = car.CarParams(openpilotLongitudinalControl=True, pcmCruise=False) fake.CC_prev = car.CarControl(enabled=True) fake.v_cruise_helper = VCruiseHelper(fake.CP) fake.v_cruise_helper.v_cruise_kph = 60.0 fake.sm = cast(card.messaging.SubMaster, ReplaySM( {'carControl': car.CarControl(enabled=True, longActive=True), 'slcState': pending, 'deviceState': SimpleNamespace(startedMonoTime=1)}, now_ns)) current_cs = [car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])] self.enterContext(mock.patch.object(fake, 'CI', SimpleNamespace( CS=SimpleNamespace(dashboard_limit=SimpleNamespace(observation=observation) if observation is not None else None), update=lambda packets: current_cs[0]), create=True)) self.enterContext(mock.patch.object(fake, 'RI', SimpleNamespace(update=lambda packets: None), create=True)) return fake, current_cs def test_wheel_confirmation_all_sources_and_normal_cruise_without_pending(self): from openpilot.cereal import messaging from openpilot.selfdrive.car import card from openpilot.selfdrive.controls.plannerd import current_cruise_event, queue_cruise_event from openpilot.starpilot.speed_limits.tests.test_maps_runtime import BOOT, packet from openpilot.starpilot.speed_limits.vision.observation import MODEL_ID start = 2_000_000_000 sources = (('Dashboard', True), ('Maps', True), ('Maps', False), ('Vision', True), ('Vision', False)) buttons = (('accelCruise', 'accept', 61.0), ('decelCruise', 'reject', 59.0)) for (source_name, dashboard_present), (button, outcome, normal_speed), has_pending in product(sources, buttons, (True, False)): with self.subTest(source=source_name, button=button, pending=has_pending, dashboard=dashboard_present): self.sm.advance(start) self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 60.0 dashboard = self.sm['slcDashboardObservation'] dashboard.status, dashboard.producerSessionId, dashboard.episode = 'valid', 'card-producer', 1 dashboard.speedMps, dashboard.observedMonoTime, dashboard.validUntilMonoTime = 25.0, start - 100_000_000, start + 1_000_000_000 if source_name == 'Maps': self.sm.data['mapdOut'] = packet(speedLimit=25.0).mapdOut if source_name == 'Vision': vision_event = messaging.new_message('slcVisionObservation') vision = vision_event.slcVisionObservation.init('vision') vision.producerSessionId, vision.status, vision.stream, vision.modelId = 'camera', 'valid', 'road', MODEL_ID vision.speedMps, vision.confidence, vision.supportCount, vision.episode, vision.frameId = 25.0, 0.9, 2, 1, 1 vision.observedMonoTime, vision.validUntilMonoTime, vision.cameraFrameEofBootTime = start, start + 250_000_000, BOOT self.sm.data['slcVisionObservation'] = vision_event.slcVisionObservation self.sm.logMonoTime['slcVisionObservation'] = start self.sm.logMonoTime['mapdOut'] = BOOT for service in self.sm.data: for metadata in (self.sm.valid, self.sm.alive, self.sm.updated): metadata[service] = True settings = parse({'SpeedLimitController': True, 'SLCPriority1': source_name, 'SLCConfirmation': True, 'SLCConfirmationLower': True, 'SLCConfirmationHigher': True}) runtime = Runtime(settings, session_id='wheel-drive', vision_enabled=True, vision_control_qualified=True) wire = runtime.step(self.sm, self.cp, now_ns=start, now_boot_ns=BOOT).message.slcState self.assertTrue(wire.hasPending) self.assertEqual(str(wire.source), {'Dashboard': 'dashboard', 'Maps': 'map', 'Vision': 'vision'}[source_name]) now = start if not has_pending: now += 10_000_000 self.sm.advance(now) self.sm.logMonoTime['mapdOut'] = BOOT wire = runtime.step(self.sm, self.cp, now_ns=now, now_boot_ns=BOOT + now - start, request=Action(runtime.session_id, 1, wire.decisionId, wire.presentationId, 'accept')).message.slcState self.assertFalse(wire.hasPending) evidence = self.sm['slcDashboardObservation'] observation = (SimpleNamespace(status=Status.VALID, speed_mps=evidence.speedMps, episode=evidence.episode, observed_ns=evidence.observedMonoTime, valid_until_ns=evidence.validUntilMonoTime) if dashboard_present else None) fake, current_cs = self._card_with_slc_state(wire, observation, now) fake.is_metric = True fake.car_params_published = True fake.sm.frame = 1 fake.sm.all_checks = lambda services: True fake.last_actuators_output = car.CarControl.Actuators() fake.rk = SimpleNamespace(remaining=0.0) fake.aol_replay = False fake.slc_cruise_event_id = 0 fake.radar_publish = mock.Mock() clock = [now] with (mock.patch.object(card, 'REPLAY', False), mock.patch.object(card.time, 'monotonic_ns', side_effect=lambda clock=clock: clock[0]), mock.patch.object(card.time, 'monotonic', side_effect=lambda clock=clock: clock[0] / 1e9), mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): for pressed in (True, False): current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type=button, pressed=pressed)]) cs, radar = fake.state_update() fake.state_publish(cs, radar) clock[0] += 10_000_000 fake.sm.logMonoTime['carControl'] = clock[0] events = [call.args[1] for call in fake.pm.send.call_args_list if call.args[0] == 'slcCruiseEvent'] self.assertEqual(len(events), 1) event = events[0].slcCruiseEvent self.assertEqual(str(event.kind), ('confirmationAccept' if outcome == 'accept' else 'confirmationReject') if has_pending else 'driverChange') self.assertEqual(fake.v_cruise_helper.v_cruise_kph, 60.0 if has_pending else normal_speed) self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons) slc_events, curve_events = deque(), deque() queue_cruise_event(events[0], slc_events, curve_events) self.sm.advance(clock[0]) self.sm.logMonoTime['mapdOut'] = BOOT publications = {call.args[0]: call.args[1] for call in fake.pm.send.call_args_list} for service in ('carState', 'slcDashboardObservation'): self.sm.data[service] = getattr(publications[service], service) self.sm.logMonoTime[service] = int(publications[service].logMonoTime) result = runtime.step(self.sm, self.cp, now_ns=clock[0], now_boot_ns=BOOT + clock[0] - start, cruise_event=current_cruise_event(slc_events, clock[0])) self.assertFalse(result.message.slcState.hasPending) if has_pending: self.assertEqual(result.message.slcState.actionStatus, outcome) def test_card_runtime_consumed_press_hold_command_release(self): from openpilot.selfdrive.car import card now_ns = time.monotonic_ns() source = self.sm['slcDashboardObservation'] source.producerSessionId = 'card-producer' source.observedMonoTime = now_ns - 20_000_000 source.validUntilMonoTime = now_ns + 1_000_000_000 source.carStateLogMonoTime = now_ns self.sm.advance(now_ns) self.sm['carState'].vCruise = 60.0 self.sm['carState'].vCruiseCluster = 60.0 runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationHigher': True}), session_id='held-press-drive') pending = runtime.step(self.sm, self.cp, now_ns=now_ns).message.slcState self.assertTrue(pending.hasPending) observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=source.observedMonoTime, valid_until_ns=source.validUntilMonoTime, episode=1) fake, current_cs = self._card_with_slc_state(pending, observation, now_ns) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): pressed, _ = fake.state_update() self.assertEqual(pressed.vCruise, 60.0) consumed = fake.v_cruise_helper.slc_consumed_button self.assertIsNotNone(consumed) self.assertEqual(consumed.pending.decision_id, pending.decisionId) press_ns = time.monotonic_ns() self.sm.advance(press_ns) accepted = runtime.step(self.sm, self.cp, now_ns=press_ns, cruise_event=CruiseEvent(1, press_ns, 0.0, 0.0, 'accel', 'card-producer', press_ns, 'confirmationAccept', runtime.session_id, pending.decisionId, pending.presentationId)) self.assertIsNotNone(accepted.command) self.assertEqual(accepted.command.slcCruiseCommand.actionId, 0) fake.sm.data['slcState'] = accepted.message.slcState fake.sm.logMonoTime['slcState'] = press_ns current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[accepted.command.as_reader(), None])): applied, _ = fake.state_update() self.assertGreater(applied.vCruise, 60.0) self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandApplied']) applied_speed = applied.vCruise current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)]) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): released, _ = fake.state_update() self.assertEqual(released.vCruise, applied_speed) self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons) self.assertEqual(fake.slc_receipts, []) def accept_next_limit(speed_mps, episode): stamp = time.monotonic_ns() source.speedMps = speed_mps source.observedMonoTime = stamp source.validUntilMonoTime = stamp + 1_000_000_000 source.episode = episode observation.speed_mps = speed_mps observation.observed_ns = stamp observation.valid_until_ns = source.validUntilMonoTime observation.episode = episode self.sm['carState'].vCruise = fake.v_cruise_helper.v_cruise_kph self.sm['carState'].vCruiseCluster = fake.v_cruise_helper.v_cruise_kph self.sm.advance(stamp) next_pending = runtime.step(self.sm, self.cp, now_ns=stamp).message.slcState self.assertTrue(next_pending.hasPending) fake.sm.data['slcState'] = next_pending fake.sm.logMonoTime['slcState'] = stamp fake.sm.logMonoTime['carControl'] = stamp current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)]) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)): fake.state_update() accepted_ns = time.monotonic_ns() self.sm.advance(accepted_ns) next_accepted = runtime.step(self.sm, self.cp, now_ns=accepted_ns, cruise_event=CruiseEvent(episode, accepted_ns, 0.0, 0.0, 'accel', 'card-producer', accepted_ns, 'confirmationAccept', runtime.session_id, next_pending.decisionId, next_pending.presentationId)) self.assertIsNotNone(next_accepted.command) fake.sm.data['slcState'] = next_accepted.message.slcState fake.sm.logMonoTime['slcState'] = accepted_ns return next_accepted.command release_command = accept_next_limit(30.0, 2) current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)]) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[release_command.as_reader(), None])): same_tick_release, _ = fake.state_update() self.assertGreater(same_tick_release.vCruise, applied_speed) self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandApplied']) self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons) unrelated_command = accept_next_limit(35.0, 3) before_unrelated = fake.v_cruise_helper.v_cruise_kph current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='decelCruise', pressed=True)]) with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]), mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[unrelated_command.as_reader(), None])): blocked, _ = fake.state_update() self.assertAlmostEqual(blocked.vCruise, before_unrelated, delta=1e-4) self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandRejected']) def test_malformed_saved_priorities_disable_runtime_without_crashing(self): for key in ('SLCPriority1', 'SLCPriority2'): for malformed in ([], {}, 1, None): with self.subTest(key=key, malformed=malformed): settings = parse({'SpeedLimitController': True, key: malformed}) self.assertFalse(settings.enabled) self.assertTrue(settings.errors) def test_actual_plannerd_loop_publishes_same_tick_state_and_plan(self): from openpilot.selfdrive.controls import plannerd from openpilot.cereal import messaging class StopReplay(Exception): pass class Publisher: def __init__(self, services): self.services = services self.sent = [] def send(self, service, message): self.sent.append((service, message)) self.sm.data['deviceState'] = messaging.new_message('deviceState').deviceState self.sm['deviceState'].startedMonoTime = 1_000_000_000 self.sm.valid['deviceState'] = True self.sm.updated['deviceState'] = True self.sm.alive['deviceState'] = True self.sm.logMonoTime['deviceState'] = 2_000_000_000 self.sm.frame = 0 self.sm.all_checks = lambda services=None: True calls = 0 def update(timeout=100): nonlocal calls calls += 1 if calls > 1: raise StopReplay self.sm.logMonoTime['modelV2'] = 2_010_000_000 self.sm.logMonoTime['carState'] = 2_010_000_000 # latest CarState arrives one card tick ahead of source publisher = Publisher([]) with tempfile.TemporaryDirectory() as directory, ExitStack() as resources: params = Params(directory) # Drain native asynchronous writes before deleting their temporary directory. resources.callback(params._finalizer) params.put('CarParams', self.cp.to_bytes(), block=True) params.put_bool('SpeedLimitController', True, block=True) params.put('SLCPriority1', 'Dashboard', block=True) with (mock.patch.object(self.sm, 'update', side_effect=update), mock.patch.object(plannerd, 'Params', return_value=params), mock.patch.object(plannerd, 'config_realtime_process'), mock.patch.object(plannerd.messaging, 'SubMaster', return_value=self.sm), mock.patch.object(plannerd.messaging, 'PubMaster', return_value=publisher), mock.patch.object(plannerd.messaging, 'sub_sock', return_value=object()), mock.patch.object(plannerd.messaging, 'recv_one_or_none', return_value=None), mock.patch.dict('os.environ', {'SLC_REPLAY_RUNTIME': '1', 'REPLAY': '1'})): with self.assertRaises(StopReplay): plannerd.main() services = [service for service, _ in publisher.sent] self.assertIn('slcState', services) self.assertIn('longitudinalPlan', services) state = next(message.slcState for service, message in publisher.sent if service == 'slcState') self.assertFalse(state.hasCeiling) self.assertTrue(state.hasAccepted) self.assertFalse(state.hasPending) self.assertFalse(state.automaticAdoptionPending) self.assertEqual(state.observationKind, 'valid') self.assertEqual(state.frameMonoTime, 2_010_000_000) def test_stored_history_is_schema_bound_and_not_reused_for_new_dashboard_episode(self): candidate = acc.Candidate('dashboard', acc.ObservationIdentity(acc.IdentityKind.PRODUCER_EPISODE, value='drive:card:car:1'), 20.0) raw = history.encode(acc.AcceptedLimit(candidate, 'drive', 2), 1000) self.assertIn('source_schema', json.loads(raw)) with tempfile.TemporaryDirectory() as directory: params = Params(directory) params.put('SLCQualifiedHistory', json.loads(raw), block=True) self.assertEqual(params.get('SLCQualifiedHistory')['source_schema'], json.loads(raw)['source_schema']) self.assertIsNone(history.decode(params.get('SLCQualifiedHistory'), session_id='next-drive', current=acc.Observation(acc.ObservationKind.VALID, candidate), now_wall_ns=1000, max_age_ns=1_000_000)) self.assertIsNone(history.decode(raw, session_id='next-drive', current=acc.Observation(acc.ObservationKind.VALID, candidate), now_wall_ns=1000, max_age_ns=1_000_000)) tampered = json.loads(raw) tampered['source_schema'] = 'wrong' self.assertIsNone(history.decode(json.dumps(tampered), session_id='next-drive', current=acc.Observation(acc.ObservationKind.VALID, candidate), now_wall_ns=1000, max_age_ns=1_000_000)) def test_native_saved_settings_default_off_and_preserve_choices(self): with tempfile.TemporaryDirectory() as directory: params = Params(directory) fresh = read_params(params) self.assertFalse(fresh.enabled) params.put_bool('SpeedLimitController', True, block=True) params.put_bool('SLCConfirmation', True, block=True) params.put_bool('SLCConfirmationLower', True, block=True) params.put('SLCFallback', 2, block=True) params.put('SLCPriority1', 'Map Data', block=True) params.put('SLCPriority2', 'Dashboard', block=True) params.put('Offset3', 5.0, block=True) restored = read_params(params) self.assertTrue(restored.enabled) self.assertTrue(restored.acceptance.confirm_lower) self.assertTrue(restored.acceptance.fallback_previous) self.assertEqual(restored.selection.slots[0], Source.MAP) self.assertEqual(restored.selection.slots[1], Source.DASHBOARD) self.assertFalse(restored.errors) def accepted_runtime(self, settings, *, session_id): runtime = Runtime(settings, session_id=session_id) candidate = runtime._observations(self.sm, self.cp, 2_000_000_000)[Source.DASHBOARD].candidate assert candidate is not None accepted = acc.AcceptedLimit(candidate, session_id, 0) runtime.state = replace(runtime.state, acceptance=replace(acc.new_session(session_id, accepted), received_speed_mps=candidate.speed_mps)) return runtime def setUp(self): self.cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC) self.assertTrue(self.cp.openpilotLongitudinalControl) data, self.envelopes = messages() from openpilot.cereal import messaging data['slcDashboardObservation'] = messaging.new_message('slcDashboardObservation').slcDashboardObservation self.sm = ReplaySM(data, 2_000_000_000) self.sm['carState'].vCruiseCluster = 100.0 self.sm['carState'].vEgoCluster = 20.0 self.sm['carControl'].enabled = True self.sm['carControl'].longActive = True self.sm['carControl'].latActive = False source = self.sm['slcDashboardObservation'] source.producerSessionId = 'card-producer' source.carStateLogMonoTime = 2_000_000_000 source.status = 'valid' source.speedMps = 25.0 source.observedMonoTime = 1_900_000_000 source.validUntilMonoTime = 9_000_000_000 source.episode = 1 def test_saved_choice_and_native_planner_replay(self): settings = parse({'SpeedLimitController': True, 'SLCFallback': 2, 'SLCPriority1': 'Dashboard'}) runtime = self.accepted_runtime(settings, session_id='replay-drive') planner = LongitudinalPlanner(self.cp, init_v=20.0) first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertEqual(first.message.slcState.observationKind, 'valid') self.assertFalse(first.message.slcState.hasCeiling) self.assertIsNone(first.result.ceiling) planner.update(self.sm, cruise_ceiling=first.result.ceiling) self.assertEqual(planner.last_cruise_ceiling_status, 'absent') self.sm['carState'].vCruise = 105.0 self.sm['carState'].vCruiseCluster = 105.0 self.sm.advance(2_050_000_000) button = runtime.step(self.sm, self.cp, now_ns=2_050_000_000, cruise_event=CruiseEvent(1, 2_010_000_000, 100 / 3.6, 105 / 3.6, 'accel', 'replay-card', 2_020_000_000)) self.assertFalse(button.result.errors) self.assertTrue(button.result.override.event_receipt.consumed) self.assertEqual(button.result.override.event_receipt.status, 'driver_intent') self.sm.advance(9_100_000_000) stale = runtime.step(self.sm, self.cp, now_ns=9_100_000_000) self.assertEqual(runtime._observations(self.sm, self.cp, 9_100_000_000)[Source.DASHBOARD].kind, ObservationKind.STALE) self.assertEqual(stale.message.slcState.observationKind, 'stale') # both configured slots use the expired dashboard self.assertFalse(stale.message.slcState.hasCeiling) self.assertEqual(stale.result.acceptance.basis, 'previous_accepted') self.assertEqual(stale.result.override.contribution_mps, 105 / 3.6) planner.update(self.sm, cruise_ceiling=stale.result.ceiling) self.assertEqual(planner.last_cruise_ceiling_status, 'absent') self.assertIsNone(stale.result.ceiling) for sequence, kind, previous, selected in ((2, 'accel', 105, 110), (3, 'decel', 110, 100)): now = 9_100_000_000 + sequence * 50_000_000 self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = float(selected) self.sm.advance(now) adjusted = runtime.step(self.sm, self.cp, now_ns=now, cruise_event=CruiseEvent(sequence, now - 40_000_000, previous / 3.6, selected / 3.6, kind, 'replay-card', now - 30_000_000)) self.assertFalse(adjusted.result.errors) self.assertFalse(adjusted.message.slcState.hasPending) self.assertEqual(adjusted.result.override.event_receipt.status, 'driver_intent') self.assertEqual(adjusted.result.override.contribution_mps, selected / 3.6) self.assertEqual(adjusted.result.acceptance.state.accepted, first.result.acceptance.state.accepted) def test_decrease_keeps_lower_slc_ceiling_in_native_planner(self): mph_to_mps = 0.44704 self.sm['slcDashboardObservation'].speedMps = 45 * mph_to_mps self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 70 * mph_to_mps * 3.6 runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='decrease-drive') runtime.step(self.sm, self.cp, now_ns=2_000_000_000) previous_mps = self.sm['carState'].vCruise / 3.6 self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 69 * mph_to_mps * 3.6 self.sm.advance(2_050_000_000) result = runtime.step(self.sm, self.cp, now_ns=2_050_000_000, cruise_event=CruiseEvent(1, 2_010_000_000, previous_mps, self.sm['carState'].vCruise / 3.6, 'decel', 'replay-card', 2_020_000_000)) self.assertFalse(result.result.errors) self.assertTrue(result.result.override.event_receipt.consumed) self.assertAlmostEqual(result.result.override.state.persistent_selected_mps, self.sm['carState'].vCruise / 3.6) self.assertTrue(result.message.slcState.driverOverrideActive) self.assertIsNone(result.result.ceiling) planner = LongitudinalPlanner(self.cp, init_v=45 * mph_to_mps) planner.update(self.sm, cruise_ceiling=result.result.ceiling) self.assertEqual(planner.last_cruise_ceiling_status, 'absent') def _runtime_with_set_speed_override(self, lateral): self.setUp() self.sm['carControl'].latActive = lateral runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='pedal-drive') runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 105.0 self.sm.advance(2_050_000_000) armed = runtime.step(self.sm, self.cp, now_ns=2_050_000_000, cruise_event=CruiseEvent(1, 2_010_000_000, 100 / 3.6, 105 / 3.6, 'accel', 'replay-card', 2_020_000_000)) self.assertEqual(armed.result.override.contribution_mps, 105 / 3.6) return runtime def test_accelerator_release_restores_saved_override_after_long_hold(self): for lateral, delayed_resume in product((False, True), (False, True)): with self.subTest(lateral=lateral, delayed_resume=delayed_resume): runtime = self._runtime_with_set_speed_override(lateral) self.sm['carState'].gasPressed = True self.sm['carState'].vEgo = self.sm['carState'].vEgoCluster = 32.0 self.sm['carControl'].longActive = False self.sm['controlsState'].longControlState = 'off' for now in (2_100_000_000, 3_100_000_000, 5_100_000_000): self.sm.advance(now) held = runtime.step(self.sm, self.cp, now_ns=now) self.assertFalse(held.result.errors) self.assertIsNone(held.result.ceiling) self.assertEqual(held.result.override.contribution_mps, 105 / 3.6) self.assertIsNotNone(held.result.override.context) self.assertEqual(held.result.state.override.persistent_selected_mps, 105 / 3.6) self.assertTrue(held.message.slcState.retainedOverride) self.assertTrue(held.result.acceptance.authority.accelerator_override) self.assertFalse(held.result.acceptance.authority.longitudinal_active) self.sm['carState'].gasPressed = False if delayed_resume: self.sm.advance(5_200_000_000) waiting = runtime.step(self.sm, self.cp, now_ns=5_200_000_000) self.assertIsNone(waiting.result.ceiling) self.assertEqual(waiting.result.state.override.persistent_selected_mps, 105 / 3.6) self.sm['carControl'].longActive = True self.sm['controlsState'].longControlState = 'pid' self.sm.advance(5_300_000_000) resumed = runtime.step(self.sm, self.cp, now_ns=5_300_000_000) self.assertFalse(resumed.result.errors) self.assertEqual(resumed.result.override.basis, 'persistent') self.assertIsNone(resumed.result.ceiling) def test_disengagement_preserves_manual_set_speed_without_an_slc_ceiling(self): for lateral, pressed, disabled_service in product((False, True), (False, True), ('selfdriveState', 'carControl')): with self.subTest(lateral=lateral, pressed=pressed, disabled_service=disabled_service): runtime = self._runtime_with_set_speed_override(lateral) self.sm['carState'].gasPressed = pressed self.sm['carControl'].longActive = False self.sm['controlsState'].longControlState = 'off' self.sm[disabled_service].enabled = False self.sm.advance(2_100_000_000) disengaged = runtime.step(self.sm, self.cp, now_ns=2_100_000_000) self.assertIsNone(disengaged.result.ceiling) self.assertEqual(disengaged.result.state.override.persistent_selected_mps, 105 / 3.6) self.sm[disabled_service].enabled = True self.sm['carState'].gasPressed = False self.sm['carControl'].longActive = True self.sm['controlsState'].longControlState = 'pid' self.sm.advance(2_200_000_000) resumed = runtime.step(self.sm, self.cp, now_ns=2_200_000_000) self.assertEqual(resumed.result.override.contribution_mps, 105 / 3.6) self.assertIsNone(resumed.result.ceiling) def test_stale_engagement_suppresses_commands_and_keeps_the_manual_choice(self): for lateral, service in product((False, True), ('carState', 'carControl', 'controlsState', 'selfdriveState')): with self.subTest(lateral=lateral, service=service): runtime = self._runtime_with_set_speed_override(lateral) self.sm['carState'].gasPressed = True self.sm['carControl'].longActive = False self.sm['controlsState'].longControlState = 'off' self.sm.advance(2_100_000_000) self.sm.valid[service] = False stale = runtime.step(self.sm, self.cp, now_ns=2_100_000_000) self.assertIsNone(stale.result.ceiling) self.assertEqual(stale.result.state.override.persistent_selected_mps, 105 / 3.6) def test_pending_action_receipt_and_stock_gate(self): settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationLower': True}) runtime = Runtime(settings, session_id='receipt-drive') runtime.step(self.sm, self.cp, now_ns=2_000_000_000) source = self.sm['slcDashboardObservation'] source.speedMps = 15.0 source.observedMonoTime = 2_040_000_000 source.validUntilMonoTime = 9_000_000_000 source.episode = 2 self.sm.advance(2_050_000_000) pending = runtime.step(self.sm, self.cp, now_ns=2_050_000_000) self.assertTrue(pending.message.slcState.hasPending) self.sm.advance(2_100_000_000) request = Action(runtime.session_id, 17, pending.message.slcState.decisionId, pending.message.slcState.presentationId, 'reject') rejected = runtime.step(self.sm, self.cp, now_ns=2_100_000_000, request=request) self.assertEqual(rejected.action_result.status, 'resolved') self.assertEqual(rejected.message.slcState.actionSequenceId, 17) self.assertEqual(rejected.message.slcState.actionStatus, 'reject') self.cp.openpilotLongitudinalControl = False self.sm.advance(2_150_000_000) stock = runtime.step(self.sm, self.cp, now_ns=2_150_000_000) self.assertFalse(stock.message.slcState.hasCeiling) self.assertIsNone(stock.result.ceiling) def test_source_requires_matching_car_frame_and_restart_changes_identity(self): runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='paired-drive') source = self.sm['slcDashboardObservation'] source.carStateLogMonoTime = 1_950_000_000 mismatched = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertEqual(runtime._observations(self.sm, self.cp, 2_000_000_000)[Source.DASHBOARD].kind, ObservationKind.UNKNOWN) self.assertFalse(mismatched.message.slcState.hasCeiling) source.carStateLogMonoTime = 2_000_000_000 matched = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) first_identity = matched.result.selection.observation.candidate.observation_identity.value self.assertTrue(matched.message.slcState.hasAccepted) self.assertFalse(matched.message.slcState.hasPending) self.sm.logMonoTime['carState'] = 2_010_000_000 # independent sockets deliver the next card tick first lagged = runtime.step(self.sm, self.cp, now_ns=2_010_000_000) self.assertTrue(lagged.message.slcState.hasAccepted) self.assertFalse(lagged.message.slcState.hasPending) source.producerSessionId = 'restarted-card' restarted = runtime.step(self.sm, self.cp, now_ns=2_010_000_000) second_identity = restarted.result.selection.observation.candidate.observation_identity.value self.assertNotEqual(first_identity, second_identity) def test_lateral_authority_uses_car_control_axis(self): runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='axis-drive') self.sm['selfdriveState'].active = True long_only = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertEqual(long_only.result.acceptance.authority.mode.value, 'longitudinal_only') self.sm['carControl'].latActive = True combined = runtime.step(self.sm, self.cp, now_ns=2_000_000_000) self.assertEqual(combined.result.acceptance.authority.mode.value, 'combined') def test_rejected_and_malformed_ui_requests_do_not_fill_action_ledger(self): runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='many-actions') runtime.step(self.sm, self.cp, now_ns=2_000_000_000) wrong = Action(runtime.session_id, 1, 0, 99999, 'adopt') runtime.step(self.sm, self.cp, now_ns=2_000_000_000, request=wrong) self.assertEqual(runtime.ledger.transactions, ()) self.assertEqual(runtime.ledger.last_action_id, -1) for sequence in range(2, 202): timestamp = 2_000_000_000 + sequence self.sm.advance(timestamp) runtime.step(self.sm, self.cp, now_ns=timestamp, request=Action(runtime.session_id, sequence, 99999, 0, 'reject')) self.assertFalse(runtime.ledger.reset_required) self.assertEqual(runtime.ledger.transactions, ()) if __name__ == '__main__': unittest.main()