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

290 lines
15 KiB
Python

"""Actual host proposals, serialized transport and the effective-mode owner."""
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import patch
import json
import tempfile
import unittest
import os
from pathlib import Path
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.starpilot.conditional_mode.consumer import ModeConsumer
from openpilot.starpilot.conditional_mode.host import ConditionalModeHost
from openpilot.starpilot.conditional_mode.policy import ManualIntent, ModeChoice
from openpilot.starpilot.conditional_mode.preferences import SavedPreferences, encode_preferences, manual_for_drive
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner, DOCUMENT_KEY
from openpilot.starpilot.conditional_mode.status import StatusPublisher, observation, settings_fingerprint
from openpilot.starpilot.conditional_mode.tests.test_projection import BOOT, CP, MONO, FakeSubMaster, serialized_scene, owner_context
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import FeaturePage, row_change
class TestModeTransport(unittest.TestCase):
def setUp(self):
directory = self.enterContext(tempfile.TemporaryDirectory())
self.params = Params(directory)
self.params.put(DOCUMENT_KEY, json.loads(encode_preferences(SavedPreferences(mode=ModeChoice.CEM))), block=True)
self.owner = ConditionalSettingsOwner(self.params)
self.host = ConditionalModeHost()
self.publisher = StatusPublisher()
self.consumer = ModeConsumer()
self.drive_id = MONO - 1_000_000_000
self.produce(MONO)
self.consume(None, MONO)
def produce(self, now, *, intent=ManualIntent.FORCE_EXPERIMENTAL, lat=False, long=True, outer=None, scene=None, context=None):
sm = FakeSubMaster(serialized_scene(stamp=BOOT + now - MONO - 2_000_000, **(scene or {})), now - 2_000_000)
control = messaging.new_message('carControl')
control.carControl.longActive = long
control.carControl.latActive = lat
sm.payloads['carControl'] = messaging.log_from_bytes(control.to_bytes()).carControl
proposal = self.host.sample(
sm,
CP,
now_mono_ns=now,
now_boot_ns=BOOT + now - MONO,
sample_skew_ns=1000,
drive_id=self.drive_id,
settings_owner=self.owner,
manual=manual_for_drive(intent, self.drive_id, self.drive_id + 1),
owner_context=context,
selected_t_follow_s=1.45,
selected_t_follow_observed_mono_ns=now - 2_000_000,
)
event = self.publisher.attach(
outer, proposal, self.owner.current, now_ns=now, drive_id=self.drive_id, model_ns=now - 2_000_000, car_state_ns=now - 2_000_000
)
return event, proposal, sm
def consume(self, event, now, **changes):
payload = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).slcState if event is not None else None
values = {
'now_ns': now,
'now_boot_ns': BOOT + now - MONO,
'sample_skew_ns': 1000,
'message_ns': now,
'receipt_ns': now,
'drive_id': self.drive_id,
'model_ns': now - 2_000_000,
'car_state_ns': now - 2_000_000,
'authority': True,
'stock_experimental': False,
'choice': ModeChoice.CEM,
'settings_fingerprint': settings_fingerprint(self.owner.current),
}
values.update(changes)
return self.consumer.sample(payload, **values)
def test_shared_chill_selector_preserves_committed_stop_in_actual_selfdrived(self):
cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC_BOSCH)
cp.openpilotLongitudinalControl = True
ui = FeatureSettingsOwner(self.params, lambda group: group == 'preferences',
vehicle_fingerprint=lambda: cp.carFingerprint, vehicle_params=lambda: cp)
drive = SelfdriveD.__new__(SelfdriveD)
drive.params, drive.CP = self.params, cp
drive.conditional_replay = True
drive.conditional_settings, drive.conditional_consumer = self.owner, self.consumer
drive.conditional_car_state_valid = drive.initialized = drive.enabled = True
drive.aol_replay = False
scene = {'speed': 10.0, 'horizon': 40.0, 'lead_present': False}
for tick in range(1, 31):
now = MONO + tick * 50_000_000
event, _, _ = self.produce(now, intent=ManualIntent.NONE, scene=scene, context=owner_context(now - 2_000_000))
self.consume(event, now)
self.assertTrue(self.host.projector.stop_detector.committed)
row = next(row for row in ui.snapshot(FeaturePage.CONDITIONAL, parked=True, system_long=True,
lateral_context=False, metric=False).rows if row.key == 'conditional:mode')
request = row_change(row)
assert request is not None
accepted = self.owner.refresh(MONO + 30 * 50_000_000, force=True)
def model_tick(tick, choice, after_verdict=None):
now = MONO + tick * 50_000_000
event, proposal, sm = self.produce(now, intent=ManualIntent.FORCE_CHILL, scene=scene,
context=owner_context(now - 2_000_000))
self.assertIs(proposal.choice, choice)
self.assertEqual(proposal.decision.reason.value, 'cem_stop')
self.assertTrue(event.slcState.conditionalMode.hasOverride)
device = messaging.new_message('deviceState', valid=True)
device.deviceState.started = True
device.deviceState.startedMonoTime = self.drive_id
sm.payloads['deviceState'] = messaging.log_from_bytes(device.to_bytes()).deviceState
sm.payloads['slcState'] = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).slcState
for service in ('deviceState', 'slcState'):
sm.logMonoTime[service] = now
sm.recv_time[service] = now / 1e9
sm.seen[service] = sm.alive[service] = sm.valid[service] = True
drive.sm, drive.aol_car_state_log_ns = sm, now - 2_000_000
original_verdict = self.owner.verdict
def interleaved_verdict(snapshot, **kwargs):
verdict = original_verdict(snapshot, **kwargs)
if after_verdict is not None:
after_verdict()
return verdict
with patch('openpilot.selfdrive.selfdrived.selfdrived.paired_clocks_ns', return_value=(now, BOOT + now - MONO, 1000)), \
patch.object(self.owner, 'verdict', side_effect=interleaved_verdict):
drive.update_conditional_mode(sm['carState'])
self.assertTrue(drive.conditional_result.accepted)
self.assertTrue(drive.experimental_mode)
self.assertTrue(self.host.projector.stop_detector.committed)
original_replace = os.replace
sampled = False
def sample_between_pair_replacements(source, destination):
nonlocal sampled
if Path(destination).name == 'ExperimentalMode':
sampled = True
# commit_mode holds the actual writer lock with its document already
# replaced. A model tick must keep the prior accepted pair and stop.
now = MONO + 31 * 50_000_000
self.assertIs(self.owner.refresh(now, force=True), accepted)
self.assertEqual(self.owner.current.verified_mono_ns, accepted.verified_mono_ns)
model_tick(31, ModeChoice.CEM)
return original_replace(source, destination)
with patch('openpilot.starpilot.conditional_mode.actions.os.replace', side_effect=sample_between_pair_replacements):
self.assertTrue(ui.apply(replace(request, value='Chill')))
self.assertTrue(sampled)
self.assertFalse(self.params.get_bool('ExperimentalMode'))
drive.refresh_saved_driving_mode(MONO + 32 * 50_000_000)
def refresh_experimental_between_main_snapshot_and_baseline():
row = next(row for row in ui.snapshot(FeaturePage.CONDITIONAL, parked=True, system_long=True,
lateral_context=False, metric=False).rows if row.key == 'conditional:mode')
change = row_change(row)
assert change is not None
self.assertTrue(ui.apply(replace(change, value='Experimental')))
drive.refresh_saved_driving_mode(MONO + 32 * 50_000_000)
self.assertEqual(self.owner.current.experimental_raw, b'1')
model_tick(32, ModeChoice.STOCK, after_verdict=refresh_experimental_between_main_snapshot_and_baseline)
self.assertFalse(drive.requested_experimental_mode) # Main consumed its captured Chill pair.
drive.refresh_saved_driving_mode(MONO + 33 * 50_000_000)
model_tick(33, ModeChoice.STOCK)
self.assertTrue(drive.requested_experimental_mode)
def test_both_directions_and_independent_longitudinal_authority(self):
now = MONO + 50_000_000
event, proposal, _ = self.produce(now)
self.assertTrue(proposal.override_experimental)
self.assertTrue(self.consume(event, now).experimental)
event, proposal, _ = self.produce(now + 50_000_000, intent=ManualIntent.FORCE_CHILL)
self.assertFalse(proposal.override_experimental)
chill = self.consume(event, now + 50_000_000, stock_experimental=True)
self.assertTrue(chill.accepted)
self.assertFalse(chill.experimental)
lateral, _, _ = self.produce(now + 100_000_000, lat=True, long=False)
self.assertFalse(lateral.slcState.conditionalMode.hasOverride)
denied = self.consume(event, now + 60_000_000, authority=False, stock_experimental=True)
self.assertFalse(denied.accepted)
self.assertTrue(denied.experimental)
def test_invalid_or_old_nested_state_never_uses_outer_slc_availability(self):
now = MONO + 50_000_000
outer = messaging.new_message('slcState', valid=False)
outer.logMonoTime = now
outer.slcState.sessionId = 'original-slc-session'
event, _, _ = self.produce(now, outer=outer)
self.assertEqual(event.slcState.sessionId, 'original-slc-session')
self.assertTrue(self.consume(event, now).accepted)
self.assertIsNone(observation(messaging.new_message('slcState').slcState, now))
for field, bad in (
('version', 99),
('sessionId', ''),
('settingsFingerprint', 'x'),
('modelMonoTime', 0),
('validUntilMonoTime', now + 1_000_000_000),
('choice', 'stock'),
('status', 'stock'),
):
changed = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).as_builder()
setattr(changed.slcState.conditionalMode, field, bad)
with self.subTest(field=field):
self.assertFalse(self.consume(changed, now).accepted)
def test_repeat_has_original_expiry_and_changed_sequence_is_rejected(self):
now = MONO + 50_000_000
event, _, _ = self.produce(now)
self.assertTrue(self.consume(event, now).accepted)
self.assertTrue(self.consume(event, now + 20_000_000, message_ns=now, receipt_ns=now).accepted)
changed = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).as_builder()
changed.slcState.conditionalMode.experimental = False
self.assertFalse(self.consume(changed, now + 20_000_000, message_ns=now, receipt_ns=now).accepted)
self.assertFalse(self.consume(event, now + 151_000_000, message_ns=now, receipt_ns=now).accepted)
later, _, _ = self.produce(now + 200_000_000)
self.assertTrue(self.consume(later, now + 200_000_000).accepted)
self.assertFalse(self.consume(event, now + 200_000_000).accepted)
def test_new_session_resume_and_drive_wait_for_new_sources(self):
now = MONO + 50_000_000
event, _, _ = self.produce(now)
self.assertTrue(self.consume(event, now).accepted)
self.publisher = StatusPublisher()
new, _, _ = self.produce(now + 50_000_000)
self.assertFalse(self.consume(new, now + 50_000_000).accepted)
self.assertFalse(self.consume(new, now + 55_000_000, message_ns=now + 50_000_000).accepted)
next_event, _, _ = self.produce(now + 100_000_000)
self.assertTrue(self.consume(next_event, now + 100_000_000).accepted)
self.assertFalse(self.consume(next_event, now + 110_000_000, now_boot_ns=BOOT + 5_000_000_000).accepted)
self.assertFalse(self.consume(next_event, now + 120_000_000, drive_id=self.drive_id + 1).accepted)
def test_settings_identity_and_malformed_stock_request(self):
now = MONO + 50_000_000
event, proposal, _ = self.produce(now)
self.assertFalse(self.consume(event, now, settings_fingerprint='0' * 64).accepted)
self.assertFalse(self.consume(event, now, stock_experimental='false').experimental)
current = self.owner.current
assert current is not None
newer = replace(current, revision=current.revision + 1)
rejected = self.publisher.attach(None, proposal, newer, now_ns=now, drive_id=self.drive_id, model_ns=now - 2_000_000, car_state_ns=now - 2_000_000)
self.assertFalse(rejected.slcState.conditionalMode.hasOverride)
def test_actual_selfdrived_owner_preserves_stock_and_separates_axes(self):
drive = SelfdriveD.__new__(SelfdriveD)
drive.conditional_replay = False
drive.requested_experimental_mode = True
drive.update_conditional_mode(None)
self.assertTrue(drive.experimental_mode)
self.params.put_bool('ExperimentalMode', True, block=True)
self.owner.refresh(MONO, force=True)
drive.conditional_replay = True
drive.conditional_settings = self.owner
drive.conditional_consumer = self.consumer
drive.conditional_car_state_valid = True
drive.initialized = True
drive.enabled = True
drive.aol_replay = True
axes = SimpleNamespace(longitudinal_active=True, lateral_active=False)
self.enterContext(patch.object(drive, "aol_axis_decision", axes, create=True))
drive.CP = SimpleNamespace(openpilotLongitudinalControl=True, passive=False)
for index, intent in enumerate((ManualIntent.FORCE_CHILL, ManualIntent.FORCE_EXPERIMENTAL), start=1):
now = MONO + index * 50_000_000
event, _, sm = self.produce(now, intent=intent)
device = messaging.new_message('deviceState', valid=True)
device.deviceState.started = True
device.deviceState.startedMonoTime = self.drive_id
sm.payloads['deviceState'] = messaging.log_from_bytes(device.to_bytes()).deviceState
sm.payloads['slcState'] = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).slcState
for service in ('deviceState', 'slcState'):
sm.logMonoTime[service] = now
sm.recv_time[service] = now / 1e9
sm.seen[service] = sm.alive[service] = sm.valid[service] = True
sm.valid['slcState'] = False # Unavailable SLC does not invalidate this nested proposal.
drive.sm = sm
drive.aol_car_state_log_ns = now - 2_000_000
with patch('openpilot.selfdrive.selfdrived.selfdrived.paired_clocks_ns', return_value=(now, BOOT + now - MONO, 1000)):
drive.update_conditional_mode(sm['carState'])
self.assertEqual(drive.experimental_mode, intent is ManualIntent.FORCE_EXPERIMENTAL)
axes.longitudinal_active = False
drive.update_conditional_mode(sm['carState'])
self.assertTrue(drive.experimental_mode) # Exact existing stock request; no conditional override.
axes.longitudinal_active = True