mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
1376 lines
78 KiB
Python
1376 lines
78 KiB
Python
"""Native parser packets through typed SLC state and the opt-in planner ceiling."""
|
|
|
|
import math
|
|
import json
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from dataclasses import replace
|
|
from collections import deque
|
|
from contextlib import ExitStack
|
|
from itertools import product
|
|
from types import SimpleNamespace
|
|
from typing import cast
|
|
from unittest import mock
|
|
|
|
from opendbc.can import CANPacker, CANParser
|
|
from opendbc.car.dashboard_speed_limit import Tracker, Status, ford_sign, honda_sign, hyundai_canfd_sign, parser_expiry, toyota_sign
|
|
from opendbc.car.honda.interface import CarInterface
|
|
from opendbc.car.honda.values import CAR
|
|
from opendbc.car.hyundai.carstate import CarState as HyundaiCarState
|
|
from opendbc.car.hyundai.interface import CarInterface as HyundaiCarInterface
|
|
from opendbc.car.hyundai.values import CAR as HyundaiCAR, HyundaiFlags
|
|
from opendbc.car import Bus
|
|
from opendbc.car.honda.carstate import CarState as HondaCarState
|
|
from opendbc.car.toyota.carstate import CarState as ToyotaCarState
|
|
from opendbc.car.toyota.interface import CarInterface as ToyotaCarInterface
|
|
from opendbc.car.toyota.values import CAR as ToyotaCAR
|
|
from opendbc.car.ford.carstate import CarState as FordCarState
|
|
from opendbc.car.ford.interface import CarInterface as FordCarInterface
|
|
from opendbc.car.ford.values import CAR as FordCAR
|
|
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
|
from openpilot.starpilot.longitudinal.tests.test_cruise_ceiling import messages
|
|
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime
|
|
from openpilot.starpilot.speed_limits.runtime_settings import parse
|
|
from openpilot.starpilot.speed_limits.runtime_settings import read_params
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.speed_limits.selection import Source
|
|
from openpilot.starpilot.speed_limits.acceptance import ObservationKind
|
|
from openpilot.starpilot.speed_limits import acceptance as acc, history
|
|
from openpilot.starpilot.speed_limits import physical_actions
|
|
from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation
|
|
from opendbc.car.structs import car
|
|
|
|
|
|
class ReplaySM:
|
|
def __init__(self, data, timestamp):
|
|
self.data = data
|
|
self.valid = dict.fromkeys(data, True)
|
|
self.updated = dict.fromkeys(data, True)
|
|
self.alive = dict.fromkeys(data, True)
|
|
self.logMonoTime = dict.fromkeys(data, timestamp)
|
|
|
|
def __getitem__(self, name):
|
|
return self.data[name]
|
|
|
|
def advance(self, timestamp):
|
|
self.logMonoTime = dict.fromkeys(self.data, timestamp)
|
|
self.data['slcDashboardObservation'].carStateLogMonoTime = timestamp
|
|
|
|
def update(self, timeout: int = 100) -> None:
|
|
pass
|
|
|
|
|
|
class NativeSourceTests(unittest.TestCase):
|
|
def test_brand_carstate_trackers_receive_real_packets(self):
|
|
cases = (
|
|
(CarInterface, CAR.HONDA_CIVIC_BOSCH, HondaCarState, 'CAMERA_MESSAGES',
|
|
{'SPEED_LIMIT_SIGN': 104}, {'SPEED_LIMIT_SIGN': 1}, 17.8816),
|
|
(ToyotaCarInterface, ToyotaCAR.TOYOTA_RAV4, ToyotaCarState, 'RSA1',
|
|
{'TSGN1': 36, 'SPDVAL1': 45}, {'TSGN1': 35, 'SPDVAL1': 45}, 20.1168),
|
|
(FordCarInterface, FordCAR.FORD_ESCAPE_MK4, FordCarState, 'Traffic_RecognitnData',
|
|
{'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 2},
|
|
{'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 0}, 20.1168),
|
|
(HyundaiCarInterface, HyundaiCAR.HYUNDAI_IONIQ_6, HyundaiCarState, 'FR_CMR_02_100ms',
|
|
{'ISLW_SysSta': 0, 'ISLW_SpdCluMainDis': 72},
|
|
{'ISLW_SysSta': 1, 'ISLW_SpdCluMainDis': 72}, 20.0),
|
|
)
|
|
for interface, fingerprint, state_type, message, signals, unsupported, expected in cases:
|
|
with self.subTest(fingerprint=fingerprint):
|
|
cp = interface.get_non_essential_params(fingerprint)
|
|
state = state_type(cp)
|
|
parsers = state.get_can_parsers(cp)
|
|
subscribed = [(bus, parser) for bus, parser in parsers.items()
|
|
if (definition := parser.dbc.name_to_msg.get(message)) is not None and
|
|
definition.address in parser.message_states]
|
|
self.assertEqual(len(subscribed), 1)
|
|
_, parser = subscribed[0]
|
|
packer = CANPacker(parser.dbc.name)
|
|
for i in range(6):
|
|
timestamp = 1_000_000_000 + i * 300_000_000
|
|
parser.update([timestamp, [packer.make_can_msg(message, parser.bus, {key: float(value) for key, value in signals.items()})]])
|
|
state.update(parsers)
|
|
observation = state.dashboard_limit.observation
|
|
self.assertEqual(observation.status, Status.VALID)
|
|
self.assertAlmostEqual(observation.speed_mps, expected)
|
|
self.assertEqual(observation.observed_ns, timestamp)
|
|
self.assertGreater(observation.valid_until_ns, timestamp)
|
|
parser.update([timestamp + 300_000_000, [packer.make_can_msg(message, parser.bus, {key: float(value) for key, value in unsupported.items()})]])
|
|
state.update(parsers)
|
|
self.assertEqual(state.dashboard_limit.observation.status, Status.UNKNOWN)
|
|
self.assertEqual(state.dashboard_limit.observation.speed_mps, 0.0)
|
|
|
|
def test_real_dbc_packets_and_parser_expiry(self):
|
|
cases = (
|
|
('honda_civic_hatchback_ex_2017_can_generated', 'CAMERA_MESSAGES',
|
|
{'SPEED_LIMIT_SIGN': 104}, 'SPEED_LIMIT_SIGN', lambda: honda_sign(104), 17.8816),
|
|
('toyota_nodsu_pt_generated', 'RSA1', {'TSGN1': 36, 'SPDVAL1': 45},
|
|
'TSGN1', lambda: toyota_sign(36, 45), 20.1168),
|
|
('ford_lincoln_base_pt', 'Traffic_RecognitnData',
|
|
{'TsrVLim1MsgTxt_D_Rq': 45, 'TsrVlUnitMsgTxt_D_Rq': 2},
|
|
'TsrVLim1MsgTxt_D_Rq', lambda: ford_sign(45, 2), 20.1168),
|
|
('hyundai_canfd_generated', 'FR_CMR_02_100ms',
|
|
{'ISLW_SysSta': 0, 'ISLW_SpdCluMainDis': 72},
|
|
'ISLW_SpdCluMainDis', lambda: hyundai_canfd_sign(72, 0, True), 20.0),
|
|
)
|
|
for dbc, message, signals, signal, decode, expected_mps in cases:
|
|
with self.subTest(message=message):
|
|
packer = CANPacker(dbc)
|
|
parser = CANParser(dbc, [(message, math.nan)], 0)
|
|
self.assertEqual(parser_expiry(parser, message, signal), (0, 0))
|
|
for i in range(6):
|
|
timestamp = 1_000_000_000 + i * 300_000_000
|
|
parser.update([timestamp, [packer.make_can_msg(message, 0, {key: float(value) for key, value in signals.items()})]])
|
|
observed, expiry = parser_expiry(parser, message, signal)
|
|
self.assertEqual(observed, timestamp)
|
|
self.assertGreater(expiry, observed)
|
|
self.assertEqual(parser.vl[message][signal], signals[signal])
|
|
kind, value = decode()
|
|
self.assertIs(kind, Status.VALID)
|
|
self.assertAlmostEqual(value, expected_mps)
|
|
|
|
def test_invalid_sign_does_not_preserve_previous_limit_and_reconnects(self):
|
|
tracker = Tracker()
|
|
first = tracker.update(100, *honda_sign(104), valid_until_ns=200)
|
|
self.assertEqual(first.status, Status.VALID)
|
|
unknown = tracker.update(150, *honda_sign(1), valid_until_ns=250)
|
|
self.assertEqual(unknown.status, Status.UNKNOWN)
|
|
self.assertEqual(unknown.speed_mps, 0)
|
|
again = tracker.update(300, *honda_sign(104), valid_until_ns=400)
|
|
self.assertEqual(again.status, Status.VALID)
|
|
self.assertGreater(again.episode, first.episode)
|
|
self.assertEqual(tracker.update(350, Status.VALID, math.inf, valid_until_ns=450), again)
|
|
self.assertEqual(hyundai_canfd_sign(0, 0, True), (Status.ABSENT, 0.0))
|
|
self.assertEqual(hyundai_canfd_sign(253, 0, True), (Status.ABSENT, 0.0))
|
|
self.assertEqual(hyundai_canfd_sign(72, 1, True), (Status.UNKNOWN, 0.0))
|
|
|
|
def test_hyundai_optional_sign_subscription_follows_source_bus(self):
|
|
cp = HyundaiCarInterface.get_non_essential_params(HyundaiCAR.HYUNDAI_IONIQ_6)
|
|
for hda2, expected_bus in ((False, Bus.cam), (True, Bus.pt)):
|
|
with self.subTest(hda2=hda2):
|
|
if hda2:
|
|
cp.flags = int(cp.flags | HyundaiFlags.CANFD_LKA_STEER_MSG)
|
|
parsers = HyundaiCarState(cp).get_can_parsers(cp)
|
|
for bus, parser in parsers.items():
|
|
message = parser.dbc.name_to_msg['FR_CMR_02_100ms']
|
|
subscribed = message.address in parser.message_states
|
|
self.assertEqual(subscribed, bus == expected_bus)
|
|
if subscribed:
|
|
self.assertTrue(parser.message_states[message.address].ignore_alive)
|
|
|
|
|
|
class RuntimeReplayTests(unittest.TestCase):
|
|
def test_card_reserves_optional_slc_before_saved_admission(self):
|
|
from opendbc.car.gm.values import CAR as GM_CAR
|
|
from opendbc.car.gm.tests.test_bolt_cc import params as bolt_params
|
|
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
|
|
from openpilot.cereal import messaging
|
|
for active in (False, True):
|
|
with self.subTest(active=active), OpenpilotPrefix(), mock.patch.dict('os.environ', {
|
|
'SLC_REPLAY_RUNTIME': '0', 'AOL_REPLAY_RUNTIME': '0',
|
|
'CONDITIONAL_MODE_REPLAY_RUNTIME': '0', 'CURVE_REPLAY_RUNTIME': '0',
|
|
}):
|
|
messaging.reset_context()
|
|
params = Params()
|
|
params.put_bool('SpeedLimitController', False, block=True)
|
|
params.put_bool('ShowSpeedLimits', False, block=True)
|
|
params.put('SLCPriority1', 'Dashboard', block=True)
|
|
params.put_bool('DisableOpenpilotLongitudinal', not active, block=True)
|
|
process = TestGmAol.card(bolt_params(GM_CAR.CHEVROLET_BOLT_CC_2018_2021,
|
|
alpha=True, present=True), params)
|
|
self.assertTrue(process.slc_transport_available)
|
|
self.assertFalse(process.slc_replay)
|
|
self.assertFalse(process.slc_control_enabled)
|
|
self.assertIn('slcState', process.sm.data)
|
|
self.assertIn('slcState', process.sm.ignore_alive)
|
|
self.assertIn('slcState', process.sm.ignore_valid)
|
|
self.assertIn('slcState', process.sm.ignore_average_freq)
|
|
self.assertIsNotNone(process.slc_command_sock)
|
|
self.assertIn('slcDashboardObservation', process.pm.sock)
|
|
shared_producer = process.slc_producer_session
|
|
params.put_bool('SpeedLimitController', True, block=True)
|
|
process.slc_requested_configuration = process.read_slc_configuration()
|
|
process.refresh_slc_configuration(1_000_000_000)
|
|
self.assertTrue(process.slc_replay)
|
|
self.assertEqual(process.slc_control_enabled, active)
|
|
self.assertEqual(process.CP.openpilotLongitudinalControl, active)
|
|
self.assertTrue(params.get_bool('SpeedLimitController'))
|
|
process.slc_commands.append(object())
|
|
process.slc_command_session = 'retired-planner-session'
|
|
process.slc_last_command_id = 4
|
|
params.put_bool('SpeedLimitController', False, block=True)
|
|
params.put_bool('ShowSpeedLimits', True, block=True)
|
|
process.slc_requested_configuration = process.read_slc_configuration()
|
|
process.refresh_slc_configuration(2_000_000_000)
|
|
self.assertTrue(process.slc_replay)
|
|
self.assertFalse(process.slc_control_enabled)
|
|
self.assertFalse(process.slc_commands)
|
|
self.assertEqual(process.slc_command_session, '')
|
|
self.assertEqual(process.slc_last_command_id, 0)
|
|
self.assertEqual(process.slc_producer_session, shared_producer)
|
|
params.put('SLCPriority1', 'broken source', block=True)
|
|
process.slc_requested_configuration = process.read_slc_configuration()
|
|
process.refresh_slc_configuration(3_000_000_000)
|
|
self.assertFalse(process.slc_replay)
|
|
self.assertFalse(process.slc_control_enabled)
|
|
|
|
def test_isolated_ipc_submaster_handles_independent_card_and_model_cadences(self):
|
|
from openpilot.cereal import messaging
|
|
with OpenpilotPrefix():
|
|
messaging.reset_context()
|
|
pm = messaging.PubMaster(['carState', 'slcDashboardObservation', 'slcState', 'modelV2'])
|
|
sm = messaging.SubMaster(['carState', 'slcDashboardObservation', 'slcState', 'modelV2'], poll='modelV2')
|
|
base_ns = time.monotonic_ns() - 50_000_000
|
|
|
|
def publish(service, stamp_ns):
|
|
message = messaging.new_message(service)
|
|
message.valid = True
|
|
message.logMonoTime = stamp_ns
|
|
pm.send(service, message)
|
|
return message
|
|
|
|
car = messaging.new_message('carState')
|
|
car.valid = True
|
|
car.logMonoTime = base_ns + 10_000_000
|
|
pm.send('carState', car)
|
|
source = messaging.new_message('slcDashboardObservation')
|
|
source.valid = True
|
|
source.logMonoTime = base_ns
|
|
source.slcDashboardObservation.producerSessionId = 'ipc-card'
|
|
source.slcDashboardObservation.carStateLogMonoTime = base_ns
|
|
source.slcDashboardObservation.status = 'valid'
|
|
source.slcDashboardObservation.speedMps = 25.0
|
|
source.slcDashboardObservation.observedMonoTime = base_ns - 20_000_000
|
|
source.slcDashboardObservation.validUntilMonoTime = base_ns + 1_000_000_000
|
|
source.slcDashboardObservation.episode = 1
|
|
pm.send('slcDashboardObservation', source)
|
|
state = messaging.new_message('slcState')
|
|
state.valid = True
|
|
state.logMonoTime = base_ns + 5_000_000
|
|
state.slcState.sessionId = 'ipc-drive'
|
|
state.slcState.frameMonoTime = base_ns + 5_000_000
|
|
state.slcState.enabled = True
|
|
state.slcState.observationKind = 'valid'
|
|
state.slcState.source = 'dashboard'
|
|
state.slcState.speedLimit = 25.0
|
|
state.slcState.hasPending = True
|
|
state.slcState.pendingSpeedLimit = 25.0
|
|
state.slcState.decisionId = 1
|
|
state.slcState.presentationId = 1
|
|
state.slcState.sourceProducerSessionId = 'ipc-card'
|
|
state.slcState.sourceEpisode = 1
|
|
state.slcState.sourceObservedMonoTime = base_ns - 20_000_000
|
|
state.slcState.sourceValidUntilMonoTime = base_ns + 1_000_000_000
|
|
pm.send('slcState', state)
|
|
publish('modelV2', base_ns + 5_000_000)
|
|
sm.update(100)
|
|
now_ns = time.monotonic_ns()
|
|
self.assertGreater(sm.logMonoTime['carState'], sm.logMonoTime['modelV2'])
|
|
self.assertEqual(sm.logMonoTime['carState'] - sm.logMonoTime['slcDashboardObservation'], 10_000_000)
|
|
self.assertTrue(physical_actions.pending_confirmation(
|
|
sm, now_ns, long_active=True))
|
|
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ipc-drive')
|
|
self.assertEqual(runtime._observations(sm, self.cp, now_ns)[Source.DASHBOARD].kind, ObservationKind.VALID)
|
|
|
|
newer = messaging.new_message('carState')
|
|
newer.valid = True
|
|
newer.logMonoTime = base_ns + 35_000_000
|
|
pm.send('carState', newer)
|
|
publish('modelV2', base_ns + 40_000_000)
|
|
sm.update(100)
|
|
self.assertNotEqual(runtime._observations(sm, self.cp, time.monotonic_ns())[Source.DASHBOARD].kind,
|
|
ObservationKind.VALID)
|
|
|
|
# Card runs at 100 Hz while the planner state runs at 20 Hz. A new TSR
|
|
# packet can outrun the planner; the fresh presented decision still owns confirmation.
|
|
local = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=base_ns - 20_000_000,
|
|
valid_until_ns=base_ns + 1_000_000_000, episode=1)
|
|
qualified = 0
|
|
for tick in range(20):
|
|
if tick in (7, 17):
|
|
local.observed_ns = time.monotonic_ns()
|
|
local.episode += 1
|
|
if tick % 5 == 0:
|
|
stamp = time.monotonic_ns()
|
|
cadence_state = messaging.new_message('slcState')
|
|
cadence_state.valid = True
|
|
cadence_state.logMonoTime = stamp
|
|
snapshot = cadence_state.slcState
|
|
snapshot.frameMonoTime = stamp
|
|
snapshot.sessionId = 'ipc-drive'
|
|
snapshot.enabled = True
|
|
snapshot.observationKind = 'valid'
|
|
snapshot.source = 'dashboard'
|
|
snapshot.speedLimit = 25.0
|
|
snapshot.hasPending = True
|
|
snapshot.pendingSpeedLimit = 25.0
|
|
snapshot.decisionId = 1
|
|
snapshot.presentationId = 1
|
|
snapshot.sourceProducerSessionId = 'ipc-card'
|
|
snapshot.sourceEpisode = local.episode
|
|
snapshot.sourceObservedMonoTime = local.observed_ns
|
|
snapshot.sourceValidUntilMonoTime = local.valid_until_ns
|
|
pm.send('slcState', cadence_state)
|
|
sm.update(0)
|
|
pending = physical_actions.pending_confirmation(sm, time.monotonic_ns(), long_active=True)
|
|
qualified += pending is not None
|
|
time.sleep(0.01)
|
|
self.assertGreaterEqual(qualified, 12)
|
|
|
|
def test_card_consumes_physical_press_through_release_without_replay(self):
|
|
helper = VCruiseHelper(car.CarParams(pcmCruise=False))
|
|
helper.v_cruise_kph = 70.0
|
|
pending = SlcPendingConfirmation('drive', 1, 1)
|
|
press = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])
|
|
held = car.CarState(cruiseState={'available': True})
|
|
release = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)])
|
|
helper.update_v_cruise(press, True, False, pending)
|
|
self.assertEqual(helper.slc_consumed_button.pending, pending)
|
|
self.assertEqual(helper.v_cruise_kph, 70.0)
|
|
for _ in range(55):
|
|
helper.update_v_cruise(held, True, False)
|
|
self.assertEqual(helper.v_cruise_kph, 70.0)
|
|
self.assertIsNone(helper.slc_consumed_button)
|
|
helper.update_v_cruise(release, True, False)
|
|
self.assertEqual(helper.v_cruise_kph, 70.0)
|
|
self.assertFalse(helper.slc_suppressed_buttons)
|
|
helper.update_v_cruise(press, True, False)
|
|
helper.update_v_cruise(release, True, False)
|
|
self.assertGreater(helper.v_cruise_kph, 70.0)
|
|
|
|
helper.v_cruise_kph = 70.0
|
|
helper.update_v_cruise(press, True, False, pending)
|
|
helper.update_v_cruise(held, False, False)
|
|
for _ in range(55):
|
|
helper.update_v_cruise(held, True, False)
|
|
self.assertEqual(helper.v_cruise_kph, 70.0)
|
|
helper.update_v_cruise(release, True, False)
|
|
self.assertEqual(helper.v_cruise_kph, 70.0)
|
|
self.assertFalse(helper.slc_suppressed_buttons)
|
|
|
|
def test_physical_accept_creates_bounded_card_command_and_receipt(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCConfirmation': True,
|
|
'SLCConfirmationHigher': True, 'SLCPriority1': 'Dashboard'})
|
|
runtime = Runtime(settings, session_id='physical-drive')
|
|
self.sm['carState'].vCruise = 60.0
|
|
self.sm['carState'].vCruiseCluster = 60.0
|
|
first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertTrue(first.message.slcState.hasPending)
|
|
self.sm.advance(2_010_000_000)
|
|
initial = runtime.step(self.sm, self.cp, now_ns=2_010_000_000,
|
|
request=Action(runtime.session_id, 1, first.message.slcState.decisionId,
|
|
first.message.slcState.presentationId, 'accept'))
|
|
self.assertTrue(initial.message.slcState.hasAccepted)
|
|
source = self.sm['slcDashboardObservation']
|
|
source.speedMps = 30.0
|
|
source.observedMonoTime = 2_040_000_000
|
|
source.episode = 2
|
|
self.sm.advance(2_050_000_000)
|
|
state = runtime.step(self.sm, self.cp, now_ns=2_050_000_000).message.slcState
|
|
self.assertTrue(state.hasPending)
|
|
self.assertEqual(state.sourceProducerSessionId, 'card-producer')
|
|
self.sm.advance(2_100_000_000)
|
|
accepted = runtime.step(self.sm, self.cp, now_ns=2_100_000_000,
|
|
cruise_event=CruiseEvent(1, 2_100_000_000, 0.0, 0.0, 'accel', 'card-producer',
|
|
2_100_000_000, 'confirmationAccept', runtime.session_id,
|
|
state.decisionId, state.presentationId))
|
|
self.assertEqual(accepted.action_result.status, 'resolved')
|
|
self.assertTrue(accepted.message.slcState.hasAccepted)
|
|
self.assertFalse(accepted.message.slcState.hasPending)
|
|
self.assertEqual(accepted.message.slcState.actionStatus, 'accept')
|
|
self.assertEqual(runtime.ledger.transactions, ())
|
|
self.assertIsNotNone(accepted.command)
|
|
command = accepted.command.slcCruiseCommand
|
|
self.assertLessEqual(command.expiresMonoTime - command.issuedMonoTime, physical_actions.COMMAND_MAX_AGE_NS)
|
|
card_sm = ReplaySM({'slcState': accepted.message.slcState}, 2_100_000_000)
|
|
observation = SimpleNamespace(status=Status.VALID, speed_mps=30.0, observed_ns=2_040_000_000,
|
|
valid_until_ns=9_000_000_000, episode=2)
|
|
current = physical_actions.state_current(card_sm, observation, 'card-producer', 2_100_000_000)
|
|
self.assertIsNotNone(current)
|
|
self.assertTrue(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 65.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=True))
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=False, button_event=False))
|
|
command.commandId += 1
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
command.commandId -= 1
|
|
command.presentationId += 1
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
command.presentationId -= 1
|
|
command.sourceEpisode = 99
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
command.sourceEpisode = 2
|
|
command.expiresMonoTime = 2_099_000_000
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
command.expiresMonoTime = 2_200_000_000
|
|
command.targetMps = math.nan
|
|
self.assertFalse(physical_actions.command_applicable(command, current, observation, 'card-producer',
|
|
2_100_000_000, 60.0 / 3.6,
|
|
long_active=True, button_event=False))
|
|
command.targetMps = 30.0
|
|
self.assertIsNone(physical_actions.state_current(card_sm, observation, 'card-producer', 2_210_000_000))
|
|
# The latest conflated CarState can already contain a newer driver choice
|
|
# when the historical card-owned application receipt reaches plannerd.
|
|
self.sm.advance(2_160_000_000)
|
|
self.sm['carState'].vCruise = 115.0
|
|
self.sm['carState'].vCruiseCluster = 115.0
|
|
def receipt(event_id, observed_ns, previous_mps, selected_mps):
|
|
return CruiseEvent(event_id, observed_ns, previous_mps, selected_mps, 'unknown', 'card-producer',
|
|
2_150_000_000, 'commandApplied', runtime.session_id, state.decisionId,
|
|
state.presentationId, command.commandId)
|
|
wrong_time = runtime.step(self.sm, self.cp, now_ns=2_160_000_000,
|
|
cruise_event=receipt(2, 2_090_000_000, 60.0 / 3.6, 30.0))
|
|
self.assertEqual(wrong_time.message.slcState.commandStatus, 'issued')
|
|
wrong_previous = runtime.step(self.sm, self.cp, now_ns=2_160_000_000,
|
|
cruise_event=receipt(3, 2_150_000_000, 20.0, 30.0))
|
|
self.assertEqual(wrong_previous.message.slcState.commandStatus, 'issued')
|
|
wrong_target = runtime.step(self.sm, self.cp, now_ns=2_160_000_000,
|
|
cruise_event=receipt(4, 2_150_000_000, 60.0 / 3.6, 20.0))
|
|
self.assertEqual(wrong_target.message.slcState.commandStatus, 'issued')
|
|
applied = runtime.step(self.sm, self.cp, now_ns=2_160_000_000,
|
|
cruise_event=receipt(5, 2_150_000_000, 60.0 / 3.6, 30.0))
|
|
self.assertEqual(applied.message.slcState.commandStatus, 'applied')
|
|
self.sm.advance(2_170_000_000)
|
|
newer_driver = runtime.step(self.sm, self.cp, now_ns=2_170_000_000,
|
|
cruise_event=CruiseEvent(6, 2_160_000_000, 30.0, 115.0 / 3.6, 'accel',
|
|
'card-producer', 2_160_000_000))
|
|
self.assertEqual(newer_driver.message.slcState.commandStatus, 'applied')
|
|
|
|
def test_first_action_zero_can_issue_and_apply_command(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationHigher': True})
|
|
runtime = Runtime(settings, session_id='first-action')
|
|
self.sm['carState'].vCruise = 60.0
|
|
self.sm['carState'].vCruiseCluster = 60.0
|
|
pending = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState
|
|
self.assertTrue(pending.hasPending)
|
|
self.sm.advance(2_050_000_000)
|
|
accepted = runtime.step(self.sm, self.cp, now_ns=2_050_000_000,
|
|
cruise_event=CruiseEvent(1, 2_050_000_000, 0.0, 0.0, 'accel', 'card-producer',
|
|
2_050_000_000, 'confirmationAccept', runtime.session_id,
|
|
pending.decisionId, pending.presentationId))
|
|
self.assertIsNotNone(accepted.command)
|
|
self.assertEqual(accepted.command.slcCruiseCommand.actionId, 0)
|
|
self.assertEqual(accepted.message.slcState.commandId, accepted.command.slcCruiseCommand.commandId)
|
|
card_sm = ReplaySM({'slcState': accepted.message.slcState}, 2_050_000_000)
|
|
observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=1_900_000_000,
|
|
valid_until_ns=9_000_000_000, episode=1)
|
|
self.assertTrue(physical_actions.command_applicable(
|
|
accepted.command.slcCruiseCommand, card_sm['slcState'], observation, 'card-producer',
|
|
2_050_000_000, 60.0 / 3.6, long_active=True, button_event=False))
|
|
|
|
def test_late_bounded_card_receipt_supersedes_clock_expiry(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationHigher': True})
|
|
runtime = Runtime(settings, session_id='late-receipt')
|
|
self.sm['carState'].vCruise = 60.0
|
|
self.sm['carState'].vCruiseCluster = 60.0
|
|
pending = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState
|
|
self.sm.advance(2_010_000_000)
|
|
accepted = runtime.step(self.sm, self.cp, now_ns=2_010_000_000,
|
|
cruise_event=CruiseEvent(1, 2_010_000_000, 0.0, 0.0, 'accel', 'card-producer',
|
|
2_010_000_000, 'confirmationAccept', runtime.session_id,
|
|
pending.decisionId, pending.presentationId))
|
|
self.assertIsNotNone(accepted.command)
|
|
command = accepted.command.slcCruiseCommand
|
|
after_expiry = int(command.expiresMonoTime) + 10_000_000
|
|
actual_application = int(command.expiresMonoTime) - 10_000_000
|
|
self.sm.advance(after_expiry)
|
|
result = runtime.step(self.sm, self.cp, now_ns=after_expiry,
|
|
cruise_event=CruiseEvent(2, actual_application, 60.0 / 3.6, 25.0, 'unknown',
|
|
'card-producer', actual_application, 'commandApplied',
|
|
runtime.session_id, pending.decisionId, pending.presentationId,
|
|
command.commandId))
|
|
self.assertEqual(result.message.slcState.commandStatus, 'applied')
|
|
|
|
def test_physical_decel_rejects_without_selected_speed_command(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationLower': True})
|
|
runtime = Runtime(settings, session_id='physical-reject')
|
|
first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertTrue(first.message.slcState.hasPending)
|
|
self.sm.advance(2_010_000_000)
|
|
first = runtime.step(self.sm, self.cp, now_ns=2_010_000_000,
|
|
request=Action(runtime.session_id, 1, first.message.slcState.decisionId,
|
|
first.message.slcState.presentationId, 'accept'))
|
|
self.assertTrue(first.message.slcState.hasAccepted)
|
|
source = self.sm['slcDashboardObservation']
|
|
source.speedMps = 15.0
|
|
source.observedMonoTime = 2_040_000_000
|
|
source.episode = 2
|
|
self.sm.advance(2_050_000_000)
|
|
pending = runtime.step(self.sm, self.cp, now_ns=2_050_000_000).message.slcState
|
|
self.assertTrue(pending.hasPending)
|
|
self.sm.advance(2_100_000_000)
|
|
rejected = runtime.step(self.sm, self.cp, now_ns=2_100_000_000,
|
|
cruise_event=CruiseEvent(1, 2_100_000_000, 0.0, 0.0, 'decel', 'card-producer',
|
|
2_100_000_000, 'confirmationReject', runtime.session_id,
|
|
pending.decisionId, pending.presentationId))
|
|
self.assertEqual(rejected.message.slcState.actionStatus, 'reject')
|
|
self.assertEqual(rejected.result.acceptance.basis, 'driver_reject')
|
|
self.assertEqual(rejected.result.acceptance.state.accepted, first.result.acceptance.state.accepted)
|
|
self.assertFalse(rejected.message.slcState.hasPending)
|
|
self.assertIsNone(rejected.command)
|
|
self.assertEqual(runtime.ledger.transactions, ())
|
|
|
|
def test_identified_ui_adopt_uses_same_card_owned_command_path(self):
|
|
runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ui-adopt-drive')
|
|
self.sm['carState'].vCruise = 60.0
|
|
self.sm['carState'].vCruiseCluster = 60.0
|
|
shown = runtime.step(self.sm, self.cp, now_ns=2_000_000_000).message.slcState
|
|
self.assertTrue(shown.hasAccepted)
|
|
self.sm.advance(2_050_000_000)
|
|
adopted = runtime.step(self.sm, self.cp, now_ns=2_050_000_000,
|
|
request=Action(runtime.session_id, 1, 0, shown.presentationId, 'adopt'))
|
|
self.assertEqual(adopted.message.slcState.actionStatus, 'adopt')
|
|
self.assertIsNotNone(adopted.command)
|
|
self.assertEqual(adopted.command.slcCruiseCommand.presentationId, shown.presentationId)
|
|
self.assertEqual(adopted.command.slcCruiseCommand.actionId, 0)
|
|
self.assertEqual(adopted.command.slcCruiseCommand.decisionId, 0)
|
|
card_sm = ReplaySM({'slcState': adopted.message.slcState}, 2_050_000_000)
|
|
observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=1_900_000_000,
|
|
valid_until_ns=9_000_000_000, episode=1)
|
|
self.assertTrue(physical_actions.command_applicable(
|
|
adopted.command.slcCruiseCommand, card_sm['slcState'], observation, 'card-producer',
|
|
2_050_000_000, 60.0 / 3.6, long_active=True, button_event=False))
|
|
|
|
def test_native_card_applies_one_shot_command_only_with_current_state(self):
|
|
from openpilot.cereal import messaging
|
|
from openpilot.selfdrive.car import card
|
|
now_ns = time.monotonic_ns()
|
|
observation = SimpleNamespace(status=Status.VALID, speed_mps=30.0, observed_ns=now_ns - 20_000_000,
|
|
valid_until_ns=now_ns + 1_000_000_000, episode=4)
|
|
state = messaging.new_message('slcState')
|
|
state.valid = True
|
|
state.logMonoTime = now_ns - 10_000_000
|
|
state.slcState.frameMonoTime = state.logMonoTime
|
|
state.slcState.sessionId = 'card-drive'
|
|
state.slcState.enabled = True
|
|
state.slcState.source = 'dashboard'
|
|
state.slcState.observationKind = 'valid'
|
|
state.slcState.speedLimit = 30.0
|
|
state.slcState.hasAccepted = True
|
|
state.slcState.acceptedSpeedLimit = 30.0
|
|
state.slcState.commandId = 1
|
|
state.slcState.presentationId = 1
|
|
state.slcState.sourceProducerSessionId = 'card-producer'
|
|
state.slcState.sourceEpisode = 4
|
|
state.slcState.sourceObservedMonoTime = observation.observed_ns
|
|
state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command = messaging.new_message('slcCruiseCommand')
|
|
command.valid = True
|
|
command.slcCruiseCommand.kind = 'adoptAcceptedLimit'
|
|
command.slcCruiseCommand.source = 'dashboard'
|
|
command.slcCruiseCommand.sessionId = 'card-drive'
|
|
command.slcCruiseCommand.commandId = 1
|
|
command.slcCruiseCommand.actionId = 1
|
|
command.slcCruiseCommand.presentationId = 1
|
|
command.slcCruiseCommand.sourceProducerSessionId = 'card-producer'
|
|
command.slcCruiseCommand.sourceEpisode = 4
|
|
command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns
|
|
command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command.slcCruiseCommand.issuedMonoTime = now_ns - 10_000_000
|
|
command.slcCruiseCommand.expiresMonoTime = now_ns + 80_000_000
|
|
command.slcCruiseCommand.expectedSelectedMps = 60.0 / 3.6
|
|
command.slcCruiseCommand.targetMps = 30.0
|
|
fake = card.Car.__new__(card.Car)
|
|
fake.conditional_replay = False
|
|
fake.switchback_button_tracker = None
|
|
fake.switchback_settings_owner = None
|
|
from openpilot.starpilot.controllers.wheel_actions import WheelPublisher
|
|
fake.params = Params()
|
|
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
|
fake.vehicle_startup = VehicleStartupOwner()
|
|
fake.wheel_publisher = WheelPublisher(fake.params)
|
|
fake.car_gps_publisher = card.CarGpsPublisher()
|
|
fake.pm = mock.Mock()
|
|
fake.ioniq6_long_prearmed = False
|
|
fake.slc_replay = True
|
|
fake.slc_control_enabled = True
|
|
fake.slc_source_floor_ns = 0
|
|
fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
|
|
fake.slc_requested_configuration = fake.slc_configuration
|
|
fake.aol_card_intent = None
|
|
fake.slc_producer_session = 'card-producer'
|
|
fake.slc_command_sock = object()
|
|
fake.slc_commands = deque(maxlen=8)
|
|
fake.slc_command_session = ''
|
|
fake.slc_last_command_id = 0
|
|
fake.slc_receipts = []
|
|
fake.can_sock = object()
|
|
fake.can_rcv_cum_timeout_counter = 0
|
|
fake.is_metric = False
|
|
fake.CP = car.CarParams(openpilotLongitudinalControl=True, pcmCruise=False)
|
|
fake.CC_prev = car.CarControl(enabled=True)
|
|
fake.v_cruise_helper = VCruiseHelper(fake.CP)
|
|
fake.v_cruise_helper.v_cruise_kph = 60.0
|
|
fake.sm = cast(messaging.SubMaster, ReplaySM(
|
|
{'carControl': car.CarControl(enabled=True, longActive=True), 'slcState': state.slcState,
|
|
'deviceState': SimpleNamespace(startedMonoTime=1)}, now_ns - 10_000_000))
|
|
self.enterContext(mock.patch.object(fake, 'CI', SimpleNamespace(
|
|
CS=SimpleNamespace(dashboard_limit=SimpleNamespace(observation=observation)),
|
|
update=lambda packets: car.CarState(canValid=True, cruiseState={'available': True})), create=True))
|
|
self.enterContext(mock.patch.object(fake, 'RI', SimpleNamespace(update=lambda packets: None), create=True))
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None, command.as_reader(), None])):
|
|
selected, _ = fake.state_update()
|
|
self.assertGreater(selected.vCruise, 60.0)
|
|
self.assertEqual(fake.slc_receipts[0][0], 'commandApplied')
|
|
first_selected = selected.vCruise
|
|
selected_again, _ = fake.state_update()
|
|
self.assertEqual(selected_again.vCruise, first_selected)
|
|
self.assertEqual(fake.slc_receipts, [])
|
|
fake.CC_prev.enabled = False
|
|
fake.CS_prev = car.CarState(vEgo=20.0)
|
|
fake.experimental_mode = False
|
|
fake.initial_cruise_speed = 40
|
|
fake.slc_last_command_id = 0
|
|
fake.slc_command_session = ''
|
|
fake.v_cruise_helper.v_cruise_kph = 60.0
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
edge, _ = fake.state_update()
|
|
self.assertNotEqual(edge.vCruise, first_selected)
|
|
self.assertEqual(fake.slc_receipts[0][0], 'commandRejected')
|
|
|
|
# The command socket can beat the conflated slcState socket by one card tick.
|
|
fake.CC_prev.enabled = True
|
|
fake.slc_last_command_id = 0
|
|
fake.slc_command_session = 'card-drive'
|
|
fake.v_cruise_helper.v_cruise_kph = 60.0
|
|
fake.slc_commands.clear()
|
|
stale_snapshot = messaging.new_message('slcState')
|
|
stale_snapshot.slcState = state.slcState
|
|
stale_snapshot.slcState.commandId = 0
|
|
fake.sm.data['slcState'] = stale_snapshot.slcState
|
|
fake.sm.logMonoTime['slcState'] = command.slcCruiseCommand.issuedMonoTime - 1
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
waiting, _ = fake.state_update()
|
|
self.assertEqual(waiting.vCruise, 60.0)
|
|
self.assertEqual(fake.slc_receipts, [])
|
|
self.assertEqual(len(fake.slc_commands), 1)
|
|
fake.sm.data['slcState'] = state.slcState
|
|
fake.sm.logMonoTime['slcState'] = state.logMonoTime
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
caught_up, _ = fake.state_update()
|
|
self.assertGreater(caught_up.vCruise, 60.0)
|
|
self.assertEqual(fake.slc_receipts[0][0], 'commandApplied')
|
|
|
|
# A saved admission replacement retires only SLC work, not shared producers
|
|
# or the physical suppression needed to consume a confirmation release.
|
|
fake.v_cruise_helper.v_cruise_kph = 60.0
|
|
press = car.CarState(cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])
|
|
release = car.CarState(cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)])
|
|
fake.v_cruise_helper.update_v_cruise(press, True, False, SlcPendingConfirmation('card-drive', 1, 1))
|
|
self.assertTrue(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
|
|
fake.slc_requested_configuration = (False, parse({'ShowSpeedLimits': False}))
|
|
fake.slc_commands.append(command.as_reader())
|
|
floor = time.monotonic_ns()
|
|
fake.refresh_slc_configuration(floor)
|
|
self.assertFalse(fake.slc_commands)
|
|
self.assertEqual(fake.slc_producer_session, 'card-producer')
|
|
self.assertTrue(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
fake.v_cruise_helper.update_v_cruise(release, True, False)
|
|
self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
self.assertEqual(fake.v_cruise_helper.v_cruise_kph, 60.0)
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
disabled, _ = fake.state_update()
|
|
self.assertEqual(disabled.vCruise, 60.0)
|
|
self.assertFalse(fake.slc_commands)
|
|
self.assertEqual(fake.slc_receipts, [])
|
|
|
|
fake.slc_requested_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
|
|
fake.refresh_slc_configuration(floor + 1)
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
retired, _ = fake.state_update()
|
|
self.assertEqual(retired.vCruise, 60.0)
|
|
self.assertFalse(fake.slc_commands)
|
|
fresh_ns = floor + 30_000_000
|
|
observation.observed_ns = floor + 10_000_000
|
|
observation.valid_until_ns = floor + 100_000_000
|
|
state.slcState.frameMonoTime = floor + 20_000_000
|
|
state.slcState.sessionId = 'new-card-drive'
|
|
state.slcState.commandId = 2
|
|
state.slcState.sourceObservedMonoTime = observation.observed_ns
|
|
state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command.slcCruiseCommand.sessionId = 'new-card-drive'
|
|
command.slcCruiseCommand.commandId = 2
|
|
command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns
|
|
command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command.slcCruiseCommand.issuedMonoTime = floor + 20_000_000
|
|
command.slcCruiseCommand.expiresMonoTime = floor + 90_000_000
|
|
fake.sm.data['slcState'] = state.slcState
|
|
fake.sm.logMonoTime['slcState'] = floor + 20_000_000
|
|
fake.sm.logMonoTime['carControl'] = fresh_ns
|
|
with (mock.patch.object(card.time, 'monotonic_ns', return_value=fresh_ns),
|
|
mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
resumed, _ = fake.state_update()
|
|
self.assertGreater(resumed.vCruise, 60.0)
|
|
self.assertEqual(fake.slc_receipts[0][0], 'commandApplied')
|
|
|
|
# A replay Card observes recorded CAN time, not the host process clock.
|
|
replay = card.Car.__new__(card.Car)
|
|
replay.__dict__.update(fake.__dict__)
|
|
replay.slc_source_floor_ns = 0
|
|
replay.slc_configuration = (False, parse({'ShowSpeedLimits': False}))
|
|
replay.slc_requested_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
|
|
replay.slc_commands = deque(maxlen=8)
|
|
replay.v_cruise_helper.v_cruise_kph = 60.0
|
|
recorded_ns, host_ns = 2_000_000_000, 90_000_000_000
|
|
packet = messaging.new_message('can', 1)
|
|
packet.logMonoTime = recorded_ns
|
|
packet.can[0] = {'address': 0x100, 'dat': b'\x00' * 8, 'src': 0}
|
|
with (mock.patch.object(card, 'REPLAY', True),
|
|
mock.patch.object(card.time, 'monotonic_ns', return_value=host_ns),
|
|
mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[packet.to_bytes()]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
replay.state_update()
|
|
self.assertEqual(replay.can_log_mono_time, recorded_ns)
|
|
self.assertEqual(replay.slc_source_floor_ns, recorded_ns)
|
|
self.assertLess(replay.slc_source_floor_ns, host_ns)
|
|
observation.observed_ns = recorded_ns + 10_000_000
|
|
observation.valid_until_ns = recorded_ns + 100_000_000
|
|
state.slcState.frameMonoTime = recorded_ns + 20_000_000
|
|
state.slcState.sourceObservedMonoTime = observation.observed_ns
|
|
state.slcState.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command.slcCruiseCommand.sourceObservedMonoTime = observation.observed_ns
|
|
command.slcCruiseCommand.sourceValidUntilMonoTime = observation.valid_until_ns
|
|
command.slcCruiseCommand.issuedMonoTime = recorded_ns + 20_000_000
|
|
command.slcCruiseCommand.expiresMonoTime = recorded_ns + 90_000_000
|
|
replay.sm.logMonoTime['slcState'] = recorded_ns + 20_000_000
|
|
replay.sm.logMonoTime['carControl'] = recorded_ns + 30_000_000
|
|
packet.logMonoTime = recorded_ns + 30_000_000
|
|
packet.clear_write_flag()
|
|
with (mock.patch.object(card, 'REPLAY', True),
|
|
mock.patch.object(card.time, 'monotonic_ns', return_value=host_ns),
|
|
mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[packet.to_bytes()]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[command.as_reader(), None])):
|
|
replayed, _ = replay.state_update()
|
|
self.assertGreater(replayed.vCruise, 60.0)
|
|
self.assertEqual(replay.slc_receipts[0][0], 'commandApplied')
|
|
self.assertEqual(replay.slc_producer_session, 'card-producer')
|
|
|
|
def _card_with_slc_state(self, pending, observation, now_ns):
|
|
from openpilot.selfdrive.car import card
|
|
fake = card.Car.__new__(card.Car)
|
|
fake.conditional_replay = False
|
|
fake.switchback_button_tracker = None
|
|
fake.switchback_settings_owner = None
|
|
from openpilot.starpilot.controllers.wheel_actions import WheelPublisher
|
|
fake.params = Params()
|
|
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
|
fake.vehicle_startup = VehicleStartupOwner()
|
|
fake.wheel_publisher = WheelPublisher(fake.params)
|
|
fake.car_gps_publisher = card.CarGpsPublisher()
|
|
fake.pm = mock.Mock()
|
|
fake.ioniq6_long_prearmed = False
|
|
fake.slc_replay = True
|
|
fake.slc_control_enabled = True
|
|
fake.slc_source_floor_ns = 0
|
|
fake.slc_configuration = (True, parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
|
|
fake.slc_requested_configuration = fake.slc_configuration
|
|
fake.aol_card_intent = None
|
|
fake.slc_producer_session = 'card-producer'
|
|
fake.slc_command_sock = object()
|
|
fake.slc_commands = deque(maxlen=8)
|
|
fake.slc_command_session = ''
|
|
fake.slc_last_command_id = 0
|
|
fake.slc_receipts = []
|
|
fake.can_sock = object()
|
|
fake.can_rcv_cum_timeout_counter = 0
|
|
fake.is_metric = False
|
|
fake.CP = car.CarParams(openpilotLongitudinalControl=True, pcmCruise=False)
|
|
fake.CC_prev = car.CarControl(enabled=True)
|
|
fake.v_cruise_helper = VCruiseHelper(fake.CP)
|
|
fake.v_cruise_helper.v_cruise_kph = 60.0
|
|
fake.sm = cast(card.messaging.SubMaster, ReplaySM(
|
|
{'carControl': car.CarControl(enabled=True, longActive=True), 'slcState': pending, 'deviceState': SimpleNamespace(startedMonoTime=1)}, now_ns))
|
|
current_cs = [car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])]
|
|
self.enterContext(mock.patch.object(fake, 'CI', SimpleNamespace(
|
|
CS=SimpleNamespace(dashboard_limit=SimpleNamespace(observation=observation) if observation is not None else None),
|
|
update=lambda packets: current_cs[0]), create=True))
|
|
self.enterContext(mock.patch.object(fake, 'RI', SimpleNamespace(update=lambda packets: None), create=True))
|
|
return fake, current_cs
|
|
|
|
def test_wheel_confirmation_all_sources_and_normal_cruise_without_pending(self):
|
|
from openpilot.cereal import messaging
|
|
from openpilot.selfdrive.car import card
|
|
from openpilot.selfdrive.controls.plannerd import current_cruise_event, queue_cruise_event
|
|
from openpilot.starpilot.speed_limits.tests.test_maps_runtime import BOOT, packet
|
|
from openpilot.starpilot.speed_limits.vision.observation import MODEL_ID
|
|
|
|
start = 2_000_000_000
|
|
sources = (('Dashboard', True), ('Maps', True), ('Maps', False), ('Vision', True), ('Vision', False))
|
|
buttons = (('accelCruise', 'accept', 61.0), ('decelCruise', 'reject', 59.0))
|
|
for (source_name, dashboard_present), (button, outcome, normal_speed), has_pending in product(sources, buttons, (True, False)):
|
|
with self.subTest(source=source_name, button=button, pending=has_pending, dashboard=dashboard_present):
|
|
self.sm.advance(start)
|
|
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 60.0
|
|
dashboard = self.sm['slcDashboardObservation']
|
|
dashboard.status, dashboard.producerSessionId, dashboard.episode = 'valid', 'card-producer', 1
|
|
dashboard.speedMps, dashboard.observedMonoTime, dashboard.validUntilMonoTime = 25.0, start - 100_000_000, start + 1_000_000_000
|
|
if source_name == 'Maps':
|
|
self.sm.data['mapdOut'] = packet(speedLimit=25.0).mapdOut
|
|
if source_name == 'Vision':
|
|
vision_event = messaging.new_message('slcVisionObservation')
|
|
vision = vision_event.slcVisionObservation.init('vision')
|
|
vision.producerSessionId, vision.status, vision.stream, vision.modelId = 'camera', 'valid', 'road', MODEL_ID
|
|
vision.speedMps, vision.confidence, vision.supportCount, vision.episode, vision.frameId = 25.0, 0.9, 2, 1, 1
|
|
vision.observedMonoTime, vision.validUntilMonoTime, vision.cameraFrameEofBootTime = start, start + 250_000_000, BOOT
|
|
self.sm.data['slcVisionObservation'] = vision_event.slcVisionObservation
|
|
self.sm.logMonoTime['slcVisionObservation'] = start
|
|
self.sm.logMonoTime['mapdOut'] = BOOT
|
|
for service in self.sm.data:
|
|
for metadata in (self.sm.valid, self.sm.alive, self.sm.updated):
|
|
metadata[service] = True
|
|
settings = parse({'SpeedLimitController': True, 'SLCPriority1': source_name,
|
|
'SLCConfirmation': True, 'SLCConfirmationLower': True, 'SLCConfirmationHigher': True})
|
|
runtime = Runtime(settings, session_id='wheel-drive', vision_enabled=True, vision_control_qualified=True)
|
|
wire = runtime.step(self.sm, self.cp, now_ns=start, now_boot_ns=BOOT).message.slcState
|
|
self.assertTrue(wire.hasPending)
|
|
self.assertEqual(str(wire.source), {'Dashboard': 'dashboard', 'Maps': 'map', 'Vision': 'vision'}[source_name])
|
|
now = start
|
|
if not has_pending:
|
|
now += 10_000_000
|
|
self.sm.advance(now)
|
|
self.sm.logMonoTime['mapdOut'] = BOOT
|
|
wire = runtime.step(self.sm, self.cp, now_ns=now, now_boot_ns=BOOT + now - start,
|
|
request=Action(runtime.session_id, 1, wire.decisionId, wire.presentationId, 'accept')).message.slcState
|
|
self.assertFalse(wire.hasPending)
|
|
evidence = self.sm['slcDashboardObservation']
|
|
observation = (SimpleNamespace(status=Status.VALID, speed_mps=evidence.speedMps, episode=evidence.episode,
|
|
observed_ns=evidence.observedMonoTime, valid_until_ns=evidence.validUntilMonoTime)
|
|
if dashboard_present else None)
|
|
fake, current_cs = self._card_with_slc_state(wire, observation, now)
|
|
fake.is_metric = True
|
|
fake.car_params_published = True
|
|
fake.sm.frame = 1
|
|
fake.sm.all_checks = lambda services: True
|
|
fake.last_actuators_output = car.CarControl.Actuators()
|
|
fake.rk = SimpleNamespace(remaining=0.0)
|
|
fake.aol_replay = False
|
|
fake.slc_cruise_event_id = 0
|
|
fake.radar_publish = mock.Mock()
|
|
clock = [now]
|
|
with (mock.patch.object(card, 'REPLAY', False),
|
|
mock.patch.object(card.time, 'monotonic_ns', side_effect=lambda clock=clock: clock[0]),
|
|
mock.patch.object(card.time, 'monotonic', side_effect=lambda clock=clock: clock[0] / 1e9),
|
|
mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
for pressed in (True, False):
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type=button, pressed=pressed)])
|
|
cs, radar = fake.state_update()
|
|
fake.state_publish(cs, radar)
|
|
clock[0] += 10_000_000
|
|
fake.sm.logMonoTime['carControl'] = clock[0]
|
|
events = [call.args[1] for call in fake.pm.send.call_args_list if call.args[0] == 'slcCruiseEvent']
|
|
self.assertEqual(len(events), 1)
|
|
event = events[0].slcCruiseEvent
|
|
self.assertEqual(str(event.kind), ('confirmationAccept' if outcome == 'accept' else 'confirmationReject')
|
|
if has_pending else 'driverChange')
|
|
self.assertEqual(fake.v_cruise_helper.v_cruise_kph, 60.0 if has_pending else normal_speed)
|
|
self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
slc_events, curve_events = deque(), deque()
|
|
queue_cruise_event(events[0], slc_events, curve_events)
|
|
self.sm.advance(clock[0])
|
|
self.sm.logMonoTime['mapdOut'] = BOOT
|
|
publications = {call.args[0]: call.args[1] for call in fake.pm.send.call_args_list}
|
|
for service in ('carState', 'slcDashboardObservation'):
|
|
self.sm.data[service] = getattr(publications[service], service)
|
|
self.sm.logMonoTime[service] = int(publications[service].logMonoTime)
|
|
result = runtime.step(self.sm, self.cp, now_ns=clock[0], now_boot_ns=BOOT + clock[0] - start,
|
|
cruise_event=current_cruise_event(slc_events, clock[0]))
|
|
self.assertFalse(result.message.slcState.hasPending)
|
|
if has_pending:
|
|
self.assertEqual(result.message.slcState.actionStatus, outcome)
|
|
|
|
def test_card_runtime_consumed_press_hold_command_release(self):
|
|
from openpilot.selfdrive.car import card
|
|
now_ns = time.monotonic_ns()
|
|
source = self.sm['slcDashboardObservation']
|
|
source.producerSessionId = 'card-producer'
|
|
source.observedMonoTime = now_ns - 20_000_000
|
|
source.validUntilMonoTime = now_ns + 1_000_000_000
|
|
source.carStateLogMonoTime = now_ns
|
|
self.sm.advance(now_ns)
|
|
self.sm['carState'].vCruise = 60.0
|
|
self.sm['carState'].vCruiseCluster = 60.0
|
|
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True,
|
|
'SLCConfirmationHigher': True}), session_id='held-press-drive')
|
|
pending = runtime.step(self.sm, self.cp, now_ns=now_ns).message.slcState
|
|
self.assertTrue(pending.hasPending)
|
|
observation = SimpleNamespace(status=Status.VALID, speed_mps=25.0, observed_ns=source.observedMonoTime,
|
|
valid_until_ns=source.validUntilMonoTime, episode=1)
|
|
fake, current_cs = self._card_with_slc_state(pending, observation, now_ns)
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
pressed, _ = fake.state_update()
|
|
self.assertEqual(pressed.vCruise, 60.0)
|
|
consumed = fake.v_cruise_helper.slc_consumed_button
|
|
self.assertIsNotNone(consumed)
|
|
self.assertEqual(consumed.pending.decision_id, pending.decisionId)
|
|
|
|
press_ns = time.monotonic_ns()
|
|
self.sm.advance(press_ns)
|
|
accepted = runtime.step(self.sm, self.cp, now_ns=press_ns,
|
|
cruise_event=CruiseEvent(1, press_ns, 0.0, 0.0, 'accel', 'card-producer', press_ns,
|
|
'confirmationAccept', runtime.session_id,
|
|
pending.decisionId, pending.presentationId))
|
|
self.assertIsNotNone(accepted.command)
|
|
self.assertEqual(accepted.command.slcCruiseCommand.actionId, 0)
|
|
fake.sm.data['slcState'] = accepted.message.slcState
|
|
fake.sm.logMonoTime['slcState'] = press_ns
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True})
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[accepted.command.as_reader(), None])):
|
|
applied, _ = fake.state_update()
|
|
self.assertGreater(applied.vCruise, 60.0)
|
|
self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandApplied'])
|
|
applied_speed = applied.vCruise
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)])
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
released, _ = fake.state_update()
|
|
self.assertEqual(released.vCruise, applied_speed)
|
|
self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
self.assertEqual(fake.slc_receipts, [])
|
|
|
|
def accept_next_limit(speed_mps, episode):
|
|
stamp = time.monotonic_ns()
|
|
source.speedMps = speed_mps
|
|
source.observedMonoTime = stamp
|
|
source.validUntilMonoTime = stamp + 1_000_000_000
|
|
source.episode = episode
|
|
observation.speed_mps = speed_mps
|
|
observation.observed_ns = stamp
|
|
observation.valid_until_ns = source.validUntilMonoTime
|
|
observation.episode = episode
|
|
self.sm['carState'].vCruise = fake.v_cruise_helper.v_cruise_kph
|
|
self.sm['carState'].vCruiseCluster = fake.v_cruise_helper.v_cruise_kph
|
|
self.sm.advance(stamp)
|
|
next_pending = runtime.step(self.sm, self.cp, now_ns=stamp).message.slcState
|
|
self.assertTrue(next_pending.hasPending)
|
|
fake.sm.data['slcState'] = next_pending
|
|
fake.sm.logMonoTime['slcState'] = stamp
|
|
fake.sm.logMonoTime['carControl'] = stamp
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', return_value=None)):
|
|
fake.state_update()
|
|
accepted_ns = time.monotonic_ns()
|
|
self.sm.advance(accepted_ns)
|
|
next_accepted = runtime.step(self.sm, self.cp, now_ns=accepted_ns,
|
|
cruise_event=CruiseEvent(episode, accepted_ns, 0.0, 0.0, 'accel', 'card-producer',
|
|
accepted_ns, 'confirmationAccept', runtime.session_id,
|
|
next_pending.decisionId, next_pending.presentationId))
|
|
self.assertIsNotNone(next_accepted.command)
|
|
fake.sm.data['slcState'] = next_accepted.message.slcState
|
|
fake.sm.logMonoTime['slcState'] = accepted_ns
|
|
return next_accepted.command
|
|
|
|
release_command = accept_next_limit(30.0, 2)
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=False)])
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[release_command.as_reader(), None])):
|
|
same_tick_release, _ = fake.state_update()
|
|
self.assertGreater(same_tick_release.vCruise, applied_speed)
|
|
self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandApplied'])
|
|
self.assertFalse(fake.v_cruise_helper.slc_suppressed_buttons)
|
|
|
|
unrelated_command = accept_next_limit(35.0, 3)
|
|
before_unrelated = fake.v_cruise_helper.v_cruise_kph
|
|
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
|
buttonEvents=[car.CarState.ButtonEvent(type='decelCruise', pressed=True)])
|
|
with (mock.patch.object(card.messaging, 'drain_sock_raw', return_value=[]),
|
|
mock.patch.object(card.messaging, 'recv_one_or_none', side_effect=[unrelated_command.as_reader(), None])):
|
|
blocked, _ = fake.state_update()
|
|
self.assertAlmostEqual(blocked.vCruise, before_unrelated, delta=1e-4)
|
|
self.assertEqual([receipt[0] for receipt in fake.slc_receipts], ['commandRejected'])
|
|
|
|
def test_malformed_saved_priorities_disable_runtime_without_crashing(self):
|
|
for key in ('SLCPriority1', 'SLCPriority2'):
|
|
for malformed in ([], {}, 1, None):
|
|
with self.subTest(key=key, malformed=malformed):
|
|
settings = parse({'SpeedLimitController': True, key: malformed})
|
|
self.assertFalse(settings.enabled)
|
|
self.assertTrue(settings.errors)
|
|
|
|
def test_actual_plannerd_loop_publishes_same_tick_state_and_plan(self):
|
|
from openpilot.selfdrive.controls import plannerd
|
|
from openpilot.cereal import messaging
|
|
|
|
class StopReplay(Exception):
|
|
pass
|
|
|
|
class Publisher:
|
|
def __init__(self, services):
|
|
self.services = services
|
|
self.sent = []
|
|
|
|
def send(self, service, message):
|
|
self.sent.append((service, message))
|
|
|
|
self.sm.data['deviceState'] = messaging.new_message('deviceState').deviceState
|
|
self.sm['deviceState'].startedMonoTime = 1_000_000_000
|
|
self.sm.valid['deviceState'] = True
|
|
self.sm.updated['deviceState'] = True
|
|
self.sm.alive['deviceState'] = True
|
|
self.sm.logMonoTime['deviceState'] = 2_000_000_000
|
|
self.sm.frame = 0
|
|
self.sm.all_checks = lambda services=None: True
|
|
calls = 0
|
|
|
|
def update(timeout=100):
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls > 1:
|
|
raise StopReplay
|
|
self.sm.logMonoTime['modelV2'] = 2_010_000_000
|
|
self.sm.logMonoTime['carState'] = 2_010_000_000 # latest CarState arrives one card tick ahead of source
|
|
|
|
publisher = Publisher([])
|
|
with tempfile.TemporaryDirectory() as directory, ExitStack() as resources:
|
|
params = Params(directory)
|
|
# Drain native asynchronous writes before deleting their temporary directory.
|
|
resources.callback(params._finalizer)
|
|
params.put('CarParams', self.cp.to_bytes(), block=True)
|
|
params.put_bool('SpeedLimitController', True, block=True)
|
|
params.put('SLCPriority1', 'Dashboard', block=True)
|
|
with (mock.patch.object(self.sm, 'update', side_effect=update),
|
|
mock.patch.object(plannerd, 'Params', return_value=params),
|
|
mock.patch.object(plannerd, 'config_realtime_process'),
|
|
mock.patch.object(plannerd.messaging, 'SubMaster', return_value=self.sm),
|
|
mock.patch.object(plannerd.messaging, 'PubMaster', return_value=publisher),
|
|
mock.patch.object(plannerd.messaging, 'sub_sock', return_value=object()),
|
|
mock.patch.object(plannerd.messaging, 'recv_one_or_none', return_value=None),
|
|
mock.patch.dict('os.environ', {'SLC_REPLAY_RUNTIME': '1', 'REPLAY': '1'})):
|
|
with self.assertRaises(StopReplay):
|
|
plannerd.main()
|
|
services = [service for service, _ in publisher.sent]
|
|
self.assertIn('slcState', services)
|
|
self.assertIn('longitudinalPlan', services)
|
|
state = next(message.slcState for service, message in publisher.sent if service == 'slcState')
|
|
self.assertFalse(state.hasCeiling)
|
|
self.assertTrue(state.hasAccepted)
|
|
self.assertFalse(state.hasPending)
|
|
self.assertFalse(state.automaticAdoptionPending)
|
|
self.assertEqual(state.observationKind, 'valid')
|
|
self.assertEqual(state.frameMonoTime, 2_010_000_000)
|
|
|
|
def test_stored_history_is_schema_bound_and_not_reused_for_new_dashboard_episode(self):
|
|
candidate = acc.Candidate('dashboard', acc.ObservationIdentity(acc.IdentityKind.PRODUCER_EPISODE,
|
|
value='drive:card:car:1'), 20.0)
|
|
raw = history.encode(acc.AcceptedLimit(candidate, 'drive', 2), 1000)
|
|
self.assertIn('source_schema', json.loads(raw))
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
params.put('SLCQualifiedHistory', json.loads(raw), block=True)
|
|
self.assertEqual(params.get('SLCQualifiedHistory')['source_schema'], json.loads(raw)['source_schema'])
|
|
self.assertIsNone(history.decode(params.get('SLCQualifiedHistory'), session_id='next-drive',
|
|
current=acc.Observation(acc.ObservationKind.VALID, candidate),
|
|
now_wall_ns=1000, max_age_ns=1_000_000))
|
|
self.assertIsNone(history.decode(raw, session_id='next-drive',
|
|
current=acc.Observation(acc.ObservationKind.VALID, candidate),
|
|
now_wall_ns=1000, max_age_ns=1_000_000))
|
|
tampered = json.loads(raw)
|
|
tampered['source_schema'] = 'wrong'
|
|
self.assertIsNone(history.decode(json.dumps(tampered), session_id='next-drive',
|
|
current=acc.Observation(acc.ObservationKind.VALID, candidate),
|
|
now_wall_ns=1000, max_age_ns=1_000_000))
|
|
|
|
def test_native_saved_settings_default_off_and_preserve_choices(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
fresh = read_params(params)
|
|
self.assertFalse(fresh.enabled)
|
|
params.put_bool('SpeedLimitController', True, block=True)
|
|
params.put_bool('SLCConfirmation', True, block=True)
|
|
params.put_bool('SLCConfirmationLower', True, block=True)
|
|
params.put('SLCFallback', 2, block=True)
|
|
params.put('SLCPriority1', 'Map Data', block=True)
|
|
params.put('SLCPriority2', 'Dashboard', block=True)
|
|
params.put('Offset3', 5.0, block=True)
|
|
restored = read_params(params)
|
|
self.assertTrue(restored.enabled)
|
|
self.assertTrue(restored.acceptance.confirm_lower)
|
|
self.assertTrue(restored.acceptance.fallback_previous)
|
|
self.assertEqual(restored.selection.slots[0], Source.MAP)
|
|
self.assertEqual(restored.selection.slots[1], Source.DASHBOARD)
|
|
self.assertFalse(restored.errors)
|
|
|
|
def accepted_runtime(self, settings, *, session_id):
|
|
runtime = Runtime(settings, session_id=session_id)
|
|
candidate = runtime._observations(self.sm, self.cp, 2_000_000_000)[Source.DASHBOARD].candidate
|
|
assert candidate is not None
|
|
accepted = acc.AcceptedLimit(candidate, session_id, 0)
|
|
runtime.state = replace(runtime.state, acceptance=replace(acc.new_session(session_id, accepted), received_speed_mps=candidate.speed_mps))
|
|
return runtime
|
|
|
|
def setUp(self):
|
|
self.cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
self.assertTrue(self.cp.openpilotLongitudinalControl)
|
|
data, self.envelopes = messages()
|
|
from openpilot.cereal import messaging
|
|
data['slcDashboardObservation'] = messaging.new_message('slcDashboardObservation').slcDashboardObservation
|
|
self.sm = ReplaySM(data, 2_000_000_000)
|
|
self.sm['carState'].vCruiseCluster = 100.0
|
|
self.sm['carState'].vEgoCluster = 20.0
|
|
self.sm['carControl'].enabled = True
|
|
self.sm['carControl'].longActive = True
|
|
self.sm['carControl'].latActive = False
|
|
source = self.sm['slcDashboardObservation']
|
|
source.producerSessionId = 'card-producer'
|
|
source.carStateLogMonoTime = 2_000_000_000
|
|
source.status = 'valid'
|
|
source.speedMps = 25.0
|
|
source.observedMonoTime = 1_900_000_000
|
|
source.validUntilMonoTime = 9_000_000_000
|
|
source.episode = 1
|
|
|
|
def test_saved_choice_and_native_planner_replay(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCFallback': 2, 'SLCPriority1': 'Dashboard'})
|
|
runtime = self.accepted_runtime(settings, session_id='replay-drive')
|
|
planner = LongitudinalPlanner(self.cp, init_v=20.0)
|
|
first = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertEqual(first.message.slcState.observationKind, 'valid')
|
|
self.assertFalse(first.message.slcState.hasCeiling)
|
|
self.assertIsNone(first.result.ceiling)
|
|
planner.update(self.sm, cruise_ceiling=first.result.ceiling)
|
|
self.assertEqual(planner.last_cruise_ceiling_status, 'absent')
|
|
|
|
self.sm['carState'].vCruise = 105.0
|
|
self.sm['carState'].vCruiseCluster = 105.0
|
|
self.sm.advance(2_050_000_000)
|
|
button = runtime.step(self.sm, self.cp, now_ns=2_050_000_000,
|
|
cruise_event=CruiseEvent(1, 2_010_000_000, 100 / 3.6, 105 / 3.6, 'accel',
|
|
'replay-card', 2_020_000_000))
|
|
self.assertFalse(button.result.errors)
|
|
self.assertTrue(button.result.override.event_receipt.consumed)
|
|
self.assertEqual(button.result.override.event_receipt.status, 'driver_intent')
|
|
|
|
self.sm.advance(9_100_000_000)
|
|
stale = runtime.step(self.sm, self.cp, now_ns=9_100_000_000)
|
|
self.assertEqual(runtime._observations(self.sm, self.cp, 9_100_000_000)[Source.DASHBOARD].kind, ObservationKind.STALE)
|
|
self.assertEqual(stale.message.slcState.observationKind, 'stale') # both configured slots use the expired dashboard
|
|
self.assertFalse(stale.message.slcState.hasCeiling)
|
|
self.assertEqual(stale.result.acceptance.basis, 'previous_accepted')
|
|
self.assertEqual(stale.result.override.contribution_mps, 105 / 3.6)
|
|
planner.update(self.sm, cruise_ceiling=stale.result.ceiling)
|
|
self.assertEqual(planner.last_cruise_ceiling_status, 'absent')
|
|
self.assertIsNone(stale.result.ceiling)
|
|
for sequence, kind, previous, selected in ((2, 'accel', 105, 110), (3, 'decel', 110, 100)):
|
|
now = 9_100_000_000 + sequence * 50_000_000
|
|
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = float(selected)
|
|
self.sm.advance(now)
|
|
adjusted = runtime.step(self.sm, self.cp, now_ns=now,
|
|
cruise_event=CruiseEvent(sequence, now - 40_000_000, previous / 3.6, selected / 3.6, kind,
|
|
'replay-card', now - 30_000_000))
|
|
self.assertFalse(adjusted.result.errors)
|
|
self.assertFalse(adjusted.message.slcState.hasPending)
|
|
self.assertEqual(adjusted.result.override.event_receipt.status, 'driver_intent')
|
|
self.assertEqual(adjusted.result.override.contribution_mps, selected / 3.6)
|
|
self.assertEqual(adjusted.result.acceptance.state.accepted, first.result.acceptance.state.accepted)
|
|
|
|
def test_decrease_keeps_lower_slc_ceiling_in_native_planner(self):
|
|
mph_to_mps = 0.44704
|
|
self.sm['slcDashboardObservation'].speedMps = 45 * mph_to_mps
|
|
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 70 * mph_to_mps * 3.6
|
|
runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='decrease-drive')
|
|
runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
previous_mps = self.sm['carState'].vCruise / 3.6
|
|
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 69 * mph_to_mps * 3.6
|
|
self.sm.advance(2_050_000_000)
|
|
result = runtime.step(self.sm, self.cp, now_ns=2_050_000_000,
|
|
cruise_event=CruiseEvent(1, 2_010_000_000, previous_mps, self.sm['carState'].vCruise / 3.6, 'decel',
|
|
'replay-card', 2_020_000_000))
|
|
self.assertFalse(result.result.errors)
|
|
self.assertTrue(result.result.override.event_receipt.consumed)
|
|
self.assertAlmostEqual(result.result.override.state.persistent_selected_mps, self.sm['carState'].vCruise / 3.6)
|
|
self.assertTrue(result.message.slcState.driverOverrideActive)
|
|
self.assertIsNone(result.result.ceiling)
|
|
planner = LongitudinalPlanner(self.cp, init_v=45 * mph_to_mps)
|
|
planner.update(self.sm, cruise_ceiling=result.result.ceiling)
|
|
self.assertEqual(planner.last_cruise_ceiling_status, 'absent')
|
|
|
|
def _runtime_with_set_speed_override(self, lateral):
|
|
self.setUp()
|
|
self.sm['carControl'].latActive = lateral
|
|
runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='pedal-drive')
|
|
runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 105.0
|
|
self.sm.advance(2_050_000_000)
|
|
armed = runtime.step(self.sm, self.cp, now_ns=2_050_000_000,
|
|
cruise_event=CruiseEvent(1, 2_010_000_000, 100 / 3.6, 105 / 3.6, 'accel', 'replay-card', 2_020_000_000))
|
|
self.assertEqual(armed.result.override.contribution_mps, 105 / 3.6)
|
|
return runtime
|
|
|
|
def test_accelerator_release_restores_saved_override_after_long_hold(self):
|
|
for lateral, delayed_resume in product((False, True), (False, True)):
|
|
with self.subTest(lateral=lateral, delayed_resume=delayed_resume):
|
|
runtime = self._runtime_with_set_speed_override(lateral)
|
|
self.sm['carState'].gasPressed = True
|
|
self.sm['carState'].vEgo = self.sm['carState'].vEgoCluster = 32.0
|
|
self.sm['carControl'].longActive = False
|
|
self.sm['controlsState'].longControlState = 'off'
|
|
for now in (2_100_000_000, 3_100_000_000, 5_100_000_000):
|
|
self.sm.advance(now)
|
|
held = runtime.step(self.sm, self.cp, now_ns=now)
|
|
self.assertFalse(held.result.errors)
|
|
self.assertIsNone(held.result.ceiling)
|
|
self.assertEqual(held.result.override.contribution_mps, 105 / 3.6)
|
|
self.assertIsNotNone(held.result.override.context)
|
|
self.assertEqual(held.result.state.override.persistent_selected_mps, 105 / 3.6)
|
|
self.assertTrue(held.message.slcState.retainedOverride)
|
|
self.assertTrue(held.result.acceptance.authority.accelerator_override)
|
|
self.assertFalse(held.result.acceptance.authority.longitudinal_active)
|
|
self.sm['carState'].gasPressed = False
|
|
if delayed_resume:
|
|
self.sm.advance(5_200_000_000)
|
|
waiting = runtime.step(self.sm, self.cp, now_ns=5_200_000_000)
|
|
self.assertIsNone(waiting.result.ceiling)
|
|
self.assertEqual(waiting.result.state.override.persistent_selected_mps, 105 / 3.6)
|
|
self.sm['carControl'].longActive = True
|
|
self.sm['controlsState'].longControlState = 'pid'
|
|
self.sm.advance(5_300_000_000)
|
|
resumed = runtime.step(self.sm, self.cp, now_ns=5_300_000_000)
|
|
self.assertFalse(resumed.result.errors)
|
|
self.assertEqual(resumed.result.override.basis, 'persistent')
|
|
self.assertIsNone(resumed.result.ceiling)
|
|
|
|
def test_disengagement_preserves_manual_set_speed_without_an_slc_ceiling(self):
|
|
for lateral, pressed, disabled_service in product((False, True), (False, True), ('selfdriveState', 'carControl')):
|
|
with self.subTest(lateral=lateral, pressed=pressed, disabled_service=disabled_service):
|
|
runtime = self._runtime_with_set_speed_override(lateral)
|
|
self.sm['carState'].gasPressed = pressed
|
|
self.sm['carControl'].longActive = False
|
|
self.sm['controlsState'].longControlState = 'off'
|
|
self.sm[disabled_service].enabled = False
|
|
self.sm.advance(2_100_000_000)
|
|
disengaged = runtime.step(self.sm, self.cp, now_ns=2_100_000_000)
|
|
self.assertIsNone(disengaged.result.ceiling)
|
|
self.assertEqual(disengaged.result.state.override.persistent_selected_mps, 105 / 3.6)
|
|
self.sm[disabled_service].enabled = True
|
|
self.sm['carState'].gasPressed = False
|
|
self.sm['carControl'].longActive = True
|
|
self.sm['controlsState'].longControlState = 'pid'
|
|
self.sm.advance(2_200_000_000)
|
|
resumed = runtime.step(self.sm, self.cp, now_ns=2_200_000_000)
|
|
self.assertEqual(resumed.result.override.contribution_mps, 105 / 3.6)
|
|
self.assertIsNone(resumed.result.ceiling)
|
|
|
|
def test_stale_engagement_suppresses_commands_and_keeps_the_manual_choice(self):
|
|
for lateral, service in product((False, True), ('carState', 'carControl', 'controlsState', 'selfdriveState')):
|
|
with self.subTest(lateral=lateral, service=service):
|
|
runtime = self._runtime_with_set_speed_override(lateral)
|
|
self.sm['carState'].gasPressed = True
|
|
self.sm['carControl'].longActive = False
|
|
self.sm['controlsState'].longControlState = 'off'
|
|
self.sm.advance(2_100_000_000)
|
|
self.sm.valid[service] = False
|
|
stale = runtime.step(self.sm, self.cp, now_ns=2_100_000_000)
|
|
self.assertIsNone(stale.result.ceiling)
|
|
self.assertEqual(stale.result.state.override.persistent_selected_mps, 105 / 3.6)
|
|
|
|
def test_pending_action_receipt_and_stock_gate(self):
|
|
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True, 'SLCConfirmationLower': True})
|
|
runtime = Runtime(settings, session_id='receipt-drive')
|
|
runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
source = self.sm['slcDashboardObservation']
|
|
source.speedMps = 15.0
|
|
source.observedMonoTime = 2_040_000_000
|
|
source.validUntilMonoTime = 9_000_000_000
|
|
source.episode = 2
|
|
self.sm.advance(2_050_000_000)
|
|
pending = runtime.step(self.sm, self.cp, now_ns=2_050_000_000)
|
|
self.assertTrue(pending.message.slcState.hasPending)
|
|
self.sm.advance(2_100_000_000)
|
|
request = Action(runtime.session_id, 17, pending.message.slcState.decisionId, pending.message.slcState.presentationId, 'reject')
|
|
rejected = runtime.step(self.sm, self.cp, now_ns=2_100_000_000, request=request)
|
|
self.assertEqual(rejected.action_result.status, 'resolved')
|
|
self.assertEqual(rejected.message.slcState.actionSequenceId, 17)
|
|
self.assertEqual(rejected.message.slcState.actionStatus, 'reject')
|
|
|
|
self.cp.openpilotLongitudinalControl = False
|
|
self.sm.advance(2_150_000_000)
|
|
stock = runtime.step(self.sm, self.cp, now_ns=2_150_000_000)
|
|
self.assertFalse(stock.message.slcState.hasCeiling)
|
|
self.assertIsNone(stock.result.ceiling)
|
|
|
|
def test_source_requires_matching_car_frame_and_restart_changes_identity(self):
|
|
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='paired-drive')
|
|
source = self.sm['slcDashboardObservation']
|
|
source.carStateLogMonoTime = 1_950_000_000
|
|
mismatched = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertEqual(runtime._observations(self.sm, self.cp, 2_000_000_000)[Source.DASHBOARD].kind,
|
|
ObservationKind.UNKNOWN)
|
|
self.assertFalse(mismatched.message.slcState.hasCeiling)
|
|
|
|
source.carStateLogMonoTime = 2_000_000_000
|
|
matched = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
first_identity = matched.result.selection.observation.candidate.observation_identity.value
|
|
self.assertTrue(matched.message.slcState.hasAccepted)
|
|
self.assertFalse(matched.message.slcState.hasPending)
|
|
self.sm.logMonoTime['carState'] = 2_010_000_000 # independent sockets deliver the next card tick first
|
|
lagged = runtime.step(self.sm, self.cp, now_ns=2_010_000_000)
|
|
self.assertTrue(lagged.message.slcState.hasAccepted)
|
|
self.assertFalse(lagged.message.slcState.hasPending)
|
|
source.producerSessionId = 'restarted-card'
|
|
restarted = runtime.step(self.sm, self.cp, now_ns=2_010_000_000)
|
|
second_identity = restarted.result.selection.observation.candidate.observation_identity.value
|
|
self.assertNotEqual(first_identity, second_identity)
|
|
|
|
def test_lateral_authority_uses_car_control_axis(self):
|
|
runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='axis-drive')
|
|
self.sm['selfdriveState'].active = True
|
|
long_only = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertEqual(long_only.result.acceptance.authority.mode.value, 'longitudinal_only')
|
|
self.sm['carControl'].latActive = True
|
|
combined = runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
self.assertEqual(combined.result.acceptance.authority.mode.value, 'combined')
|
|
|
|
def test_rejected_and_malformed_ui_requests_do_not_fill_action_ledger(self):
|
|
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='many-actions')
|
|
runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
|
wrong = Action(runtime.session_id, 1, 0, 99999, 'adopt')
|
|
runtime.step(self.sm, self.cp, now_ns=2_000_000_000, request=wrong)
|
|
self.assertEqual(runtime.ledger.transactions, ())
|
|
self.assertEqual(runtime.ledger.last_action_id, -1)
|
|
for sequence in range(2, 202):
|
|
timestamp = 2_000_000_000 + sequence
|
|
self.sm.advance(timestamp)
|
|
runtime.step(self.sm, self.cp, now_ns=timestamp,
|
|
request=Action(runtime.session_id, sequence, 99999, 0, 'reject'))
|
|
self.assertFalse(runtime.ledger.reset_required)
|
|
self.assertEqual(runtime.ledger.transactions, ())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|