Files
StarPilot/openpilot/starpilot/conditional_mode/tests/test_projection.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

1030 lines
50 KiB
Python

"""Serialized current-event and clock-domain tests for the inactive projector."""
from types import SimpleNamespace
from dataclasses import replace
import math
import unittest
from openpilot.cereal import messaging
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
from openpilot.starpilot.conditional_mode.projection import ConditionalOwnerContext, ObservedBool, ObservedFloat, SERVICES, SceneProjector
from openpilot.starpilot.conditional_mode.preferences import CEMOptions
from openpilot.starpilot.conditional_mode.policy import ConditionalModePolicy, ManualIntent, ModeChoice, ModeSettings
MONO = 100_000_000_000
BOOT = MONO + 2_000_000_000
class FakeSubMaster:
def __init__(self, payloads, stamp):
self.payloads = payloads
self.logMonoTime = dict.fromkeys(SERVICES, stamp)
self.recv_time = dict.fromkeys(SERVICES, stamp / 1e9)
self.seen = dict.fromkeys(SERVICES, True)
self.alive = dict.fromkeys(SERVICES, True)
self.valid = dict.fromkeys(SERVICES, True)
def __getitem__(self, service):
return self.payloads[service]
def stamp(self, stamp):
for service in SERVICES:
self.logMonoTime[service] = stamp
self.recv_time[service] = stamp / 1e9
def serialized_scene(
stamp=BOOT, *, origin=0.0, later_negative=False, speed=15.0, horizon=192.0, lead_present=True, lead_distance=30.0,
lead_speed=12.0, lead_relative=None, lead_accel=-0.1, standstill=False, plan_should_stop=False, plan_allow_throttle=True,
left_blinker=False, right_blinker=False, left_edge=-8.0, gas_pressed=False, experimental_mode=False,
steering_angle=0.0,
):
messages = {name: messaging.new_message(name, valid=True) for name in SERVICES}
car = messages['carState'].carState
car.vEgo = speed
car.vCruise = 72.0
car.canValid = True
car.canTimeout = False
car.leftBlinker = left_blinker
car.rightBlinker = right_blinker
car.standstill = standstill
car.steeringAngleDeg = steering_angle
car.gasPressed = gas_pressed
messages['controlsState'].controlsState.curvature = 0.008
messages['carControl'].carControl.longActive = True
messages['carControl'].carControl.enabled = True
messages['carControl'].carControl.latActive = False
messages['selfdriveState'].selfdriveState.enabled = True
messages['selfdriveState'].selfdriveState.experimentalMode = experimental_mode
lead = messages['radarState'].radarState.leadOne
lead.present = lead_present
lead.radar = True
lead.dRel = lead_distance
lead.vLead = lead_speed
lead.aLeadK = lead_accel
lead.vRel = lead_speed - speed if lead_relative is None else lead_relative
lead.modelProb = 0.95
messages['longitudinalPlan'].longitudinalPlan.shouldStop = plan_should_stop
messages['longitudinalPlan'].longitudinalPlan.allowThrottle = plan_allow_throttle
model = messages['modelV2'].modelV2
model.timestampEof = stamp
path_x = [horizon * i / 32 for i in range(33)]
path_x[0] = origin
if later_negative:
path_x[5] = -1e-5
model.position.x = path_x
model.position.y = [0.0] * 33
model.orientationRate.z = [0.15] * 33
model.orientationRate.t = [float(i) / 10 for i in range(33)]
model.velocity.x = [speed] * 33
model.action.shouldStop = True
lane_x = [float(index * 5) for index in range(33)]
for lane, y in zip(model.init('laneLines', 4), (-5.4, -1.8, 1.8, 5.4), strict=True):
lane.x = lane_x
lane.y = [y] * 33
for edge, y in zip(model.init('roadEdges', 2), (left_edge, 8.0), strict=True):
edge.x = lane_x
edge.y = [y] * 33
return {name: getattr(messaging.log_from_bytes(message.to_bytes()), name) for name, message in messages.items()}
CP = SimpleNamespace(openpilotLongitudinalControl=True, pcmCruise=False, carFingerprint='HYUNDAI_IONIQ_6')
def owner_context(stamp: int, *, stop_time=8.0, slower=True, stopped=False):
def flag(value: bool) -> ObservedBool:
return ObservedBool(value, stamp)
return ConditionalOwnerContext(
traffic_mode=flag(False),
stop_sign_confirmed=flag(False),
forcing_stop=flag(False),
dashboard_stop_sign=flag(False),
pedal_override=flag(False),
model_stop_time_s=ObservedFloat(stop_time, stamp),
slower_option=flag(slower),
stopped_option=flag(stopped),
previous_experimental=flag(False),
committed_turn_scene=flag(False),
red_light=flag(False),
plan_forcing_stop=flag(False),
)
class TestSceneProjector(unittest.TestCase):
def test_standstill_filter_undershoot_requires_fresh_physical_zero_speed(self):
cases = ((-1.401298464324817e-45, True, 0., True),
(-0.0008391447481699288, True, 0., True),
(-0.001509889611043036, True, 0., True),
(-0.01, True, 0., True),
(-0.0101, True, 0., False),
(-0.0015, False, 0., False),
(-0.0015, True, 0.01, False),
(-0.0015, True, -0.01, False),
(float('nan'), True, 0., False),
(float('inf'), True, 0., False),
(-0.0015, True, float('nan'), False))
for speed, standstill, raw_speed, accepted in cases:
with self.subTest(speed=speed, standstill=standstill, raw_speed=raw_speed):
projector = SceneProjector()
sm = FakeSubMaster(serialized_scene(speed=0., lead_present=False), MONO - 5_000_000)
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
event = MONO + 50_000_000
sm.stamp(event)
sm.payloads.update(serialized_scene(stamp=BOOT + 50_000_000, speed=speed,
standstill=standstill, lead_present=False))
car = messaging.new_message('carState', valid=True)
car.carState = sm.payloads['carState']
car.carState.vEgoRaw = raw_speed
sm.payloads['carState'] = messaging.log_from_bytes(car.to_bytes()).carState
result = projector.project(sm, CP, now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 51_000_000, sample_skew_ns=1000,
safe_mode=False, owner_context=owner_context(event))
if accepted:
self.assertIsNotNone(result.authority)
self.assertEqual(result.scene.speed_mps, 0.)
self.assertTrue(result.ready)
else:
self.assertIsNone(result.authority)
self.assertIsNone(result.scene.speed_mps)
sm.seen['carState'] = False
missing = projector.project(sm, CP, now_mono_ns=event + 2_000_000,
now_boot_ns=BOOT + 52_000_000, sample_skew_ns=1000,
safe_mode=False, owner_context=owner_context(event))
self.assertIsNone(missing.authority)
self.assertIsNone(missing.scene.speed_mps)
def test_fresh_committed_turn_retains_next_tick_slow_lead_filter_alpha(self):
sm = FakeSubMaster(serialized_scene(speed=20.0, lead_present=False), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
def frame(offset: int, *, speed: float, blinker: bool, angle: float):
stamp = MONO + offset
sm.stamp(stamp)
sm.payloads.update(serialized_scene(stamp=BOOT + offset, speed=speed, lead_present=False,
left_blinker=blinker, steering_angle=angle))
return projector.project(sm, CP, now_mono_ns=stamp + 1_000_000,
now_boot_ns=BOOT + offset + 1_000_000, sample_skew_ns=1000,
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=stamp,
owner_context=replace(owner_context(stamp), committed_turn_scene=None))
high = frame(10_000_000, speed=20.0, blinker=False, angle=0.0)
self.assertIsNotNone(high.slower_source_observed_mono_s)
high_alpha = projector.slower_lead_detector.filter.alpha
self.assertLess(high_alpha, 0.5)
turn = frame(60_000_000, speed=5.0, blinker=True, angle=50.0)
self.assertIsNotNone(turn.slower_source_observed_mono_s)
self.assertEqual(projector.slower_lead_detector.filter.alpha, high_alpha)
straight = frame(110_000_000, speed=5.0, blinker=False, angle=0.0)
self.assertIsNotNone(straight.slower_source_observed_mono_s)
self.assertEqual(projector.slower_lead_detector.filter.alpha, 1.0)
sm.stamp(MONO + 500_000_000)
stale = projector.project(sm, CP, now_mono_ns=MONO + 501_000_000,
now_boot_ns=BOOT + 501_000_000, sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 500_000_000,
owner_context=replace(owner_context(MONO + 500_000_000), committed_turn_scene=None))
self.assertIsNone(stale.scene.slow_lead_detected)
def test_brief_missing_car_does_not_erase_stop_hysteresis(self):
sm = FakeSubMaster(serialized_scene(lead_present=False), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(1, 35):
stamp = MONO + tick * 50_000_000
sm.stamp(stamp)
sm.payloads.update(serialized_scene(stamp=BOOT + tick * 50_000_000, horizon=30., lead_present=False))
if tick == 30:
sm.seen['carState'] = False
else:
sm.seen['carState'] = True
result = projector.project(sm, CP, now_mono_ns=stamp + 1_000_000,
now_boot_ns=BOOT + tick * 50_000_000 + 1_000_000, sample_skew_ns=1000,
safe_mode=False, selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=stamp, owner_context=owner_context(stamp))
if tick == 29:
self.assertTrue(result.scene.stop_light_detected)
held_filter = projector.stop_detector.light_filter.x
elif tick == 30:
self.assertIsNone(result.scene.stop_light_detected)
self.assertIsNone(result.authority)
self.assertEqual(projector.stop_detector.light_filter.x, held_filter)
elif tick > 30:
self.assertTrue(result.scene.stop_light_detected)
def test_actual_nidec_pcmcruise_with_system_long_is_capable(self):
sm = FakeSubMaster(serialized_scene(), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
nidec = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
self.assertTrue(nidec.openpilotLongitudinalControl)
self.assertTrue(nidec.pcmCruise)
result = projector.project(sm, nidec, now_mono_ns=MONO + 12_000_000,
now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=False)
self.assertTrue(result.authority.system_long_capable)
stock_acc = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC_BOSCH)
self.assertFalse(stock_acc.openpilotLongitudinalControl)
blocked = projector.project(sm, stock_acc, now_mono_ns=MONO + 13_000_000,
now_boot_ns=BOOT + 13_000_000, sample_skew_ns=1000, safe_mode=False)
self.assertFalse(blocked.authority.system_long_capable)
def test_qualified_adjacent_wire_is_source_bound_and_replay_guarded(self):
event_ns = MONO + 10_000_000
sm = FakeSubMaster(serialized_scene(stamp=BOOT + 10_000_000), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(event_ns)
def adjacent(sequence=1, status='ambiguous', *, valid=True, model_ns=event_ns):
event = messaging.new_message('starpilotRadarState', valid=valid)
wire = event.starpilotRadarState.qualifiedAdjacent
wire.version = 1
wire.status = status
wire.producerSessionId = 'a' * 32
wire.sequence = sequence
wire.radarTracksMonoTime = event_ns
wire.modelMonoTime = model_ns
wire.carStateMonoTime = event_ns
wire.cameraEofBootTime = BOOT + 10_000_000
wire.observedMonoTime = event_ns - 1000
wire.validUntilMonoTime = event_ns + 150_000_000
return messaging.log_from_bytes(event.to_bytes()).starpilotRadarState
sm.payloads['starpilotRadarState'] = adjacent()
result = projector.project(sm, CP, now_mono_ns=event_ns + 1_000_000, now_boot_ns=BOOT + 11_000_000, sample_skew_ns=1000)
self.assertTrue(result.scene.adjacent_lead_ambiguous)
repeated = projector.project(sm, CP, now_mono_ns=event_ns + 2_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000)
self.assertTrue(repeated.scene.adjacent_lead_ambiguous)
sm.payloads['starpilotRadarState'] = adjacent(status='clear')
self.assertIsNone(projector.project(sm, CP, now_mono_ns=event_ns + 3_000_000,
now_boot_ns=BOOT + 13_000_000, sample_skew_ns=1000).scene.adjacent_lead_ambiguous)
sm.payloads['starpilotRadarState'] = adjacent(sequence=2, status='unknown', valid=False)
sm.valid['starpilotRadarState'] = False
self.assertIsNone(projector.project(sm, CP, now_mono_ns=event_ns + 4_000_000,
now_boot_ns=BOOT + 14_000_000, sample_skew_ns=1000).scene.adjacent_lead_ambiguous)
sm.payloads['starpilotRadarState'] = adjacent()
sm.valid['starpilotRadarState'] = True
self.assertIsNone(projector.project(sm, CP, now_mono_ns=event_ns + 5_000_000,
now_boot_ns=BOOT + 15_000_000, sample_skew_ns=1000).scene.adjacent_lead_ambiguous)
sm.payloads['starpilotRadarState'] = adjacent(sequence=3, model_ns=event_ns + 100_000_000)
self.assertIsNone(projector.project(sm, CP, now_mono_ns=event_ns + 6_000_000,
now_boot_ns=BOOT + 16_000_000, sample_skew_ns=1000).scene.adjacent_lead_ambiguous)
def test_serialized_signal_width_four_model_updates_and_loss(self):
options = CEMOptions(signal_speed_mps=20.0, signal_lane_width_m=3.0)
sm = FakeSubMaster(serialized_scene(), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000, signal_options=options)
sm.seen['radarState'] = False # Optional, slower-cadence radar cannot stall model-width ticks.
for index in range(1, 5):
event = MONO + index * 50_000_000
sm.stamp(event)
sm.payloads = serialized_scene(stamp=BOOT + index * 50_000_000, left_blinker=True, left_edge=-2.0)
result = projector.project(sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + index * 50_000_000 + 1_000_000,
sample_skew_ns=1000, signal_options=options)
self.assertEqual(result.scene.lane_available, False if index == 4 else None)
repeated = projector.project(sm, CP, now_mono_ns=event + 2_000_000, now_boot_ns=BOOT + 4 * 50_000_000 + 2_000_000,
sample_skew_ns=1000, signal_options=options)
self.assertFalse(repeated.scene.lane_available)
sm.seen['modelV2'] = False
missing = projector.project(sm, CP, now_mono_ns=event + 3_000_000, now_boot_ns=BOOT + 4 * 50_000_000 + 3_000_000,
sample_skew_ns=1000, signal_options=options)
self.assertIsNone(missing.scene.lane_available)
def test_serialized_projection_and_unknown_fields(self):
sm = FakeSubMaster(serialized_scene(), MONO - 5_000_000)
projector = SceneProjector()
self.assertFalse(projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000).ready)
sm.stamp(MONO + 10_000_000)
result = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertTrue(result.ready)
self.assertEqual(result.scene.set_speed_mps, 20.0)
self.assertTrue(result.raw_driving_in_curve)
self.assertTrue(result.raw_lead.present)
self.assertFalse(result.raw_model_stopped)
self.assertTrue(result.current_model_should_stop)
self.assertIsNone(result.scene.following_lead)
self.assertIsNone(result.scene.curve_detected)
self.assertIsNone(result.scene.stop_light_detected)
self.assertFalse(result.authority.lat_active)
observed = result.scene.observed_mono_s
self.assertIsNotNone(observed)
if observed is not None:
self.assertAlmostEqual(observed, (MONO + 10_000_000) / 1e9)
def test_stale_services_and_repeated_source_do_not_become_false_or_new(self):
sm = FakeSubMaster(serialized_scene(), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
first = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True)
repeated = projector.project(sm, CP, now_mono_ns=MONO + 16_000_000, now_boot_ns=BOOT + 16_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertEqual(first.scene.observed_mono_s, repeated.scene.observed_mono_s)
sm.logMonoTime['radarState'] = MONO - 500_000_000
sm.recv_time['radarState'] = (MONO - 500_000_000) / 1e9
stale = projector.project(sm, CP, now_mono_ns=MONO + 17_000_000, now_boot_ns=BOOT + 17_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertIsNone(stale.raw_lead)
self.assertTrue(stale.ready)
def test_optional_missing_radar_does_not_block_core_projection(self):
sm = FakeSubMaster(serialized_scene(), MONO - 5_000_000)
sm.seen['radarState'] = False
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(MONO + 10_000_000)
result = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertTrue(result.ready)
self.assertIsNone(result.raw_lead)
self.assertNotIn('radarState', result.fresh_services)
def test_missing_controls_curvature_does_not_erase_car_speed(self):
sm = FakeSubMaster(serialized_scene(), MONO - 5_000_000)
sm.seen['controlsState'] = False
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(MONO + 10_000_000)
result = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(MONO + 10_000_000))
self.assertTrue(result.ready)
self.assertEqual(result.scene.speed_mps, 15.0)
self.assertIsNone(result.raw_driving_in_curve)
self.assertIsNone(result.scene.curve_detected)
def test_fresh_envelope_does_not_renew_old_camera_eof(self):
sm = FakeSubMaster(serialized_scene(stamp=BOOT - 140_000_000), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
near = projector.project(
sm,
CP,
now_mono_ns=MONO + 12_000_000,
now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
self.assertTrue(near.ready)
self.assertIsNone(near.model_horizon_m) # 152 ms old: past the 150 ms EOF limit.
self.assertIsNone(near.raw_model_stopped)
self.assertIsNone(near.raw_road_curve)
self.assertIsNone(near.scene.curve_detected)
self.assertTrue(near.raw_lead.present)
self.assertIsNone(near.lead_observation.tracked)
def test_invalid_model_path_remains_unknown(self):
payloads = serialized_scene()
# Serialize a second current wire message with a zero-length path rather
# than mutating a decoded Cap'n Proto reader.
invalid = messaging.new_message('modelV2', valid=True)
invalid.modelV2.timestampEof = BOOT
invalid.modelV2.position.x = []
payloads['modelV2'] = messaging.log_from_bytes(invalid.to_bytes()).modelV2
sm = FakeSubMaster(payloads, MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
result = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertTrue(result.ready)
self.assertIsNone(result.model_horizon_m)
self.assertIsNone(result.raw_model_stopped)
self.assertIsNone(result.current_model_should_stop)
def test_resume_rearms_only_after_post_barrier_events(self):
sm = FakeSubMaster(serialized_scene(), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
self.assertTrue(projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True).ready)
# MONOTONIC advances only 20 ms across a 90 ms suspend, while BOOTTIME
# advances 110 ms. The old model is still inside its BOOTTIME TTL.
self.assertFalse(projector.project(sm, CP, now_mono_ns=MONO + 32_000_000, now_boot_ns=BOOT + 122_000_000, sample_skew_ns=1000, safe_mode=True).ready)
self.assertFalse(projector.project(sm, CP, now_mono_ns=MONO + 34_000_000, now_boot_ns=BOOT + 124_000_000, sample_skew_ns=1000, safe_mode=True).ready)
sm.stamp(MONO + 35_000_000)
self.assertTrue(projector.project(sm, CP, now_mono_ns=MONO + 36_000_000, now_boot_ns=BOOT + 126_000_000, sample_skew_ns=1000, safe_mode=True).ready)
def test_current_model_curve_filters_only_with_explicit_mode_evidence(self):
sm = FakeSubMaster(serialized_scene(), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
first = projector.project(sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000, safe_mode=True)
self.assertTrue(first.raw_road_curve)
self.assertIsNone(first.scene.curve_detected)
# A caller-proven non-traffic mode makes the actual frozen filter active.
for tick in range(1, 30):
event = MONO + 10_000_000 + tick * 50_000_000
sm.stamp(event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000)['modelV2']
result = projector.project(
sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(event),
)
self.assertTrue(result.scene.curve_detected)
repeated = projector.project(
sm, CP, now_mono_ns=event + 2_000_000, now_boot_ns=BOOT + 12_000_000 + tick * 50_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(event),
)
self.assertIsNone(repeated.scene.curve_detected)
self.assertGreater(projector.curve_detector.value, 0.6)
def test_exact_curve_gap_preserves_proposal_at_different_clock_offsets(self):
for base in (100_000_000_000, 100_100_000_000):
with self.subTest(base_ns=base):
projector, policy = SceneProjector(), ConditionalModePolicy()
sm = FakeSubMaster(serialized_scene(speed=30.0, lead_present=False), base)
projector.project(sm, CP, now_mono_ns=base - 10_000_000,
now_boot_ns=base + 1_990_000_000, sample_skew_ns=1000)
stamps = [base + index * 50_000_000 for index in range(40)]
stamps += [stamps[-1] + 100_000_000 + index * 50_000_000 for index in range(28)]
curves, proposals = [], []
for event in stamps:
sm.stamp(event)
sm.recv_time = dict.fromkeys(SERVICES, (event + 2_000_000) / 1e9)
sm.payloads = serialized_scene(stamp=event + 2_000_000_000, speed=30.0, lead_present=False)
result = projector.project(
sm, CP, now_mono_ns=event + 5_000_000, now_boot_ns=event + 2_005_000_000,
sample_skew_ns=1000, safe_mode=False, owner_context=owner_context(event),
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=event,
)
self.assertIsNotNone(result.authority)
assert result.authority is not None
decision = policy.step((event + 5_000_000) / 1e9, ModeChoice.CEM, ManualIntent.NONE,
result.authority, result.scene, ModeSettings(cem_curves=True, cem_lead=False, cem_stop=False))
curves.append(result.scene.curve_detected)
proposals.append(decision.requested_experimental)
self.assertEqual(proposals[40:], [True] * 28)
self.assertEqual(curves[40:], [True] * 28)
def test_curve_timestamp_retains_event_receipt_minimum_and_invalid_reset(self):
projector = SceneProjector()
sm = FakeSubMaster(serialized_scene(), MONO)
projector.project(sm, CP, now_mono_ns=MONO - 10_000_000,
now_boot_ns=BOOT - 10_000_000, sample_skew_ns=1000)
for event, receipt, expected in ((MONO + 1, MONO, MONO),
(MONO + 50_000_000, MONO + 60_000_000, MONO + 50_000_000)):
sm.stamp(event)
sm.recv_time['modelV2'] = receipt / 1e9
sm.payloads['modelV2'] = serialized_scene(stamp=event + 2_000_000_000)['modelV2']
projector.project(
sm, CP, now_mono_ns=event + 15_000_000, now_boot_ns=event + 2_015_000_000,
sample_skew_ns=1000, safe_mode=False, owner_context=owner_context(event),
)
self.assertEqual(projector.curve_detector.last_observed_mono_ns, expected)
sm.valid['modelV2'] = False
result = projector.project(
sm, CP, now_mono_ns=event + 16_000_000, now_boot_ns=event + 2_016_000_000,
sample_skew_ns=1000, safe_mode=False, owner_context=owner_context(event),
)
self.assertIsNone(result.scene.curve_detected)
self.assertEqual(projector.curve_detector.value, 0.0)
self.assertIsNone(projector.curve_detector.last_observed_mono_ns)
def test_joined_serialized_lead_tracks_once_per_model_tick(self):
sm = FakeSubMaster(serialized_scene(origin=-6.64888977208733e-11), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(15):
event = MONO + 10_000_000 + tick * 50_000_000
sm.stamp(event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000, origin=-6.64888977208733e-11)['modelV2']
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
)
self.assertTrue(result.raw_lead.present)
self.assertTrue(result.lead_observation.tracked)
self.assertTrue(result.lead_observation.following)
self.assertTrue(result.scene.lead.tracked)
self.assertTrue(result.scene.following_lead)
self.assertEqual(result.model_horizon_m, 192.0)
previous_filter = projector.lead_detector.filter_value
repeated = projector.project(
sm,
CP,
now_mono_ns=event + 2_000_000,
now_boot_ns=BOOT + 12_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event + 1_000_000,
)
self.assertTrue(repeated.raw_lead.present)
self.assertIsNone(repeated.lead_observation.tracked)
self.assertIsNone(repeated.scene.lead)
self.assertEqual(projector.lead_detector.filter_value, previous_filter)
def test_stationary_clear_endpoint_does_not_relax_spatial_path_validation(self):
from openpilot.starpilot.conditional_mode.projection import _horizon, _standstill_clear_horizon
from openpilot.starpilot.conditional_mode.tests.test_stop_commit_transport import CLEAR_97_X
model = messaging.new_message('modelV2').modelV2
model.position.x = CLEAR_97_X
self.assertIsNone(_horizon(model))
self.assertEqual(_standstill_clear_horizon(model), CLEAR_97_X[-1])
malformed = (list(CLEAR_97_X[:-1]), list(CLEAR_97_X), list(CLEAR_97_X), list(CLEAR_97_X), list(CLEAR_97_X))
malformed[1][3] = -.011
malformed[2][18] = malformed[2][17] - .001
malformed[3][0] = -2e-6
malformed[4][4] = float('nan')
for values in malformed:
with self.subTest(values=values):
model.position.x = values
self.assertIsNone(_standstill_clear_horizon(model))
def test_headway_stale_and_real_negative_path_leave_raw_only(self):
for origin, later_negative in ((-2e-6, False), (-6.64888977208733e-11, True)):
sm = FakeSubMaster(serialized_scene(origin=origin, later_negative=later_negative), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(MONO + 10_000_000)
result = projector.project(
sm,
CP,
now_mono_ns=MONO + 12_000_000,
now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
self.assertTrue(result.raw_lead.present)
self.assertIsNone(result.model_horizon_m)
self.assertIsNone(result.lead_observation.tracked)
sm = FakeSubMaster(serialized_scene(origin=-6.64888977208733e-11), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(MONO + 10_000_000)
stale = projector.project(
sm,
CP,
now_mono_ns=MONO + 12_000_000,
now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO - 5_000_000,
)
self.assertTrue(stale.raw_lead.present)
self.assertIsNone(stale.lead_observation.tracked)
self.assertIsNone(stale.scene.following_lead)
def test_malformed_lead_value_preserves_raw_presence(self):
payloads = serialized_scene()
invalid = messaging.new_message('radarState', valid=True)
invalid.radarState.leadOne.present = True
invalid.radarState.leadOne.radar = True
invalid.radarState.leadOne.dRel = math.nan
payloads['radarState'] = messaging.log_from_bytes(invalid.to_bytes()).radarState
sm = FakeSubMaster(payloads, MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
sm.stamp(MONO + 10_000_000)
result = projector.project(
sm,
CP,
now_mono_ns=MONO + 12_000_000,
now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
self.assertTrue(result.raw_lead.present)
self.assertIsNone(result.raw_lead.distance_m)
self.assertIsNone(result.lead_observation.tracked)
self.assertIsNone(result.scene.lead)
def test_joined_serialized_stop_filters_only_with_original_owner_sources(self):
sm = FakeSubMaster(serialized_scene(horizon=40, lead_present=False), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(28):
event = MONO + 10_000_000 + tick * 50_000_000
sm.stamp(event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000, horizon=40, lead_present=False)['modelV2']
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
safe_mode=True,
owner_context=owner_context(event),
)
self.assertTrue(result.scene.stop_light_detected)
self.assertFalse(result.scene.standstill_stop_hold)
self.assertIsNone(result.scene.slow_lead_detected) # No selected headway: unavailable, not a negative.
self.assertEqual(result.stop_source_observed_mono_s, event / 1e9)
filter_value = projector.stop_detector.light_filter.x
repeated = projector.project(
sm,
CP,
now_mono_ns=event + 2_000_000,
now_boot_ns=BOOT + 12_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
owner_context=owner_context(event + 1_000_000),
)
self.assertIsNone(repeated.scene.stop_light_detected)
self.assertEqual(repeated.stop_source_observed_mono_s, event / 1e9)
self.assertEqual(projector.stop_detector.light_filter.x, filter_value)
lost_context = projector.project(
sm,
CP,
now_mono_ns=event + 3_000_000,
now_boot_ns=BOOT + 13_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
owner_context=None,
)
self.assertIsNone(lost_context.scene.stop_light_detected)
self.assertEqual(projector.stop_detector.light_filter.x, 0.0)
next_event = event + 300_000_000
sm.stamp(next_event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000 + 300_000_000, horizon=40, lead_present=False)['modelV2']
stale_context = projector.project(
sm,
CP,
now_mono_ns=next_event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000 + 300_000_000,
sample_skew_ns=1000,
owner_context=owner_context(event),
)
self.assertIsNone(stale_context.scene.stop_light_detected)
self.assertIsNone(stale_context.scene.slow_lead_detected)
self.assertEqual(stale_context.scene.speed_mps, 15.0)
def test_joined_standstill_sign_hold_is_explicit_policy_evidence(self):
sm = FakeSubMaster(serialized_scene(speed=0, horizon=192, lead_present=False, standstill=True), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
event = MONO + 10_000_000
sm.stamp(event)
context = replace(owner_context(event), stop_sign_confirmed=ObservedBool(True, event))
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000,
sample_skew_ns=1000,
owner_context=context,
)
self.assertTrue(result.scene.stop_light_detected)
self.assertTrue(result.scene.standstill_stop_hold)
self.assertTrue(result.scene.stop_sign_confirmed)
def test_joined_detector_stamp_is_oldest_original_input_not_poll_time(self):
sm = FakeSubMaster(serialized_scene(horizon=40, lead_present=False), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
event = MONO + 10_000_000
sm.stamp(event)
context = replace(owner_context(event), forcing_stop=ObservedBool(False, event - 2_000_000))
first = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000,
sample_skew_ns=1000,
owner_context=context,
)
self.assertIsNotNone(first.stop_source_observed_mono_s)
assert first.stop_source_observed_mono_s is not None
self.assertAlmostEqual(first.stop_source_observed_mono_s, (event - 2_000_000) / 1e9)
repeated = projector.project(
sm,
CP,
now_mono_ns=event + 2_000_000,
now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000,
owner_context=owner_context(event + 1_000_000),
)
self.assertIsNotNone(repeated.stop_source_observed_mono_s)
assert repeated.stop_source_observed_mono_s is not None
self.assertAlmostEqual(repeated.stop_source_observed_mono_s, (event - 2_000_000) / 1e9)
self.assertIsNone(repeated.scene.stop_light_detected)
def test_joined_model_cadence_with_half_rate_radar_and_owner_inputs(self):
for speed, horizon, present, lead_speed, count in ((15.0, 40.0, False, 0.0, 28), (30.0, 192.0, True, 20.0, 50)):
sm = FakeSubMaster(serialized_scene(speed=speed, horizon=horizon, lead_present=present, lead_distance=50, lead_speed=lead_speed), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(count):
event = MONO + 10_000_000 + tick * 50_000_000
older = MONO + 10_000_000 + (tick // 2) * 100_000_000
sm.stamp(event)
sm.logMonoTime['radarState'] = older
sm.recv_time['radarState'] = older / 1e9
sm.payloads.update(
serialized_scene(
stamp=BOOT + 10_000_000 + tick * 50_000_000,
speed=speed,
horizon=horizon,
lead_present=present,
lead_distance=50,
lead_speed=lead_speed,
)
)
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
owner_context=owner_context(older),
)
if present:
self.assertTrue(result.scene.slow_lead_detected)
assert result.slower_source_observed_mono_s is not None
assert projector.slower_lead_detector.last_model_tick_mono_s is not None
self.assertAlmostEqual(result.slower_source_observed_mono_s, older / 1e9)
self.assertAlmostEqual(projector.slower_lead_detector.last_model_tick_mono_s, event / 1e9)
else:
self.assertTrue(result.scene.stop_light_detected)
assert result.stop_source_observed_mono_s is not None
assert projector.stop_detector.last_model_tick_mono_s is not None
self.assertAlmostEqual(result.stop_source_observed_mono_s, older / 1e9)
self.assertAlmostEqual(projector.stop_detector.last_model_tick_mono_s, event / 1e9)
expired_event = event + 300_000_000
sm.stamp(expired_event)
sm.payloads['modelV2'] = serialized_scene(
stamp=BOOT + 10_000_000 + (count - 1) * 50_000_000 + 300_000_000,
speed=speed,
horizon=horizon,
lead_present=present,
lead_distance=50,
lead_speed=lead_speed,
)['modelV2']
expired = projector.project(
sm,
CP,
now_mono_ns=expired_event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + (count - 1) * 50_000_000 + 300_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=expired_event,
owner_context=owner_context(older),
)
self.assertIsNone(expired.scene.slow_lead_detected)
self.assertIsNone(expired.scene.stop_light_detected)
self.assertEqual(expired.scene.speed_mps, speed)
def test_joined_serialized_slower_lead_and_context_barriers(self):
sm = FakeSubMaster(serialized_scene(speed=30, lead_distance=50, lead_speed=20), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(48):
event = MONO + 10_000_000 + tick * 50_000_000
sm.stamp(event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000, speed=30, lead_distance=50, lead_speed=20)['modelV2']
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
owner_context=owner_context(event),
)
self.assertTrue(result.scene.lead.tracked)
self.assertTrue(result.scene.slow_lead_detected)
self.assertEqual(result.slower_source_observed_mono_s, event / 1e9)
self.assertEqual(result.scene.speed_mps, 30.0)
# A stale owner flag invalidates only the detector that needs it; speed,
# raw lead and independent stop/curve projection remain available.
next_event = event + 50_000_000
sm.stamp(next_event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + 48 * 50_000_000, speed=30, lead_distance=50, lead_speed=20)['modelV2']
stale = owner_context(next_event)
stale = replace(stale, slower_option=ObservedBool(True, MONO - 5_000_000))
invalid = projector.project(
sm,
CP,
now_mono_ns=next_event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + 48 * 50_000_000,
sample_skew_ns=1000,
safe_mode=True,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=next_event,
owner_context=stale,
)
self.assertIsNone(invalid.scene.slow_lead_detected)
self.assertEqual(invalid.scene.speed_mps, 30.0)
self.assertTrue(invalid.raw_lead.present)
self.assertIsNotNone(invalid.scene.stop_light_detected)
# Resume invalidates pre-suspend owner evidence even when its MONOTONIC
# age is small. Fresh model and owner events are needed before rearming.
self.assertFalse(
projector.project(
sm,
CP,
now_mono_ns=next_event + 20_000_000,
now_boot_ns=BOOT + 101_000_000 + 48 * 50_000_000,
sample_skew_ns=1000,
owner_context=owner_context(next_event),
).ready
)
resumed_event = next_event + 25_000_000
sm.stamp(resumed_event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 106_000_000 + 48 * 50_000_000, speed=30, lead_distance=50, lead_speed=20)['modelV2']
resumed = projector.project(
sm,
CP,
now_mono_ns=resumed_event + 1_000_000,
now_boot_ns=BOOT + 107_000_000 + 48 * 50_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=resumed_event,
owner_context=owner_context(next_event),
)
self.assertIsNone(resumed.scene.slow_lead_detected)
self.assertEqual(resumed.scene.speed_mps, 30.0)
def test_joined_fresh_no_lead_releases_then_stale_radar_is_unknown(self):
sm = FakeSubMaster(serialized_scene(speed=30, lead_distance=50, lead_speed=20), MONO - 5_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
for tick in range(70):
event = MONO + 10_000_000 + tick * 50_000_000
present = tick < 45
sm.stamp(event)
sm.payloads.update(
serialized_scene(
stamp=BOOT + 10_000_000 + tick * 50_000_000,
speed=30,
lead_present=present,
lead_distance=50,
lead_speed=20,
)
)
result = projector.project(
sm,
CP,
now_mono_ns=event + 1_000_000,
now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
owner_context=owner_context(event),
)
if tick == 44:
self.assertTrue(result.scene.slow_lead_detected)
self.assertFalse(result.raw_lead.present)
self.assertFalse(result.scene.slow_lead_detected)
self.assertFalse(result.scene.following_lead)
sm.logMonoTime['radarState'] = event - 500_000_000
sm.recv_time['radarState'] = (event - 500_000_000) / 1e9
stale = projector.project(
sm,
CP,
now_mono_ns=event + 2_000_000,
now_boot_ns=BOOT + 12_000_000 + 69 * 50_000_000,
sample_skew_ns=1000,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
owner_context=owner_context(event),
)
self.assertIsNone(stale.scene.slow_lead_detected)
self.assertIsNone(stale.raw_lead)
self.assertEqual(stale.scene.speed_mps, 30.0)
def test_chill_launch_uses_real_longitudinal_plan_not_model_action(self):
sm = FakeSubMaster(serialized_scene(speed=0.0, standstill=True, lead_present=False), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
context = owner_context(MONO + 10_000_000)
launched = projector.project(
sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=context, selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
self.assertTrue(sm['modelV2'].action.shouldStop) # Current model action is not frozen longitudinalPlan.shouldStop.
self.assertTrue(launched.scene.launch_candidate)
self.assertFalse(launched.scene.launch_forced_exit)
self.assertFalse(launched.scene.launch_lead)
self.assertFalse(launched.scene.low_speed_stop_scene)
self.assertEqual(launched.chill_observation.lead_reason, 'no_lead')
event = MONO + 60_000_000
sm.stamp(event)
sm.payloads.update(serialized_scene(stamp=BOOT + 60_000_000, speed=0.0, standstill=True,
lead_present=False, plan_should_stop=True))
stopped = projector.project(
sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + 61_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(event), selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
)
self.assertFalse(stopped.scene.launch_candidate)
self.assertTrue(stopped.scene.launch_forced_exit)
self.assertEqual(stopped.chill_observation.speed_reason, 'longitudinal_stop')
def test_same_cycle_native_plan_owner_without_loopback_service(self):
event = MONO + 10_000_000
sm = FakeSubMaster(serialized_scene(speed=0.0, standstill=True, lead_present=False), event)
sm.seen['longitudinalPlan'] = False
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
context = replace(owner_context(event), plan_should_stop=ObservedBool(False, event),
plan_allow_throttle=ObservedBool(True, event))
owned = projector.project(sm, CP, now_mono_ns=event + 2_000_000, now_boot_ns=BOOT + 12_000_000,
sample_skew_ns=1000, safe_mode=True, owner_context=context,
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=event)
self.assertTrue(owned.scene.launch_candidate)
self.assertNotIn('longitudinalPlan', owned.fresh_services)
old = replace(context, plan_should_stop=ObservedBool(False, MONO - 500_000_000))
stale = projector.project(sm, CP, now_mono_ns=event + 3_000_000, now_boot_ns=BOOT + 13_000_000,
sample_skew_ns=1000, safe_mode=True, owner_context=old,
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=event)
self.assertIsNone(stale.scene.launch_candidate)
def test_chill_stop_proof_survives_missing_launch_only_owners(self):
sm = FakeSubMaster(serialized_scene(speed=0.0, horizon=30.0, lead_present=False), MONO + 10_000_000)
sm.seen['longitudinalPlan'] = False
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
missing_plan = projector.project(
sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=replace(owner_context(MONO + 10_000_000), red_light=None),
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
self.assertTrue(missing_plan.ready) # New optional plan never changes core readiness.
self.assertTrue(missing_plan.scene.low_speed_stop_scene)
self.assertIsNone(missing_plan.scene.launch_candidate)
self.assertNotIn('longitudinalPlan', missing_plan.fresh_services)
self.assertIsNone(missing_plan.scene.launch_lead)
# Missing launch-only context cannot erase a fresh raw-model stop proof.
sm.seen['longitudinalPlan'] = True
event = MONO + 60_000_000
sm.stamp(event)
sm.payloads.update(serialized_scene(stamp=BOOT + 60_000_000, speed=0.0, horizon=30.0, lead_present=False))
stale_red = projector.project(
sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + 61_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=replace(owner_context(event),
red_light=ObservedBool(False, MONO - 500_000_000)),
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=event,
)
self.assertIsNone(stale_red.scene.launch_candidate)
self.assertTrue(stale_red.scene.low_speed_stop_scene)
def test_chill_launch_requires_raw_lead_relative_speed(self):
sm = FakeSubMaster(serialized_scene(speed=0.0, lead_present=True, lead_speed=0.8,
lead_relative=0.05, lead_accel=0.2), MONO + 10_000_000)
projector = SceneProjector()
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
blocked = projector.project(
sm, CP, now_mono_ns=MONO + 12_000_000, now_boot_ns=BOOT + 12_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(MONO + 10_000_000), selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=MONO + 10_000_000,
)
assert blocked.raw_lead is not None and blocked.raw_lead.relative_speed_mps is not None
self.assertAlmostEqual(blocked.raw_lead.relative_speed_mps, 0.05)
self.assertFalse(blocked.scene.launch_candidate)
self.assertEqual(blocked.chill_observation.lead_reason, 'not_departing')
event = MONO + 60_000_000
sm.stamp(event)
sm.payloads.update(serialized_scene(stamp=BOOT + 60_000_000, speed=0.0, lead_present=True,
lead_speed=0.8, lead_relative=0.5, lead_accel=0.2))
departure = projector.project(
sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + 61_000_000, sample_skew_ns=1000,
safe_mode=True, owner_context=owner_context(event), selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=event,
)
self.assertTrue(departure.scene.launch_candidate)
self.assertTrue(departure.scene.launch_lead)
self.assertEqual(departure.chill_observation.lead_reason, 'departing')
def test_host_warms_curve_with_only_stamped_owner_context(self):
from openpilot.starpilot.conditional_mode.host import ConditionalModeHost
from openpilot.starpilot.conditional_mode.policy import ManualIntent, ModeChoice, ModeSettings
from openpilot.starpilot.conditional_mode.preferences import manual_for_drive, selection_for_drive
sm = FakeSubMaster(serialized_scene(), MONO - 5_000_000)
host = ConditionalModeHost()
selection = selection_for_drive(ModeChoice.CCM, ModeSettings(), MONO)
manual = manual_for_drive(ManualIntent.NONE, MONO, MONO + 1_000_000)
host.sample(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000, drive_id=MONO,
selection=selection, manual=manual, safe_mode=ObservedBool(False, MONO - 5_000_000))
for tick in range(30):
event = MONO + 10_000_000 + tick * 50_000_000
sm.stamp(event)
sm.payloads['modelV2'] = serialized_scene(stamp=BOOT + 10_000_000 + tick * 50_000_000)['modelV2']
result = host.sample(
sm, CP, now_mono_ns=event + 1_000_000, now_boot_ns=BOOT + 11_000_000 + tick * 50_000_000,
sample_skew_ns=1000, drive_id=MONO, selection=selection, manual=manual,
safe_mode=ObservedBool(False, event), owner_context=owner_context(event),
selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=event,
)
self.assertTrue(result.projected.scene.curve_detected)
self.assertGreater(host.projector.curve_detector.value, 0.6)
if __name__ == '__main__':
unittest.main()