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

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

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

482 lines
21 KiB
Python

import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
from opendbc.car import gen_empty_fingerprint
from opendbc.car.can_definitions import CanData
from opendbc.car.gm.interface import CarInterface as GMInterface
from opendbc.car.gm.values import CAR as GMCar, GMFlags
from openpilot.cereal import messaging
from openpilot.selfdrive.car.card import Car, EventName
from openpilot.selfdrive.pandad import can_capnp_to_list
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
class CardInitLifecycleTest(unittest.TestCase):
def defer_radar_fixture(self, card):
eager = card.state_update
batch = []
def receive(*, defer_radar=False):
self.assertTrue(defer_radar)
card._deferred_radar_can = batch
return eager()
def radar_update(value):
self.assertIs(value, batch)
return None
card.state_update = Mock(side_effect=receive)
card.RI = SimpleNamespace(update=Mock(side_effect=radar_update))
self.addCleanup(lambda: self.assertEqual(card.RI.update.call_count, card.state_update.call_count))
def test_bolt_pedal_waits_for_control_then_initializes_once_before_output(self):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x201] = 6
cp = GMInterface.get_params(GMCar.CHEVROLET_BOLT_ACC_2022_2023_PEDAL,
fingerprint, [], False, False, False)
self.assertTrue(cp.flags & GMFlags.PEDAL_LONG.value)
order = []
card = Car.__new__(Car)
card.CP = cp
card.vehicle_startup = VehicleStartupOwner()
card.ci_initialized = False
card.initialized_prev = False
card.aol_replay = False
card.ioniq6_long_prearmed = False
card.ioniq6_long_selected = False
card.can_callbacks = (lambda wait_for_one=False: [], lambda frames: None)
def apply(*_):
order.append('apply')
return object(), [CanData(0x200, b'\x00' * 8, 0)]
fake_ci = SimpleNamespace(
CS=GMInterface(cp).CS,
CC=SimpleNamespace(),
init=Mock(side_effect=lambda *_: order.append('init')),
apply=Mock(side_effect=apply),
)
self.enterContext(patch.object(card, 'CI', fake_ci, create=True))
self.enterContext(patch.object(card, 'params', SimpleNamespace(put_bool=lambda key, value: order.append((key, value))), create=True))
sent = []
def publish(service, packet):
assert service == 'sendcan'
sent.append(packet)
order.append(('sendcan', tuple(can_capnp_to_list([packet], msgtype='sendcan')[0][1]),
bool(messaging.log_from_bytes(packet).valid)))
self.enterContext(patch.object(card, 'pm', SimpleNamespace(send=publish), create=True))
card.state_publish = Mock(side_effect=lambda *_: order.append('carState'))
cs = SimpleNamespace(canValid=True, canTimeout=False)
card.state_update = Mock(return_value=(cs, None))
self.defer_radar_fixture(card)
control = object()
class SubMaster:
valid = {'carControl': False}
alive = {'carControl': False}
seen = {'onroadEvents': False, 'carControl': True}
logMonoTime = {}
recv_time = {}
events = []
def __getitem__(self, key):
return self.events if key == 'onroadEvents' else control
def all_alive(self, services):
return all(self.alive[name] for name in services)
sm = SubMaster()
self.enterContext(patch.object(card, 'sm', sm, create=True))
for _ in range(30):
card.step()
self.assertNotIn('init', order)
sm.seen['onroadEvents'] = True
sm.events = [SimpleNamespace(name=EventName.selfdriveInitializing)]
card.step()
self.assertNotIn('init', order)
sm.events = []
card.step() # Controls still have no live producer.
self.assertNotIn('init', order)
sm.valid['carControl'] = True
sm.alive['carControl'] = True
import time
sm.logMonoTime['carControl'] = time.monotonic_ns()
sm.recv_time['carControl'] = time.monotonic()
cs.canValid = False
card.step() # A live controller cannot initialize against invalid CAN.
self.assertNotIn('init', order)
cs.canValid = True
card.step()
self.assertEqual(order[-5:-1], ['carState', 'init', ('ControlsReady', True), 'apply'])
self.assertEqual(order[-1][0], 'sendcan')
self.assertEqual([(frame[0], frame[2]) for frame in order[-1][1]], [(0x200, 0)])
self.assertTrue(order[-1][2])
self.assertTrue(all(not can_capnp_to_list([packet], msgtype='sendcan')[0][1] for packet in sent[:-1]))
self.assertTrue(all(not messaging.log_from_bytes(packet).valid for packet in sent[:-1]))
card.step()
fake_ci.init.assert_called_once_with(cp, *card.can_callbacks)
self.assertEqual(order.count(('ControlsReady', True)), 1)
applied = fake_ci.apply.call_count
sent_before = len(sent)
sm.valid['carControl'] = False
card.step() # A live but invalid native command cannot renew CAN output.
self.assertEqual(fake_ci.apply.call_count, applied)
self.assertEqual(len(sent), sent_before)
sm.valid['carControl'] = True
card.step()
self.assertEqual(fake_ci.apply.call_count, applied + 1)
self.assertEqual(len(sent), sent_before + 1)
self.assertTrue(messaging.log_from_bytes(sent[-1]).valid)
fake_ci.init.assert_called_once_with(cp, *card.can_callbacks)
def test_volt_sdgm_inactive_startup_continues_until_initialized(self):
from opendbc.car import Bus, structs
from opendbc.can import CANPacker
from opendbc.car.gm.values import DBC
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x180] = 4
fingerprint[0][0x2FF] = 8
fingerprint[0][0xBE] = 6
cp = GMInterface.get_params(GMCar.CHEVROLET_VOLT_2019, fingerprint, [], True, False, False)
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x5007)
cp.alternativeExperience = 32
card = Car.__new__(Car)
card.CP = cp
card.CI = GMInterface(cp)
card.vehicle_startup = VehicleStartupOwner()
card.ci_initialized = False
card.initialized_prev = False
card.aol_replay = True
card.ioniq6_long_prearmed = False
card.ioniq6_long_selected = False
card.can_callbacks = (lambda wait_for_one=False: [], lambda _: None)
params = SimpleNamespace(put_bool=Mock())
self.enterContext(patch.object(card, 'params', params, create=True))
card.state_publish = Mock()
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
cs, _ = feed_camera(card.CI, packer, 22_000_000_000, active=False)
self.assertTrue(cs.canValid)
card.state_update = Mock(return_value=(cs, None))
self.defer_radar_fixture(card)
control = structs.CarControl.new_message().as_reader()
panda = SimpleNamespace(safetyModel=cp.safetyConfigs[0].safetyModel, safetyParam=0x5007,
alternativeExperience=cp.alternativeExperience, safetyRxChecksInvalid=True)
mono, boot = 20_000_000_000, 22_000_000_000
class SubMaster:
seen = dict.fromkeys(('carControl', 'pandaStates', 'onroadEvents'), True)
valid = dict.fromkeys(('carControl', 'pandaStates'), True)
alive = dict(valid)
logMonoTime = {'carControl': mono, 'pandaStates': boot}
recv_time = dict.fromkeys(('carControl', 'pandaStates'), mono / 1e9)
events = [SimpleNamespace(name=EventName.selfdriveInitializing)]
def __getitem__(self, key):
return {'carControl': control, 'pandaStates': [panda], 'onroadEvents': self.events}[key]
def all_alive(self, services):
return all(self.alive[name] for name in services)
sm = SubMaster()
self.enterContext(patch.object(card, 'sm', sm, create=True))
sent = []
self.enterContext(patch.object(card, 'publish_sendcan', lambda frames, valid=True: sent.extend(frames)))
card.learning_send_committed_mono_ns = 0
# These saved requests cannot acquire startup brake ownership.
card.CI.CC.gm_auto_hold = True
card.CI.CC.volt_one_pedal = True
with patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=mono), \
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=boot), \
patch.object(card.CI, 'apply', wraps=card.CI.apply) as apply, \
patch.object(card.CI, 'init', wraps=card.CI.init) as init:
self.assertFalse(card.startup_panda_configured())
self.assertTrue(card.startup_panda_configured(inactive_keepalive=True))
for _ in range(30):
card.step()
self.assertEqual(apply.call_count, 30)
init.assert_called_once()
params.put_bool.assert_called_once_with('ControlsReady', True)
self.assertEqual(card.CI.CC.gm_auto_hold_state.drive_ns, 0)
self.assertEqual(card.CI.CC.apply_brake, 0)
for call in apply.call_args_list:
cc, timestamp = call.args
self.assertEqual(timestamp, boot)
self.assertTrue(call.kwargs['startup_keepalive'])
self.assertFalse(cc.enabled or cc.latActive or cc.longActive)
self.assertEqual((cc.actuators.torque, cc.actuators.accel), (0., 0.))
self.assertTrue(sent)
for address, data, _ in sent:
if address == 0x180:
self.assertEqual(data[0] & 15, 0)
self.assertEqual(data[1], 0)
elif address == 0x315:
self.assertEqual(((data[0] & 15) << 8) | data[1], 0)
elif address == 0x2CB:
self.assertEqual(data[0] & 1, 0)
panda.safetyParam = 0x5087
card.step()
self.assertEqual(apply.call_count, 30)
panda.safetyParam = 0x5007
sm.logMonoTime['pandaStates'] = boot - 300_000_001
card.step()
self.assertEqual(apply.call_count, 30)
sm.logMonoTime['pandaStates'] = boot
active = structs.CarControl.new_message(latActive=True)
control = active.as_reader()
card.step()
self.assertEqual(apply.call_count, 30)
control = structs.CarControl.new_message().as_reader()
sm.events = []
card.step()
self.assertEqual(apply.call_count, 31)
self.assertEqual(apply.call_args.args[1], boot)
self.assertFalse(apply.call_args.kwargs)
def test_volt_ascm_inactive_startup_has_no_six_second_handoff_gap(self):
self._assert_volt_ascm_inactive_startup(alpha=True)
def test_volt_stock_ascm_inactive_startup_has_no_six_second_steering_gap(self):
self._assert_volt_ascm_inactive_startup(alpha=False)
def _assert_volt_ascm_inactive_startup(self, *, alpha):
from opendbc.car import Bus, structs
from opendbc.can import CANPacker
from opendbc.car.gm.values import DBC
from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
cp = ascm_params(GMCar.CHEVROLET_VOLT_ASCM, sascm=True, alpha=alpha, accelerator=True, radar=True)
word = 0xD114 if alpha else 0xA05
if alpha:
cp.safetyConfigs[0].safetyParam = word
self.assertEqual(cp.safetyConfigs[0].safetyParam, word)
cadence = ((0x180, 110_000_000), (0x2CB, 50_000_000), (0x315, 50_000_000)) if alpha else ((0x180, 110_000_000),)
cp.alternativeExperience = 32
card = Car.__new__(Car)
card.CP = cp
card.CI = GMInterface(cp)
card.vehicle_startup = VehicleStartupOwner()
card.ci_initialized = False
card.initialized_prev = False
card.aol_replay = True
card.ioniq6_long_prearmed = False
card.ioniq6_long_selected = False
card.can_callbacks = (lambda wait_for_one=False: [], lambda _: None)
params = SimpleNamespace(put_bool=Mock())
self.enterContext(patch.object(card, 'params', params, create=True))
card.state_publish = Mock()
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
cs, _ = feed_camera(card.CI, packer, 22_000_000_000, active=False)
self.assertTrue(cs.canValid)
counter = [1]
def state_update():
state, _ = feed_camera(card.CI, packer, boot, counter=counter[0] % 4, speed=0., active=False)
counter[0] += 1
self.assertTrue(state.canValid)
return state, None
self.enterContext(patch.object(card, 'state_update', state_update))
self.defer_radar_fixture(card)
control = structs.CarControl.new_message().as_reader()
panda = SimpleNamespace(safetyModel=cp.safetyConfigs[0].safetyModel, safetyParam=word,
alternativeExperience=cp.alternativeExperience, safetyRxChecksInvalid=True)
mono, boot = 20_000_000_000, 22_000_000_000
class SubMaster:
seen = dict.fromkeys(('carControl', 'pandaStates', 'onroadEvents'), True)
valid = dict.fromkeys(('carControl', 'pandaStates'), True)
alive = dict(valid)
logMonoTime = {'carControl': mono, 'pandaStates': boot}
recv_time = dict.fromkeys(('carControl', 'pandaStates'), mono / 1e9)
events = [SimpleNamespace(name=EventName.selfdriveInitializing)]
def __getitem__(self, key):
return {'carControl': control, 'pandaStates': [panda], 'onroadEvents': self.events}[key]
def all_alive(self, services):
return all(self.alive[name] for name in services)
sm = SubMaster()
self.enterContext(patch.object(card, 'sm', sm, create=True))
sent = []
timestamps = {}
envelopes = []
def capture_packet(service, packet):
self.assertEqual(service, 'sendcan')
event = messaging.log_from_bytes(packet)
self.assertTrue(event.valid)
self.assertEqual(event.logMonoTime, boot)
# Match pandad's unsigned BOOTTIME subtraction and strict one-second bound.
self.assertLess((boot - int(event.logMonoTime)) & ((1 << 64) - 1), 1_000_000_000)
envelopes.append(event.logMonoTime)
self.enterContext(patch.object(card, 'pm', SimpleNamespace(send=capture_packet), create=True))
def publish(frames, valid=True):
Car.publish_sendcan(card, frames, valid=valid)
self.assertTrue(valid)
sent.extend(frames)
for address, _, _ in frames:
timestamps.setdefault(address, []).append(boot)
self.enterContext(patch.object(card, 'publish_sendcan', publish))
# These saved requests cannot acquire startup brake ownership.
card.CI.CC.gm_auto_hold = True
card.CI.CC.volt_one_pedal = True
with patch('openpilot.selfdrive.car.card.time.monotonic', side_effect=lambda: mono / 1e9), \
patch('openpilot.selfdrive.car.card.time.monotonic_ns', side_effect=lambda: mono), \
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', side_effect=lambda _clock: boot), \
patch.object(card.CI, 'apply', wraps=card.CI.apply) as apply, \
patch.object(card.CI, 'init', wraps=card.CI.init) as init:
self.assertFalse(card.startup_panda_configured())
self.assertTrue(card.startup_panda_configured(inactive_keepalive=True))
for _ in range(601):
sm.logMonoTime.update(carControl=mono, pandaStates=boot)
sm.recv_time.update(carControl=mono / 1e9, pandaStates=mono / 1e9)
card.step()
mono += 10_000_000
boot += 10_000_000
sm.logMonoTime.update(carControl=mono, pandaStates=boot)
sm.recv_time.update(carControl=mono / 1e9, pandaStates=mono / 1e9)
self.assertEqual(apply.call_count, 601)
init.assert_called_once()
params.put_bool.assert_called_once_with('ControlsReady', True)
self.assertEqual(card.CI.CC.gm_auto_hold_state.drive_ns, 0)
self.assertEqual(card.CI.CC.apply_brake, 0)
for call in apply.call_args_list:
cc, timestamp = call.args
self.assertGreaterEqual(timestamp, 22_000_000_000)
self.assertLessEqual(timestamp, 28_000_000_000)
self.assertTrue(call.kwargs['startup_keepalive'])
self.assertFalse(cc.enabled or cc.latActive or cc.longActive)
self.assertEqual((cc.actuators.torque, cc.actuators.accel), (0., 0.))
for address, max_gap in cadence:
times = timestamps[address]
self.assertLessEqual(times[0], 22_100_000_000)
self.assertGreaterEqual(times[-1], 27_900_000_000)
self.assertLessEqual(max(b - a for a, b in zip(times, times[1:], strict=False)), max_gap)
self.assertTrue(sent)
if not alpha:
self.assertFalse(any(address in (0x2CB, 0x315) for address, _, _ in sent))
self.assertTrue(card.CP.pcmCruise)
self.assertFalse(card.CP.openpilotLongitudinalControl)
for address, data, _ in sent:
if address == 0x180:
self.assertEqual(data[0] & 15, 0)
self.assertEqual(data[1], 0)
elif address == 0x315:
self.assertEqual(((data[0] & 15) << 8) | data[1], 0)
elif address == 0x2CB:
self.assertEqual(data[0] & 1, 0)
self.assertEqual(data[1] & 0x20, 0)
self.assertEqual((((data[1] & 7) << 16) | (data[2] << 8) | data[3]) * .125 - 22534, -650)
panda.safetyParam = word ^ 1
card.step()
self.assertEqual(apply.call_count, 601)
panda.safetyParam = word
sm.logMonoTime['pandaStates'] = boot - 300_000_001
card.step()
self.assertEqual(apply.call_count, 601)
sm.logMonoTime['pandaStates'] = boot
sm.valid['carControl'] = False
card.step()
self.assertEqual(apply.call_count, 601)
sm.valid['carControl'] = True
sm.logMonoTime['carControl'] = mono - 150_000_001
card.step()
self.assertEqual(apply.call_count, 601)
sm.logMonoTime['carControl'] = mono
active = structs.CarControl.new_message(latActive=True)
control = active.as_reader()
card.step()
self.assertEqual(apply.call_count, 601)
control = structs.CarControl.new_message().as_reader()
sm.events = []
# The physical CAN clock remains continuous after inactive startup.
for _ in range(10):
sm.logMonoTime.update(carControl=mono, pandaStates=boot)
sm.recv_time.update(carControl=mono / 1e9, pandaStates=mono / 1e9)
card.step()
self.assertEqual(apply.call_args.args[1], boot)
self.assertFalse(apply.call_args.kwargs)
mono += 10_000_000
boot += 10_000_000
self.assertEqual(apply.call_count, 611)
self.assertEqual(len(envelopes), 611)
self.assertEqual(envelopes, [call.args[1] for call in apply.call_args_list])
self.assertGreaterEqual((boot - mono) & ((1 << 64) - 1), 1_000_000_000)
for before, after in zip(apply.call_args_list, apply.call_args_list[1:], strict=False):
self.assertEqual(after.args[1] - before.args[1], 10_000_000)
for address, max_gap in cadence:
times = timestamps[address]
self.assertGreaterEqual(times[-1], 28_060_000_000)
self.assertLessEqual(max(b - a for a, b in zip(times, times[1:], strict=False)), max_gap)
def test_volt_startup_selector_denies_other_profiles_and_permissions(self):
from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
card = Car.__new__(Car)
self.assertFalse(card.volt_startup_keepalive())
self.assertFalse(card.volt_sdgm_startup_keepalive())
for cp in (None, SimpleNamespace(safetyConfigs=[]), SimpleNamespace(brand='hyundai'),
SimpleNamespace(brand='toyota', alternativeExperience=32, flags=0,
carFingerprint=GMCar.CHEVROLET_VOLT_ASCM)):
with self.subTest(cp=cp):
card.CP = cp
self.assertFalse(card.volt_startup_keepalive())
self.assertFalse(card.volt_sdgm_startup_keepalive())
for radar in (False, True):
for accelerator in (False, True):
cp = ascm_params(GMCar.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True,
accelerator=accelerator, radar=radar)
cp.alternativeExperience = 32
card.CP = cp
self.assertTrue(card.volt_startup_keepalive())
for field, value in (('alternativeExperience', 0), ('openpilotLongitudinalControl', False),
('pcmCruise', True), ('passive', True), ('dashcamOnly', True),
('flags', int(cp.flags) | int(GMFlags.PEDAL_LONG))):
saved = getattr(cp, field)
setattr(cp, field, value)
self.assertFalse(card.volt_startup_keepalive())
setattr(cp, field, saved)
for radar in (False, True):
for accelerator in (False, True):
cp = ascm_params(GMCar.CHEVROLET_VOLT_ASCM, sascm=True, alpha=False,
accelerator=accelerator, radar=radar)
cp.alternativeExperience = 32
card.CP = cp
self.assertTrue(card.volt_startup_keepalive())
for field, value in (('alternativeExperience', 0), ('openpilotLongitudinalControl', True),
('pcmCruise', False), ('passive', True), ('dashcamOnly', True),
('radarUnavailable', not cp.radarUnavailable),
('flags', int(cp.flags) | int(GMFlags.PEDAL_LONG))):
saved = getattr(cp, field)
setattr(cp, field, value)
self.assertFalse(card.volt_startup_keepalive())
setattr(cp, field, saved)
word = cp.safetyConfigs[0].safetyParam
cp.safetyConfigs[0].safetyParam = word | 0x4000
self.assertFalse(card.volt_startup_keepalive())
cp.safetyConfigs[0].safetyParam = word
cc = GMInterface.get_params(GMCar.CHEVROLET_VOLT_CC, gen_empty_fingerprint(), [], True, False, False)
for cp in (cc, camera_params(), GMInterface.get_params(GMCar.CHEVROLET_BOLT_ACC_2022_2023_PEDAL,
gen_empty_fingerprint(), [], True, False, False)):
cp.alternativeExperience = 32
card.CP = cp
self.assertFalse(card.volt_startup_keepalive())
if __name__ == '__main__':
unittest.main()