mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 23:43:44 +08:00
db1c5a7445
Release inherited Params locks correctly across fork and exec. Preserve qualified lateral armed intent through temporary faults, restore the Big UI CEM gauge, improve speed typography, and hide unavailable CEM initialization text. Include matching device artifacts and focused regression coverage.
290 lines
15 KiB
Python
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
|