"""Finalized GM startup ownership and independent-axis caller lifecycle.""" import json import os from types import SimpleNamespace import unittest from unittest.mock import patch from opendbc.can import CANPacker from opendbc.car import Bus, structs from opendbc.car.gm.aol import GM_AOL_WORDS, GM_BASE_GATEWAY_IDS, qualified_gm from opendbc.car.gm.lateral import lane_centering_supported from opendbc.car.gm.interface import CarInterface from opendbc.car.gm.tests.test_bolt_cc import params as bolt_params, feed as feed_car, control, native from opendbc.safety.tests.libsafety import libsafety_py from opendbc.car.gm.tests.test_cc_gateway_stock import silverado_stock_params from opendbc.car.gm.tests.test_bolt_factory_acc import factory_params from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params from opendbc.car.gm.tests.test_volt_camera_control import camera_params from opendbc.car.gm.tests.test_volt_camera_removed import removed_params from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params from opendbc.car.gm.values import CAR, DBC, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CC_CAR, is_volt_one_pedal from openpilot.cereal import messaging from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.selfdrive.car.card import Car from openpilot.selfdrive.controls.controlsd import Controls from openpilot.starpilot.aol.runtime import decide_axes from openpilot.starpilot.aol.transport_pause import TransportPause from openpilot.starpilot.aol.vehicle import policy_for from openpilot.starpilot.aol.wire import IntentState, SafetyState, encode_safety from openpilot.starpilot.lateral.tests.test_lane_runtime import feed from openpilot.starpilot.tests.test_volt_disable_longitudinal import configured from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences from openpilot.starpilot.aol.tests.gm_bolt_recovery import exercise_bolt_cancel_recovery BOLT_IDS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL) def _configurations(): from opendbc.car.gm.tests.test_ordinary_cc import malibu_hybrid_params from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal for pedal in (False, True): for removed in (False, True): for alternate in (False, True): for radar in ((False, True) if pedal else (False,)): cp = malibu_hybrid_params(pedal=pedal, removed=removed, alternate=alternate, radar=radar) yield cp disabled = cp.as_reader().as_builder() prepare_disable_longitudinal(disabled, True) yield disabled from opendbc.car.gm.tests.test_bolt_pedal import params as pedal_params for identity in BOLT_IDS: cp = pedal_params(identity, pedal=True, removed=True) yield cp disabled = cp.as_reader().as_builder() VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled) yield disabled for identity in GM_BASE_GATEWAY_IDS: cp = ordinary_params(identity, radar=True) yield cp if identity == CAR.BUICK_LACROSSE: disabled = cp.as_reader().as_builder() VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled) yield disabled held = cp.as_reader().as_builder() VehicleStartupPreferences(gm_auto_hold=True).prepare(held) yield held for identity in BOLT_IDS: for pedal in (False, True): for removed in (False, True): if identity == BOLT_IDS[-1] and pedal and removed: continue cp = bolt_params(identity, present=pedal, pedal=pedal, removed=removed) yield cp if pedal: disabled = cp.as_reader().as_builder() VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled, fingerprints={2: {0x180: 4}}) yield disabled for alpha in (False, True): yield factory_params(alpha=alpha) yield camera_params(alpha=alpha) yield removed_params(alpha=alpha) yield removed_params(alpha=alpha, alternate=True) for c9 in (False, True): yield sdgm_params(alpha=alpha, brake_c9=c9) for c9 in (False, True): for radar in (False, True): yield ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=alpha, sascm=True, accelerator=not c9, radar=radar) for variant in ('be', 'f1', 'cc'): cp = configured(variant) yield cp disabled = cp.as_reader().as_builder() VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled) yield disabled from opendbc.car.gm.tests.test_ascm_intercept import params as intercept_params from opendbc.car.gm.tests.test_ordinary_camera import params as ordinary_camera from opendbc.car.gm.tests.test_ordinary_camera_removed import params as ordinary_removed from opendbc.car.gm.tests.test_conventional_pedal import params as conventional_pedal from opendbc.car.gm.tests.test_silverado_cc_pedal import params as silverado_pedal from opendbc.car.gm.tests.test_camera_acc_pedal import params as camera_pedal for identity in ORDINARY_ASCM_CAR | ORDINARY_SDGM_CAR: for alpha in (False, True): for c9 in (False, True): for radar in (False, True): yield intercept_params(identity, sascm=True, accelerator=not c9, radar=radar, alpha=alpha) for identity in ORDINARY_CAMERA_CAR: for alpha in (False, True): for analog in (False, True): yield ordinary_camera(identity, alpha=alpha, be=analog) yield ordinary_removed(identity, alpha=alpha, analog=analog) for camera in (False, True): for be in (False, True): for release in (False, True): yield camera_pedal(identity, camera=camera, be=be, release=release) from opendbc.car.gm.tests.test_ordinary_cc import malibu_f1_params for release in (False, True): cp = malibu_f1_params(release=release) yield cp disabled = cp.as_reader().as_builder() VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled) yield disabled for identity in ORDINARY_CC_CAR: yield intercept_params(identity) for removed in (False, True): for disabled in (False, True): yield conventional_pedal(identity, removed=removed, disabled=disabled) from opendbc.car.gm.tests.test_camera_acc_pedal import fingerprint as pedal_fingerprint from opendbc.car.gm.tests.test_volt_transitions import (volt_ascm_pedal_params, volt_cc_pedal_params, volt_gateway_pedal_params, volt_sdgm_pedal_params) for camera in (False, True): for be in (False, True): observed = pedal_fingerprint(camera=camera, be=be) observed[0].update({0xBD: 7, 0x232: 8}) for release in (False, True): yield CarInterface.get_params(CAR.CHEVROLET_VOLT_CAMERA, observed, [], False, release, False) yield volt_gateway_pedal_params(camera=camera, be=be, release=release) for be in (False, True): for radar in (False, True): for release in (False, True): yield volt_ascm_pedal_params(be=be, radar=radar, release=release) yield volt_sdgm_pedal_params(be=be, radar=radar, release=release) for removed in (False, True): yield volt_cc_pedal_params(be=be, radar=radar, release=release, removed=removed) for removed in (False, True): for disabled in (False, True): yield silverado_pedal(removed=removed, disabled=disabled) def configurations(): from opendbc.car.gm.tests.test_volt_cc_f1 import params as f1_params yield f1_params() yield silverado_stock_params() for cp in _configurations(): yield cp selected = cp.as_reader().as_builder() VehicleStartupPreferences(gm_auto_hold=True).prepare(selected) if selected.safetyConfigs[0].safetyParam != cp.safetyConfigs[0].safetyParam: yield selected for paired in (False, True): selected = cp.as_reader().as_builder() VehicleStartupPreferences(volt_one_pedal=True, gm_auto_hold=paired).prepare(selected) if is_volt_one_pedal(selected): yield selected class TestGmAol(unittest.TestCase): def test_actual_final_cp_registry_and_isolation(self): words = set() for cp in configurations(): with self.subTest(identity=cp.carFingerprint, word=hex(cp.safetyConfigs[0].safetyParam)): self.assertTrue(qualified_gm(cp)) words.add(cp.safetyConfigs[0].safetyParam) self.assertTrue(policy_for(cp).normal_runtime_supported) for field in ('passive', 'dashcamOnly', 'notCar'): denied = cp.as_reader().as_builder() setattr(denied, field, True) self.assertFalse(qualified_gm(denied)) denied = cp.as_reader().as_builder() denied.safetyConfigs[0].safetyModel = 'noOutput' self.assertFalse(qualified_gm(denied)) denied = cp.as_reader().as_builder() denied.alternativeExperience = 33 self.assertFalse(qualified_gm(denied)) self.assertEqual(words, GM_AOL_WORDS, {"missing": GM_AOL_WORDS - words, "unexpected": words - GM_AOL_WORDS}) def test_base_gateway_aol_preserves_tuning_and_lane_capability(self): for identity in GM_BASE_GATEWAY_IDS: cp = ordinary_params(identity, radar=True) with self.subTest(identity=identity): self.assertTrue(qualified_gm(cp)) self.assertFalse(lane_centering_supported(cp)) selected = cp.as_reader().as_builder() selected.alternativeExperience = 32 self.assertTrue(qualified_gm(selected)) self.assertFalse(lane_centering_supported(selected)) for field, value in (('pcmCruise', True), ('radarUnavailable', True), ('alphaLongitudinalAvailable', True), ('networkLocation', 'fwdCamera'), ('transmissionType', 'direct'), ('flags', 1)): denied = cp.as_reader().as_builder() setattr(denied, field, value) self.assertFalse(qualified_gm(denied)) denied = cp.as_reader().as_builder() denied.safetyConfigs[0].safetyParam = 0x80 self.assertEqual(qualified_gm(denied), identity == CAR.BUICK_LACROSSE) denied = cp.as_reader().as_builder() denied.openpilotLongitudinalControl = False self.assertEqual(qualified_gm(denied), identity == CAR.BUICK_LACROSSE) @staticmethod def card(cp, settings): settings.put_bool('OpenpilotEnabledToggle', True, block=True) def get_car(*args, pre_create_hook, **kwargs): selected = pre_create_hook(cp.as_reader().as_builder(), cp.carFingerprint, {}, []) return CarInterface(selected) with patch('openpilot.selfdrive.car.card.messaging.recv_one_retry', return_value=SimpleNamespace(can=[1])), \ patch('openpilot.selfdrive.car.card.get_car', side_effect=get_car): return Car() def test_automatic_bolt_identity_reaches_real_card_factory_and_native(self): from opendbc.car.can_definitions import CanData from opendbc.car import car_helpers from opendbc.car.gm.values import CarControllerParams from opendbc.car.gm.pedal_capability import automatic_bolt_candidate from opendbc.car.mock.values import CAR as MOCK from opendbc.car.gm.tests.test_bolt_cc import setup from openpilot.starpilot.vehicle_selection import encode # Recorded pre-controller PT fingerprints: 2020 nonACC pedal and Zik Gen2 # ACC pedal. DLCs are literal; stock370 bytes are the first recorded true # camera observation, not a fabricated native/control grant. gen1 = { 170: 8, 188: 8, 189: 7, 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 257: 8, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 368: 3, 381: 8, 384: 4, 386: 8, 388: 8, 390: 7, 407: 7, 417: 7, 419: 1, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 513: 6, 528: 5, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 564: 5, 566: 7, 567: 5, 568: 1, 569: 3, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 717: 5, 753: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 866: 4, 869: 4, 872: 1, 961: 8, 967: 4, 969: 8, 975: 2, 977: 8, 979: 7, 985: 5, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 5, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1236: 8, 1243: 3, 1249: 8, 1257: 6, 1265: 8, 1275: 3, 1279: 4, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1904: 7, 1905: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1922: 7, 1927: 7, 2020: 8, 2028: 8, } gen2 = { 189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 513: 6, 528: 5, 532: 6, 560: 8, 562: 8, 566: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1280: 4, 1296: 4, 1300: 8, 1930: 7, } safety = libsafety_py.libsafety release = safety.set_safety_hooks(int(structs.CarParams.SafetyModel.allOutput), 0) != 0 stock_acc = (0x370, bytes.fromhex('000221820000'), 2) def receive_for(pt, camera_messages): packets = [CanData(address, bytes(length), 0) for address, length in pt.items()] packets += [CanData(0x180, bytes(4), 2), CanData(0x320, bytes(pt.get(0x320, 8)), 2)] packets += [CanData(address, data, bus) for address, data, bus in camera_messages] return lambda wait_for_one=False: [packets] if wait_for_one else [] # Unknown, mixed stock modes, wrong bus/echo and malformed lengths cannot # manufacture ACC capability from the Gen2 alias. for camera in ([], [(0x370, bytes(6), 2)], [(0x370, b'\0\4' + bytes(4), 2)], [(0x370, b'\0\5' + bytes(4), 2)], [stock_acc, (0x370, b'\0\4' + bytes(4), 2)], [(stock_acc[0], stock_acc[1], 0)], [(stock_acc[0], stock_acc[1], 130)], [(stock_acc[0], stock_acc[1][:5], 2)]): with self.subTest(camera=camera): candidate, fp = car_helpers.can_fingerprint(receive_for(gen2, camera)) self.assertIsNone(candidate) self.assertEqual(fp[0], gen2) fp = {0: gen2} self.assertEqual(automatic_bolt_candidate(CAR.CHEVROLET_BOLT_CC_2022_2023, fp, {4}), CAR.CHEVROLET_BOLT_CC_2022_2023) malformed = {0: dict(gen2)} malformed[0][0x201] = 5 self.assertIsNone(automatic_bolt_candidate(CAR.CHEVROLET_BOLT_CC_2018_2021, malformed, {2})) malformed[0][0x201] = 6 malformed[0][0x236] = 7 self.assertIsNone(automatic_bolt_candidate(CAR.CHEVROLET_BOLT_CC_2018_2021, malformed, {2})) no_pedal = {0: {address: length for address, length in gen2.items() if address != 0x201}} for alias in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_EUV): self.assertIsNone(automatic_bolt_candidate(alias, no_pedal, set())) self.assertIsNone(automatic_bolt_candidate(alias, no_pedal, {4, 5})) self.assertEqual(automatic_bolt_candidate(CAR.CHEVROLET_BOLT_ACC_2022_2023, no_pedal, set()), CAR.CHEVROLET_BOLT_ACC_2022_2023) self.assertIsNone(automatic_bolt_candidate(None, fp, {2})) self.assertEqual(automatic_bolt_candidate(MOCK.MOCK, fp, {2}), MOCK.MOCK) def startup(pt, forced, camera): with OpenpilotPrefix(), patch.dict(os.environ, {'SKIP_FW_QUERY': '1', 'FINGERPRINT': ''}): settings = Params() settings.put_bool('OpenpilotEnabledToggle', True, block=True) settings.put_bool('AlwaysOnLateral', True, block=True) settings.put_bool('AlphaLongitudinalEnabled', True, block=True) settings.put_bool('IsReleaseBranch', release, block=True) settings.put('VehicleSelection', json.loads(encode(None if forced is None else forced.value)), block=True) receive = receive_for(pt, camera) def discover(*args, **kwargs): return car_helpers.get_car(receive, *args[1:], **kwargs) with patch('openpilot.selfdrive.car.card.messaging.recv_one_retry', return_value=SimpleNamespace(can=[1])), \ patch('openpilot.selfdrive.car.card.get_car', side_effect=discover): card = Car() self.assertTrue(settings.get_bool('FirmwareQueryDone')) self.assertFalse(card.ci_initialized) self.assertEqual(card.CP.alternativeExperience, 32) return card.CP stock_pt = dict(gen2) del stock_pt[0x201] cases = ((gen1, None, CAR.CHEVROLET_BOLT_CC_2018_2021, 0x9D, []), (gen2, None, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, 0x1CD, [stock_acc]), (gen2, CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2017, 0xBD, [stock_acc]), (gen2, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2018_2021, 0x9D, [stock_acc]), (gen2, CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_CC_2022_2023, 0x19D, [stock_acc]), (stock_pt, CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023, 5 if release else 7, [stock_acc]), (gen2, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, 0x1CD, [stock_acc])) for pt, forced, identity, word, camera in cases: with self.subTest(forced=forced, identity=identity, release=release): cp = startup(pt, forced, camera) self.assertEqual(cp.carFingerprint, identity) self.assertEqual(cp.safetyConfigs[0].safetyParam, word) self.assertEqual(cp.fingerprintSource, structs.CarParams.FingerprintSource.fixed if forced else structs.CarParams.FingerprintSource.can) self.assertTrue(cp.flags & 16) # actual recorded BSM presence, not a manual flag assignment self.assertFalse(cp.dashcamOnly) self.assertFalse(cp.passive) self.assertAlmostEqual(cp.lateralTuning.torque.latAccelFactor, 1.2 if identity == CAR.CHEVROLET_BOLT_CC_2017 else 2.) self.assertAlmostEqual(cp.lateralTuning.torque.friction, .21 if identity == CAR.CHEVROLET_BOLT_CC_2017 else .13) self.assertEqual(CarControllerParams(cp).STEER_MAX, 450 if identity == CAR.CHEVROLET_BOLT_CC_2017 else 300) pedal = 0x201 in pt self.assertEqual(cp.openpilotLongitudinalControl, pedal or not release) self.assertEqual(cp.pcmCruise, not pedal and release) setup(cp) safety.set_alternative_experience(cp.alternativeExperience) self.assertEqual(safety.get_current_safety_mode(), int(structs.CarParams.SafetyModel.gm)) self.assertEqual(safety.get_current_safety_param(), word) self.assertFalse(safety.get_controls_allowed()) def test_marked_gm_transport_survives_master_off_restart(self): from openpilot.starpilot.feature_runtime import enabled with OpenpilotPrefix(): settings = Params() settings.put_bool('AlwaysOnLateral', False, block=True) for cp in configurations(): cp = cp.as_reader().as_builder() with self.subTest(identity=cp.carFingerprint, word=cp.safetyConfigs[0].safetyParam): cp.alternativeExperience = 0 self.assertFalse(policy_for(cp).full_axis_runtime_required) self.assertFalse(enabled(settings, cp, 'aol', {'AOL_REPLAY_RUNTIME': '0'})) cp.alternativeExperience = 32 self.assertTrue(policy_for(cp).full_axis_runtime_required) self.assertTrue(enabled(settings, cp, 'aol', {'AOL_REPLAY_RUNTIME': '0'})) marked = silverado_stock_params() marked.alternativeExperience = 32 card = self.card(marked, settings) self.assertEqual(card.CP.alternativeExperience, 0) if card.aol_card_intent is not None: self.assertFalse(card.aol_card_intent.allowed_latch) def test_silverado_stock_card_publishes_lateral_only_aol_owner(self): with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) card = self.card(silverado_stock_params(), settings) self.assertFalse(card.CP.dashcamOnly) self.assertTrue(card.CP.pcmCruise) self.assertFalse(card.CP.openpilotLongitudinalControl) self.assertEqual(card.CP.safetyConfigs[0].safetyParam, 16) self.assertEqual(card.CP.alternativeExperience, 32) self.assertIsNotNone(card.aol_card_intent) cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20.) cs.cruiseState.available = True cs.cruiseState.enabled = False card.aol_card_intent.update(cs) self.assertTrue(card.aol_card_intent.allowed_latch) cs.gearShifter = 'reverse' card.aol_card_intent.update(cs) self.assertTrue(card.aol_card_intent.allowed_latch) cs.cruiseState.available = False card.aol_card_intent.update(cs) self.assertFalse(card.aol_card_intent.allowed_latch) def test_bolt_main_intent_ignores_unavailable_lkas_assignment(self): from openpilot.starpilot.aol.intent import read_settings from opendbc.car.gm.tests.test_bolt_pedal import params as pedal_params with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) settings.put('LKASButtonControl', 9, block=True) cp = pedal_params(CAR.CHEVROLET_BOLT_CC_2018_2021, pedal=True, camera=True) self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x9D) selected = self.card(cp, settings) owner = selected.aol_card_intent cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20.) cs.cruiseState.available = True for _ in range(2): owner.settings = read_settings(settings) owner.update(cs) self.assertTrue(owner.allowed_latch) self.assertEqual(settings.get('LKASButtonControl'), 9) settings.put('MainCruiseButtonControl', 9, block=True) owner = self.card(cp, settings).aol_card_intent owner.update(cs) self.assertTrue(owner.allowed_latch) self.assertEqual(settings.get('MainCruiseButtonControl'), 9) cs.buttonEvents = [structs.CarState.ButtonEvent(type='mainCruise', pressed=True)] owner.update(cs) self.assertTrue(owner.allowed_latch) owner.update(cs, fault_active=True) self.assertFalse(owner.allowed_latch) def test_actual_card_startup_default_off_and_main_level_fault_recovery(self): with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', False, block=True) # Explicit Off survives new-install On enrollment. cp = factory_params(alpha=False) ordinary = self.card(cp, settings) self.assertIsNone(ordinary.aol_card_intent) self.assertEqual(ordinary.CP.alternativeExperience, 0) settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(cp, settings) self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, cp.safetyConfigs[0].safetyParam) self.assertEqual(selected.CP.alternativeExperience, 32) self.assertFalse(selected.aol_card_intent.explicit_latch) cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20.) cs.cruiseState.available = True selected.aol_card_intent.update(cs) self.assertTrue(selected.aol_card_intent.allowed_latch) cs.steerFaultTemporary = True selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) cs.steerFaultTemporary = False cs.gearShifter = 'reverse' selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) self.assertFalse(selected.aol_card_intent.output(cs)[0]) cs.gearShifter = 'drive' cs.steerFaultPermanent = True selected.aol_card_intent.update(cs, fault_active=True) self.assertFalse(selected.aol_card_intent.allowed_latch) cs.steerFaultPermanent = False selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) cs.cruiseState.available = False cs.canValid = False selected.aol_card_intent.update(cs, fault_active=False) cs.cruiseState.available = True cs.canValid = True selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) cs.cruiseState.available = False selected.aol_card_intent.update(cs, fault_active=False) cs.cruiseState.available = True selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) def test_actual_bolt_card_lagging_preserves_intent_but_withdraws_both_axes(self): from opendbc.car.gm.tests.test_bolt_pedal import params as pedal_params from openpilot.cereal import log from openpilot.selfdrive.selfdrived.events import Events, ET from openpilot.starpilot.aol.intent import disarming_fault from openpilot.starpilot.nostalgia import aol_no_entry with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(pedal_params(CAR.CHEVROLET_BOLT_CC_2018_2021, pedal=True, camera=True), settings) self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, 0x9D) cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20.) cs.cruiseState.available = True owner = selected.aol_card_intent owner.update(cs) self.assertTrue(owner.allowed_latch) events = Events() events.add(log.OnroadEvent.EventName.selfdrivedLagging) self.assertTrue(events.contains(ET.NO_ENTRY)) self.assertTrue(events.contains(ET.SOFT_DISABLE)) self.assertFalse(disarming_fault(events.to_msg(), cs)) # SOFT withdraws output while retaining driver intent. for standard in (False, True): with patch.object(selected, 'sm', {'onroadEvents': events.to_msg()}): fault = selected.aol_disarming_fault(cs, 1_000_000_000, 1_000_000_001) self.assertFalse(fault) owner.update(cs, fault_active=fault) self.assertTrue(owner.allowed_latch) allowed, pause_lat, pause_long = owner.output(cs) intent = SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long) native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=True, lateralAllowed=True, longitudinalAllowed=True) blocked = decide_axes(standard_lateral=standard, standard_longitudinal=standard, intent=intent, native=native, car_state=cs, initialized=True, model_ready=True, no_entry=aol_no_entry(events.names, cs, paddle_only_cancel=False), immediate_disable=False, dm_lockout=False, pause_brake_mps=0.) self.assertFalse(blocked.desired_lateral or blocked.desired_longitudinal) self.assertFalse(blocked.lateral_active or blocked.longitudinal_active) events.clear() with patch.object(selected, 'sm', {'onroadEvents': events.to_msg()}): owner.update(cs, fault_active=selected.aol_disarming_fault(cs, 1_100_000_000, 1_100_000_001)) self.assertTrue(owner.allowed_latch) # No main cycle, standard engagement, or gesture. allowed, pause_lat, pause_long = owner.output(cs) intent = SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long) for native, expected_active in ((None, False), (SimpleNamespace(requestedLateral=False, requestedLongitudinal=False, lateralAllowed=True, longitudinalAllowed=True), False), (SimpleNamespace(requestedLateral=True, requestedLongitudinal=False, lateralAllowed=True, longitudinalAllowed=False), True)): recovered = decide_axes(standard_lateral=False, standard_longitudinal=False, intent=intent, native=native, car_state=cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=0.) self.assertTrue(recovered.desired_lateral) self.assertEqual(recovered.lateral_active, expected_active) self.assertFalse(recovered.desired_longitudinal or recovered.longitudinal_active) events.add(log.OnroadEvent.EventName.selfdrivedLagging) denied = selected.CP.as_reader().as_builder() denied.passive = True with patch.object(selected, 'CP', denied), patch.object(selected, 'sm', {'onroadEvents': events.to_msg()}): self.assertTrue(selected.aol_disarming_fault(cs, 1_200_000_000, 1_200_000_001)) def test_actual_bolt_card_mixed_faults_block_until_explicit_healthy_observation(self): from opendbc.car.gm.tests.test_bolt_pedal import params as pedal_params from openpilot.cereal import log from openpilot.selfdrive.selfdrived.events import Events with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) for extra in (log.OnroadEvent.EventName.controlsMismatch, log.OnroadEvent.EventName.steerUnavailable, None): with self.subTest(extra=extra): selected = self.card(pedal_params(CAR.CHEVROLET_BOLT_CC_2018_2021, pedal=True, camera=True), settings) cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20.) cs.cruiseState.available = True owner = selected.aol_card_intent owner.update(cs) events = Events() events.add(log.OnroadEvent.EventName.selfdrivedLagging) if extra is not None: events.add(extra) else: cs.steerFaultPermanent = True with patch.object(selected, 'sm', {'onroadEvents': events.to_msg()}): fault = selected.aol_disarming_fault(cs, 1_000_000_000, 1_000_000_001) self.assertTrue(fault) owner.update(cs, fault_active=fault) self.assertFalse(owner.allowed_latch) cs.steerFaultPermanent = False owner.update(cs, fault_active=False) self.assertTrue(owner.allowed_latch) cs.canValid = False cs.cruiseState.available = False owner.update(cs, fault_active=False) cs.canValid = True cs.cruiseState.available = True owner.update(cs, fault_active=False) self.assertTrue(owner.allowed_latch) cs.cruiseState.available = False owner.update(cs, fault_active=False) cs.cruiseState.available = True owner.update(cs, fault_active=False) self.assertTrue(owner.allowed_latch) def test_actual_controls_axis_receipt_and_fault_gates(self): with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(factory_params(alpha=False), settings) controls = Controls() cases = ((None, True), ('temporary', False), (None, True), ('reverse', False), ('brake_allowed', True), ('brake_paused', False), ('regen', True), ('permanent', False), (None, True), ('off', False), ('on', True)) for tick, (fault, expected) in enumerate(cases): now = 1_000_000_000 + tick * 10_000_000 feed(controls, now, tick, active=False, enabled=False) cs = structs.CarState(canValid=True, vEgo=20., gearShifter='drive') cs.cruiseState.available = fault != 'off' cs.steerFaultTemporary = fault == 'temporary' cs.steerFaultPermanent = fault == 'permanent' cs.gearShifter = 'reverse' if fault == 'reverse' else 'drive' cs.brakePressed = fault in ('brake_allowed', 'brake_paused') cs.regenBraking = fault == 'regen' selected.aol_card_intent.update(cs, fault_active=cs.steerFaultPermanent) intent = IntentState('card', tick + 1, now, now, now + 30_000_000, selected.aol_card_intent.allowed_latch, False, False, True, True) native = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), selected.CP.safetyConfigs[0].safetyParam, True, False, True, False, 'panda', 'gm-test') decision = decide_axes(standard_lateral=False, standard_longitudinal=False, intent=intent, native=native, car_state=cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=25. if fault == 'brake_paused' else 0.) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = True axis.aolAxisState.nativeAcknowledged = decision.native_acknowledged axis.aolAxisState.desiredLateral = decision.desired_lateral axis.aolAxisState.lateralActive = decision.lateral_active axis.aolAxisState.sessionId = 'gm-test' axis.aolAxisState.observedMonoTime = now axis.aolAxisState.validUntilMonoTime = now + 30_000_000 receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) receipt.aolSafetyWire = encode_safety(native) state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, _ = controls.state_control() self.assertEqual(command.latActive, expected) self.assertFalse(command.longActive) def test_base_gateway_actual_card_controls_parser_and_native_axes(self): cases = [(identity, False, False) for identity in GM_BASE_GATEWAY_IDS] cases += [(CAR.BUICK_LACROSSE, True, False), (CAR.BUICK_LACROSSE, False, True)] safety = libsafety_py.libsafety for identity, held, disabled in cases: with self.subTest(identity=identity, held=held, disabled=disabled), OpenpilotPrefix(), \ patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', False, block=True) # Test the preserved explicit-Off startup branch. settings.put_bool('GMAutoHold', held, block=True) settings.put_bool('DisableOpenpilotLongitudinal', disabled, block=True) factory = ordinary_params(identity, radar=True) default = self.card(factory, settings) self.assertIsNone(default.aol_card_intent) self.assertEqual(default.CP.alternativeExperience, 0) settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(factory, settings) self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, default.CP.safetyConfigs[0].safetyParam) self.assertEqual(selected.CP.lateralTuning.to_dict(), default.CP.lateralTuning.to_dict()) self.assertEqual(selected.CP.alternativeExperience, 32) self.assertEqual(selected.CP.openpilotLongitudinalControl, not disabled) self.assertFalse(selected.CP.pcmCruise) self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, 0x80 if held else 0) self.assertEqual(selected.CP.lateralTuning.to_dict(), factory.lateralTuning.to_dict()) self.assertFalse(lane_centering_supported(selected.CP)) controls = Controls() self.assertEqual(controls.CP.lateralTuning.to_dict(), factory.lateralTuning.to_dict()) self.assertEqual(controls.CP.lateralTuning.which(), factory.lateralTuning.which()) ci = selected.CI packer = CANPacker(DBC[identity][Bus.pt]) safety.set_alternative_experience(32) self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, selected.CP.safetyConfigs[0].safetyParam), 0) safety.init_tests() seen_lateral = seen_longitudinal = False for tick in range(150): now = 1_000_000_000 + tick * 10_000_000 ordinary = 41 <= tick < 60 and not disabled main = not 20 <= tick < 30 gas, brake, reverse = 70 <= tick < 80, 80 <= tick < 90, 90 <= tick < 100 _, packets = feed_car(SimpleNamespace(update=lambda _: None), packer, now, counter=tick % 4, active=ordinary, speed=20., camera=False) packets = [packet for packet in packets if packet[0] not in (0xC9, 0x1C4, 0xBE, 0x1E1, 0x1F5, 0xBD)] packets += [packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 2 if ordinary else 0, 'AcceleratorPedal2': 30 if gas else 0}), packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 20 if brake else 0}), packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 2 if reverse else 4}), packer.make_can_msg('ASCMSteeringButton', 0, {'ACCButtons': 3 if tick == 40 else 6 if tick == 60 else 1, 'RollingCounter': tick % 4})] if not 100 <= tick < 140: packets.append(packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)})) ci.update([(now - 1_000_000, packets)]) cs = ci.update([(now, packets)]) for packet in packets: native('rx', packet, now // 1000) safety.safety_tick() safety.set_aol_test_heartbeat(True) selected.aol_card_intent.update(cs, now_ns=now, standard_enabled=ordinary) intent = IntentState('card', tick + 1, now, now, now + 30_000_000, selected.aol_card_intent.allowed_latch, False, False, True, True) wanted = decide_axes(standard_lateral=ordinary, standard_longitudinal=ordinary, intent=intent, native=None, car_state=cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=25.) safety.aol_set_host_request(int(wanted.desired_lateral) | (int(wanted.desired_longitudinal) << 1)) safety.set_timer(now // 1000 + 1) mask = safety.aol_get_permission_mask() receipt_state = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), selected.CP.safetyConfigs[0].safetyParam, bool(mask & 1), bool(mask & 2), wanted.desired_lateral, wanted.desired_longitudinal, 'panda', 'gm-gateway-test') decision = decide_axes(standard_lateral=ordinary, standard_longitudinal=ordinary, intent=intent, native=receipt_state, car_state=cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=25.) feed(controls, now, tick, active=ordinary, enabled=ordinary) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = True axis.aolAxisState.nativeAcknowledged = True axis.aolAxisState.desiredLateral = decision.desired_lateral axis.aolAxisState.desiredLongitudinal = decision.desired_longitudinal axis.aolAxisState.lateralActive = decision.lateral_active axis.aolAxisState.longitudinalActive = decision.longitudinal_active axis.aolAxisState.sessionId = 'gm-gateway-test' axis.aolAxisState.observedMonoTime = now axis.aolAxisState.validUntilMonoTime = now + 30_000_000 receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) from dataclasses import replace transport_receipt = receipt_state if tick == 11: transport_receipt = replace(receipt_state, safetyParam=5) elif tick == 12: transport_receipt = replace(receipt_state, axisSessionId='wrong-session') elif tick == 13: transport_receipt = replace(receipt_state, observedMonoTime=now + 1) elif tick == 14: transport_receipt = replace(receipt_state, validUntilMonoTime=now - 1) receipt.aolSafetyWire = encode_safety(transport_receipt) state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, _ = controls.state_control() self.assertEqual(command.latActive, decision.lateral_active and tick not in (11, 12, 13, 14)) self.assertEqual(command.longActive, ordinary) self.assertEqual(bool(safety.get_controls_allowed()), 41 <= tick < 60) if not main or brake or reverse or 131 <= tick < 140: self.assertFalse(command.latActive) if tick == 10: self.assertTrue(command.latActive) command.actuators.torque = .02 if command.latActive else 0. _, messages = ci.apply(command.as_reader(), now + 2) for message in messages: self.assertTrue(native('tx', message, now // 1000 + 1), (tick, message)) seen_lateral |= command.latActive and not command.longActive seen_longitudinal |= command.longActive self.assertTrue(seen_lateral) self.assertEqual(seen_longitudinal, not disabled) safety.set_alternative_experience(0) def test_camera_interceptor_actual_controls_and_native_axis_ownership(self): from opendbc.car.gm.tests.test_camera_acc_pedal import params from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames from opendbc.car.gm import gmcan safety = libsafety_py.libsafety release = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0 for camera in (False, True): for be in (False, True): with self.subTest(camera=camera, be=be), OpenpilotPrefix(), \ patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) settings.put_bool('IsReleaseBranch', release, block=True) factory = params(camera=camera, be=be, release=release) selected = self.card(factory, settings) cp, ci = selected.CP, selected.CI self.assertEqual(cp.alternativeExperience, 32) controls = Controls() self.assertEqual(controls.CP.to_dict(), cp.to_dict()) safety.set_alternative_experience(32) self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, cp.safetyConfigs[0].safetyParam), 0) safety.init_tests() packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) loopback = [] for tick in range(70): now = 1_000_000_000 + tick * 10_000_000 ordinary = tick >= 42 and not release packets = pt_frames(packer, counter=tick % 4, acc_cruise=2 if ordinary else 0) packets = [packet for packet in packets if packet[0] not in (0xBE, 0xF1, 0x1E1)] packets += [packer.make_can_msg('ECMAcceleratorPos' if be else 'EBCMBrakePedalPosition', 0, {}), packer.make_can_msg('ASCMSteeringButton', 0, {'ACCButtons': 3 if tick == 40 else 1, 'RollingCounter': tick % 4})] sensor = bytearray.fromhex('0279012a0000') sensor[4] = tick % 16 sensor[5] = gmcan.pedal_crc(sensor) packets.append((0x201, bytes(sensor), 0)) if camera: packets += [packer.make_can_msg('ASCMLKASteeringCmd', 2, {'RollingCounter': tick % 4}), packer.make_can_msg('AEBCmd', 2, {}), packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 2 if ordinary else 0})] safety.set_timer(now // 1000) for packet in packets: self.assertTrue(native('rx', packet, now // 1000)) safety.safety_tick() state = ci.update([(now, packets + loopback)]) selected.aol_card_intent.update(state, now_ns=now, standard_enabled=ordinary) requested = (1 if selected.aol_card_intent.allowed_latch else 0) | (2 if ordinary else 0) safety.set_aol_test_heartbeat(True) safety.aol_set_host_request(requested) safety.set_timer(now // 1000 + 1) mask = safety.aol_get_permission_mask() feed(controls, now, tick, active=ordinary, enabled=ordinary) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = axis.aolAxisState.nativeAcknowledged = True axis.aolAxisState.desiredLateral = bool(requested & 1) axis.aolAxisState.desiredLongitudinal = bool(requested & 2) axis.aolAxisState.lateralActive = bool(mask & 1) axis.aolAxisState.longitudinalActive = bool(mask & 2) axis.aolAxisState.sessionId = 'camera-interceptor' axis.aolAxisState.observedMonoTime, axis.aolAxisState.validUntilMonoTime = now, now + 30_000_000 receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) receipt.aolSafetyWire = encode_safety(SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), cp.safetyConfigs[0].safetyParam, bool(mask & 1), bool(mask & 2), bool(requested & 1), bool(requested & 2), 'panda', 'camera-interceptor')) actual = messaging.new_message('carState', valid=True, logMonoTime=now) actual.carState = state controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), actual.as_reader()]) command, lateral_log = controls.state_control() controls.publish(command, lateral_log) if tick >= 42: self.assertTrue(command.latActive, (tick, mask, requested, state.canValid, safety.safety_config_valid(), selected.CP.safetyConfigs[0].safetyParam)) self.assertEqual(command.longActive, not release) _, commands = ci.apply(command.as_reader(), now + 2) loopback = [(addr, data, 128) for addr, data, _ in commands if addr == 0x180] for packet in commands: self.assertTrue(native('tx', packet, now // 1000 + 1), (tick, packet)) safety.set_alternative_experience(0) def test_actual_card_forwards_shared_disarming_events(self): class IntentUpdated(Exception): pass with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(factory_params(alpha=False), settings) cs = structs.CarState(canValid=True, vEgo=20., gearShifter='drive') cs.cruiseState.available = True selected.aol_card_intent.update(cs) self.assertTrue(selected.aol_card_intent.allowed_latch) now = 1_000_000_000 events = messaging.new_message('onroadEvents', 1, valid=True, logMonoTime=now) events.onroadEvents[0].name = 'controlsMismatch' events.onroadEvents[0].immediateDisable = True control = messaging.new_message('carControl', valid=True, logMonoTime=now) selected.sm.update_msgs(now / 1e9, [events.as_reader(), control.as_reader()]) with patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'fixture']), \ patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[]), \ patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \ patch('openpilot.selfdrive.car.card.REPLAY', False), \ patch.object(selected.CI, 'update', return_value=cs), \ patch.object(selected.RI, 'update', return_value=None), \ patch.object(selected.sm, 'update'), \ patch.object(selected, 'observe_distance_personality', side_effect=IntentUpdated): with self.assertRaises(IntentUpdated): selected.state_update() self.assertFalse(selected.aol_card_intent.allowed_latch) selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) def test_actual_controller_inactive_cruise_brake_and_regen_lateral_only(self): safety = libsafety_py.libsafety release = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0 cases = [bolt_params(BOLT_IDS[0]), configured('cc'), camera_params(alpha=False), camera_params(alpha=True), removed_params(alpha=False), removed_params(alpha=True), sdgm_params(alpha=False), sdgm_params(alpha=True)] for variant in ('be', 'f1'): cp = configured(variant) VehicleStartupPreferences(disable_bolt_long=True).prepare(cp) cases.append(cp) for cp in cases: with self.subTest(identity=cp.carFingerprint, word=hex(cp.safetyConfigs[0].safetyParam)): cp.alternativeExperience = 32 ci = CarInterface(cp) packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) safety.init_tests() safety.set_alternative_experience(32) safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, cp.safetyConfigs[0].safetyParam) safety.set_aol_test_heartbeat(True) supported = not release or cp.safetyConfigs[0].safetyParam not in (20, 0x4007, 0x5007, 0xC151) requested = [] for tick in range(18): now = 1_000_000_000 + tick * 10_000_000 out, sources = feed_car(ci, packer, now, counter=tick % 4, active=False, brake=True, regen=True, eps_status=1) sources = [source for source in sources if source[0] not in (0x1C4, 0xBE, 0xF1)] sources.extend([packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 0}), packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 12}), packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': 6}), packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0})]) out = ci.update([(now + 1, sources)]) for source in sources: native('rx', source, now // 1000) safety.safety_tick() safety.aol_set_host_request(1) permission = safety.aol_get_permission_mask() cc = control(enabled=False, long_active=False) cc.latActive = bool(permission & 1) ci.CC.frame = tick _, messages = ci.apply(cc.as_reader(), now + 2) self.assertFalse(cc.longActive) for message in messages: if message[0] == 0x180: requested.append(bool(message[1][0] & 8)) self.assertEqual(native('tx', message, now // 1000 + 1), supported) self.assertTrue(out.canValid) self.assertFalse(out.cruiseState.enabled) self.assertTrue(out.brakePressed) self.assertEqual(any(requested), supported) def test_calibration_pause_retains_intent_and_recovers_without_new_main_edge(self): from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD from openpilot.starpilot.aol.tests.test_runtime import sample_car_state from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.events import Events, EventName, ET from openpilot.starpilot.aol.runtime import AxisDecision from openpilot.starpilot.aol.wire import encode_intent for calibration_event in (EventName.calibrationInvalid, EventName.calibrationIncomplete, EventName.calibrationRecalibrating): with self.subTest(calibration=calibration_event), OpenpilotPrefix(), patch.dict( os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(factory_params(alpha=False), settings) controls = Controls() sd = SelfdriveD.__new__(SelfdriveD) sd.CP, sd.initialized, sd.aol_replay = selected.CP, True, True sd.aol_session_id, sd.aol_axis_decision = 'calibration-test', AxisDecision() sd.aol_authority_lost = False sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id) sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings sd.nostalgia_paddle_cancel = False sd.state_machine, sd.events = StateMachine(), Events() sd.sm = messaging.SubMaster(['aolIntentWire', 'aolSafetyWire', 'modelV2', 'extrinsicsCalibration', 'driverMonitoringState', 'pandaStates']) cs = structs.CarState(canValid=True, vEgo=20., gearShifter='drive') for tick, (phase, expected) in enumerate((('unbuckled', True), ('unbuckled', True), ('healthy', True), ('invalid', False), ('recovered', True), ('held', True), ('unmapped_lkas', True), ('unmapped_main', True), ('release_lkas', True), ('main_off', False), ('fault_edge', False), ('recovered_again', True), ('main_off', False), ('new_main', True), ('temporary', False), ('healthy', True), ('reverse', False), ('healthy', True), ('model_stale', False), ('healthy', True))): now = 1_000_000_000 + tick * 10_000_000 cs.cruiseState.available = phase != 'main_off' cs.seatbeltUnlatched = phase == 'unbuckled' cs.buttonEvents = [] if phase in ('unmapped_lkas', 'unmapped_main', 'release_lkas'): cs.buttonEvents = [structs.CarState.ButtonEvent( type='mainCruise' if phase == 'unmapped_main' else 'lkas', pressed=phase != 'release_lkas')] cs.steerFaultTemporary = phase == 'temporary' cs.gearShifter = 'reverse' if phase == 'reverse' else 'drive' events = Events() if cs.seatbeltUnlatched: events.add(EventName.seatbeltNotLatched) if phase == 'invalid': events.add(calibration_event) if phase == 'fault_edge': events.add(EventName.overheat) onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now) onroad.onroadEvents = events.to_msg() selected.sm.update_msgs(now / 1e9, [onroad.as_reader()]) selected.aol_card_intent.update(cs) selected.observe_aol_calibration(cs, now, False) self.assertEqual(selected.aol_card_intent.allowed_latch, phase != 'main_off') intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now) allowed, pause_lat, pause_long = selected.aol_card_intent.output(cs) intent.aolIntentWire = encode_intent(IntentState('card', tick + 1, now, now, now + 30_000_000, allowed, pause_lat, pause_long, True, True)) native = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), selected.CP.safetyConfigs[0].safetyParam, expected, False, expected, False, 'panda', 'calibration-test') receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) receipt.aolSafetyWire = encode_safety(native) model = messaging.new_message('modelV2', valid=phase != 'model_stale', logMonoTime=now) calibration = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=now) calibration.extrinsicsCalibration.calStatus = 'uncalibrated' if phase == 'invalid' else 'calibrated' monitoring = messaging.new_message('driverMonitoringState', valid=True, logMonoTime=now) sd.sm.update_msgs(now / 1e9, [intent.as_reader(), receipt.as_reader(), model.as_reader(), calibration.as_reader(), monitoring.as_reader()]) sd.aol_car_state_log_ns, sd.enabled, sd.active = now, False, False sd.events = events if cs.seatbeltUnlatched: sd.events.add(EventName.seatbeltNotLatched) self.assertTrue(sd.events.contains(ET.NO_ENTRY)) self.assertTrue(sd.events.contains(ET.SOFT_DISABLE)) with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \ patch.object(sd, 'data_sample', side_effect=lambda sd=sd, cs=cs, now=now: sample_car_state(sd, cs, now)), patch.object(sd, 'update_events'), \ patch.object(sd, 'update_alerts'), patch.object(sd, 'update_conditional_mode'), \ patch.object(sd, 'publish_selfdriveState'): sd.step() self.assertEqual(sd.aol_axis_decision.lateral_active, expected, phase) feed(controls, now, tick, active=False, enabled=False) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = axis.aolAxisState.nativeAcknowledged = True axis.aolAxisState.desiredLateral = sd.aol_axis_decision.desired_lateral axis.aolAxisState.lateralActive = sd.aol_axis_decision.lateral_active axis.aolAxisState.sessionId = 'calibration-test' axis.aolAxisState.observedMonoTime = now axis.aolAxisState.validUntilMonoTime = now + 30_000_000 state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, _ = controls.state_control() self.assertEqual(command.latActive, expected, phase) self.assertFalse(command.longActive) def test_actual_selfdrived_gas_override_keeps_longitudinal_request(self): self.exercise_actual_selfdrived_override() def test_sustained_unbuckling_keeps_aol_through_standard_soft_disable(self): self.exercise_actual_selfdrived_override(unlatched=True) def exercise_actual_selfdrived_override(self, *, unlatched=False): from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD from openpilot.starpilot.aol.tests.test_runtime import sample_car_state from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.events import Events, EventName from openpilot.starpilot.aol.runtime import AxisDecision from openpilot.starpilot.aol.wire import encode_intent from openpilot.starpilot.aol.intent import AolSettings from openpilot.cereal import log with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1'}): cp = configured('be') cp.alternativeExperience = 32 now = 1_000_000_000 cs = structs.CarState(canValid=True, vEgo=20., gearShifter='drive', gasPressed=True) cs.cruiseState.available = cs.cruiseState.enabled = True sd = SelfdriveD.__new__(SelfdriveD) sd.CP, sd.initialized, sd.aol_replay = cp, True, True sd.enabled = sd.active = True sd.aol_car_state_log_ns = now sd.aol_session_id = 'gm-test' sd.aol_authority_lost = False sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id) sd.aol_axis_decision = AxisDecision() sd.aol_dm_lateral_inhibit = False sd.aol_settings = AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0)) sd.nostalgia_paddle_cancel = False sd.state_machine = StateMachine() sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled sd.events = Events() sd.events.add(EventName.gasPressedOverride) sd.sm = messaging.SubMaster(['aolIntentWire', 'aolSafetyWire', 'modelV2', 'extrinsicsCalibration', 'driverMonitoringState', 'pandaStates']) intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now) intent.aolIntentWire = encode_intent(IntentState('card', 1, now, now, now + 30_000_000, True, False, False, True, True)) receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) receipt.aolSafetyWire = encode_safety(SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), cp.safetyConfigs[0].safetyParam, True, True, True, True, 'panda', 'gm-test')) model = messaging.new_message('modelV2', valid=True, logMonoTime=now) calibration = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=now) calibration.extrinsicsCalibration.calStatus = 'calibrated' monitoring = messaging.new_message('driverMonitoringState', valid=True, logMonoTime=now) controls = None if unlatched: params = Params() params.put_bool('OpenpilotEnabledToggle', True, block=True) params.put_bool('AlwaysOnLateral', True, block=True) params.put('CarParams', cp.to_bytes(), block=True) controls = Controls() for tick in range(325 if unlatched else 1): now = 1_000_000_000 + tick * 10_000_000 cs.gasPressed = not unlatched cs.seatbeltUnlatched = unlatched and tick > 0 sd.events.clear() if cs.seatbeltUnlatched: sd.events.add(EventName.seatbeltNotLatched) elif not unlatched: sd.events.add(EventName.gasPressedOverride) intent.aolIntentWire = encode_intent(IntentState('card', tick + 1, now, now, now + 30_000_000, True, False, False, True, True)) receipt.aolSafetyWire = encode_safety(SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), cp.safetyConfigs[0].safetyParam, True, not cs.seatbeltUnlatched, True, not cs.seatbeltUnlatched, 'panda', 'gm-test')) for event in (intent, receipt, model, calibration, monitoring): event.logMonoTime = now sd.aol_car_state_log_ns = now sd.sm.update_msgs(now / 1e9, [intent.as_reader(), receipt.as_reader(), model.as_reader(), calibration.as_reader(), monitoring.as_reader()]) with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \ patch.object(sd, 'data_sample', side_effect=lambda sd=sd, cs=cs, now=now: sample_car_state(sd, cs, now)), patch.object(sd, 'update_events'), \ patch.object(sd, 'update_alerts'), patch.object(sd, 'update_conditional_mode'), \ patch.object(sd, 'publish_selfdriveState'): sd.step() self.assertTrue(sd.aol_axis_decision.lateral_active) self.assertEqual(sd.aol_axis_decision.desired_longitudinal, not cs.seatbeltUnlatched) if unlatched: self.assertEqual(sd.enabled, tick <= 300) feed(controls, now, tick, active=sd.active, enabled=sd.enabled) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = axis.aolAxisState.nativeAcknowledged = True axis.aolAxisState.desiredLateral = axis.aolAxisState.lateralActive = True axis.aolAxisState.desiredLongitudinal = axis.aolAxisState.longitudinalActive = not cs.seatbeltUnlatched axis.aolAxisState.sessionId = 'gm-test' axis.aolAxisState.observedMonoTime, axis.aolAxisState.validUntilMonoTime = now, now + 30_000_000 state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, lateral_log = controls.state_control() controls.publish(command, lateral_log) self.assertTrue(command.latActive) self.assertEqual(command.longActive, not cs.seatbeltUnlatched) self.assertEqual(command.cruiseControl.cancel, cs.cruiseState.enabled and (not command.enabled or not cp.pcmCruise)) else: self.assertTrue(sd.enabled) def test_existing_vehicle_factories_preserve_intent_lifecycle(self): from opendbc.car import gen_empty_fingerprint from opendbc.car.honda.interface import CarInterface as HondaInterface from opendbc.car.honda.values import CAR as HondaCars from openpilot.starpilot.lateral.tests.test_lane_runtime import ioniq_candidate from openpilot.starpilot.aol.intent import AolSettings, AolCardIntent from openpilot.starpilot.aol.vehicle import create_intent honda = HondaInterface.get_params(HondaCars.HONDA_ACCORD, gen_empty_fingerprint(), [], True, False, False) _, hyundai = ioniq_candidate() # Existing Card startup adds AOL bit 11 inside the qualified Ioniq namespace. hyundai.safetyConfigs[-1].safetyParam |= 0x0800 settings = AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0)) for cp in (honda, hyundai): policy = policy_for(cp) self.assertTrue(policy.intent_supported, (cp.carFingerprint, cp.safetyConfigs[-1].safetyParam)) selected = create_intent(cp, settings, policy) from openpilot.starpilot.car.hyundai.aol import independent_cruise_buttons_supported reference = AolCardIntent(settings, explicit_latch=policy.explicit_latch, independent_cruise_buttons=cp.brand == 'hyundai' and independent_cruise_buttons_supported(cp)) self.assertIs(type(selected), AolCardIntent) for main, permanent, temporary, gear in ((True, False, False, 'drive'), (True, False, True, 'drive'), (True, False, False, 'reverse'), (True, True, False, 'drive'), (True, False, False, 'drive'), (False, False, False, 'drive'), (True, False, False, 'drive')): cs = structs.CarState(canValid=True, gearShifter=gear, steerFaultPermanent=permanent, steerFaultTemporary=temporary) cs.cruiseState.available = main selected.update(cs, fault_active=permanent) reference.update(cs, fault_active=permanent) self.assertEqual(selected.__dict__.keys(), reference.__dict__.keys()) for key, value in vars(selected).items(): expected = vars(reference)[key] self.assertEqual(vars(value) if hasattr(value, '__dict__') else value, vars(expected) if hasattr(expected, '__dict__') else expected, key) self.assertEqual(selected.output(cs), reference.output(cs)) def test_bolt_cancel_lateral_recovery(self): for identity in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL): for removed in (False, True): with self.subTest(identity=identity, removed=removed): self.exercise_bolt_cancel_recovery(identity, removed) def test_bolt_cancel_fault_rearm(self): for critical in ('controlsMismatch', 'steerUnavailable', 'permanentEPS'): with self.subTest(critical=critical): self.exercise_bolt_cancel_recovery(CAR.CHEVROLET_BOLT_CC_2018_2021, False, critical=critical) def test_bolt_communications_pause_preserves_intent(self): for event in ('commIssueAvgFreq', 'commIssue'): with self.subTest(event=event): self.exercise_bolt_cancel_recovery(CAR.CHEVROLET_BOLT_CC_2018_2021, False, communications=event) def test_bolt_communications_preserves_critical_rearm(self): for critical in ('controlsMismatch', 'steerUnavailable', 'permanentEPS'): with self.subTest(critical=critical): self.exercise_bolt_cancel_recovery(CAR.CHEVROLET_BOLT_CC_2018_2021, False, critical=critical, communications='commIssueAvgFreq') def test_bolt_pedal_cancel_braking_and_physical_paddle_recovery(self): for identity in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023): with self.subTest(identity=identity): self.exercise_bolt_cancel_recovery(identity, False, pedal_scene=True) def test_bolt_pedal_current_inventory_and_published_control_join(self): exercise_bolt_cancel_recovery(self, CAR.CHEVROLET_BOLT_CC_2018_2021, False, pedal_scene=True, inventory_publish=True) def exercise_bolt_cancel_recovery(self, identity, removed, *, critical=None, communications: str | None = None, pedal_scene=False): exercise_bolt_cancel_recovery(self, identity, removed, critical=critical, communications=communications, pedal_scene=pedal_scene) def test_volt_brake_lateral_recovery(self): from dataclasses import replace from openpilot.selfdrive.car.car_events import CarEvents from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.events import Events, EventName, ET from openpilot.starpilot.aol.runtime import AxisDecision from openpilot.starpilot.aol.wire import encode_intent, decode_intent from openpilot.starpilot.nostalgia import aol_no_entry from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params from opendbc.car.gm.values import GMFlags with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() for key in ('AlwaysOnLateral', 'AlphaLongitudinalEnabled', 'GMAutoHold', 'VoltOnePedalMode'): settings.put_bool(key, True, block=True) settings.put('AolBrakePauseSpeedMps', 10., block=True) selected = self.card(ascm_params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, accelerator=True, radar=True), settings) cp, ci = selected.CP, selected.CI self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xD114) self.assertEqual(cp.alternativeExperience, 32) self.assertFalse(cp.flags & GMFlags.PEDAL_LONG) self.assertTrue(cp.openpilotLongitudinalControl) self.assertFalse(cp.pcmCruise) self.assertEqual(selected.aol_settings.pause_brake_mps, 10.) controls = Controls() self.assertEqual(controls.CP.to_dict(), cp.to_dict()) sd = SelfdriveD.__new__(SelfdriveD) sd.CP, sd.initialized, sd.aol_replay = cp, True, True sd.enabled = sd.active = False sd.aol_session_id, sd.aol_axis_decision = 'volt-brake', AxisDecision() sd.aol_authority_lost = False sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id) sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings sd.nostalgia_paddle_cancel = False sd.state_machine, sd.events = StateMachine(), Events() sd.sm = messaging.SubMaster(['aolIntentWire', 'aolSafetyWire', 'modelV2', 'extrinsicsCalibration', 'driverMonitoringState']) car_events = CarEvents(cp) packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) safety = libsafety_py.libsafety safety.set_alternative_experience(32) self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xD114), 0) safety.init_tests() resumed = paused = 0 previous_sd_onroad = None ctx = {'identity': str(cp.carFingerprint), 'word': '0xd114', 'phase': 'prime'} try: # Warm all selected parser/native sources, with stock cruise inactive. for warmup in range(20): stamp = 800_000_000 + warmup * 10_000_000 _, frames = feed_car(SimpleNamespace(update=lambda _: None), packer, stamp, counter=warmup % 4, active=False, speed=8., camera=True) frames = [frame for frame in frames if frame[0] != 0x184] frames.append(packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 1})) frames.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0})) previous = ci.update([(stamp, frames)]) for frame in frames: self.assertTrue(native('rx', frame, stamp // 1000), (frame, ctx)) safety.safety_tick() self.assertTrue(previous.canValid, msg=ctx) self.assertTrue(safety.safety_config_valid(), msg=ctx) self.assertFalse(safety.get_controls_allowed(), msg=ctx) for tick in range(146): now = 1_000_000_000 + tick * 10_000_000 brake = any(start <= tick < start + 12 for start in (20, 60, 100)) fault = tick == 138 main = tick not in (140, 141) ctx.update(phase='scenario', tick=tick, brake=brake, fault=fault, main=main) _, packets = feed_car(SimpleNamespace(update=lambda _: None), packer, now, counter=tick % 4, active=False, speed=8., camera=True) packets = [frame for frame in packets if frame[0] not in (0xC9, 0x1C4, 0xBE, 0x184)] packets += [packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 1}), packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0}), packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)}), packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 0}), packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 30 if brake else 0})] cs = ci.update([(now, packets)]) self.assertTrue(cs.canValid, msg=ctx) self.assertEqual(cs.brakePressed, brake, msg=ctx) self.assertFalse(cs.cruiseState.enabled, msg=ctx) self.assertEqual(cs.cruiseState.available, main, msg=ctx) for frame in packets: self.assertTrue(native('rx', frame, now // 1000), (frame, ctx)) safety.safety_tick() # The native test heartbeat isolates transport negotiation. safety.set_aol_test_heartbeat(True) events = car_events.update(cs, previous, structs.CarControl()) if cs.brakePressed and (not previous.brakePressed or not cs.standstill): events.add(EventName.pedalPressed) if fault: events.add(EventName.controlsMismatch) ctx['events'] = [int(event) for event in events.names] self.assertEqual(EventName.pedalPressed in events.names, brake, msg=ctx) self.assertEqual(events.contains(ET.IMMEDIATE_DISABLE), fault, msg=ctx) onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now) onroad.onroadEvents = events.to_msg() card_onroad = onroad.as_reader() if fault or previous_sd_onroad is None else previous_sd_onroad selected.sm.update_msgs(now / 1e9, [card_onroad]) disarm = selected.aol_disarming_fault(cs, int(card_onroad.logMonoTime), now) self.assertEqual(disarm, tick in (136, 138, 139), msg=ctx) selected.aol_card_intent.update(cs, fault_active=disarm, now_ns=now, standard_enabled=False) expected_latch = main and not disarm self.assertEqual(selected.aol_card_intent.allowed_latch, expected_latch, msg=ctx) self.assertEqual(selected.aol_card_intent._main_cycle_required, disarm, msg=ctx) allowed, pause_lat, pause_long = selected.aol_card_intent.output(cs) intent_value = IntentState('card', tick + 1, now, now, now + 200_000_000, allowed, pause_lat, pause_long, selected.aol_qualified, selected.aol_card_intent.allowed_latch) intent = decode_intent(encode_intent(intent_value)) self.assertIsNotNone(intent, msg=ctx) self.assertEqual(intent.allowedLatch, expected_latch, msg=ctx) self.assertFalse(intent.pauseLateral, msg=ctx) self.assertFalse(intent.pauseLongitudinal, msg=ctx) no_entry = aol_no_entry(events.names, cs, paddle_only_cancel=False, allow_below_engage_speed=True) immediate_disable = events.contains(ET.IMMEDIATE_DISABLE) wanted = decide_axes(native=None, standard_lateral=False, standard_longitudinal=False, intent=intent, car_state=cs, initialized=True, model_ready=True, no_entry=no_entry, immediate_disable=immediate_disable, dm_lockout=False, pause_brake_mps=10.) expected_lat = not brake and expected_latch self.assertEqual(wanted.desired_lateral, expected_lat, msg=ctx) self.assertFalse(wanted.desired_longitudinal, msg=ctx) safety.aol_set_host_request(int(wanted.desired_lateral)) mask = safety.aol_get_permission_mask() ctx['native_permission_mask'] = int(mask) self.assertEqual(mask, int(expected_lat), msg=ctx) self.assertFalse(safety.get_controls_allowed(), msg=ctx) receipt_state = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), 0xD114, bool(mask & 1), False, wanted.desired_lateral, False, 'panda', 'volt-brake') receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) transport = replace(receipt_state, validUntilMonoTime=now - 1) if tick == 135 else receipt_state receipt.aolSafetyWire = encode_safety(transport) intent_message = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now) intent_message.aolIntentWire = encode_intent(intent_value) model = messaging.new_message('modelV2', valid=True, logMonoTime=now) calibration = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=now) calibration.extrinsicsCalibration.calStatus = 'calibrated' monitoring = messaging.new_message('driverMonitoringState', valid=True, logMonoTime=now) sd.sm.update_msgs(now / 1e9, [intent_message.as_reader(), receipt.as_reader(), model.as_reader(), calibration.as_reader(), monitoring.as_reader()]) sd.aol_car_state_log_ns = now sd.conditional_car_state_valid = cs.canValid and not cs.canTimeout sd.events = events with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \ patch.object(sd, 'data_sample', return_value=cs), patch.object(sd, 'update_events'), \ patch.object(sd, 'update_alerts'), patch.object(sd, 'update_conditional_mode'), \ patch.object(sd, 'publish_selfdriveState'): sd.step() actual_lat = expected_lat and tick != 135 actual_onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now) actual_onroad.onroadEvents = sd.events.to_msg() previous_sd_onroad = actual_onroad.as_reader() self.assertFalse(sd.enabled or sd.active, msg=ctx) self.assertEqual(sd.aol_axis_decision.lateral_active, actual_lat, msg=ctx) self.assertFalse(sd.aol_axis_decision.longitudinal_active, msg=ctx) if tick == 135: self.assertTrue(sd.aol_authority_lost, msg=ctx) self.assertIn(EventName.controlsMismatch, sd.events.names, msg=ctx) self.assertFalse(sd.aol_axis_decision.native_acknowledged, msg=ctx) if tick == 136: self.assertFalse(sd.aol_axis_decision.lateral_active, msg=ctx) self.assertFalse(selected.aol_card_intent.allowed_latch, msg=ctx) if tick == 137: self.assertTrue(selected.aol_card_intent.allowed_latch, msg=ctx) self.assertTrue(sd.aol_axis_decision.lateral_active, msg=ctx) if tick == 142: self.assertTrue(sd.aol_axis_decision.lateral_active, msg=ctx) self.assertFalse(selected.aol_card_intent._main_cycle_required, msg=ctx) feed(controls, now, tick, active=sd.active, enabled=sd.enabled) axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) axis.aolAxisState.qualified = True axis.aolAxisState.nativeAcknowledged = sd.aol_axis_decision.native_acknowledged axis.aolAxisState.desiredLateral = sd.aol_axis_decision.desired_lateral axis.aolAxisState.desiredLongitudinal = sd.aol_axis_decision.desired_longitudinal axis.aolAxisState.lateralActive = sd.aol_axis_decision.lateral_active axis.aolAxisState.longitudinalActive = sd.aol_axis_decision.longitudinal_active axis.aolAxisState.sessionId = 'volt-brake' axis.aolAxisState.observedMonoTime = now axis.aolAxisState.validUntilMonoTime = now + 30_000_000 state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, _ = controls.state_control() self.assertEqual(command.latActive, actual_lat, msg=ctx) self.assertFalse(command.longActive, msg=ctx) command.actuators.torque = .02 if command.latActive else 0. _, messages = ci.apply(command.as_reader(), now + 2) for frame in messages: self.assertTrue(native('tx', frame, now // 1000 + 1), (frame, ctx)) if brake: paused += 1 if tick in (32, 72, 112): self.assertTrue(command.latActive, msg=ctx) resumed += 1 if tick == 134: # These are policy-unit boundaries; the three cycles use real 8m/s CAN. probe = cs.as_reader().as_builder() probe.brakePressed = True for speed in (9.99, 10., 10.01): probe.vEgo = speed self.assertEqual(decide_axes(native=receipt_state, standard_lateral=False, standard_longitudinal=False, intent=intent, car_state=probe, initialized=True, model_ready=True, no_entry=no_entry, immediate_disable=immediate_disable, dm_lockout=False, pause_brake_mps=10.).desired_lateral, speed >= 10., (speed, ctx)) previous = cs self.assertEqual(resumed, 3) self.assertEqual(paused, 36) finally: safety.set_alternative_experience(0) selected.vehicle_startup.close() def test_volt_ascm_transient_acc_real_parser_card_controls_native(self): self.exercise_volt_ascm_acc_intent() def test_volt_ascm_mixed_critical_acc_fault_requires_main_cycle(self): for critical in ('controlsMismatch', 'steerUnavailable', 'permanentEPS'): with self.subTest(critical=critical): self.exercise_volt_ascm_acc_intent(critical=critical) def exercise_volt_ascm_acc_intent(self, *, critical=None): from openpilot.cereal import log from openpilot.selfdrive.car.car_events import CarEvents from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.events import Events, EventName, ET from openpilot.starpilot.aol.runtime import AxisDecision from openpilot.starpilot.aol.wire import encode_intent from opendbc.can import CANParser from opendbc.car.gm.bolt_cc import button_bytes from opendbc.car.gm.values import AccState, CruiseButtons, GMFlags from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() for key in ('AlwaysOnLateral', 'AlphaLongitudinalEnabled', 'GMAutoHold', 'VoltOnePedalMode'): settings.put_bool(key, True, block=True) factory = ascm_params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, accelerator=True, radar=True) selected = self.card(factory, settings) cp, ci = selected.CP, selected.CI self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xD114) self.assertEqual(cp.alternativeExperience, 32) self.assertTrue(cp.openpilotLongitudinalControl) self.assertFalse(cp.pcmCruise) self.assertFalse(cp.flags & GMFlags.PEDAL_LONG) controls = Controls() self.assertEqual(controls.CP.to_dict(), cp.to_dict()) sd = SelfdriveD.__new__(SelfdriveD) sd.CP, sd.initialized, sd.aol_replay = cp, True, True sd.enabled = sd.active = False sd.aol_session_id, sd.aol_axis_decision = 'ascm-acc-fault', AxisDecision() sd.aol_authority_lost = False sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id) sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings sd.nostalgia_paddle_cancel = False sd.state_machine, sd.events = StateMachine(), Events() sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled sd.sm = messaging.SubMaster(['aolIntentWire', 'aolSafetyWire', 'modelV2', 'extrinsicsCalibration', 'driverMonitoringState']) car_events = CarEvents(cp) previous = structs.CarState() packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) output = CANParser(DBC[cp.carFingerprint][Bus.pt], [('ASCMGasRegenCmd', float('nan'))], 0) brake_output = CANParser(DBC[cp.carFingerprint][Bus.chassis], [('EBCMFrictionBrakeCmd', float('nan'))], 2) neutral_gas_seen = neutral_brake_seen = False safety = libsafety_py.libsafety safety.set_alternative_experience(32) self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xD114), 0) safety.init_tests() try: # The recorded window is already engaged: precede it with real physical SET release. for offset, counter, button in ((-20_000_000, 2, CruiseButtons.DECEL_SET), (-10_000_000, 3, CruiseButtons.UNPRESS)): prime_now = 102_289_000_000 + offset _, prime_packets = feed_car(SimpleNamespace(update=lambda _: None), packer, prime_now, counter=counter, active=True, speed=20., camera=True) prime_packets = [packet for packet in prime_packets if packet[0] not in (0x1C4, 0x1E1)] prime_packets += [packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': AccState.ACTIVE}), (0x1E1, button_bytes(button, counter), 0)] previous = ci.update([(prime_now, prime_packets)]) for packet in prime_packets: self.assertTrue(native('rx', packet, prime_now // 1000), packet) safety.safety_tick() self.assertTrue(safety.get_controls_allowed()) # Fault begins102.389s and clears102.469s: eight100Hz samples, no button/MAIN edge. for tick in range(36): now = 102_289_000_000 + tick * 10_000_000 fault = 10 <= tick < 18 main = tick not in (26, 27) ordinary = tick < 10 _, packets = feed_car(SimpleNamespace(update=lambda _: None), packer, now, counter=tick % 4, active=ordinary, speed=20., camera=True) packets = [packet for packet in packets if packet[0] not in (0x1C4, 0x3D1, 0x184)] packets += [packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': AccState.FAULTED if fault else AccState.ACTIVE if ordinary else 0}), packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)}), packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 3 if critical == 'permanentEPS' and fault else 2 if tick == 20 else 1}), packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0})] self.assertFalse(any(address == 0x201 for address, _, _ in packets)) cs = ci.update([(now, packets)]) self.assertTrue(cs.canValid, tick) self.assertEqual(cs.accFaulted, fault) self.assertEqual(cs.cruiseState.available, main) for packet in packets: self.assertTrue(native('rx', packet, now // 1000), (tick, packet)) safety.safety_tick() safety.set_aol_test_heartbeat(True) events = car_events.update(cs, previous, structs.CarControl()) if fault and critical in ('controlsMismatch', 'steerUnavailable'): events.add(getattr(EventName, critical)) self.assertEqual(EventName.accFaulted in events.names, fault) if fault: self.assertTrue(events.contains(ET.IMMEDIATE_DISABLE)) onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now) onroad.onroadEvents = events.to_msg() selected.sm.update_msgs(now / 1e9, [onroad.as_reader()]) disarm = selected.aol_disarming_fault(cs, now, now) self.assertEqual(disarm, bool(fault and critical), (tick, critical)) selected.aol_card_intent.update(cs, fault_active=disarm, now_ns=now, standard_enabled=ordinary) expected_latch = main and not disarm self.assertEqual(selected.aol_card_intent.allowed_latch, expected_latch, (tick, critical)) allowed, pause_lat, pause_long = selected.aol_card_intent.output(cs) intent_state = IntentState('card', tick + 1, now, now, now + 30_000_000, allowed, pause_lat, pause_long, True, True) desired = decide_axes(standard_lateral=ordinary, standard_longitudinal=ordinary, intent=intent_state, native=None, car_state=cs, initialized=True, model_ready=True, no_entry=events.contains(ET.NO_ENTRY), immediate_disable=events.contains(ET.IMMEDIATE_DISABLE), dm_lockout=False, pause_brake_mps=5.) safety.aol_set_host_request(int(desired.desired_lateral) | (int(desired.desired_longitudinal) << 1)) mask = safety.aol_get_permission_mask() receipt_state = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), 0xD114, bool(mask & 1), bool(mask & 2), desired.desired_lateral, desired.desired_longitudinal, 'panda', 'ascm-acc-fault') intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now) intent.aolIntentWire = encode_intent(intent_state) receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) receipt.aolSafetyWire = encode_safety(receipt_state) model = messaging.new_message('modelV2', valid=True, logMonoTime=now) calibration = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=now) calibration.extrinsicsCalibration.calStatus = 'calibrated' monitoring = messaging.new_message('driverMonitoringState', valid=True, logMonoTime=now) sd.sm.update_msgs(now / 1e9, [intent.as_reader(), receipt.as_reader(), model.as_reader(), calibration.as_reader(), monitoring.as_reader()]) sd.aol_car_state_log_ns = now sd.conditional_car_state_valid = cs.canValid and not cs.canTimeout sd.events = events with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \ patch.object(sd, 'data_sample', return_value=cs), patch.object(sd, 'update_events'), \ patch.object(sd, 'update_alerts'), patch.object(sd, 'update_conditional_mode'), \ patch.object(sd, 'publish_selfdriveState'): sd.step() expected_lat = expected_latch and not fault and tick != 20 self.assertEqual(sd.aol_axis_decision.lateral_active, expected_lat, (tick, critical)) self.assertEqual(sd.aol_axis_decision.longitudinal_active, ordinary, (tick, critical)) if tick >= 10: self.assertFalse(sd.enabled) # Clearing ACC fault never silently resumes longitudinal. feed(controls, now, tick, active=sd.active, enabled=sd.enabled) axis = messaging.new_message('aolAxisState', valid=tick != 23, logMonoTime=now) axis.aolAxisState.qualified = axis.aolAxisState.nativeAcknowledged = True axis.aolAxisState.desiredLateral = sd.aol_axis_decision.desired_lateral axis.aolAxisState.desiredLongitudinal = sd.aol_axis_decision.desired_longitudinal axis.aolAxisState.lateralActive = sd.aol_axis_decision.lateral_active axis.aolAxisState.longitudinalActive = sd.aol_axis_decision.longitudinal_active axis.aolAxisState.sessionId = 'ascm-acc-fault' axis.aolAxisState.observedMonoTime = now axis.aolAxisState.validUntilMonoTime = now + 30_000_000 state = messaging.new_message('carState', valid=True, logMonoTime=now) state.carState = cs controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) command, _ = controls.state_control() self.assertEqual(command.latActive, expected_lat and tick != 23, (tick, critical)) self.assertEqual(command.longActive, ordinary, (tick, critical)) command.actuators.torque = .02 if command.latActive else 0. _, messages = ci.apply(command.as_reader(), now + 2) output.update([(now, messages)]) brake_output.update([(now, messages)]) if fault: if any(address == 0x2CB for address, _, _ in messages): self.assertEqual(output.vl['ASCMGasRegenCmd']['GasRegenCmd'], -650.) self.assertFalse(output.vl['ASCMGasRegenCmd']['GasRegenCmdActive']) neutral_gas_seen = True if any(address == 0x315 for address, _, _ in messages): self.assertEqual(brake_output.vl['EBCMFrictionBrakeCmd']['FrictionBrakeCmd'], 0) neutral_brake_seen = True for message in messages: self.assertTrue(native('tx', message, now // 1000 + 1), (tick, message)) previous = cs self.assertTrue(neutral_gas_seen) self.assertTrue(neutral_brake_seen) stale_now = now + 2_000_000_000 for _ in range(10): stale_now += 10_000_000 stale_cs = ci.update([(stale_now, [])]) self.assertFalse(stale_cs.canValid) safety.set_timer(stale_now // 1000) safety.safety_tick() stale_intent = IntentState('card', 100, stale_now, stale_now, stale_now + 30_000_000, selected.aol_card_intent.allowed_latch, False, False, True, True) stale_decision = decide_axes(standard_lateral=False, standard_longitudinal=False, intent=stale_intent, native=receipt_state, car_state=stale_cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=5.) self.assertFalse(stale_decision.lateral_active) self.assertFalse(stale_decision.longitudinal_active) safety.aol_set_host_request(0) feed(controls, stale_now, 100, active=False, enabled=False, can_valid=False, can_timeout=True) # Last axis/receipt are deliberately not restamped: their exact bytes have expired. stale_state = messaging.new_message('carState', valid=False, logMonoTime=stale_now) stale_state.carState = stale_cs controls.sm.update_msgs(stale_now / 1e9, [stale_state.as_reader()]) withdrawn, _ = controls.state_control() self.assertFalse(withdrawn.latActive) self.assertFalse(withdrawn.longActive) _, neutral_messages = ci.apply(withdrawn.as_reader(), stale_now + 2) for message in neutral_messages: self.assertTrue(native('tx', message, stale_now // 1000 + 1), message) finally: safety.set_alternative_experience(0) selected.vehicle_startup.close() def test_transient_acc_intent_exception_stays_inside_nonpedal_ascm_long(self): from openpilot.selfdrive.selfdrived.events import Events, EventName from opendbc.car.gm.tests.test_volt_transitions import volt_ascm_pedal_params cases = (ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=False, sascm=True, accelerator=True, radar=True), volt_ascm_pedal_params(be=True, radar=True, sascm=True, alpha=True), ordinary_params(CAR.GMC_ACADIA_ASCM, alpha=True, sascm=True, accelerator=True, radar=True)) for factory in cases: with self.subTest(identity=factory.carFingerprint, flags=factory.flags), OpenpilotPrefix(), \ patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() settings.put_bool('AlwaysOnLateral', True, block=True) selected = self.card(factory, settings) try: cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20., accFaulted=True) cs.cruiseState.available = True events = Events() events.add(EventName.accFaulted) onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=1_000_000_000) onroad.onroadEvents = events.to_msg() selected.sm.update_msgs(1., [onroad.as_reader()]) self.assertTrue(selected.aol_disarming_fault(cs, 1_000_000_000, 1_000_000_000)) selected.aol_card_intent.update(cs, fault_active=True) cs.accFaulted = False selected.aol_card_intent.update(cs, fault_active=False) self.assertTrue(selected.aol_card_intent.allowed_latch) finally: selected.vehicle_startup.close() def test_sdgm_optional_saved_selection_actual_card_controls_cap(self): from opendbc.car.gm.values import CarControllerParams, is_gm_auto_hold, is_volt_one_pedal, volt_sdgm_accepted_accel_max from openpilot.starpilot.saved_source import read_saved for c9 in (False, True): for held, one_pedal, word in ((True, False, 0x5487 if c9 else 0x5087), (False, True, 0xD108 if c9 else 0xD107), (True, True, 0xD118 if c9 else 0xD117)): with self.subTest(word=hex(word)), OpenpilotPrefix(), \ patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): settings = Params() for key, value in (('AlwaysOnLateral', True), ('AlphaLongitudinalEnabled', True), ('DisableOpenpilotLongitudinal', False), ('SafeMode', False), ('GMAutoHold', held), ('VoltOnePedalMode', one_pedal)): settings.put_bool(key, value, block=True) saved = {key: read_saved(settings, key, 8) for key in ('GMAutoHold', 'VoltOnePedalMode')} selected = self.card(sdgm_params(brake_c9=c9), settings) try: cp = selected.CP self.assertEqual(cp.safetyConfigs[0].safetyParam, word) self.assertTrue(cp.openpilotLongitudinalControl) self.assertFalse(cp.pcmCruise) self.assertEqual(cp.alternativeExperience, 32) self.assertTrue(is_gm_auto_hold(cp)) self.assertEqual(is_volt_one_pedal(cp), one_pedal) self.assertEqual(selected.CI.CC.gm_auto_hold, held) self.assertEqual(selected.CI.CC.volt_one_pedal, one_pedal) self.assertEqual(CarControllerParams(cp).MAX_GAS, 2041) self.assertEqual(selected.CI.CC.params.MAX_GAS, 2041) controls = Controls() self.assertEqual(controls.CP.to_dict(), cp.to_dict()) upper = volt_sdgm_accepted_accel_max(cp, 20.) limits = controls.CI.get_pid_accel_limits(cp, 20., 20.) self.assertEqual(limits[1], upper) cs = structs.CarState(vEgo=20., canValid=True, gearShifter='drive') controls.LoC.update(True, cs, upper + 10., False, limits) self.assertEqual(controls.LoC.pid.pos_limit, upper) self.assertLessEqual(controls.LoC.last_output_accel, upper) self.assertEqual({key: read_saved(settings, key, 8) for key in saved}, saved) finally: selected.vehicle_startup.close()