Remove unnecessary 3-second wait from automatic SLC adoption

This commit is contained in:
firestarsdog
2026-10-26 00:50:59 -04:00
parent 632fa39562
commit 6a07f58ea6
15 changed files with 101 additions and 89 deletions
@@ -55,7 +55,7 @@ DESCRIPTIONS = {
'curve_reset': 'Deletes saved cornering learning so Curve Speed Controller can learn your preferences again.',
'SpeedLimitController': 'Adjusts the set speed to accepted speed limits using your source priority and offsets.',
'ShowSpeedLimits': 'Shows recognized speed-limit signs on the driving screen.',
'SLCConfirmation': 'Auto adopts after a 3-second flash. With Automatic adoption off, + accepts and − declines within 10 seconds.',
'SLCConfirmation': 'Auto adopts new limits immediately. With Automatic adoption off, + accepts and − declines within 10 seconds.',
'SLCPriority1': 'Chooses the first source checked for a fresh speed limit.',
'SLCPriority2': 'Chooses the source used when the primary source has no fresh speed limit.',
'SLCFallback': 'Keeps the previously accepted speed limit when sources are unavailable, until another limit is accepted.',
@@ -453,7 +453,7 @@ class SettingsGatewayTest(unittest.TestCase):
self.assertNotIn("require your confirmation", primary["reason"])
confirmation = next(row for row in rows if row["label"] == "Automatic adoption")
self.assertEqual(confirmation["value"], "On")
self.assertIn("3-second flash", confirmation["reason"])
self.assertIn("immediately", confirmation["reason"])
self.assertIn("accepts", confirmation["reason"])
with mock.patch.dict(os.environ, {"SLC_REPLAY_RUNTIME": "0", "SLC_VISION_DEVELOPMENT": "0"}):
@@ -9,8 +9,8 @@ confirmation presses, duplicated effects and foreign or obsolete contexts.
Override is presentation state; it never adds another planner target. Card's set
speed remains the sole cruise target. The widget crosses out its latest source
limit while override is active. A new limit still follows the three-second automatic
or ten-second manual adoption policy. Acceptance clears the old override. Returning
limit while override is active. A new limit is adopted immediately in automatic mode
or follows the ten-second manual confirmation policy. Acceptance clears the old override. Returning
the selected speed to the adopted adjusted limit also clears it.
Accelerator use neither creates an override nor cancels a preexisting set-speed
+4 -4
View File
@@ -15,8 +15,8 @@ numeric limit does not start another decision. A decline remains effective acros
a source gap or same-value provider change; a later different limit starts a new
window, including when an earlier numeric limit returns.
With Automatic adoption on, the widget slowly flashes for three seconds before
adoption. With it off, the stationary widget pulses purple twice slowly (700 ms
With Automatic adoption on, qualified new limits are adopted immediately.
With it off, the stationary widget pulses purple twice slowly (700 ms
each), rests for one second, pulses twice quickly (300 ms each), and rests for one
second before repeating. Each pair has a 200 ms gap. The arrow remains relative
to vehicle speed; Vienna numerals stay black while their ring pulses.
@@ -92,8 +92,8 @@ decision or manual override. Applied commands are not repeated.
Declining or timing out a returned limit never revives an earlier adoption command;
the historical adoption remains recorded independently of the new rejection.
Transport changes append to reserved custom messages. `SlcState` appends automatic
countdown metadata at ordinals 51–52 and latest-received display metadata at 53–55.
Transport changes append to reserved custom messages. `SlcState` retains automatic
pending metadata at ordinals 51–52 and latest-received display metadata at 53–55.
`SlcCruiseCommand` appends its source at 14 and
a bidirectional adoption kind; historical fields and enum ordinals remain reserved.
Official vehicle schemas are unchanged. Historical SLC ceiling fields stay empty.
@@ -7,7 +7,6 @@ from dataclasses import dataclass, replace
from enum import Enum
CONFIRMATION_NS = 10_000_000_000
AUTOMATIC_ADOPTION_NS = 3_000_000_000
MAX_REJECTIONS = 256
@@ -443,7 +442,7 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
state = replace(state, pending=pending, timer_running=True)
if pending.automatic:
if pending.active_elapsed_ns >= AUTOMATIC_ADOPTION_NS and (authority.can_control or authority.fully_disengaged):
if authority.can_control or authority.fully_disengaged:
state, history = _accept(state, candidate, now_ns)
return finish("accepted" if authority.can_control else "disengaged_accept",
target=candidate.speed_mps if authority.can_control else None, history=history)
@@ -5,7 +5,7 @@ from dataclasses import FrozenInstanceError, replace
from typing import Any, cast
from openpilot.starpilot.speed_limits.acceptance import (
AUTOMATIC_ADOPTION_NS, CONFIRMATION_NS, MAX_REJECTIONS, AcceptedLimit, ActionKind, AdoptRequest, Authority, Candidate, DriverAction,
CONFIRMATION_NS, MAX_REJECTIONS, AcceptedLimit, ActionKind, AdoptRequest, Authority, Candidate, DriverAction,
IdentityKind, LongitudinalOwner, Mode, Observation, ObservationIdentity, ObservationKind, Policy, State, new_session, step,
)
@@ -39,10 +39,10 @@ class TestAcceptance(unittest.TestCase):
policy = Policy(confirmation, confirmation, fallback)
candidate = replace(LOW, source=source)
first = step(new_session('drive'), valid(candidate), DISENGAGED, policy, now_ns=0)
self.assertIsNone(first.history_write)
self.assertIsNotNone(first.state.pending)
self.assertEqual(first.state.pending.automatic, not confirmation)
ready = step(first.state, valid(candidate), ACTIVE, policy, now_ns=AUTOMATIC_ADOPTION_NS)
self.assertEqual(first.state.pending is not None, confirmation)
self.assertEqual(first.history_write is None, confirmation)
self.assertIsNone(first.control_target_mps)
ready = step(first.state, valid(candidate), ACTIVE, policy, now_ns=NS)
accepted = self.act(ready.state, ActionKind.ACCEPT, now_ns=4 * NS, policy=policy) if confirmation else ready
self.assertEqual(accepted.control_target_mps, candidate.speed_mps)
lost = step(accepted.state, ABSENT, ACTIVE, policy, now_ns=5 * NS)
@@ -203,18 +203,23 @@ class TestAcceptance(unittest.TestCase):
self.assertEqual(result.basis, "driver_accept" if kind is ActionKind.ACCEPT else "driver_reject")
self.assertTrue(result.action_receipt.consumed)
def test_first_and_changed_limits_use_countdown_or_confirmation(self):
def test_first_and_changed_limits_adopt_immediately_or_require_confirmation(self):
for confirmation in (False, True):
policy = Policy(confirmation, confirmation)
for candidate in (LOW, replace(HIGH, speed_mps=31.)):
first = step(saved(), valid(candidate), ACTIVE, policy, now_ns=0)
self.assertEqual(first.state.pending.automatic, not confirmation)
self.assertEqual(first.control_target_mps, HIGH.speed_mps)
ready = step(first.state, valid(candidate), ACTIVE, policy, now_ns=AUTOMATIC_ADOPTION_NS - 1)
self.assertIsNotNone(ready.state.pending)
done = step(ready.state, valid(candidate), ACTIVE, policy, now_ns=AUTOMATIC_ADOPTION_NS)
self.assertEqual(done.state.pending is not None, confirmation)
self.assertEqual(done.control_target_mps, HIGH.speed_mps if confirmation else candidate.speed_mps)
for prior, candidate in itertools.product((new_session('drive'), saved()), (LOW, replace(HIGH, speed_mps=31.))):
with self.subTest(confirmation=confirmation, prior=prior.accepted, candidate=candidate):
first = step(prior, valid(candidate), ACTIVE, policy, now_ns=0)
self.assertEqual(first.state.pending is not None, confirmation)
if confirmation:
self.assertFalse(first.state.pending.automatic)
self.assertEqual(first.state.accepted, prior.accepted)
self.assertIsNone(first.history_write)
else:
self.assertEqual(first.state.accepted.candidate, candidate)
self.assertEqual(first.history_write, first.state.accepted)
self.assertEqual(first.control_target_mps, candidate.speed_mps)
repeated = step(first.state, valid(candidate), ACTIVE, policy, now_ns=1)
self.assertIsNone(repeated.history_write)
def test_unchanged_speed_keeps_approval_without_silently_accepting_new_identity(self):
for toward in (0.0, math.inf):
@@ -231,14 +236,21 @@ class TestAcceptance(unittest.TestCase):
automatic = step(saved(), valid(same_speed), ACTIVE, Policy(False, False), now_ns=0)
self.assertIsNone(automatic.history_write)
def test_disengaged_automatic_adoption_waits_three_seconds(self):
pending = step(saved(), valid(LOW), DISENGAGED, Policy(False, False), now_ns=0)
self.assertEqual(pending.state.accepted.candidate, HIGH)
ready = step(pending.state, valid(LOW), DISENGAGED, Policy(False, False), now_ns=AUTOMATIC_ADOPTION_NS)
def test_disengaged_automatic_adoption_is_immediate_without_commanding_speed(self):
ready = step(saved(), valid(LOW), DISENGAGED, Policy(False, False), now_ns=0)
self.assertEqual(ready.state.accepted.candidate, LOW)
self.assertIsNotNone(ready.history_write)
self.assertIsNone(ready.control_target_mps)
def test_automatic_adoption_still_waits_for_authority_then_adopts_without_delay(self):
policy = Policy(False, False)
pending = self.pending(authority=PAUSED, policy=policy)
self.assertEqual(pending.state.accepted.candidate, HIGH)
self.assertIsNone(pending.history_write)
ready = step(pending.state, valid(LOW), ACTIVE, policy, now_ns=1)
self.assertIsNone(ready.state.pending)
self.assertEqual(ready.control_target_mps, LOW.speed_mps)
def test_zero_active_axes_does_not_imply_fully_disengaged(self):
result = self.pending(authority=PAUSED)
self.assertEqual(result.state.accepted.candidate, HIGH)
@@ -209,10 +209,8 @@ class CompositionTests(unittest.TestCase):
self.assertEqual(rejected.state.acceptance.last_timestamp_ns, 0)
def test_new_limit_never_creates_a_hidden_planner_target(self):
pending = self.run_frame(frame(0, speed=20.))
self.assertEqual(pending.acceptance.basis, 'automatic_pending')
self.assertIsNone(pending.ceiling)
accepted = self.run_frame(frame(acc.AUTOMATIC_ADOPTION_NS, speed=20.))
accepted = self.run_frame(frame(0, speed=20.))
self.assertEqual(accepted.acceptance.basis, 'accepted')
self.assertEqual(accepted.domain.context.effective_cluster_mps, 22.)
self.assertIsNone(accepted.ceiling)
self.assertIsNone(accepted.state.lead_prior)
@@ -61,9 +61,9 @@ class OverrideTests(unittest.TestCase):
self.decision(3)
self.assertEqual(self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.0)
self.decision(4, candidate=LOW)
self.assertEqual(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.)
self.decision(3_000_000_004, candidate=LOW)
self.assertIsNone(self.reduce(3_000_000_004, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps)
self.assertIsNone(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps)
self.decision(5, candidate=LOW)
self.assertIsNone(self.reduce(5, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps)
def test_decrease_only_reduces_an_existing_override(self):
for existing, previous, chosen, expected in (
@@ -92,9 +92,7 @@ class OverrideTests(unittest.TestCase):
self.assertEqual(self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.)).contribution_mps, 25.)
mid = replace(SAME, speed_mps=22.)
self.decision(4, candidate=mid)
self.assertEqual(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.)).contribution_mps, 25.)
self.decision(3_000_000_004, candidate=mid)
self.assertIsNone(self.reduce(3_000_000_004, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.)).contribution_mps)
self.assertIsNone(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.)).contribution_mps)
def test_pedal_tracks_current_ego_and_release_restores_persistence(self):
self.arm()
@@ -315,18 +313,17 @@ class OverrideTests(unittest.TestCase):
self.arm()
old_effect = self.classified(effect=2, now=3, chosen=26.0)
self.decision(3, candidate=LOW)
self.decision(3_000_000_003, candidate=LOW)
lower = self.reduce(3_000_000_003, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0), change=old_effect)
lower = self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0), change=old_effect)
self.assertIsNone(lower.contribution_mps)
self.assertFalse(lower.event_receipt.consumed)
self.assertNotEqual(lower.context, self.first.context)
ledger = arb.step(arb.new_ledger(SESSION), arb.BeginAction(SESSION, 2, arb.Origin.DRIVER_CRUISE,
lower.context.context_id), now_ns=3_000_000_004).state
ledger = arb.step(ledger, arb.ResolveAction(SESSION, 2, arb.Disposition.DRIVER_INTENT), now_ns=3_000_000_004).state
fresh = arb.step(ledger, arb.CruiseChange(SESSION, 3, 2, 26.0, 27.0), now_ns=3_000_000_005).change
self.decision(3_000_000_005, candidate=LOW)
rearmed = self.reduce(3_000_000_005, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 27.0), change=fresh)
lower.context.context_id), now_ns=4).state
ledger = arb.step(ledger, arb.ResolveAction(SESSION, 2, arb.Disposition.DRIVER_INTENT), now_ns=4).state
fresh = arb.step(ledger, arb.CruiseChange(SESSION, 3, 2, 26.0, 27.0), now_ns=5).change
self.decision(5, candidate=LOW)
rearmed = self.reduce(5, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 27.0), change=fresh)
self.assertTrue(rearmed.event_receipt.consumed)
self.assertEqual(rearmed.contribution_mps, 27.0)
@@ -1077,7 +1077,9 @@ class RuntimeReplayTests(unittest.TestCase):
self.assertIn('longitudinalPlan', services)
state = next(message.slcState for service, message in publisher.sent if service == 'slcState')
self.assertFalse(state.hasCeiling)
self.assertTrue(state.automaticAdoptionPending)
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)
@@ -1332,10 +1334,12 @@ class RuntimeReplayTests(unittest.TestCase):
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.hasPending)
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.hasPending)
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
@@ -73,11 +73,11 @@ def test_failed_adoption_retries_with_current_evidence_and_stops_after_applicati
@pytest.mark.parametrize('change', ['manual', 'unclassified', 'source', 'returned_source', 'disengaged'])
def test_expired_adoption_does_not_retry_after_driver_or_authority_changes(drive, change):
def test_expired_adoption_does_not_revive_superseded_commands(drive, change):
sm, cp = drive
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
tick(runtime, sm, cp, 0)
tick(runtime, sm, cp, 3)
old_command = tick(runtime, sm, cp, 3).command.slcCruiseCommand
kwargs = {}
if change in ('manual', 'unclassified'):
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 61.
@@ -93,7 +93,13 @@ def test_expired_adoption_does_not_retry_after_driver_or_authority_changes(drive
else:
sm['selfdriveState'].enabled = sm['carControl'].enabled = sm['carControl'].longActive = False
sm['controlsState'].longControlState = 'off'
assert tick(runtime, sm, cp, 3.2, **kwargs).command is None
result = tick(runtime, sm, cp, 3.2, **kwargs)
if change in ('source', 'returned_source'):
assert result.message.slcState.acceptedMonoTime == START + 3_200_000_000
assert result.command.slcCruiseCommand.commandId > old_command.commandId
assert result.command.slcCruiseCommand.targetMps == (20. if change == 'source' else 25.)
else:
assert result.command is None
@pytest.mark.parametrize('failure', ['rejected', 'expired'])
@@ -213,18 +219,17 @@ def test_source_loss_preserves_latest_received_limit_and_offset_without_actions(
@pytest.mark.parametrize('limit', [12., 25.])
@pytest.mark.parametrize('pcm', [False, True])
def test_automatic_adoption_updates_set_speed_up_or_down_after_three_seconds(drive, limit, pcm):
def test_automatic_adoption_updates_set_speed_up_or_down_immediately(drive, limit, pcm):
from openpilot.starpilot.ui.onroad_state import OnroadState, speed_limit_from_message
from openpilot.starpilot.ui.speed_limit_pulse import SpeedLimitPulse
sm, cp = drive
cp.pcmCruise = pcm
sm['slcDashboardObservation'].speedMps = limit
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}))
first = tick(runtime, sm, cp, 0)
assert first.message.slcState.automaticAdoptionPending
assert first.message.slcState.pendingSinceMonoTime == START
assert not first.message.slcState.hasAccepted
assert first.command is None and first.result.ceiling is None
assert tick(runtime, sm, cp, 2.999).command is None
adopted = tick(runtime, sm, cp, 3)
adopted = tick(runtime, sm, cp, 0)
assert adopted.message.slcState.hasAccepted
assert not adopted.message.slcState.automaticAdoptionPending
assert adopted.message.slcState.pendingSinceMonoTime == 0
assert adopted.command is not None
assert adopted.command.slcCruiseCommand.targetMps == pytest.approx(limit)
assert adopted.result.ceiling is None and not adopted.message.slcState.hasCeiling
@@ -233,16 +238,18 @@ def test_automatic_adoption_updates_set_speed_up_or_down_after_three_seconds(dri
evidence = sm['slcDashboardObservation']
observation = SimpleNamespace(status=Status.VALID, speed_mps=limit, episode=evidence.episode,
observed_ns=evidence.observedMonoTime, valid_until_ns=evidence.validUntilMonoTime)
current = physical_actions.state_current(ReplaySM({'slcState': state}, START + 3_000_000_000), observation,
'card-producer', START + 3_000_000_000)
current = physical_actions.state_current(ReplaySM({'slcState': state}, START), observation, 'card-producer', START)
assert physical_actions.command_applicable(adopted.command.slcCruiseCommand, current, observation, 'card-producer',
START + 3_000_000_000, 60 / 3.6, long_active=True, button_event=False)
START, 60 / 3.6, long_active=True, button_event=False)
helper = VCruiseHelper(cp)
helper.v_cruise_kph = 60.
helper.apply_slc_target(limit, True)
sm['carState'].vCruise = sm['carState'].vCruiseCluster = helper.v_cruise_kph
assert helper.v_cruise_kph == round(limit * 3.6)
assert tick(runtime, sm, cp, 3.05).command is None
pulse = SpeedLimitPulse()
pulse.update(OnroadState(True, False, 20., helper.v_cruise_kph, speed_limit_from_message(state)), START / 1e9)
assert not pulse.display_pulsing and pulse.start_time == -float('inf')
assert tick(runtime, sm, cp, 0.05).command is None
@pytest.mark.parametrize('response', ['accept', 'reject', 'timeout'])
@@ -274,9 +281,8 @@ def test_new_limits_still_adopt_during_manual_override_in_either_direction(drive
selected / 3.6, 'accel' if selected > 90 else 'decel', 'card-producer', START + 3_100_000_000))
assert changed.message.slcState.driverOverrideActive
sm['slcDashboardObservation'].speedMps = 18.
new = tick(runtime, sm, cp, 3.2)
assert new.message.slcState.automaticAdoptionPending and new.message.slcState.driverOverrideActive
adopted = tick(runtime, sm, cp, 6.2)
adopted = tick(runtime, sm, cp, 3.2)
assert not adopted.message.slcState.hasPending
assert adopted.command.slcCruiseCommand.targetMps == pytest.approx(18.)
assert not adopted.message.slcState.driverOverrideActive
@@ -372,17 +378,17 @@ def test_ford_parser_buttons_adjust_set_speed_or_confirm_without_a_second_step(p
assert (helper.slc_cruise_change is None) == pending
def test_automatic_countdown_never_consumes_plus_or_minus_and_late_accept_is_declined():
def test_automatic_adoption_does_not_consume_confirmation_actions_and_late_manual_accept_is_declined():
candidate = acc.Candidate('dashboard', acc.ObservationIdentity(acc.IdentityKind.SESSION_VALUE, session_id='drive'), 20.)
observation = acc.Observation(acc.ObservationKind.VALID, candidate)
authority = acc.Authority(acc.Mode.LONGITUDINAL_ONLY, acc.LongitudinalOwner.SYSTEM, False, True, False, False)
for automatic in (False, True):
policy = acc.Policy(not automatic, not automatic)
pending = acc.step(acc.new_session('drive'), observation, authority, policy, now_ns=0)
action = acc.DriverAction('drive', 1, pending.state.pending.decision_id, acc.ActionKind.ACCEPT)
output = acc.step(pending.state, observation, authority, policy, now_ns=1 if automatic else acc.CONFIRMATION_NS + 1, action=action)
first = acc.step(acc.new_session('drive'), observation, authority, policy, now_ns=0)
action = acc.DriverAction('drive', 1, first.state.next_decision_id - 1, acc.ActionKind.ACCEPT)
output = acc.step(first.state, observation, authority, policy, now_ns=1 if automatic else acc.CONFIRMATION_NS + 1, action=action)
assert not output.action_receipt.consumed
assert output.state.accepted is None
assert (output.state.accepted is not None) == automatic
@pytest.mark.parametrize('source_name', ['map', 'vision'])
@@ -240,8 +240,7 @@ class SpeedDomainTests(unittest.TestCase):
armed, _ = self.frame(1, selected_raw=24.0, change=effect)
self.assertEqual(armed.state.persistent_selected_mps, 24.0)
self.decision(2, speed=23.0)
self.decision(3_000_000_002, speed=23.0)
caught, coordinate = self.frame(3_000_000_002, selected_raw=24.0)
caught, coordinate = self.frame(2, selected_raw=24.0)
self.assertIsNone(caught.state.persistent_selected_mps)
self.assertNotEqual(caught.context.context_id, first.context.context_id)
self.assertEqual(coordinate.cap_mps, 25.0)
@@ -14,7 +14,6 @@ from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation
from openpilot.starpilot.car.toyota.tests.test_pedal_caller_native import firmware
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime
from openpilot.starpilot.speed_limits.runtime_settings import parse
from openpilot.starpilot.speed_limits.acceptance import AUTOMATIC_ADOPTION_NS
from openpilot.starpilot.speed_limits.tests import test_runtime_replay as replay
from openpilot.starpilot.speed_limits.toyota import eligible, projection
@@ -308,7 +307,7 @@ def test_runtime_waits_for_bound_fresh_card_projection(helper, manual):
return runtime.step(sm, helper.CP, now_ns=stamp, **kwargs)
pending = tick(2_000_000_000).message.slcState
accepted_ns = 2_000_000_000 + (10_000_000 if manual else AUTOMATIC_ADOPTION_NS)
accepted_ns = 2_000_000_000 + (10_000_000 if manual else 0)
request = Action('drive', 1, pending.decisionId, pending.presentationId, 'accept') if manual else None
accepted = tick(accepted_ns, request=request)
assert accepted.message.slcState.hasAccepted and accepted.command is None
@@ -1073,7 +1073,7 @@ class FeatureSettingsOwner:
rows.append(self._bool_row("ShowSpeedLimits", "Show speed limit signs", self.authority("preferences"),
reason="Display signs independently of Speed Limit Controller; does not change cruise speed"))
confirmation = self._bool_row("SLCConfirmation", "Automatic adoption", allowed,
reason="On flashes new limits for 3 seconds, then sets cruise speed; Off asks for confirmation within 10 seconds")
reason="On adopts new limits immediately; Off asks for confirmation within 10 seconds")
rows.append(replace(confirmation, value={"On": "Off", "Off": "On"}.get(confirmation.value, confirmation.value), default_value="On"))
source_capability = self._source_capability()
vision_dev, maps_available = source_capability
@@ -39,21 +39,24 @@ def main() -> None:
view = ShellView(fonts, args.asset_directory)
try:
scenes = {}
for name, pending in (("pending", True), ("automatic", True), ("adoptable", False), ("override", False)):
for name, pending in (("pending", True), ("automatic", False), ("adoptable", False), ("override", False)):
observation = SpeedLimitObservation(kind=ObservationKind.VALID, source="dashboard", speed_limit_mps=22.0,
accepted_speed_limit_mps=18.0, accepted_source="dashboard",
accepted_adjusted_limit_mps=20.0, effective_cluster_target_mps=20.0,
accepted_speed_limit_mps=22.0 if name == 'automatic' else 18.0, accepted_source="dashboard",
accepted_adjusted_limit_mps=24.0 if name == 'automatic' else 20.0,
effective_cluster_target_mps=24.0 if name == 'automatic' else 20.0,
pending_adjusted_limit_mps=24.0 if pending else None,
presentation_adjusted_limit_mps=24.0, limiting_max_set=True,
pending_source="dashboard" if pending else "none",
pending_speed_limit_mps=22.0 if pending else None,
session_id="preview-drive", decision_id=7 if pending else 0,
presentation_id=9, action_enabled=True,
automatic_adoption_pending=name == "automatic", pending_since_ns=10_000_000_000,
pending_since_ns=10_000_000_000 if pending else 0,
driver_override_active=name == "override")
onroad = replace(reference_onroad("onroad_engaged_no_camera"), speed_limit=observation,
longitudinal_active=True, slc_system_long_available=True, slc_prompt_open=name == "pending",
observed_ns=10_500_000_000)
if name == 'automatic':
onroad = replace(onroad, cruise_kph=24. * 3.6)
snapshot = ShellSnapshot(ShellMode.ONROAD, reference_state(), reference_settings_state(), onroad,
DeviceState(), SoftwareState(), TogglesState(), Destination.STAR)
for _ in range(18):
@@ -156,11 +156,6 @@ class RetentionDrive:
self.planner = native.LongitudinalPlanner(self.cp, init_v=45 * MPH)
def step(self, milliseconds, *, received=True, alive=True, status='matchedLimit', limit=45, request=None, **changes):
if (self.runtime.state.last_now_ns is None and not self.runtime.settings.acceptance.confirm_higher and
not getattr(self, '_priming', False)):
self._priming = True
self.step(milliseconds - 3000, received=received, alive=alive, status=status, limit=limit, **changes)
self._priming = False
now, boot = NOW + milliseconds * 1_000_000, BOOT + milliseconds * 1_000_000
previous_stamp = self.sm.logMonoTime.get('mapdOut', BOOT)
self.sm.advance(now)
@@ -362,9 +357,9 @@ def test_retention_respects_lower_max_secondary_source_and_reset(tmp_path):
secondary = RetentionDrive(tmp_path / 'secondary', dashboard=True)
secondary.step(0)
output, _, target = secondary.step(300, status='noCoverage')
assert target == pytest.approx(published_cruise_mph(45), rel=0, abs=1e-10)
assert output.message.slcState.automaticAdoptionPending
assert output.message.slcState.acceptedSource == 'map'
assert target == pytest.approx(published_cruise_mph(70), rel=0, abs=1e-10)
assert not output.message.slcState.automaticAdoptionPending
assert output.message.slcState.acceptedSource == 'dashboard'
def test_compact_sign_keeps_accepted_value_without_pulsing_on_recovery(tmp_path):