mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-05 03:54:04 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68c0b55a00 | |||
| 446801ca74 | |||
| 7fe8dd5e83 | |||
| 72d886cef9 | |||
| b161b33ae8 | |||
| b0c03bdbda | |||
| 08cfff03af | |||
| af4d7e5a0d |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -360,6 +360,7 @@ class CarController(CarControllerBase):
|
||||
self.volt_cc_cancel_pending = False
|
||||
self.volt_cc_last_cancel_frame = 0
|
||||
self.volt_cc_metric = False
|
||||
self.volt_cc_speed_ceiling_mps = None
|
||||
self.volt_cc_last_button_frame = 0
|
||||
self.volt_cc_last_direction_frame = None
|
||||
self.volt_cc_consumed_source_ns = 0
|
||||
@@ -1396,7 +1397,10 @@ class CarController(CarControllerBase):
|
||||
self.volt_cc_consumed_source_ns = source_ns
|
||||
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, button))
|
||||
elif self.frame % 4 == 0:
|
||||
button, rate = button_request(CS.out.vEgo, CS.out.cruiseState.speed, actuators.accel, CS.out.vCruise,
|
||||
cruise_kph = CS.out.vCruise
|
||||
if self.volt_cc_speed_ceiling_mps is not None:
|
||||
cruise_kph = min(cruise_kph, self.volt_cc_speed_ceiling_mps * CV.MS_TO_KPH)
|
||||
button, rate = button_request(CS.out.vEgo, CS.out.cruiseState.speed, actuators.accel, cruise_kph,
|
||||
bool(CC.hudControl.leadVisible), self.volt_cc_metric)
|
||||
source_ns = physical.button_credit_ns
|
||||
if ((self.frame - self.volt_cc_last_button_frame) * DT_CTRL > rate and
|
||||
|
||||
@@ -335,8 +335,8 @@ def test_actual_card_active_observation_requires_fresh_selfdrive_state(monkeypat
|
||||
host.observe_ioniq6_long_authority = lambda *args: None
|
||||
host.update_vehicle_state_context = lambda *args: None
|
||||
|
||||
def update_cruise_speed(_state, _pending, *, approved=None):
|
||||
assert approved is None
|
||||
def update_cruise_speed(_state, _pending, *, wheel_context=None):
|
||||
assert wheel_context is None
|
||||
host.update_cruise_speed = update_cruise_speed
|
||||
host.v_cruise_helper = SimpleNamespace(slc_consumed_button=None, toyota=None)
|
||||
host.startup_panda_configured = lambda: True
|
||||
|
||||
@@ -870,7 +870,7 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card_owner.is_metric = True
|
||||
card_owner.experimental_mode = False
|
||||
card_owner.initial_cruise_speed = 40
|
||||
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False, slc_approved=None: None,
|
||||
card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_, slc_enabled=False, wheel_context=None: None,
|
||||
initialize_v_cruise=lambda *_, **kwargs: None, slc_consumed_button=None, toyota=None,
|
||||
slc_cruise_change=None, v_cruise_kph=60.0, v_cruise_cluster_kph=60.0)
|
||||
card_owner.RI = SimpleNamespace(update=lambda _: None)
|
||||
|
||||
@@ -333,7 +333,7 @@ struct SlcState @0xa1680744031fdb2d {
|
||||
hasPendingAdjustedLimit @45 :Bool;
|
||||
presentationAdjustedLimit @46 :Float32;
|
||||
hasPresentationAdjustedLimit @47 :Bool;
|
||||
overrideBasis @48 :Text; # none/pedal/persistent; qualified contribution only
|
||||
overrideBasis @48 :Text; # none/pedal/persistent/wheel; qualified contribution only
|
||||
retainedOverride @49 :Bool;
|
||||
commandActionSequenceId @50 :UInt64; # UI sequence that issued this command
|
||||
automaticAdoptionPending @51 :Bool;
|
||||
@@ -346,6 +346,10 @@ struct SlcState @0xa1680744031fdb2d {
|
||||
# Qualified adoption proposal; the current displayed observation stays above.
|
||||
upcomingSpeedLimit @57 :Float32;
|
||||
upcomingBoundaryId @58 :Text;
|
||||
acceptedIsPreview @59 :Bool; # a temporary ceiling, ineligible for initializing SET
|
||||
wheelOverrideSince @60 :UInt64; # nonzero while a wheel tap may cross the current adjusted limit
|
||||
wheelAdoptMonoTime @61 :UInt64; # Card's original wheel event timestamp, only on the approval frame
|
||||
wheelAdoptTargetMps @62 :Float32; # exact adjusted SET approved for that event
|
||||
struct SourceReading {
|
||||
source @0 :Text;
|
||||
enabled @1 :Bool;
|
||||
@@ -447,6 +451,7 @@ struct SlcAction @0x9ccdc8676701b412 {
|
||||
conditionalManual @5 :ConditionalManualAction;
|
||||
controllerCruise @6 :ControllerCruiseAction;
|
||||
controllerMode @7 :ControllerCruiseAction;
|
||||
wheelAdoptMonoTime @8 :UInt64; # original timestamp of a qualified Adopt Speed Limit wheel event
|
||||
|
||||
struct ControllerCruiseAction {
|
||||
version @0 :UInt16;
|
||||
@@ -514,6 +519,7 @@ struct SlcCruiseEvent @0xcd96dafb67a082d0 {
|
||||
trafficMode @13 :TrafficModeGesture;
|
||||
wheelAction @14 :SlcAction.ControllerCruiseAction;
|
||||
teslaStockCruise @15 :TeslaStockCruise;
|
||||
wheelOverrideSince @16 :UInt64; # SLC eligibility captured by a physical + tap; zero for ordinary SET changes
|
||||
|
||||
struct TeslaStockCruise {
|
||||
version @0 :UInt16;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -730,7 +730,7 @@ struct SlcState {
|
||||
struct ConditionalModeState;
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(a1680744031fdb2d, 18, 22)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(a1680744031fdb2d, 21, 22)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -826,7 +826,7 @@ struct SlcAction {
|
||||
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(9ccdc8676701b412, 4, 4)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(9ccdc8676701b412, 5, 4)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -878,7 +878,7 @@ struct SlcCruiseEvent {
|
||||
struct ManualModeGesture;
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(cd96dafb67a082d0, 7, 6)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(cd96dafb67a082d0, 8, 6)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -4366,6 +4366,14 @@ public:
|
||||
inline bool hasUpcomingBoundaryId() const;
|
||||
inline ::capnp::Text::Reader getUpcomingBoundaryId() const;
|
||||
|
||||
inline bool getAcceptedIsPreview() const;
|
||||
|
||||
inline ::uint64_t getWheelOverrideSince() const;
|
||||
|
||||
inline ::uint64_t getWheelAdoptMonoTime() const;
|
||||
|
||||
inline float getWheelAdoptTargetMps() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -4659,6 +4667,18 @@ public:
|
||||
inline void adoptUpcomingBoundaryId(::capnp::Orphan< ::capnp::Text>&& value);
|
||||
inline ::capnp::Orphan< ::capnp::Text> disownUpcomingBoundaryId();
|
||||
|
||||
inline bool getAcceptedIsPreview();
|
||||
inline void setAcceptedIsPreview(bool value);
|
||||
|
||||
inline ::uint64_t getWheelOverrideSince();
|
||||
inline void setWheelOverrideSince( ::uint64_t value);
|
||||
|
||||
inline ::uint64_t getWheelAdoptMonoTime();
|
||||
inline void setWheelAdoptMonoTime( ::uint64_t value);
|
||||
|
||||
inline float getWheelAdoptTargetMps();
|
||||
inline void setWheelAdoptTargetMps(float value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -5511,6 +5531,8 @@ public:
|
||||
inline bool hasControllerMode() const;
|
||||
inline ::cereal::SlcAction::ControllerCruiseAction::Reader getControllerMode() const;
|
||||
|
||||
inline ::uint64_t getWheelAdoptMonoTime() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -5579,6 +5601,9 @@ public:
|
||||
inline void adoptControllerMode(::capnp::Orphan< ::cereal::SlcAction::ControllerCruiseAction>&& value);
|
||||
inline ::capnp::Orphan< ::cereal::SlcAction::ControllerCruiseAction> disownControllerMode();
|
||||
|
||||
inline ::uint64_t getWheelAdoptMonoTime();
|
||||
inline void setWheelAdoptMonoTime( ::uint64_t value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -5970,6 +5995,8 @@ public:
|
||||
inline bool hasTeslaStockCruise() const;
|
||||
inline ::cereal::SlcCruiseEvent::TeslaStockCruise::Reader getTeslaStockCruise() const;
|
||||
|
||||
inline ::uint64_t getWheelOverrideSince() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -6070,6 +6097,9 @@ public:
|
||||
inline void adoptTeslaStockCruise(::capnp::Orphan< ::cereal::SlcCruiseEvent::TeslaStockCruise>&& value);
|
||||
inline ::capnp::Orphan< ::cereal::SlcCruiseEvent::TeslaStockCruise> disownTeslaStockCruise();
|
||||
|
||||
inline ::uint64_t getWheelOverrideSince();
|
||||
inline void setWheelOverrideSince( ::uint64_t value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -19713,6 +19743,62 @@ inline ::capnp::Orphan< ::capnp::Text> SlcState::Builder::disownUpcomingBoundary
|
||||
::capnp::bounded<21>() * ::capnp::POINTERS));
|
||||
}
|
||||
|
||||
inline bool SlcState::Reader::getAcceptedIsPreview() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<141>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool SlcState::Builder::getAcceptedIsPreview() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<141>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcState::Builder::setAcceptedIsPreview(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<141>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcState::Reader::getWheelOverrideSince() const {
|
||||
return _reader.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcState::Builder::getWheelOverrideSince() {
|
||||
return _builder.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcState::Builder::setWheelOverrideSince( ::uint64_t value) {
|
||||
_builder.setDataField< ::uint64_t>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcState::Reader::getWheelAdoptMonoTime() const {
|
||||
return _reader.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcState::Builder::getWheelAdoptMonoTime() {
|
||||
return _builder.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcState::Builder::setWheelAdoptMonoTime( ::uint64_t value) {
|
||||
_builder.setDataField< ::uint64_t>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float SlcState::Reader::getWheelAdoptTargetMps() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<40>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float SlcState::Builder::getWheelAdoptTargetMps() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<40>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcState::Builder::setWheelAdoptTargetMps(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<40>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool SlcState::SourceReading::Reader::hasSource() const {
|
||||
return !_reader.getPointerField(
|
||||
::capnp::bounded<0>() * ::capnp::POINTERS).isNull();
|
||||
@@ -21220,6 +21306,20 @@ inline ::capnp::Orphan< ::cereal::SlcAction::ControllerCruiseAction> SlcAction::
|
||||
::capnp::bounded<3>() * ::capnp::POINTERS));
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcAction::Reader::getWheelAdoptMonoTime() const {
|
||||
return _reader.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcAction::Builder::getWheelAdoptMonoTime() {
|
||||
return _builder.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcAction::Builder::setWheelAdoptMonoTime( ::uint64_t value) {
|
||||
_builder.setDataField< ::uint64_t>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline ::uint16_t SlcAction::ControllerCruiseAction::Reader::getVersion() const {
|
||||
return _reader.getDataField< ::uint16_t>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
@@ -22088,6 +22188,20 @@ inline ::capnp::Orphan< ::cereal::SlcCruiseEvent::TeslaStockCruise> SlcCruiseEve
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS));
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcCruiseEvent::Reader::getWheelOverrideSince() const {
|
||||
return _reader.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<7>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline ::uint64_t SlcCruiseEvent::Builder::getWheelOverrideSince() {
|
||||
return _builder.getDataField< ::uint64_t>(
|
||||
::capnp::bounded<7>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void SlcCruiseEvent::Builder::setWheelOverrideSince( ::uint64_t value) {
|
||||
_builder.setDataField< ::uint64_t>(
|
||||
::capnp::bounded<7>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline ::uint16_t SlcCruiseEvent::TeslaStockCruise::Reader::getVersion() const {
|
||||
return _reader.getDataField< ::uint16_t>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -327,12 +327,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"HideDMIcon", {PERSISTENT, BOOL, "0"}},
|
||||
{"HidePersonalityAlerts", {PERSISTENT, BOOL, "0"}},
|
||||
{"ShowSpeedLimits", {PERSISTENT, BOOL, "1"}},
|
||||
{"SLCConfirmation", {PERSISTENT, BOOL, "0"}},
|
||||
{"SLCAdvanceSeconds", {PERSISTENT, INT, "0"}},
|
||||
{"SLCConfirmationHigher", {PERSISTENT, BOOL, "0"}},
|
||||
{"SLCConfirmationLower", {PERSISTENT, BOOL, "0"}},
|
||||
{"SLCFallback", {PERSISTENT, INT}}, // Retired; preserve saved bytes for older versions.
|
||||
{"SLCOverride", {PERSISTENT, INT, "1"}},
|
||||
{"SLCOnEngage", {PERSISTENT, BOOL, "0"}},
|
||||
{"SLCConfirmation", {PERSISTENT, INT, "0"}},
|
||||
{"SLCAdvanceHigherSeconds", {PERSISTENT, INT, "0"}},
|
||||
{"SLCAdvanceLowerSeconds", {PERSISTENT, INT, "0"}},
|
||||
{"SLCOverrideMode", {PERSISTENT, INT, "1"}},
|
||||
{"SLCWheelOverride", {PERSISTENT, BOOL, "0"}},
|
||||
{"SLCQualifiedHistory", {PERSISTENT, JSON}},
|
||||
{"ConditionalModeConfig", {PERSISTENT | DONT_LOG, JSON}},
|
||||
{"ConditionalManualState", {PERSISTENT | DONT_LOG, JSON}},
|
||||
@@ -361,7 +361,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"VASMPreferences", {PERSISTENT | DONT_LOG, JSON}},
|
||||
{"LaneChangePreferences", {PERSISTENT | DONT_LOG, JSON}},
|
||||
{"SLCPriority1", {PERSISTENT, STRING, "Vision"}},
|
||||
{"SLCPriority2", {PERSISTENT, STRING, "Dashboard"}},
|
||||
{"SLCPriority2", {PERSISTENT, STRING, "Map Data"}},
|
||||
{"SLCPriority3", {PERSISTENT, STRING, "Off"}},
|
||||
{"SpeedLimitController", {PERSISTENT, BOOL, "0"}},
|
||||
{"RouteCount", {PERSISTENT, INT, "0"}},
|
||||
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import time
|
||||
import threading
|
||||
import uuid
|
||||
from collections import deque
|
||||
from typing import cast
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
@@ -32,7 +31,7 @@ from opendbc.car.hyundai.ioniq6_media import Ioniq6MediaButtons
|
||||
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR
|
||||
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
||||
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation
|
||||
from openpilot.selfdrive.car.cruise import SlcWheelContext, VCruiseHelper
|
||||
from openpilot.starpilot.speed_limits import physical_actions as slc_physical
|
||||
from openpilot.starpilot.aol.intent import AolProcessFaultContext, independent_axis_requested, read_settings
|
||||
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback, cp_identity
|
||||
@@ -93,7 +92,7 @@ from openpilot.starpilot.vehicle_selection import startup_candidate
|
||||
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
|
||||
from openpilot.starpilot.controller_extensions import configure_controller, observe_controller_can
|
||||
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
||||
from openpilot.starpilot.controllers.wheel_actions import WheelPublisher, key_for
|
||||
from openpilot.starpilot.controllers.wheel_actions import LIFETIME_NS as WHEEL_ACTION_MAX_AGE_NS, WheelPublisher, key_for
|
||||
from openpilot.starpilot.controllers.cruise_action import CruiseActionConsumer, eligible as cruise_action_eligible
|
||||
from openpilot.starpilot.schema_cache import get_cache, prewarm_cache_contracts, put_cache
|
||||
|
||||
@@ -139,15 +138,15 @@ def can_comm_callbacks(logcan: messaging.SubSocket, sendcan: messaging.PubSocket
|
||||
|
||||
def curve_press_receipt(CS: car.CarState, helper: VCruiseHelper, sm, *, curve_replay: bool,
|
||||
slc_replay: bool, host_long_active: bool, observed_ns: int):
|
||||
"""Reserve SLC presentation presses even when their command is stale or rejected."""
|
||||
"""A confirmation or native standstill RES must not also override Curve control."""
|
||||
accelerate = frozenset((car.CarState.ButtonEvent.Type.accelCruise,
|
||||
car.CarState.ButtonEvent.Type.resumeCruise))
|
||||
pressed = {b.type.raw for b in CS.buttonEvents if b.pressed and b.type.raw in accelerate}
|
||||
if (not curve_replay or not host_long_active or not CS.canValid or CS.canTimeout or not CS.cruiseState.available or
|
||||
not pressed):
|
||||
CS.standstill or CS.cruiseState.standstill or not pressed):
|
||||
return None
|
||||
if slc_replay and (helper.slc_consumed_button is not None or bool(pressed & helper.slc_suppressed_buttons) or
|
||||
bool(sm['slcState'].hasPending)):
|
||||
if (helper.slc_consumed_button is not None or bool(pressed & helper.slc_suppressed_buttons) or
|
||||
slc_replay and bool(sm['slcState'].hasPending)):
|
||||
return None
|
||||
return ('curveAccelPress', None, float(helper.v_cruise_kph_last / 3.6),
|
||||
float(helper.v_cruise_kph / 3.6), observed_ns)
|
||||
@@ -187,10 +186,6 @@ class Car:
|
||||
ignore_valid=['autoSteeringLearning'], ignore_avg_freq=['autoSteeringLearning'])
|
||||
self.slc_cruise_event_id = 0
|
||||
self.slc_producer_session = uuid.uuid4().hex
|
||||
self.slc_command_sock = messaging.sub_sock('slcCruiseCommand', conflate=False) if self.slc_replay else None
|
||||
self.slc_commands = deque(maxlen=8)
|
||||
self.slc_last_command_id = 0
|
||||
self.slc_command_session = ''
|
||||
self.slc_receipts: list[tuple[str, object, float, float, int]] = []
|
||||
self.manual_receipt = None
|
||||
self.manual_event_sequence = 0
|
||||
@@ -427,14 +422,13 @@ class Car:
|
||||
for ignored in (self.sm.ignore_alive, self.sm.ignore_valid, self.sm.ignore_average_freq):
|
||||
if 'slcState' not in ignored:
|
||||
ignored.append('slcState')
|
||||
if self.slc_command_sock is None:
|
||||
self.slc_command_sock = messaging.sub_sock('slcCruiseCommand', conflate=False)
|
||||
if 'slcDashboardObservation' not in self.pm.sock:
|
||||
self.pm.sock['slcDashboardObservation'] = messaging.pub_sock('slcDashboardObservation')
|
||||
self.slc_requested_configuration = self.read_slc_configuration()
|
||||
self.slc_configuration = self.slc_requested_configuration
|
||||
self.slc_replay = self.slc_configuration[0]
|
||||
self.slc_control_enabled = self.slc_replay and self.slc_configuration[1].enabled
|
||||
self.slc_match_on_engage = self.slc_transport_available and self.read_slc_match_on_engage()
|
||||
self.slc_source_floor_ns = 0
|
||||
|
||||
# Write previous route's CarParams
|
||||
@@ -475,6 +469,9 @@ class Car:
|
||||
feature_enabled(self.params, self.CP, 'slc', os.environ))
|
||||
return bool(admitted), settings
|
||||
|
||||
def read_slc_match_on_engage(self) -> bool:
|
||||
return read_saved(self.params, 'SLCOnEngage', 1) == (b'1', True)
|
||||
|
||||
def refresh_slc_configuration(self, now_ns: int) -> None:
|
||||
configuration = getattr(self, 'slc_requested_configuration', None)
|
||||
if configuration is None or configuration == getattr(self, 'slc_configuration', None):
|
||||
@@ -482,12 +479,8 @@ class Car:
|
||||
self.slc_configuration = configuration
|
||||
self.slc_replay = configuration[0]
|
||||
self.slc_control_enabled = self.slc_replay and configuration[1].enabled
|
||||
# Invalidate old SLC choices while keeping consumed buttons latched until release.
|
||||
self.slc_source_floor_ns = max(getattr(self, 'slc_source_floor_ns', 0), now_ns)
|
||||
self.slc_commands.clear()
|
||||
self.slc_command_session = ''
|
||||
self.slc_last_command_id = 0
|
||||
# Keep the shared event producer and held suppression until physical release.
|
||||
# Only SLC's sourced/issued work predating this admission is retired.
|
||||
|
||||
def observe_distance_personality(self, CS: car.CarState, now_ns: int) -> None:
|
||||
tracker = getattr(self, 'distance_personality_tracker', None)
|
||||
@@ -573,18 +566,22 @@ class Car:
|
||||
0 < control_log_ns <= now_ns and now_ns - control_log_ns <= slc_physical.STATE_MAX_AGE_NS and
|
||||
self.sm['carControl'].enabled)
|
||||
host_control_enabled = bool(self.CP.openpilotLongitudinalControl and not self.CP.pcmCruise and host_enabled)
|
||||
host_long_active = host_control_enabled and self.sm['carControl'].longActive
|
||||
host_long_active = bool(self.CP.openpilotLongitudinalControl and host_enabled and self.sm['carControl'].longActive)
|
||||
slc_long_active = bool(getattr(self, 'slc_control_enabled', self.slc_replay) and self.CP.openpilotLongitudinalControl and
|
||||
host_enabled and (self.sm['carControl'].longActive or CS.gasPressed))
|
||||
pending = None
|
||||
if self.slc_replay:
|
||||
pending = slc_physical.pending_confirmation(
|
||||
self.sm, now_ns, long_active=slc_long_active, after_ns=floor_ns)
|
||||
approved = (slc_physical.state_current(self.sm, observation, self.slc_producer_session, now_ns)
|
||||
if self.v_cruise_helper.toyota is not None and self.slc_control_enabled else None)
|
||||
if approved is not None and int(approved.frameMonoTime) <= floor_ns:
|
||||
approved = None
|
||||
self.update_cruise_speed(CS, pending, approved=approved)
|
||||
wheel_context = None
|
||||
slc_settings = getattr(self, 'slc_configuration', (False, None))[1]
|
||||
cruise_helper = self.v_cruise_helper
|
||||
if (self.slc_replay and getattr(slc_settings, 'wheel_override', False) and host_long_active and can_rcv_valid and
|
||||
not self.CP.pcmCruise and pending is None and (cruise_helper.slc_wheel_press is not None or len(CS.buttonEvents) > 0) and
|
||||
not (CS.brakePressed or CS.gasPressed or CS.standstill or CS.cruiseState.standstill)):
|
||||
wheel_context = slc_physical.wheel_override_context(self.sm, now_ns, after_ns=floor_ns,
|
||||
selected_kph=cruise_helper.v_cruise_kph, is_metric=self.is_metric)
|
||||
self.update_cruise_speed(CS, pending, wheel_context=wheel_context)
|
||||
if self.controller_cruise_sock is not None:
|
||||
for _ in range(8):
|
||||
request = messaging.recv_one_or_none(self.controller_cruise_sock)
|
||||
@@ -593,6 +590,8 @@ class Car:
|
||||
self.controller_cruise_consumer.apply(request, self.CP, CS, self.sm, self.v_cruise_helper,
|
||||
now_ns=now_ns, is_metric=self.is_metric,
|
||||
enabled=bool(host_control_enabled and can_rcv_valid and not self.params.get_bool("SafeMode")))
|
||||
if self.slc_replay:
|
||||
self.apply_slc_adopt(now_ns, host_control_enabled=host_control_enabled, after_ns=floor_ns)
|
||||
consumed = self.v_cruise_helper.slc_consumed_button
|
||||
if self.aol_card_intent is not None:
|
||||
if self.aol_process_fault_context is None:
|
||||
@@ -784,52 +783,6 @@ class Car:
|
||||
slc_replay=self.slc_replay, host_long_active=host_long_active, observed_ns=now_ns)
|
||||
if curve_receipt is not None:
|
||||
self.slc_receipts.append(curve_receipt)
|
||||
if self.slc_command_sock is not None:
|
||||
for _ in range(8):
|
||||
command_message = messaging.recv_one_or_none(self.slc_command_sock)
|
||||
if command_message is None:
|
||||
break
|
||||
if self.slc_replay and command_message.valid:
|
||||
self.slc_commands.append(command_message)
|
||||
for _ in range(len(self.slc_commands)):
|
||||
command_message = self.slc_commands.popleft()
|
||||
command = command_message.slcCruiseCommand
|
||||
if int(command.issuedMonoTime) <= floor_ns:
|
||||
continue
|
||||
if now_ns < int(command.issuedMonoTime):
|
||||
if now_ns <= int(command.expiresMonoTime):
|
||||
self.slc_commands.append(command_message)
|
||||
continue
|
||||
state = slc_physical.state_current(self.sm, observation, self.slc_producer_session, now_ns)
|
||||
if (state is None or int(self.sm.logMonoTime['slcState']) < int(command.issuedMonoTime)) and now_ns < int(command.expiresMonoTime):
|
||||
self.slc_commands.append(command_message)
|
||||
continue
|
||||
if state is not None and str(state.sessionId) != self.slc_command_session:
|
||||
self.slc_command_session = str(state.sessionId)
|
||||
self.slc_last_command_id = 0
|
||||
if not self.slc_command_session:
|
||||
self.slc_command_session = str(command.sessionId)
|
||||
if str(command.sessionId) != self.slc_command_session or int(command.commandId) <= self.slc_last_command_id:
|
||||
continue
|
||||
self.slc_last_command_id = int(command.commandId)
|
||||
previous_mps = float(self.v_cruise_helper.v_cruise_kph / 3.6)
|
||||
origin = SlcPendingConfirmation(str(command.sessionId), int(command.decisionId), int(command.presentationId))
|
||||
owners = {**self.v_cruise_helper.slc_suppression_owner, **self.v_cruise_helper.slc_released_owner}
|
||||
allowed_buttons = {button for button, owner in owners.items() if owner == origin} if int(command.decisionId) > 0 else set()
|
||||
intervening_button = (bool(self.v_cruise_helper.slc_cruise_change) or
|
||||
any(b.type.raw not in allowed_buttons for b in CS.buttonEvents) or
|
||||
any(timer > 0 and button not in allowed_buttons
|
||||
for button, timer in self.v_cruise_helper.button_timers.items()))
|
||||
applicable = slc_physical.command_applicable(
|
||||
command, state, observation, self.slc_producer_session, now_ns, previous_mps,
|
||||
long_active=slc_long_active,
|
||||
button_event=intervening_button)
|
||||
if self.v_cruise_helper.toyota is not None:
|
||||
applicable = applicable and self.v_cruise_helper.toyota.can_apply(float(command.targetMps))
|
||||
if applicable:
|
||||
self.v_cruise_helper.apply_slc_target(float(command.targetMps), self.is_metric)
|
||||
self.slc_receipts.append(('commandApplied' if applicable else 'commandRejected', command,
|
||||
previous_mps, float(self.v_cruise_helper.v_cruise_kph / 3.6), now_ns))
|
||||
blocked_keys = set()
|
||||
if self.manual_receipt is not None:
|
||||
blocked_keys.add(key_for(self.manual_receipt[0]))
|
||||
@@ -845,28 +798,49 @@ class Car:
|
||||
|
||||
return CS, RD
|
||||
|
||||
def update_cruise_speed(self, CS, pending=None, *, approved=None):
|
||||
def apply_slc_adopt(self, now_ns: int, *, host_control_enabled: bool, after_ns: int = 0) -> None:
|
||||
pending = getattr(self, 'slc_wheel_adopt', None)
|
||||
if pending is None:
|
||||
return
|
||||
wheel_ns, selected_kph = pending
|
||||
helper = self.v_cruise_helper
|
||||
if (not after_ns < wheel_ns <= now_ns <= wheel_ns + WHEEL_ACTION_MAX_AGE_NS or
|
||||
not host_control_enabled or not self.slc_control_enabled or
|
||||
not helper.v_cruise_initialized or helper.v_cruise_kph != selected_kph or any(helper.button_timers.values())):
|
||||
self.slc_wheel_adopt = None
|
||||
return
|
||||
target = slc_physical.adopted_target(self.sm, now_ns, wheel_ns=wheel_ns)
|
||||
if target is not None:
|
||||
self.slc_wheel_adopt = None
|
||||
helper.apply_slc_target(target, self.is_metric)
|
||||
|
||||
def update_cruise_speed(self, CS, pending=None, *, wheel_context: SlcWheelContext | None = None):
|
||||
self.v_cruise_helper.update_v_cruise(CS, self.sm['carControl'].enabled, self.is_metric, pending,
|
||||
slc_enabled=getattr(self, 'slc_control_enabled', getattr(self, 'slc_replay', False)),
|
||||
slc_approved=approved)
|
||||
wheel_context=wheel_context)
|
||||
if self.sm['carControl'].enabled and not self.CC_prev.enabled:
|
||||
initial = None
|
||||
target, initial = None, None
|
||||
now_ns = int(self.can_log_mono_time) if REPLAY and hasattr(self, 'can_log_mono_time') else time.monotonic_ns()
|
||||
if (getattr(self, 'slc_control_enabled', False) and self.CP.openpilotLongitudinalControl and
|
||||
if (getattr(self, 'slc_match_on_engage', False) and getattr(self, 'slc_control_enabled', False) and
|
||||
not self.CP.pcmCruise and self.CP.openpilotLongitudinalControl and
|
||||
CS.canValid and not CS.canTimeout and CS.cruiseState.available and
|
||||
self.sm.valid['carControl'] and self.sm.alive['carControl'] and
|
||||
0 < int(self.sm.logMonoTime['carControl']) <= now_ns and
|
||||
now_ns - int(self.sm.logMonoTime['carControl']) <= slc_physical.STATE_MAX_AGE_NS and
|
||||
(self.sm['carControl'].longActive or CS.gasPressed)):
|
||||
evidence = getattr(self.CI.CS, 'dashboard_limit', None)
|
||||
initial = slc_physical.engagement_state(
|
||||
self.sm, evidence.observation if evidence is not None else None, self.slc_producer_session,
|
||||
now_ns, after_ns=getattr(self, 'slc_source_floor_ns', 0))
|
||||
# Initialization owns this edge; evaluate commands against its final speed.
|
||||
now_ns - int(self.sm.logMonoTime['carControl']) <= slc_physical.STATE_MAX_AGE_NS):
|
||||
long_requested = self.sm['carControl'].longActive or CS.gasPressed
|
||||
if not long_requested and 'aolAxisState' in self.sm.logMonoTime:
|
||||
# AOL can acknowledge the request before longitudinal actuators become active.
|
||||
axis = current_axis(self.sm, now_ns=now_ns)
|
||||
long_requested = axis is not None and axis.nativeAcknowledged and axis.desiredLongitudinal
|
||||
if long_requested:
|
||||
evidence = getattr(self.CI.CS, 'dashboard_limit', None)
|
||||
target, initial = slc_physical.engagement_target(
|
||||
self.sm, evidence.observation if evidence is not None else None, self.slc_producer_session,
|
||||
now_ns, after_ns=getattr(self, 'slc_source_floor_ns', 0))
|
||||
adopted = self.v_cruise_helper.initialize_v_cruise(
|
||||
self.CS_prev, self.experimental_mode, resume=self.vehicle_startup.consume_cruise_resume(), initial_speed=self.initial_cruise_speed,
|
||||
slc_target_mps=float(initial.presentationAdjustedLimit) if initial is not None else None, is_metric=self.is_metric)
|
||||
if adopted:
|
||||
slc_target_mps=target, is_metric=self.is_metric)
|
||||
if adopted and initial is not None:
|
||||
self.slc_receipts.append(('engagementAdopt', initial, 0.0, float(self.v_cruise_helper.v_cruise_kph / 3.6), now_ns))
|
||||
|
||||
def aol_cancel_source(self, CS, now_ns: int) -> int:
|
||||
@@ -1135,11 +1109,6 @@ class Car:
|
||||
elif self.CP.brand == 'hyundai':
|
||||
from openpilot.starpilot.car.hyundai.dashboard import compose as hyundai_compose
|
||||
companion = hyundai_compose(self.CP, self.CI.CS, CS, companion, car_state_stamp=int(cs_send.logMonoTime))
|
||||
if self.v_cruise_helper.toyota is not None:
|
||||
if companion is None:
|
||||
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
||||
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
||||
self.v_cruise_helper.toyota.publish(companion.starpilotCarState.toyotaCruise)
|
||||
self.car_gps_publisher.update(self.CI.CS, self.pm, companion)
|
||||
self.pm.send('carState', cs_send)
|
||||
if getattr(self, 'learning_stamp_observer', None) is not None:
|
||||
@@ -1147,6 +1116,9 @@ class Car:
|
||||
|
||||
wheel = getattr(self, 'wheel_publisher', None)
|
||||
if wheel is not None:
|
||||
if (getattr(self, 'slc_control_enabled', False) and self.CC_prev.enabled and self.sm['carControl'].enabled and
|
||||
any(action == 10 for _, action in self.wheel_commands)):
|
||||
self.slc_wheel_adopt = (int(cs_send.logMonoTime), self.v_cruise_helper.v_cruise_kph)
|
||||
wheel.publish(self.wheel_commands, self.CP, self.pm, now_ns=int(cs_send.logMonoTime),
|
||||
drive_id=int(self.sm['deviceState'].startedMonoTime), source_car_ns=int(cs_send.logMonoTime),
|
||||
source_control_ns=int(self.sm.logMonoTime['carControl']))
|
||||
@@ -1186,6 +1158,8 @@ class Car:
|
||||
if kind == 'driverChange':
|
||||
assert isinstance(detail, tuple) and len(detail) == 4
|
||||
record.button, record.longPress = detail[2], detail[3]
|
||||
if self.v_cruise_helper.slc_wheel_tap is not None:
|
||||
record.sessionId, record.wheelOverrideSince = self.v_cruise_helper.slc_wheel_tap
|
||||
elif kind in ('confirmationAccept', 'confirmationReject'):
|
||||
record.button = detail.button
|
||||
record.sessionId = detail.pending.session_id
|
||||
@@ -1193,13 +1167,11 @@ class Car:
|
||||
record.presentationId = detail.pending.presentation_id
|
||||
elif kind == 'curveAccelPress':
|
||||
record.button = 'accel'
|
||||
else:
|
||||
elif kind == 'engagementAdopt':
|
||||
record.sessionId = str(detail.sessionId)
|
||||
record.commandId = 0 if kind == 'engagementAdopt' else int(detail.commandId)
|
||||
record.decisionId = int(detail.decisionId)
|
||||
record.presentationId = int(detail.presentationId)
|
||||
if kind == 'engagementAdopt':
|
||||
record.button = 'decel'
|
||||
record.button = 'decel'
|
||||
self.pm.send('slcCruiseEvent', event)
|
||||
|
||||
if getattr(self, 'conditional_replay', False) and self.manual_receipt is not None:
|
||||
@@ -1619,6 +1591,10 @@ class Car:
|
||||
if physical.button_credit_ns - self.volt_cc_boot_offset_ns <= max(self.volt_cc_drive_id, self.volt_cc_source_floor_ns):
|
||||
self.CI.CC.volt_cc_consumed_source_ns = physical.button_credit_ns
|
||||
self.CI.CC.volt_cc_metric = self.is_metric
|
||||
self.CI.CC.volt_cc_speed_ceiling_mps = (slc_physical.active_ceiling(
|
||||
self.sm, self.volt_cc_now_mono_ns,
|
||||
after_ns=max(self.volt_cc_drive_id, self.volt_cc_source_floor_ns, getattr(self, 'slc_source_floor_ns', 0)))
|
||||
if getattr(self, 'slc_control_enabled', False) else None)
|
||||
|
||||
if getattr(self.CI.CC, "bolt_cc_profile", False):
|
||||
self.CI.CC.bolt_cc_metric = self.is_metric
|
||||
@@ -1871,6 +1847,8 @@ class Car:
|
||||
now = time.monotonic()
|
||||
if now >= next_cruise_read:
|
||||
self.slc_requested_configuration = self.read_slc_configuration()
|
||||
if getattr(self, 'slc_transport_available', False):
|
||||
self.slc_match_on_engage = self.read_slc_match_on_engage()
|
||||
self.v_cruise_helper.intervals = read_cruise_intervals(self.params, self.CP)
|
||||
pitch_input = getattr(self.CI.CC, 'long_pitch_input', None)
|
||||
if pitch_input is not None:
|
||||
|
||||
@@ -43,6 +43,9 @@ class SlcConsumedButton:
|
||||
button: str
|
||||
|
||||
|
||||
SlcWheelContext = tuple[str, int]
|
||||
|
||||
|
||||
def validated_initial_cruise_speed(value: object) -> int:
|
||||
if type(value) is int and V_CRUISE_INITIAL <= value <= V_CRUISE_MAX:
|
||||
return value
|
||||
@@ -66,35 +69,27 @@ class VCruiseHelper:
|
||||
self.button_change_states = {btn: {"standstill": False, "enabled": False} for btn in self.button_timers}
|
||||
self.pcm_slc_sample: tuple[float, int] | None = None
|
||||
self.pcm_slc_button = ButtonType.unknown
|
||||
# Set by software cruise steps or a checked Toyota PCM physical +/- change.
|
||||
self.slc_cruise_change: tuple[float, float, str, bool] | None = None
|
||||
self.slc_consumed_button: SlcConsumedButton | None = None
|
||||
self.slc_suppressed_buttons: set[ButtonType] = set()
|
||||
self.slc_suppression_owner: dict[ButtonType, SlcPendingConfirmation] = {}
|
||||
self.slc_released_owner: dict[ButtonType, SlcPendingConfirmation] = {}
|
||||
self.slc_released_suppressed = False
|
||||
self.slc_pcm = False
|
||||
from openpilot.starpilot.speed_limits.toyota import eligible, ToyotaCruise
|
||||
self.toyota = ToyotaCruise() if eligible(CP) else None
|
||||
self.slc_wheel_press: SlcWheelContext | None = None
|
||||
self.slc_wheel_tap: SlcWheelContext | None = None
|
||||
|
||||
@property
|
||||
def v_cruise_initialized(self):
|
||||
return self.v_cruise_kph != V_CRUISE_UNSET
|
||||
|
||||
def update_v_cruise(self, CS, enabled, is_metric, slc_pending: SlcPendingConfirmation | None = None, *, slc_enabled: bool = False,
|
||||
slc_approved=None):
|
||||
self.slc_pcm = bool(slc_enabled and self.CP.pcmCruise and self.CP.openpilotLongitudinalControl)
|
||||
def update_v_cruise(self, CS, enabled, is_metric, slc_pending: SlcPendingConfirmation | None = None, *,
|
||||
slc_enabled: bool = False, wheel_context: SlcWheelContext | None = None):
|
||||
from opendbc.car.subaru.software_cruise import selected as subaru_software_selected
|
||||
subaru_software = subaru_software_selected(self.CP)
|
||||
software_cruise = not self.CP.pcmCruise or self.slc_pcm or subaru_software
|
||||
software_cruise = not self.CP.pcmCruise or subaru_software
|
||||
self.slc_cruise_change = None
|
||||
self.slc_consumed_button = None
|
||||
self.slc_released_suppressed = False
|
||||
self.slc_released_owner = {}
|
||||
self.slc_wheel_tap = None
|
||||
self.v_cruise_kph_last = self.v_cruise_kph
|
||||
if self.toyota is not None:
|
||||
self.slc_pcm = self.toyota.update(self, CS, enabled, slc_enabled, slc_approved, is_metric)
|
||||
software_cruise = self.slc_pcm
|
||||
now_ns = monotonic_ns()
|
||||
if not CS.cruiseState.available or now_ns - self.pending_resume_ns > 500_000_000:
|
||||
self.pending_resume_ns = 0
|
||||
@@ -106,21 +101,26 @@ class VCruiseHelper:
|
||||
button.type in (ButtonType.accelCruise, ButtonType.resumeCruise) and self.v_cruise_initialized):
|
||||
self.pending_resume_ns = now_ns
|
||||
|
||||
if software_cruise or self.slc_suppressed_buttons:
|
||||
if software_cruise or slc_enabled and self.CP.openpilotLongitudinalControl or self.slc_suppressed_buttons:
|
||||
self._update_slc_button_latches(CS, enabled, slc_pending)
|
||||
|
||||
if (not slc_enabled or not enabled or self.CP.pcmCruise or slc_pending is not None or
|
||||
not CS.cruiseState.available or ButtonType.accelCruise in self.slc_suppressed_buttons):
|
||||
wheel_context = None
|
||||
if wheel_context != self.slc_wheel_press:
|
||||
self.slc_wheel_press = None
|
||||
if wheel_context is not None and self.button_timers[ButtonType.accelCruise] == 0:
|
||||
pressed = {b.type.raw for b in CS.buttonEvents if b.pressed}
|
||||
if ButtonType.accelCruise in pressed and ButtonType.resumeCruise not in pressed:
|
||||
self.slc_wheel_press = wheel_context
|
||||
|
||||
if CS.cruiseState.available:
|
||||
if software_cruise:
|
||||
if self.toyota is None and (self.slc_pcm or subaru_software) and (not enabled or not self.v_cruise_initialized):
|
||||
# PCM still owns engagement; SLC owns the software set speed while system long is selected.
|
||||
if subaru_software and (not enabled or not self.v_cruise_initialized):
|
||||
stock_kph = CS.cruiseState.speed * CV.MS_TO_KPH
|
||||
self.v_cruise_kph = stock_kph if 0 < stock_kph < V_CRUISE_UNSET else V_CRUISE_UNSET
|
||||
# if stock cruise is completely disabled, then we can use our own set speed logic
|
||||
self._update_v_cruise_non_pcm(CS, enabled, is_metric)
|
||||
if self.toyota is None:
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
if self.toyota is not None:
|
||||
self.toyota.constrain(self)
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
self.update_button_timers(CS, enabled)
|
||||
else:
|
||||
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
|
||||
@@ -139,6 +139,8 @@ class VCruiseHelper:
|
||||
self.pcm_slc_button = ButtonType.unknown
|
||||
if software_cruise:
|
||||
self.update_button_timers(CS, enabled)
|
||||
if self.button_timers[ButtonType.accelCruise] == 0:
|
||||
self.slc_wheel_press = None
|
||||
|
||||
def _update_toyota_pcm_slc_change(self, CS, enabled: bool, now_ns: int) -> None:
|
||||
"""Observe a physical PCM speed change without writing stock selected speed."""
|
||||
@@ -165,8 +167,9 @@ class VCruiseHelper:
|
||||
held = ButtonType.unknown
|
||||
if continuous:
|
||||
delta = self.v_cruise_kph - previous[0]
|
||||
if ((button == ButtonType.accelCruise and delta > 0.) or
|
||||
(button == ButtonType.decelCruise and delta < 0.)):
|
||||
if (not self.slc_suppressed_buttons and not self.slc_released_suppressed and
|
||||
((button == ButtonType.accelCruise and delta > 0.) or
|
||||
(button == ButtonType.decelCruise and delta < 0.))):
|
||||
self.slc_cruise_change = (previous[0] / 3.6, self.v_cruise_kph / 3.6,
|
||||
'accel' if delta > 0. else 'decel', False)
|
||||
self.pcm_slc_button = held
|
||||
@@ -179,17 +182,13 @@ class VCruiseHelper:
|
||||
continue
|
||||
if not b.pressed and button in self.slc_suppressed_buttons:
|
||||
self.slc_suppressed_buttons.remove(button)
|
||||
prior_owner = self.slc_suppression_owner.pop(button, None)
|
||||
if prior_owner is not None:
|
||||
self.slc_released_owner[button] = prior_owner
|
||||
self.slc_released_suppressed = True
|
||||
self.button_timers[button] = 0
|
||||
elif (b.pressed and enabled and CS.cruiseState.available and slc_pending is not None and
|
||||
not (self.toyota is not None and button == ButtonType.accelCruise and
|
||||
not (self.CP.pcmCruise and button == ButtonType.accelCruise and
|
||||
(CS.standstill or CS.cruiseState.standstill or not CS.cruiseState.enabled)) and
|
||||
button not in self.slc_suppressed_buttons):
|
||||
self.slc_suppressed_buttons.add(button)
|
||||
self.slc_suppression_owner[button] = slc_pending
|
||||
self.slc_consumed_button = SlcConsumedButton(slc_pending, 'accel' if button == ButtonType.accelCruise else 'decel')
|
||||
|
||||
def _update_v_cruise_non_pcm(self, CS, enabled, is_metric):
|
||||
@@ -231,26 +230,20 @@ class VCruiseHelper:
|
||||
if not self.button_change_states[button_type]["enabled"]:
|
||||
return
|
||||
|
||||
self.adjust_v_cruise(button_type, CS, is_metric, long_press=long_press)
|
||||
changed = self.adjust_v_cruise(button_type, CS, is_metric, long_press=long_press)
|
||||
if changed and button_type == ButtonType.accelCruise and not long_press and self.button_timers[button_type] > 0:
|
||||
self.slc_wheel_tap = self.slc_wheel_press
|
||||
|
||||
def adjust_v_cruise(self, button_type, CS, is_metric: bool, *, long_press: bool = False) -> bool:
|
||||
"""Apply the normal software cruise step without synthesizing button events."""
|
||||
from opendbc.car.subaru.software_cruise import selected as subaru_software_selected
|
||||
subaru_software = subaru_software_selected(self.CP)
|
||||
if ((self.CP.pcmCruise and not self.slc_pcm and not subaru_software) or button_type not in (ButtonType.accelCruise, ButtonType.decelCruise) or
|
||||
if ((self.CP.pcmCruise and not subaru_software) or button_type not in (ButtonType.accelCruise, ButtonType.decelCruise) or
|
||||
not 0 < self.v_cruise_kph < V_CRUISE_UNSET or
|
||||
(button_type == ButtonType.accelCruise and CS.cruiseState.standstill)):
|
||||
return False
|
||||
v_cruise_delta = 1. if is_metric else IMPERIAL_INCREMENT
|
||||
previous_kph = self.v_cruise_kph
|
||||
toyota = self.toyota if self.slc_pcm else None
|
||||
previous_cluster_kph = self.v_cruise_cluster_kph
|
||||
if toyota is not None:
|
||||
if toyota.requested is None:
|
||||
return False
|
||||
if button_type == ButtonType.decelCruise:
|
||||
previous_kph = toyota.requested[0]
|
||||
self.v_cruise_kph = toyota.requested[1]
|
||||
interval = self.intervals.held if long_press else self.intervals.short
|
||||
v_cruise_delta = v_cruise_delta * interval
|
||||
display_speed = round(self.v_cruise_kph if is_metric else self.v_cruise_kph * CV.KPH_TO_MPH)
|
||||
@@ -266,11 +259,6 @@ class VCruiseHelper:
|
||||
|
||||
self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX)
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
if toyota is not None:
|
||||
toyota.requested = (float(self.v_cruise_kph), float(self.v_cruise_cluster_kph))
|
||||
toyota.manual_step = (previous_kph, previous_cluster_kph, 'accel' if button_type == ButtonType.accelCruise else 'decel', long_press)
|
||||
toyota.constrain(self)
|
||||
return self.v_cruise_kph != previous_kph
|
||||
if 0 < previous_kph < V_CRUISE_UNSET and self.v_cruise_kph != previous_kph:
|
||||
button = "accel" if button_type == ButtonType.accelCruise else "decel"
|
||||
self.slc_cruise_change = (float(previous_kph / 3.6), float(self.v_cruise_kph / 3.6), button, long_press)
|
||||
@@ -291,23 +279,19 @@ class VCruiseHelper:
|
||||
self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled}
|
||||
|
||||
def apply_slc_target(self, target_mps: float, is_metric: bool) -> None:
|
||||
"""Apply a qualified planner command to the card-owned software set speed."""
|
||||
"""Set the software cruise ceiling for engagement or a qualified driver request."""
|
||||
# Match ordinary button precision so stepping away and back restores this target.
|
||||
self.v_cruise_kph = slc_target_kph(target_mps, is_metric)
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
if self.toyota is not None:
|
||||
self.toyota.manual_step = None
|
||||
self.toyota.constrain(self)
|
||||
self.toyota.requested = (self.v_cruise_kph, self.v_cruise_cluster_kph)
|
||||
|
||||
def initialize_v_cruise(self, CS, experimental_mode: bool, *, resume: bool | None = None,
|
||||
initial_speed: object = V_CRUISE_INITIAL, slc_target_mps: float | None = None, is_metric: bool = False) -> bool:
|
||||
"""Return whether engagement adopts the qualified current SLC limit."""
|
||||
"""Return whether engagement used the supplied SLC target."""
|
||||
if (not self.CP.openpilotLongitudinalControl or slc_target_mps is None or type(slc_target_mps) not in (int, float) or
|
||||
not math.isfinite(slc_target_mps) or not V_CRUISE_MIN / 3.6 - 1e-5 <= slc_target_mps <= V_CRUISE_MAX / 3.6 + 1e-5):
|
||||
slc_target_mps = None
|
||||
# PCM initializes when SLC does not own SET.
|
||||
if self.CP.pcmCruise and (not self.slc_pcm or slc_target_mps is None):
|
||||
# Native cruise owns PCM SET, including when SLC supplies a planner cap.
|
||||
if self.CP.pcmCruise:
|
||||
return False
|
||||
# A car-owned acknowledgment can outlive the physical button event.
|
||||
if resume is None:
|
||||
@@ -315,15 +299,12 @@ class VCruiseHelper:
|
||||
resume = (0 < self.pending_resume_ns and 0 <= age <= 500_000_000) or any(
|
||||
b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents)
|
||||
self.pending_resume_ns = 0
|
||||
if slc_target_mps is None and resume and self.v_cruise_initialized:
|
||||
if not self.CP.pcmCruise:
|
||||
self.v_cruise_kph = self.v_cruise_kph_last
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
if resume and self.v_cruise_initialized:
|
||||
self.v_cruise_kph = self.v_cruise_kph_last
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
return False
|
||||
elif slc_target_mps is not None:
|
||||
# Toyota still needs its native capacity/release approval before application.
|
||||
if self.toyota is None:
|
||||
self.apply_slc_target(slc_target_mps, is_metric)
|
||||
self.apply_slc_target(slc_target_mps, is_metric)
|
||||
return True
|
||||
else:
|
||||
self.v_cruise_kph = int(round(np.clip(max(CS.vEgo * CV.MS_TO_KPH, validated_initial_cruise_speed(initial_speed)), V_CRUISE_INITIAL, V_CRUISE_MAX)))
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestCruiseIntervals(unittest.TestCase):
|
||||
Path(self.params.get_param_path("CustomCruiseLong")).write_bytes(b"200")
|
||||
self.assertEqual(read_cruise_intervals(self.params, self.cp), CruiseIntervals())
|
||||
|
||||
def test_slc_uses_saved_intervals_for_every_other_make_and_pcm_mode(self):
|
||||
def test_slc_preserves_software_intervals_and_native_pcm_set(self):
|
||||
brands = [brand.__module__.split('.')[-2] for brand in BRANDS if brand.__module__.split('.')[-2] != 'toyota'] + ['future']
|
||||
self.params.put("CustomCruise", 3.0, block=True)
|
||||
self.params.put("CustomCruiseLong", 7.0, block=True)
|
||||
@@ -67,6 +67,11 @@ class TestCruiseIntervals(unittest.TestCase):
|
||||
helper.v_cruise_kph = helper.v_cruise_cluster_kph = initial
|
||||
helper.intervals = read_cruise_intervals(self.params, cp)
|
||||
helper.update_v_cruise(state(button, True, native=140.), True, metric, slc_enabled=True)
|
||||
if pcm:
|
||||
helper.update_v_cruise(state(button, False, native=140.), True, metric, slc_enabled=True)
|
||||
self.assertAlmostEqual(helper.v_cruise_kph, 140., places=4)
|
||||
self.assertIsNone(helper.slc_cruise_change)
|
||||
continue
|
||||
if held:
|
||||
for _ in range(CRUISE_LONG_PRESS):
|
||||
helper.update_v_cruise(state(native=140.), True, metric, slc_enabled=True)
|
||||
|
||||
@@ -373,12 +373,11 @@ class LongitudinalPlanner:
|
||||
cruise_profile = replace(cruise_profile, cruise_brake_magnitude=global_magnitude)
|
||||
if sm['controlsState'].forceDecel:
|
||||
cruise_profile = None
|
||||
elif (ceiling.effective_mps is not None or curve.effective_mps is not None) and cruise_profile is not None:
|
||||
# An eligible lower ceiling must retain at least native approach
|
||||
# braking even when the saved profile requests a softer cruise response.
|
||||
elif curve.effective_mps is not None and cruise_profile is not None:
|
||||
# Curve approach retains native braking when the saved cruise response is softer.
|
||||
cruise_profile = replace(cruise_profile, cruise_brake_magnitude=max(cruise_profile.cruise_brake_magnitude,
|
||||
abs(A_CRUISE_MIN)))
|
||||
if ceiling.effective_mps is not None or curve.effective_mps is not None:
|
||||
if curve.effective_mps is not None:
|
||||
global_floor = min(global_floor, A_CRUISE_MIN)
|
||||
if selected_acceleration is not None and cruise_profile is not None:
|
||||
# The selected curve owns acceleration; following/jerks retain their owner.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import json
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.params import Params
|
||||
@@ -12,8 +10,9 @@ from openpilot.selfdrive.controls.lib.ldw import LaneDepartureWarning
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, NATIVE_PLAN_INPUTS
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.starpilot.speed_limits import history as slc_history
|
||||
from openpilot.starpilot.speed_limits.runtime import Action as SlcAction, CruiseEvent as SlcCruiseEvent, Runtime as SlcRuntime
|
||||
from openpilot.starpilot.speed_limits.planner_host import (
|
||||
SLC_ACTIONS, SavedSlcLifecycle, action_from_message, cruise_event_from_message, current_cruise_event, receive_slc_events,
|
||||
)
|
||||
from openpilot.starpilot.longitudinal.profile_runtime import ProfileHost
|
||||
from openpilot.starpilot.longitudinal.lane_change_gap import LaneGapPreferences
|
||||
from openpilot.starpilot.longitudinal.experimental_release import ConditionalHandoff
|
||||
@@ -40,62 +39,8 @@ from openpilot.starpilot.feature_capabilities import longitudinal_supported
|
||||
from openpilot.starpilot.conditional_mode.projection import paired_clocks_ns
|
||||
from openpilot.starpilot.conditional_mode.ui_action import observation as ui_manual_observation
|
||||
from openpilot.starpilot.longitudinal.cruise_ceiling import CurveCeiling
|
||||
from openpilot.starpilot.feature_runtime import (enabled as feature_enabled, requested as feature_requested,
|
||||
slc_runtime_settings, slc_transport_capable, vision_control_enabled)
|
||||
from openpilot.starpilot.feature_runtime import enabled as feature_enabled, requested as feature_requested, slc_transport_capable
|
||||
import time
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import read_params as read_slc_settings
|
||||
|
||||
|
||||
class SavedSlcLifecycle:
|
||||
"""Refresh saved admission; offsets and display units preserve driver choices."""
|
||||
|
||||
def __init__(self, params, cp, environment):
|
||||
self.params, self.cp, self.environment = params, cp, dict(environment)
|
||||
self.runtime = None
|
||||
self.configuration = None
|
||||
self.last_refresh_ns = None
|
||||
|
||||
def refresh(self, now_ns: int, *, pending_actions=None, pending_cruise=None):
|
||||
if self.last_refresh_ns is not None and 0 <= now_ns - self.last_refresh_ns < 1_000_000_000:
|
||||
return self.runtime, False
|
||||
self.last_refresh_ns = now_ns
|
||||
# One immutable parsed snapshot owns this refresh. Repeated qualification
|
||||
# used to reopen the same saved keys five times in a due model iteration.
|
||||
saved_slc = read_slc_settings(self.params)
|
||||
admitted = feature_enabled(self.params, self.cp, 'slc', self.environment, saved_slc=saved_slc)
|
||||
settings = slc_runtime_settings(self.params, self.cp, self.environment, saved_slc=saved_slc)
|
||||
vision = admitted and feature_enabled(self.params, self.cp, 'vision', self.environment, saved_slc=saved_slc)
|
||||
configuration = (admitted and not settings.errors, settings, vision,
|
||||
vision_control_enabled(self.params, self.cp, saved_slc=saved_slc),
|
||||
feature_enabled(self.params, self.cp, 'vision', {}, saved_slc=saved_slc),
|
||||
self.params.get_bool('IsMetric'))
|
||||
if configuration == self.configuration:
|
||||
return self.runtime, False
|
||||
previous_runtime = self.runtime
|
||||
if (self.runtime is not None and self.configuration is not None and
|
||||
configuration[:-1] == (self.configuration[0], replace(self.runtime.settings, offsets=settings.offsets,
|
||||
advance_seconds=settings.advance_seconds),
|
||||
*self.configuration[2:-1])):
|
||||
if settings.offsets != self.runtime.settings.offsets or configuration[5] != self.configuration[5]:
|
||||
self.runtime.update_offsets(settings.offsets, is_metric=configuration[5])
|
||||
if settings.advance_seconds != self.runtime.settings.advance_seconds:
|
||||
self.runtime.update_advance(settings.advance_seconds)
|
||||
else:
|
||||
set_speed_after_ns = self.runtime.set_speed_after_ns if self.runtime is not None else 0
|
||||
if self.runtime is not None:
|
||||
self.runtime.reset()
|
||||
self.runtime = (SlcRuntime(settings, vision_enabled=vision,
|
||||
vision_control_qualified=configuration[3],
|
||||
vision_display_qualified=configuration[4], is_metric=configuration[5])
|
||||
if configuration[0] else None)
|
||||
if self.runtime is not None:
|
||||
self.runtime.set_speed_after_ns = set_speed_after_ns
|
||||
self.configuration = configuration
|
||||
if pending_actions is not None:
|
||||
pending_actions.clear()
|
||||
if pending_cruise is not None and (self.runtime is None or self.runtime is not previous_runtime):
|
||||
pending_cruise.clear()
|
||||
return self.runtime, True
|
||||
|
||||
|
||||
def profile_for_frame(host: ProfileHost | None, sm, CP, now_ns: int, *, traffic_mode: bool | None = False):
|
||||
@@ -159,11 +104,13 @@ def traffic_profile_status(mode: bool | None, target, applied, settings) -> tupl
|
||||
|
||||
|
||||
def curve_for_frame(host: CurveHost | None, sm, CP, now_ns: int, *, drive_id: int = 0,
|
||||
event: CurveDriverEvent | None = None, follow_time_s: float | None = None):
|
||||
event: CurveDriverEvent | None = None, follow_time_s: float | None = None,
|
||||
cruise_ceiling_mps: float | None = None):
|
||||
"""A diagnostic runtime result is not planner authority by itself."""
|
||||
if host is None:
|
||||
return None, None
|
||||
result = host.sample(sm, CP, now_ns, drive_id=drive_id, event=event, follow_time_s=follow_time_s)
|
||||
result = host.sample(sm, CP, now_ns, drive_id=drive_id, event=event, follow_time_s=follow_time_s,
|
||||
cruise_ceiling_mps=cruise_ceiling_mps)
|
||||
if result.ceiling_mps is None:
|
||||
return None, result
|
||||
model_ns = sm.logMonoTime.get('modelV2', 0)
|
||||
@@ -177,6 +124,12 @@ def confirm_curve_frame(host: CurveHost | None, planner: LongitudinalPlanner, no
|
||||
host.runtime.confirm_applied(now_ns, applied=bool(planner.last_curve_ceiling_applied))
|
||||
|
||||
|
||||
def wheel_override_blocked(force_stop: ForceStopRuntime | None, curve_host: CurveHost | None, *, curve_backlog: bool = False) -> bool:
|
||||
# Force Stop can consume RES on the tick it commits, before a forcing plan is published.
|
||||
return ((force_stop is not None and (force_stop.policy.can_commit_next_frame or force_stop.plan.speed_ceiling_mps is not None)) or
|
||||
(curve_host is not None and (curve_backlog or curve_host.runtime.was_controlling)))
|
||||
|
||||
|
||||
def conditional_handoff_for_frame(host: ConditionalPlannerHost | None, sm, CP, now_ns: int,
|
||||
drive_id: int) -> ConditionalHandoff | None:
|
||||
if host is None or not CP.openpilotLongitudinalControl or CP.passive or CP.dashcamOnly:
|
||||
@@ -290,7 +243,8 @@ def update_curve_frame(planner: LongitudinalPlanner, sm, CP, now_ns: int, *, hos
|
||||
results = []
|
||||
def provider(follow_time_s: float | None):
|
||||
ceiling, result = curve_for_frame(host, sm, CP, now_ns, drive_id=drive_id,
|
||||
event=event, follow_time_s=follow_time_s)
|
||||
event=event, follow_time_s=follow_time_s,
|
||||
cruise_ceiling_mps=cruise_ceiling.speed_mps if cruise_ceiling is not None else None)
|
||||
results.append(result)
|
||||
return ceiling
|
||||
planner.update(sm, cruise_ceiling=cruise_ceiling, profile_tuning=profile_tuning,
|
||||
@@ -320,18 +274,7 @@ def queue_cruise_event(event, slc_events, curve_events, conditional_events=None,
|
||||
curve_events.append(CurveDriverEvent(str(cruise.producerSessionId), int(cruise.eventId),
|
||||
int(cruise.observedMonoTime), kind, str(cruise.button), int(event.logMonoTime)))
|
||||
else:
|
||||
slc_events.append(SlcCruiseEvent(int(cruise.eventId), int(cruise.observedMonoTime),
|
||||
float(cruise.previousMps), float(cruise.selectedMps), str(cruise.button),
|
||||
str(cruise.producerSessionId), int(event.logMonoTime), kind,
|
||||
str(cruise.sessionId), int(cruise.decisionId),
|
||||
int(cruise.presentationId), int(cruise.commandId)))
|
||||
|
||||
|
||||
def current_cruise_event(events, now_ns: int):
|
||||
max_event_age_ns = int(2e9 / SERVICE_LIST['modelV2'].frequency)
|
||||
while events and now_ns - events[0].observed_ns > max_event_age_ns:
|
||||
events.popleft()
|
||||
return events.popleft() if events and events[0].observed_ns <= now_ns else None
|
||||
slc_events.append(cruise_event_from_message(event))
|
||||
|
||||
|
||||
def queue_ui_action(event, slc_actions, conditional_actions, mode_actions=None) -> None:
|
||||
@@ -343,9 +286,8 @@ def queue_ui_action(event, slc_actions, conditional_actions, mode_actions=None)
|
||||
mode_actions.append(event)
|
||||
elif kind == 'conditionalModeCycle':
|
||||
conditional_actions.append(event)
|
||||
elif kind in ('accept', 'reject', 'adopt'):
|
||||
slc_actions.append((int(event.logMonoTime), SlcAction(str(request.sessionId), int(request.sequenceId),
|
||||
int(request.decisionId), int(request.presentationId), kind)))
|
||||
elif kind in SLC_ACTIONS:
|
||||
slc_actions.append(action_from_message(event))
|
||||
|
||||
|
||||
def current_ui_manual_event(events, now_ns: int):
|
||||
@@ -417,7 +359,7 @@ def starpilot_main():
|
||||
pending_switchback = deque(maxlen=8)
|
||||
mode_settings = ConditionalSettingsOwner(params) if mode_owner is not None else None
|
||||
slc_lifecycle = SavedSlcLifecycle(params, CP, os.environ)
|
||||
slc_runtime, _ = slc_lifecycle.refresh(0)
|
||||
slc_lifecycle.refresh(0)
|
||||
slc_action_sock = (messaging.sub_sock("slcAction", conflate=False)
|
||||
if slc_available or conditional_host is not None or mode_owner is not None else None)
|
||||
slc_cruise_sock = messaging.sub_sock("slcCruiseEvent", conflate=False)
|
||||
@@ -431,18 +373,14 @@ def starpilot_main():
|
||||
publish_services = ['longitudinalPlan', 'starpilotLongitudinalPlan', 'driverAssistance']
|
||||
if slc_available or curve_host is not None or conditional_host is not None or traffic_owner is not None or mode_owner is not None:
|
||||
publish_services.append('slcState')
|
||||
if slc_available:
|
||||
publish_services.append('slcCruiseCommand')
|
||||
pm = messaging.PubMaster(publish_services)
|
||||
native_plan_checks = list(NATIVE_PLAN_INPUTS)
|
||||
subscribed_services = list(NATIVE_PLAN_INPUTS)
|
||||
subscribed_services.append('deviceState')
|
||||
if conditional_host is not None:
|
||||
subscribed_services.append('starpilotRadarState')
|
||||
optional_slc = ['slcDashboardObservation', 'slcVisionObservation', 'mapdOut', 'starpilotCarState'] if slc_available else []
|
||||
if slc_available and feature_enabled(params, CP, 'aol', os.environ):
|
||||
optional_slc.append('aolAxisState')
|
||||
if force_stop_owner is not None and 'starpilotCarState' not in optional_slc:
|
||||
optional_slc = slc_lifecycle.source_services() if slc_available else []
|
||||
if force_stop_owner is not None:
|
||||
optional_slc.append('starpilotCarState')
|
||||
subscribed_services.extend(optional_slc)
|
||||
subscribed_services.append('starpilotNavigation')
|
||||
@@ -459,16 +397,14 @@ def starpilot_main():
|
||||
# Due saved-setting verification can block on Params. Complete it
|
||||
# before acquiring the model and fast-source batch, not after that
|
||||
# batch has already begun aging toward the CEM freshness deadline.
|
||||
slc_runtime, _ = slc_lifecycle.refresh(time.monotonic_ns(), pending_actions=pending_slc_actions,
|
||||
pending_cruise=pending_slc_cruise)
|
||||
slc_lifecycle.refresh(time.monotonic_ns(), pending_actions=pending_slc_actions, pending_cruise=pending_slc_cruise)
|
||||
sm.update()
|
||||
if sm.updated['modelV2']:
|
||||
# Only explicit replay uses the recorded model timeline. In a live host,
|
||||
# modelV2 may lag newer inputs in the same SubMaster update.
|
||||
now_ns = int(sm.logMonoTime['modelV2']) if 'REPLAY' in os.environ else time.monotonic_ns()
|
||||
if 'REPLAY' in os.environ:
|
||||
slc_runtime, _ = slc_lifecycle.refresh(now_ns, pending_actions=pending_slc_actions,
|
||||
pending_cruise=pending_slc_cruise)
|
||||
slc_lifecycle.refresh(now_ns, pending_actions=pending_slc_actions, pending_cruise=pending_slc_cruise)
|
||||
current_start_ns = (int(sm['deviceState'].startedMonoTime)
|
||||
if 'deviceState' in subscribed_services and sm.valid['deviceState'] and
|
||||
sm.alive['deviceState'] and 0 < sm.logMonoTime['deviceState'] <= now_ns else 0)
|
||||
@@ -479,10 +415,7 @@ def starpilot_main():
|
||||
planner_status = None
|
||||
drive_changed = current_start_ns > 0 and drive_start_ns > 0 and current_start_ns != drive_start_ns
|
||||
if slc_cruise_sock is not None:
|
||||
for _ in range(8):
|
||||
event = messaging.recv_one_or_none(slc_cruise_sock)
|
||||
if event is None:
|
||||
break
|
||||
for event in receive_slc_events(slc_cruise_sock):
|
||||
if str(event.slcCruiseEvent.kind) == 'wheelAction' and event.valid:
|
||||
pending_wheel.append(event)
|
||||
continue
|
||||
@@ -505,10 +438,7 @@ def starpilot_main():
|
||||
if traffic_owner is not None:
|
||||
traffic_owner.reset()
|
||||
if slc_action_sock is not None:
|
||||
for _ in range(8):
|
||||
event = messaging.recv_one_or_none(slc_action_sock)
|
||||
if event is None:
|
||||
break
|
||||
for event in receive_slc_events(slc_action_sock):
|
||||
queue_ui_action(event, pending_slc_actions, pending_conditional_ui, pending_modes)
|
||||
waiting_wheel = deque(maxlen=32)
|
||||
while pending_wheel:
|
||||
@@ -520,25 +450,13 @@ def starpilot_main():
|
||||
else:
|
||||
wheel_coast.receive(event, params, CP, sm, now_ns=now_ns)
|
||||
pending_wheel = waiting_wheel
|
||||
if slc_runtime is not None:
|
||||
if drive_changed or slc_runtime.state.reset_required or slc_runtime.ledger.reset_required:
|
||||
slc_runtime.reset()
|
||||
pending_slc_actions.clear()
|
||||
pending_slc_cruise.clear()
|
||||
max_event_age_ns = int(2e9 / SERVICE_LIST['modelV2'].frequency)
|
||||
while pending_slc_actions and now_ns - pending_slc_actions[0][0] > max_event_age_ns:
|
||||
pending_slc_actions.popleft()
|
||||
cruise_event = current_cruise_event(pending_slc_cruise, now_ns)
|
||||
request = (pending_slc_actions.popleft()[1] if cruise_event is None and pending_slc_actions and
|
||||
pending_slc_actions[0][0] <= now_ns else None)
|
||||
output = slc_runtime.step(sm, CP, now_ns=now_ns, request=request, cruise_event=cruise_event)
|
||||
planner_status = output.message
|
||||
if output.command is not None:
|
||||
pm.send('slcCruiseCommand', output.command)
|
||||
history_write = output.result.acceptance.history_write if output.result.acceptance is not None else None
|
||||
# A camera episode has no road-location identity for a later drive.
|
||||
if history_write is not None and history_write.candidate.source != 'vision':
|
||||
params.put("SLCQualifiedHistory", json.loads(slc_history.encode(history_write, time.time_ns())))
|
||||
# Curve observes the press before SLC sees the release. A queued Curve press can still own the tap.
|
||||
curve_event = current_cruise_event(pending_curve_events, now_ns)
|
||||
slc_output = slc_lifecycle.step(sm, now_ns, pending_slc_actions, pending_slc_cruise, drive_changed=drive_changed,
|
||||
wheel_override_blocked=wheel_override_blocked(force_stop_owner, curve_host, curve_backlog=bool(pending_curve_events)))
|
||||
cruise_ceiling = slc_output.result.ceiling if slc_output is not None else None
|
||||
if slc_output is not None:
|
||||
planner_status = slc_output.message
|
||||
needs_clock = conditional_host is not None or traffic_owner is not None or force_stop_owner is not None or mode_owner is not None
|
||||
clock_pair = paired_clocks_ns() if needs_clock and 'REPLAY' not in os.environ else None
|
||||
controller_traffic_toggle = bool(wheel_coast.traffic_toggles % 2)
|
||||
@@ -598,7 +516,6 @@ def starpilot_main():
|
||||
lead_approach_key = lead_approach_for_frame(approach_preferences, sm, CP, now_ns)
|
||||
if curve_preferences is not None and curve_host is not None:
|
||||
curve_preferences.refresh(curve_host, now_ns)
|
||||
curve_event = current_cruise_event(pending_curve_events, now_ns)
|
||||
conditional_handoff = (conditional_handoff_for_frame(conditional_host, sm, CP, clock_pair[0], current_start_ns)
|
||||
if clock_pair is not None else None)
|
||||
wheel_coast.conditional_stop = bool(conditional_handoff is not None and conditional_host is not None and
|
||||
@@ -615,6 +532,7 @@ def starpilot_main():
|
||||
drive_id=current_start_ns, follow_seconds=follow_seconds,
|
||||
traffic_mode=traffic_mode, takeoff_enabled=faster_lead_takeoff)
|
||||
curve_result = update_curve_frame(longitudinal_planner, sm, CP, now_ns, host=curve_host,
|
||||
cruise_ceiling=cruise_ceiling,
|
||||
profile_tuning=profile_tuning,
|
||||
traffic_mode=traffic_mode, lane_change_policy=lane_change_policy,
|
||||
global_braking_response=global_braking_response, selected_profiles=selected_profiles,
|
||||
@@ -641,6 +559,7 @@ def starpilot_main():
|
||||
proposal, snapshot = conditional_host.sample(
|
||||
sm, CP, longitudinal_planner, now_mono_ns=conditional_now_ns, now_boot_ns=conditional_boot_ns,
|
||||
sample_skew_ns=conditional_skew_ns, drive_id=current_start_ns,
|
||||
cruise_ceiling_mps=cruise_ceiling.speed_mps if cruise_ceiling is not None else None,
|
||||
owner_context=ConditionalOwnerContext(
|
||||
traffic_mode=traffic_observation(traffic_verdict, model_ns=int(sm.logMonoTime['modelV2']),
|
||||
owner_present=traffic_owner is not None),
|
||||
@@ -675,6 +594,8 @@ def starpilot_main():
|
||||
mode_owner.sample('switchback', sm, CP, now_ns=now_ns), session=mode_session,
|
||||
sequence=mode_sequence, now_ns=now_ns, source_car_control_ns=int(sm.logMonoTime['carControl']))
|
||||
if planner_status is not None:
|
||||
if planner_status.slcState.wheelOverrideSince and wheel_override_blocked(force_stop_owner, curve_host):
|
||||
planner_status.slcState.wheelOverrideSince = 0
|
||||
pm.send('slcState', planner_status)
|
||||
if current_start_ns > 0:
|
||||
drive_start_ns = current_start_ns
|
||||
|
||||
@@ -9,6 +9,7 @@ import unittest
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot import feature_runtime
|
||||
from openpilot.starpilot.speed_limits import planner_host as slc_host
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import read_params as read_slc_settings
|
||||
from openpilot.selfdrive.controls import plannerd
|
||||
from openpilot.starpilot.conditional_mode.tests import test_status_consumer as transport
|
||||
@@ -98,7 +99,7 @@ class TestCemInputAcquisitionOrder(unittest.TestCase):
|
||||
def read(params):
|
||||
reads.append(True)
|
||||
return read_slc_settings(params)
|
||||
with patch.object(plannerd, 'read_slc_settings', side_effect=read, create=True), \
|
||||
with patch.object(slc_host, 'read_slc_settings', side_effect=read), \
|
||||
patch.object(feature_runtime, 'read_slc_settings', side_effect=read):
|
||||
runtime, changed = owner.refresh(MONO)
|
||||
self.assertTrue(changed)
|
||||
|
||||
@@ -23,21 +23,21 @@ from openpilot.starpilot.conditional_mode.projection import ConditionalOwnerCont
|
||||
from openpilot.starpilot.conditional_mode.runtime_settings import DOCUMENT_KEY
|
||||
from openpilot.starpilot.conditional_mode.status import settings_fingerprint
|
||||
from openpilot.starpilot.conditional_mode.tests.test_projection import BOOT, MONO, FakeSubMaster, serialized_scene
|
||||
from openpilot.starpilot.speed_limits.runtime import Action as SlcAction, CruiseEvent as SlcCruiseEvent
|
||||
|
||||
|
||||
class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
def test_slc_unit_change_keeps_receipts_for_the_current_command(self):
|
||||
from openpilot.selfdrive.car.cruise import slc_target_kph
|
||||
def test_slc_unit_change_preserves_cap_and_queued_driver_event(self):
|
||||
from openpilot.starpilot.speed_limits import offset_document as od
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent
|
||||
from openpilot.starpilot.speed_limits.tests.test_runtime_replay import RuntimeReplayTests
|
||||
fixture = RuntimeReplayTests()
|
||||
from openpilot.starpilot.speed_limits.tests import test_runtime_replay as replay
|
||||
fixture = replay.RuntimeReplayTests()
|
||||
fixture.setUp()
|
||||
sm, cp = fixture.sm, fixture.cp
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put_bool('SLCConfirmation', True, block=True)
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
params.put('SLCPriority1', 'Dashboard', block=True)
|
||||
params.put('SLCOffsetSchedule', od.to_value(od.default_document(False)), block=True)
|
||||
owner = plannerd.SavedSlcLifecycle(params, cp, {})
|
||||
@@ -46,20 +46,19 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
sm.advance(2_900_000_000)
|
||||
pending = runtime.step(sm, cp, now_ns=2_900_000_000).message.slcState
|
||||
sm.advance(2_950_000_000)
|
||||
issued = runtime.step(sm, cp, now_ns=2_950_000_000,
|
||||
accepted = runtime.step(sm, cp, now_ns=2_950_000_000,
|
||||
request=Action(runtime.session_id, 1, pending.decisionId, pending.presentationId, 'accept'))
|
||||
command = issued.command.slcCruiseCommand
|
||||
selected = slc_target_kph(command.targetMps, False)
|
||||
receipts = deque([CruiseEvent(1, 2_990_000_000, 60 / 3.6, selected / 3.6, '', 'card-producer',
|
||||
2_990_000_000, 'commandApplied', runtime.session_id, command.decisionId, command.presentationId, command.commandId)])
|
||||
self.assertEqual(accepted.result.ceiling.speed_mps, 25.)
|
||||
events = deque([CruiseEvent(1, 2_990_000_000, 60/3.6, 65/3.6, 'accel', 'card-producer', 2_990_000_000)])
|
||||
params.put_bool('IsMetric', True, block=True)
|
||||
metric, _ = owner.refresh(3_000_000_000, pending_cruise=receipts)
|
||||
self.assertEqual(len(receipts), 1)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = selected
|
||||
metric, _ = owner.refresh(3_000_000_000, pending_cruise=events)
|
||||
self.assertIs(metric, runtime)
|
||||
self.assertEqual(len(events), 1)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 65.
|
||||
sm.advance(3_000_000_000)
|
||||
applied = metric.step(sm, cp, now_ns=3_000_000_000, cruise_event=receipts.popleft())
|
||||
self.assertEqual(applied.message.slcState.commandStatus, 'applied')
|
||||
self.assertIsNone(applied.command)
|
||||
output = metric.step(sm, cp, now_ns=3_000_000_000, cruise_event=events.popleft())
|
||||
self.assertEqual(output.result.ceiling.speed_mps, 25.)
|
||||
self.assertEqual(sm['carState'].vCruise, 65.)
|
||||
|
||||
def test_slc_unit_changes_preserve_set_speed_and_driver_choices(self):
|
||||
from openpilot.common.constants import CV
|
||||
@@ -68,14 +67,14 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent
|
||||
from openpilot.starpilot.speed_limits.tests.test_runtime_replay import RuntimeReplayTests
|
||||
now = 2_000_000_000
|
||||
for confirmation, choice in ((False, 'applied'), (True, 'applied'), (False, 'override'), (True, 'decline')):
|
||||
for confirmation, choice in ((False, 'applied'), (True, 'applied'), (False, 'ceiling'), (True, 'decline')):
|
||||
with self.subTest(confirmation=confirmation, choice=choice), tempfile.TemporaryDirectory() as directory:
|
||||
fixture = RuntimeReplayTests()
|
||||
fixture.setUp()
|
||||
sm, cp = fixture.sm, fixture.cp
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put_bool('SLCConfirmation', confirmation, block=True)
|
||||
params.put('SLCConfirmation', int(confirmation), block=True)
|
||||
params.put('SLCPriority1', 'Dashboard', block=True)
|
||||
params.put('SLCOffsetSchedule', od.to_value(od.default_document(False)), block=True)
|
||||
owner = plannerd.SavedSlcLifecycle(params, cp, {})
|
||||
@@ -84,14 +83,14 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
baseline = slc_target_kph(65 * CV.MPH_TO_MS, False)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = baseline
|
||||
aligned = original.step(sm, cp, now_ns=now)
|
||||
self.assertIsNone(aligned.command)
|
||||
if choice == 'override':
|
||||
self.assertTrue(aligned.message.slcState.hasAccepted)
|
||||
if choice == 'ceiling':
|
||||
selected = slc_target_kph(70 * CV.MPH_TO_MS, False)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = selected
|
||||
sm.advance(now + 50_000_000)
|
||||
chosen = original.step(sm, cp, now_ns=now + 50_000_000, cruise_event=CruiseEvent(
|
||||
1, now + 50_000_000, baseline / 3.6, selected / 3.6, 'accel', 'card-producer', now + 50_000_000))
|
||||
self.assertTrue(chosen.message.slcState.driverOverrideActive)
|
||||
self.assertFalse(chosen.message.slcState.driverOverrideActive)
|
||||
elif choice == 'decline':
|
||||
sm['slcDashboardObservation'].speedMps = 55 * CV.MPH_TO_MS
|
||||
sm.advance(now + 50_000_000)
|
||||
@@ -105,11 +104,10 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
self.assertTrue(changed)
|
||||
sm.advance(now + 1_000_000_000)
|
||||
output = metric.step(sm, cp, now_ns=now + 1_000_000_000)
|
||||
self.assertIsNone(output.command)
|
||||
self.assertFalse(output.message.slcState.hasPending)
|
||||
self.assertTrue(output.message.slcState.hasAccepted)
|
||||
self.assertEqual(sm['carState'].vCruise, selected)
|
||||
self.assertEqual(output.message.slcState.driverOverrideActive, choice == 'override')
|
||||
self.assertFalse(output.message.slcState.driverOverrideActive)
|
||||
if choice == 'decline':
|
||||
self.assertEqual(output.result.acceptance.basis, 'rejected')
|
||||
|
||||
@@ -122,13 +120,11 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
owner = plannerd.SavedSlcLifecycle(params, cp, {})
|
||||
imperial, _ = owner.refresh(MONO)
|
||||
self.assertFalse(imperial.is_metric)
|
||||
imperial.set_speed_after_ns = MONO + 1_050_000_000
|
||||
actions, cruise = deque([object()]), deque([object()])
|
||||
params.put_bool('IsMetric', True, block=True)
|
||||
metric, changed = owner.refresh(MONO + 1_000_000_000, pending_actions=actions, pending_cruise=cruise)
|
||||
self.assertTrue(changed)
|
||||
self.assertTrue(metric.is_metric)
|
||||
self.assertEqual(metric.set_speed_after_ns, MONO + 1_050_000_000)
|
||||
self.assertIs(metric, imperial)
|
||||
self.assertFalse(actions)
|
||||
self.assertEqual(len(cruise), 1)
|
||||
@@ -168,7 +164,7 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
# Status and transport are reserved independently of saved SLC/CEM
|
||||
# admission, so a later saved change can attach without restarting.
|
||||
self.assertEqual(publisher.call_args.args[0], ['longitudinalPlan', 'starpilotLongitudinalPlan',
|
||||
'driverAssistance', 'slcState', 'slcCruiseCommand'])
|
||||
'driverAssistance', 'slcState'])
|
||||
self.assertEqual(conditional_ctor.call_count, int(enabled))
|
||||
if enabled:
|
||||
self.assertIs(conditional_ctor.call_args.args[0], params)
|
||||
@@ -200,8 +196,8 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
self.assertFalse(vision.vision_control_qualified)
|
||||
self.assertEqual(vision.settings.selection.slots[0], Source.VISION)
|
||||
session = vision.session_id
|
||||
pending_actions = deque([(3_000_000_000, plannerd.SlcAction(session, 1, 1, 1, 'accept'))])
|
||||
pending_cruise = deque([plannerd.SlcCruiseEvent(1, 3_000_000_000, 20., 21., 'set', session_id=session)])
|
||||
pending_actions = deque([(3_000_000_000, SlcAction(session, 1, 1, 1, 'accept'))])
|
||||
pending_cruise = deque([SlcCruiseEvent(1, 3_000_000_000, 20., 21., 'set', session_id=session)])
|
||||
self.assertEqual(owner.refresh(3_050_000_000, pending_actions=pending_actions,
|
||||
pending_cruise=pending_cruise), (vision, False))
|
||||
self.assertEqual(len(pending_actions), 1)
|
||||
@@ -217,8 +213,8 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
self.assertNotEqual(vision.session_id, session) # Retired session was reset.
|
||||
params.put_bool('ShowSpeedLimits', False, block=True)
|
||||
params.put_bool('SpeedLimitController', False, block=True)
|
||||
pending_actions.append((4_050_000_000, plannerd.SlcAction(control.session_id, 1, 1, 1, 'accept')))
|
||||
pending_cruise.append(plannerd.SlcCruiseEvent(2, 4_050_000_000, 20., 21., 'set', session_id=control.session_id))
|
||||
pending_actions.append((4_050_000_000, SlcAction(control.session_id, 1, 1, 1, 'accept')))
|
||||
pending_cruise.append(SlcCruiseEvent(2, 4_050_000_000, 20., 21., 'set', session_id=control.session_id))
|
||||
self.assertEqual(owner.refresh(5_000_000_000, pending_actions=pending_actions,
|
||||
pending_cruise=pending_cruise), (None, True))
|
||||
self.assertFalse(pending_actions)
|
||||
@@ -251,7 +247,7 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
|
||||
patch.object(messaging, 'SubMaster', return_value=sm) as subscriber, patch.object(messaging, 'sub_sock'):
|
||||
with self.assertRaises(EndLoop):
|
||||
plannerd.starpilot_main()
|
||||
self.assertIn('slcCruiseCommand', publisher.call_args.args[0])
|
||||
self.assertNotIn('slcCruiseCommand', publisher.call_args.args[0])
|
||||
for service in ('slcDashboardObservation', 'slcVisionObservation'):
|
||||
self.assertIn(service, subscriber.call_args.args[0])
|
||||
for check in ('ignore_alive', 'ignore_valid', 'ignore_avg_freq'):
|
||||
|
||||
@@ -156,7 +156,7 @@ class CurveLoopTests(unittest.TestCase):
|
||||
stamp = NOW + n * 50_000_000
|
||||
sm = bus(stamp)
|
||||
result = update_curve_frame(planner, sm, cp, stamp, host=host, cruise_ceiling=slc)
|
||||
self.assertIsNotNone(result.ceiling_mps)
|
||||
self.assertIsNone(result.ceiling_mps)
|
||||
self.assertFalse(planner.last_curve_ceiling_applied)
|
||||
self.assertFalse(host.runtime.was_controlling)
|
||||
self.assertEqual(host.runtime.curve.revision, 0)
|
||||
|
||||
@@ -45,326 +45,326 @@ const static double MAHA_THRESH_31 = 3.8414588206941227;
|
||||
* *
|
||||
* This file is part of 'ekf' *
|
||||
******************************************************************************/
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_7353202191454035649) {
|
||||
out_7353202191454035649[0] = delta_x[0] + nom_x[0];
|
||||
out_7353202191454035649[1] = delta_x[1] + nom_x[1];
|
||||
out_7353202191454035649[2] = delta_x[2] + nom_x[2];
|
||||
out_7353202191454035649[3] = delta_x[3] + nom_x[3];
|
||||
out_7353202191454035649[4] = delta_x[4] + nom_x[4];
|
||||
out_7353202191454035649[5] = delta_x[5] + nom_x[5];
|
||||
out_7353202191454035649[6] = delta_x[6] + nom_x[6];
|
||||
out_7353202191454035649[7] = delta_x[7] + nom_x[7];
|
||||
out_7353202191454035649[8] = delta_x[8] + nom_x[8];
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_7567495437456270106) {
|
||||
out_7567495437456270106[0] = delta_x[0] + nom_x[0];
|
||||
out_7567495437456270106[1] = delta_x[1] + nom_x[1];
|
||||
out_7567495437456270106[2] = delta_x[2] + nom_x[2];
|
||||
out_7567495437456270106[3] = delta_x[3] + nom_x[3];
|
||||
out_7567495437456270106[4] = delta_x[4] + nom_x[4];
|
||||
out_7567495437456270106[5] = delta_x[5] + nom_x[5];
|
||||
out_7567495437456270106[6] = delta_x[6] + nom_x[6];
|
||||
out_7567495437456270106[7] = delta_x[7] + nom_x[7];
|
||||
out_7567495437456270106[8] = delta_x[8] + nom_x[8];
|
||||
}
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_5273149315250963980) {
|
||||
out_5273149315250963980[0] = -nom_x[0] + true_x[0];
|
||||
out_5273149315250963980[1] = -nom_x[1] + true_x[1];
|
||||
out_5273149315250963980[2] = -nom_x[2] + true_x[2];
|
||||
out_5273149315250963980[3] = -nom_x[3] + true_x[3];
|
||||
out_5273149315250963980[4] = -nom_x[4] + true_x[4];
|
||||
out_5273149315250963980[5] = -nom_x[5] + true_x[5];
|
||||
out_5273149315250963980[6] = -nom_x[6] + true_x[6];
|
||||
out_5273149315250963980[7] = -nom_x[7] + true_x[7];
|
||||
out_5273149315250963980[8] = -nom_x[8] + true_x[8];
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_6277676899296321355) {
|
||||
out_6277676899296321355[0] = -nom_x[0] + true_x[0];
|
||||
out_6277676899296321355[1] = -nom_x[1] + true_x[1];
|
||||
out_6277676899296321355[2] = -nom_x[2] + true_x[2];
|
||||
out_6277676899296321355[3] = -nom_x[3] + true_x[3];
|
||||
out_6277676899296321355[4] = -nom_x[4] + true_x[4];
|
||||
out_6277676899296321355[5] = -nom_x[5] + true_x[5];
|
||||
out_6277676899296321355[6] = -nom_x[6] + true_x[6];
|
||||
out_6277676899296321355[7] = -nom_x[7] + true_x[7];
|
||||
out_6277676899296321355[8] = -nom_x[8] + true_x[8];
|
||||
}
|
||||
void H_mod_fun(double *state, double *out_5463203357507594110) {
|
||||
out_5463203357507594110[0] = 1.0;
|
||||
out_5463203357507594110[1] = 0.0;
|
||||
out_5463203357507594110[2] = 0.0;
|
||||
out_5463203357507594110[3] = 0.0;
|
||||
out_5463203357507594110[4] = 0.0;
|
||||
out_5463203357507594110[5] = 0.0;
|
||||
out_5463203357507594110[6] = 0.0;
|
||||
out_5463203357507594110[7] = 0.0;
|
||||
out_5463203357507594110[8] = 0.0;
|
||||
out_5463203357507594110[9] = 0.0;
|
||||
out_5463203357507594110[10] = 1.0;
|
||||
out_5463203357507594110[11] = 0.0;
|
||||
out_5463203357507594110[12] = 0.0;
|
||||
out_5463203357507594110[13] = 0.0;
|
||||
out_5463203357507594110[14] = 0.0;
|
||||
out_5463203357507594110[15] = 0.0;
|
||||
out_5463203357507594110[16] = 0.0;
|
||||
out_5463203357507594110[17] = 0.0;
|
||||
out_5463203357507594110[18] = 0.0;
|
||||
out_5463203357507594110[19] = 0.0;
|
||||
out_5463203357507594110[20] = 1.0;
|
||||
out_5463203357507594110[21] = 0.0;
|
||||
out_5463203357507594110[22] = 0.0;
|
||||
out_5463203357507594110[23] = 0.0;
|
||||
out_5463203357507594110[24] = 0.0;
|
||||
out_5463203357507594110[25] = 0.0;
|
||||
out_5463203357507594110[26] = 0.0;
|
||||
out_5463203357507594110[27] = 0.0;
|
||||
out_5463203357507594110[28] = 0.0;
|
||||
out_5463203357507594110[29] = 0.0;
|
||||
out_5463203357507594110[30] = 1.0;
|
||||
out_5463203357507594110[31] = 0.0;
|
||||
out_5463203357507594110[32] = 0.0;
|
||||
out_5463203357507594110[33] = 0.0;
|
||||
out_5463203357507594110[34] = 0.0;
|
||||
out_5463203357507594110[35] = 0.0;
|
||||
out_5463203357507594110[36] = 0.0;
|
||||
out_5463203357507594110[37] = 0.0;
|
||||
out_5463203357507594110[38] = 0.0;
|
||||
out_5463203357507594110[39] = 0.0;
|
||||
out_5463203357507594110[40] = 1.0;
|
||||
out_5463203357507594110[41] = 0.0;
|
||||
out_5463203357507594110[42] = 0.0;
|
||||
out_5463203357507594110[43] = 0.0;
|
||||
out_5463203357507594110[44] = 0.0;
|
||||
out_5463203357507594110[45] = 0.0;
|
||||
out_5463203357507594110[46] = 0.0;
|
||||
out_5463203357507594110[47] = 0.0;
|
||||
out_5463203357507594110[48] = 0.0;
|
||||
out_5463203357507594110[49] = 0.0;
|
||||
out_5463203357507594110[50] = 1.0;
|
||||
out_5463203357507594110[51] = 0.0;
|
||||
out_5463203357507594110[52] = 0.0;
|
||||
out_5463203357507594110[53] = 0.0;
|
||||
out_5463203357507594110[54] = 0.0;
|
||||
out_5463203357507594110[55] = 0.0;
|
||||
out_5463203357507594110[56] = 0.0;
|
||||
out_5463203357507594110[57] = 0.0;
|
||||
out_5463203357507594110[58] = 0.0;
|
||||
out_5463203357507594110[59] = 0.0;
|
||||
out_5463203357507594110[60] = 1.0;
|
||||
out_5463203357507594110[61] = 0.0;
|
||||
out_5463203357507594110[62] = 0.0;
|
||||
out_5463203357507594110[63] = 0.0;
|
||||
out_5463203357507594110[64] = 0.0;
|
||||
out_5463203357507594110[65] = 0.0;
|
||||
out_5463203357507594110[66] = 0.0;
|
||||
out_5463203357507594110[67] = 0.0;
|
||||
out_5463203357507594110[68] = 0.0;
|
||||
out_5463203357507594110[69] = 0.0;
|
||||
out_5463203357507594110[70] = 1.0;
|
||||
out_5463203357507594110[71] = 0.0;
|
||||
out_5463203357507594110[72] = 0.0;
|
||||
out_5463203357507594110[73] = 0.0;
|
||||
out_5463203357507594110[74] = 0.0;
|
||||
out_5463203357507594110[75] = 0.0;
|
||||
out_5463203357507594110[76] = 0.0;
|
||||
out_5463203357507594110[77] = 0.0;
|
||||
out_5463203357507594110[78] = 0.0;
|
||||
out_5463203357507594110[79] = 0.0;
|
||||
out_5463203357507594110[80] = 1.0;
|
||||
void H_mod_fun(double *state, double *out_7420380263460044385) {
|
||||
out_7420380263460044385[0] = 1.0;
|
||||
out_7420380263460044385[1] = 0.0;
|
||||
out_7420380263460044385[2] = 0.0;
|
||||
out_7420380263460044385[3] = 0.0;
|
||||
out_7420380263460044385[4] = 0.0;
|
||||
out_7420380263460044385[5] = 0.0;
|
||||
out_7420380263460044385[6] = 0.0;
|
||||
out_7420380263460044385[7] = 0.0;
|
||||
out_7420380263460044385[8] = 0.0;
|
||||
out_7420380263460044385[9] = 0.0;
|
||||
out_7420380263460044385[10] = 1.0;
|
||||
out_7420380263460044385[11] = 0.0;
|
||||
out_7420380263460044385[12] = 0.0;
|
||||
out_7420380263460044385[13] = 0.0;
|
||||
out_7420380263460044385[14] = 0.0;
|
||||
out_7420380263460044385[15] = 0.0;
|
||||
out_7420380263460044385[16] = 0.0;
|
||||
out_7420380263460044385[17] = 0.0;
|
||||
out_7420380263460044385[18] = 0.0;
|
||||
out_7420380263460044385[19] = 0.0;
|
||||
out_7420380263460044385[20] = 1.0;
|
||||
out_7420380263460044385[21] = 0.0;
|
||||
out_7420380263460044385[22] = 0.0;
|
||||
out_7420380263460044385[23] = 0.0;
|
||||
out_7420380263460044385[24] = 0.0;
|
||||
out_7420380263460044385[25] = 0.0;
|
||||
out_7420380263460044385[26] = 0.0;
|
||||
out_7420380263460044385[27] = 0.0;
|
||||
out_7420380263460044385[28] = 0.0;
|
||||
out_7420380263460044385[29] = 0.0;
|
||||
out_7420380263460044385[30] = 1.0;
|
||||
out_7420380263460044385[31] = 0.0;
|
||||
out_7420380263460044385[32] = 0.0;
|
||||
out_7420380263460044385[33] = 0.0;
|
||||
out_7420380263460044385[34] = 0.0;
|
||||
out_7420380263460044385[35] = 0.0;
|
||||
out_7420380263460044385[36] = 0.0;
|
||||
out_7420380263460044385[37] = 0.0;
|
||||
out_7420380263460044385[38] = 0.0;
|
||||
out_7420380263460044385[39] = 0.0;
|
||||
out_7420380263460044385[40] = 1.0;
|
||||
out_7420380263460044385[41] = 0.0;
|
||||
out_7420380263460044385[42] = 0.0;
|
||||
out_7420380263460044385[43] = 0.0;
|
||||
out_7420380263460044385[44] = 0.0;
|
||||
out_7420380263460044385[45] = 0.0;
|
||||
out_7420380263460044385[46] = 0.0;
|
||||
out_7420380263460044385[47] = 0.0;
|
||||
out_7420380263460044385[48] = 0.0;
|
||||
out_7420380263460044385[49] = 0.0;
|
||||
out_7420380263460044385[50] = 1.0;
|
||||
out_7420380263460044385[51] = 0.0;
|
||||
out_7420380263460044385[52] = 0.0;
|
||||
out_7420380263460044385[53] = 0.0;
|
||||
out_7420380263460044385[54] = 0.0;
|
||||
out_7420380263460044385[55] = 0.0;
|
||||
out_7420380263460044385[56] = 0.0;
|
||||
out_7420380263460044385[57] = 0.0;
|
||||
out_7420380263460044385[58] = 0.0;
|
||||
out_7420380263460044385[59] = 0.0;
|
||||
out_7420380263460044385[60] = 1.0;
|
||||
out_7420380263460044385[61] = 0.0;
|
||||
out_7420380263460044385[62] = 0.0;
|
||||
out_7420380263460044385[63] = 0.0;
|
||||
out_7420380263460044385[64] = 0.0;
|
||||
out_7420380263460044385[65] = 0.0;
|
||||
out_7420380263460044385[66] = 0.0;
|
||||
out_7420380263460044385[67] = 0.0;
|
||||
out_7420380263460044385[68] = 0.0;
|
||||
out_7420380263460044385[69] = 0.0;
|
||||
out_7420380263460044385[70] = 1.0;
|
||||
out_7420380263460044385[71] = 0.0;
|
||||
out_7420380263460044385[72] = 0.0;
|
||||
out_7420380263460044385[73] = 0.0;
|
||||
out_7420380263460044385[74] = 0.0;
|
||||
out_7420380263460044385[75] = 0.0;
|
||||
out_7420380263460044385[76] = 0.0;
|
||||
out_7420380263460044385[77] = 0.0;
|
||||
out_7420380263460044385[78] = 0.0;
|
||||
out_7420380263460044385[79] = 0.0;
|
||||
out_7420380263460044385[80] = 1.0;
|
||||
}
|
||||
void f_fun(double *state, double dt, double *out_6125045598488451197) {
|
||||
out_6125045598488451197[0] = state[0];
|
||||
out_6125045598488451197[1] = state[1];
|
||||
out_6125045598488451197[2] = state[2];
|
||||
out_6125045598488451197[3] = state[3];
|
||||
out_6125045598488451197[4] = state[4];
|
||||
out_6125045598488451197[5] = dt*((-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]))*state[6] - 9.8100000000000005*state[8] + stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*state[1]) + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*state[4])) + state[5];
|
||||
out_6125045598488451197[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_6125045598488451197[7] = state[7];
|
||||
out_6125045598488451197[8] = state[8];
|
||||
void f_fun(double *state, double dt, double *out_3045596088569131513) {
|
||||
out_3045596088569131513[0] = state[0];
|
||||
out_3045596088569131513[1] = state[1];
|
||||
out_3045596088569131513[2] = state[2];
|
||||
out_3045596088569131513[3] = state[3];
|
||||
out_3045596088569131513[4] = state[4];
|
||||
out_3045596088569131513[5] = dt*((-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]))*state[6] - 9.8100000000000005*state[8] + stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*state[1]) + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*state[4])) + state[5];
|
||||
out_3045596088569131513[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_3045596088569131513[7] = state[7];
|
||||
out_3045596088569131513[8] = state[8];
|
||||
}
|
||||
void F_fun(double *state, double dt, double *out_548031277288491442) {
|
||||
out_548031277288491442[0] = 1;
|
||||
out_548031277288491442[1] = 0;
|
||||
out_548031277288491442[2] = 0;
|
||||
out_548031277288491442[3] = 0;
|
||||
out_548031277288491442[4] = 0;
|
||||
out_548031277288491442[5] = 0;
|
||||
out_548031277288491442[6] = 0;
|
||||
out_548031277288491442[7] = 0;
|
||||
out_548031277288491442[8] = 0;
|
||||
out_548031277288491442[9] = 0;
|
||||
out_548031277288491442[10] = 1;
|
||||
out_548031277288491442[11] = 0;
|
||||
out_548031277288491442[12] = 0;
|
||||
out_548031277288491442[13] = 0;
|
||||
out_548031277288491442[14] = 0;
|
||||
out_548031277288491442[15] = 0;
|
||||
out_548031277288491442[16] = 0;
|
||||
out_548031277288491442[17] = 0;
|
||||
out_548031277288491442[18] = 0;
|
||||
out_548031277288491442[19] = 0;
|
||||
out_548031277288491442[20] = 1;
|
||||
out_548031277288491442[21] = 0;
|
||||
out_548031277288491442[22] = 0;
|
||||
out_548031277288491442[23] = 0;
|
||||
out_548031277288491442[24] = 0;
|
||||
out_548031277288491442[25] = 0;
|
||||
out_548031277288491442[26] = 0;
|
||||
out_548031277288491442[27] = 0;
|
||||
out_548031277288491442[28] = 0;
|
||||
out_548031277288491442[29] = 0;
|
||||
out_548031277288491442[30] = 1;
|
||||
out_548031277288491442[31] = 0;
|
||||
out_548031277288491442[32] = 0;
|
||||
out_548031277288491442[33] = 0;
|
||||
out_548031277288491442[34] = 0;
|
||||
out_548031277288491442[35] = 0;
|
||||
out_548031277288491442[36] = 0;
|
||||
out_548031277288491442[37] = 0;
|
||||
out_548031277288491442[38] = 0;
|
||||
out_548031277288491442[39] = 0;
|
||||
out_548031277288491442[40] = 1;
|
||||
out_548031277288491442[41] = 0;
|
||||
out_548031277288491442[42] = 0;
|
||||
out_548031277288491442[43] = 0;
|
||||
out_548031277288491442[44] = 0;
|
||||
out_548031277288491442[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/(mass*state[1]) + (-stiffness_front - stiffness_rear)*state[5]/(mass*state[4]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/(mass*state[4]));
|
||||
out_548031277288491442[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*pow(state[1], 2));
|
||||
out_548031277288491442[47] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_548031277288491442[48] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_548031277288491442[49] = dt*((-1 - (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*pow(state[4], 2)))*state[6] - (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*pow(state[4], 2)));
|
||||
out_548031277288491442[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/(mass*state[4]) + 1;
|
||||
out_548031277288491442[51] = dt*(-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]));
|
||||
out_548031277288491442[52] = dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_548031277288491442[53] = -9.8100000000000005*dt;
|
||||
out_548031277288491442[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_548031277288491442[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_548031277288491442[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_548031277288491442[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_548031277288491442[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_548031277288491442[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_548031277288491442[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_548031277288491442[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_548031277288491442[62] = 0;
|
||||
out_548031277288491442[63] = 0;
|
||||
out_548031277288491442[64] = 0;
|
||||
out_548031277288491442[65] = 0;
|
||||
out_548031277288491442[66] = 0;
|
||||
out_548031277288491442[67] = 0;
|
||||
out_548031277288491442[68] = 0;
|
||||
out_548031277288491442[69] = 0;
|
||||
out_548031277288491442[70] = 1;
|
||||
out_548031277288491442[71] = 0;
|
||||
out_548031277288491442[72] = 0;
|
||||
out_548031277288491442[73] = 0;
|
||||
out_548031277288491442[74] = 0;
|
||||
out_548031277288491442[75] = 0;
|
||||
out_548031277288491442[76] = 0;
|
||||
out_548031277288491442[77] = 0;
|
||||
out_548031277288491442[78] = 0;
|
||||
out_548031277288491442[79] = 0;
|
||||
out_548031277288491442[80] = 1;
|
||||
void F_fun(double *state, double dt, double *out_8700545601531255316) {
|
||||
out_8700545601531255316[0] = 1;
|
||||
out_8700545601531255316[1] = 0;
|
||||
out_8700545601531255316[2] = 0;
|
||||
out_8700545601531255316[3] = 0;
|
||||
out_8700545601531255316[4] = 0;
|
||||
out_8700545601531255316[5] = 0;
|
||||
out_8700545601531255316[6] = 0;
|
||||
out_8700545601531255316[7] = 0;
|
||||
out_8700545601531255316[8] = 0;
|
||||
out_8700545601531255316[9] = 0;
|
||||
out_8700545601531255316[10] = 1;
|
||||
out_8700545601531255316[11] = 0;
|
||||
out_8700545601531255316[12] = 0;
|
||||
out_8700545601531255316[13] = 0;
|
||||
out_8700545601531255316[14] = 0;
|
||||
out_8700545601531255316[15] = 0;
|
||||
out_8700545601531255316[16] = 0;
|
||||
out_8700545601531255316[17] = 0;
|
||||
out_8700545601531255316[18] = 0;
|
||||
out_8700545601531255316[19] = 0;
|
||||
out_8700545601531255316[20] = 1;
|
||||
out_8700545601531255316[21] = 0;
|
||||
out_8700545601531255316[22] = 0;
|
||||
out_8700545601531255316[23] = 0;
|
||||
out_8700545601531255316[24] = 0;
|
||||
out_8700545601531255316[25] = 0;
|
||||
out_8700545601531255316[26] = 0;
|
||||
out_8700545601531255316[27] = 0;
|
||||
out_8700545601531255316[28] = 0;
|
||||
out_8700545601531255316[29] = 0;
|
||||
out_8700545601531255316[30] = 1;
|
||||
out_8700545601531255316[31] = 0;
|
||||
out_8700545601531255316[32] = 0;
|
||||
out_8700545601531255316[33] = 0;
|
||||
out_8700545601531255316[34] = 0;
|
||||
out_8700545601531255316[35] = 0;
|
||||
out_8700545601531255316[36] = 0;
|
||||
out_8700545601531255316[37] = 0;
|
||||
out_8700545601531255316[38] = 0;
|
||||
out_8700545601531255316[39] = 0;
|
||||
out_8700545601531255316[40] = 1;
|
||||
out_8700545601531255316[41] = 0;
|
||||
out_8700545601531255316[42] = 0;
|
||||
out_8700545601531255316[43] = 0;
|
||||
out_8700545601531255316[44] = 0;
|
||||
out_8700545601531255316[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/(mass*state[1]) + (-stiffness_front - stiffness_rear)*state[5]/(mass*state[4]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/(mass*state[4]));
|
||||
out_8700545601531255316[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*pow(state[1], 2));
|
||||
out_8700545601531255316[47] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_8700545601531255316[48] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_8700545601531255316[49] = dt*((-1 - (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*pow(state[4], 2)))*state[6] - (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*pow(state[4], 2)));
|
||||
out_8700545601531255316[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/(mass*state[4]) + 1;
|
||||
out_8700545601531255316[51] = dt*(-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]));
|
||||
out_8700545601531255316[52] = dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_8700545601531255316[53] = -9.8100000000000005*dt;
|
||||
out_8700545601531255316[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_8700545601531255316[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_8700545601531255316[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_8700545601531255316[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_8700545601531255316[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_8700545601531255316[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_8700545601531255316[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_8700545601531255316[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_8700545601531255316[62] = 0;
|
||||
out_8700545601531255316[63] = 0;
|
||||
out_8700545601531255316[64] = 0;
|
||||
out_8700545601531255316[65] = 0;
|
||||
out_8700545601531255316[66] = 0;
|
||||
out_8700545601531255316[67] = 0;
|
||||
out_8700545601531255316[68] = 0;
|
||||
out_8700545601531255316[69] = 0;
|
||||
out_8700545601531255316[70] = 1;
|
||||
out_8700545601531255316[71] = 0;
|
||||
out_8700545601531255316[72] = 0;
|
||||
out_8700545601531255316[73] = 0;
|
||||
out_8700545601531255316[74] = 0;
|
||||
out_8700545601531255316[75] = 0;
|
||||
out_8700545601531255316[76] = 0;
|
||||
out_8700545601531255316[77] = 0;
|
||||
out_8700545601531255316[78] = 0;
|
||||
out_8700545601531255316[79] = 0;
|
||||
out_8700545601531255316[80] = 1;
|
||||
}
|
||||
void h_25(double *state, double *unused, double *out_7825793012049214991) {
|
||||
out_7825793012049214991[0] = state[6];
|
||||
void h_25(double *state, double *unused, double *out_2824946100795669147) {
|
||||
out_2824946100795669147[0] = state[6];
|
||||
}
|
||||
void H_25(double *state, double *unused, double *out_8986251996152959258) {
|
||||
out_8986251996152959258[0] = 0;
|
||||
out_8986251996152959258[1] = 0;
|
||||
out_8986251996152959258[2] = 0;
|
||||
out_8986251996152959258[3] = 0;
|
||||
out_8986251996152959258[4] = 0;
|
||||
out_8986251996152959258[5] = 0;
|
||||
out_8986251996152959258[6] = 1;
|
||||
out_8986251996152959258[7] = 0;
|
||||
out_8986251996152959258[8] = 0;
|
||||
void H_25(double *state, double *unused, double *out_2516688438275379545) {
|
||||
out_2516688438275379545[0] = 0;
|
||||
out_2516688438275379545[1] = 0;
|
||||
out_2516688438275379545[2] = 0;
|
||||
out_2516688438275379545[3] = 0;
|
||||
out_2516688438275379545[4] = 0;
|
||||
out_2516688438275379545[5] = 0;
|
||||
out_2516688438275379545[6] = 1;
|
||||
out_2516688438275379545[7] = 0;
|
||||
out_2516688438275379545[8] = 0;
|
||||
}
|
||||
void h_24(double *state, double *unused, double *out_3863850671382452828) {
|
||||
out_3863850671382452828[0] = state[4];
|
||||
out_3863850671382452828[1] = state[5];
|
||||
void h_24(double *state, double *unused, double *out_3049085865514909722) {
|
||||
out_3049085865514909722[0] = state[4];
|
||||
out_3049085865514909722[1] = state[5];
|
||||
}
|
||||
void H_24(double *state, double *unused, double *out_5428326397350636122) {
|
||||
out_5428326397350636122[0] = 0;
|
||||
out_5428326397350636122[1] = 0;
|
||||
out_5428326397350636122[2] = 0;
|
||||
out_5428326397350636122[3] = 0;
|
||||
out_5428326397350636122[4] = 1;
|
||||
out_5428326397350636122[5] = 0;
|
||||
out_5428326397350636122[6] = 0;
|
||||
out_5428326397350636122[7] = 0;
|
||||
out_5428326397350636122[8] = 0;
|
||||
out_5428326397350636122[9] = 0;
|
||||
out_5428326397350636122[10] = 0;
|
||||
out_5428326397350636122[11] = 0;
|
||||
out_5428326397350636122[12] = 0;
|
||||
out_5428326397350636122[13] = 0;
|
||||
out_5428326397350636122[14] = 1;
|
||||
out_5428326397350636122[15] = 0;
|
||||
out_5428326397350636122[16] = 0;
|
||||
out_5428326397350636122[17] = 0;
|
||||
void H_24(double *state, double *unused, double *out_339474014668229572) {
|
||||
out_339474014668229572[0] = 0;
|
||||
out_339474014668229572[1] = 0;
|
||||
out_339474014668229572[2] = 0;
|
||||
out_339474014668229572[3] = 0;
|
||||
out_339474014668229572[4] = 1;
|
||||
out_339474014668229572[5] = 0;
|
||||
out_339474014668229572[6] = 0;
|
||||
out_339474014668229572[7] = 0;
|
||||
out_339474014668229572[8] = 0;
|
||||
out_339474014668229572[9] = 0;
|
||||
out_339474014668229572[10] = 0;
|
||||
out_339474014668229572[11] = 0;
|
||||
out_339474014668229572[12] = 0;
|
||||
out_339474014668229572[13] = 0;
|
||||
out_339474014668229572[14] = 1;
|
||||
out_339474014668229572[15] = 0;
|
||||
out_339474014668229572[16] = 0;
|
||||
out_339474014668229572[17] = 0;
|
||||
}
|
||||
void h_30(double *state, double *unused, double *out_3653635896979304131) {
|
||||
out_3653635896979304131[0] = state[4];
|
||||
void h_30(double *state, double *unused, double *out_6312771183888182035) {
|
||||
out_6312771183888182035[0] = state[4];
|
||||
}
|
||||
void H_30(double *state, double *unused, double *out_6467919037645710631) {
|
||||
out_6467919037645710631[0] = 0;
|
||||
out_6467919037645710631[1] = 0;
|
||||
out_6467919037645710631[2] = 0;
|
||||
out_6467919037645710631[3] = 0;
|
||||
out_6467919037645710631[4] = 1;
|
||||
out_6467919037645710631[5] = 0;
|
||||
out_6467919037645710631[6] = 0;
|
||||
out_6467919037645710631[7] = 0;
|
||||
out_6467919037645710631[8] = 0;
|
||||
void H_30(double *state, double *unused, double *out_1644520231869082) {
|
||||
out_1644520231869082[0] = 0;
|
||||
out_1644520231869082[1] = 0;
|
||||
out_1644520231869082[2] = 0;
|
||||
out_1644520231869082[3] = 0;
|
||||
out_1644520231869082[4] = 1;
|
||||
out_1644520231869082[5] = 0;
|
||||
out_1644520231869082[6] = 0;
|
||||
out_1644520231869082[7] = 0;
|
||||
out_1644520231869082[8] = 0;
|
||||
}
|
||||
void h_26(double *state, double *unused, double *out_8655309164640149776) {
|
||||
out_8655309164640149776[0] = state[7];
|
||||
void h_26(double *state, double *unused, double *out_6908065673986044634) {
|
||||
out_6908065673986044634[0] = state[7];
|
||||
}
|
||||
void H_26(double *state, double *unused, double *out_5718988758682536134) {
|
||||
out_5718988758682536134[0] = 0;
|
||||
out_5718988758682536134[1] = 0;
|
||||
out_5718988758682536134[2] = 0;
|
||||
out_5718988758682536134[3] = 0;
|
||||
out_5718988758682536134[4] = 0;
|
||||
out_5718988758682536134[5] = 0;
|
||||
out_5718988758682536134[6] = 0;
|
||||
out_5718988758682536134[7] = 1;
|
||||
out_5718988758682536134[8] = 0;
|
||||
void H_26(double *state, double *unused, double *out_6258191757149435769) {
|
||||
out_6258191757149435769[0] = 0;
|
||||
out_6258191757149435769[1] = 0;
|
||||
out_6258191757149435769[2] = 0;
|
||||
out_6258191757149435769[3] = 0;
|
||||
out_6258191757149435769[4] = 0;
|
||||
out_6258191757149435769[5] = 0;
|
||||
out_6258191757149435769[6] = 0;
|
||||
out_6258191757149435769[7] = 1;
|
||||
out_6258191757149435769[8] = 0;
|
||||
}
|
||||
void h_27(double *state, double *unused, double *out_5968325673074535865) {
|
||||
out_5968325673074535865[0] = state[3];
|
||||
void h_27(double *state, double *unused, double *out_709679758613972836) {
|
||||
out_709679758613972836[0] = state[3];
|
||||
}
|
||||
void H_27(double *state, double *unused, double *out_4244324966461767414) {
|
||||
out_4244324966461767414[0] = 0;
|
||||
out_4244324966461767414[1] = 0;
|
||||
out_4244324966461767414[2] = 0;
|
||||
out_4244324966461767414[3] = 1;
|
||||
out_4244324966461767414[4] = 0;
|
||||
out_4244324966461767414[5] = 0;
|
||||
out_4244324966461767414[6] = 0;
|
||||
out_4244324966461767414[7] = 0;
|
||||
out_4244324966461767414[8] = 0;
|
||||
void H_27(double *state, double *unused, double *out_2173118791568555829) {
|
||||
out_2173118791568555829[0] = 0;
|
||||
out_2173118791568555829[1] = 0;
|
||||
out_2173118791568555829[2] = 0;
|
||||
out_2173118791568555829[3] = 1;
|
||||
out_2173118791568555829[4] = 0;
|
||||
out_2173118791568555829[5] = 0;
|
||||
out_2173118791568555829[6] = 0;
|
||||
out_2173118791568555829[7] = 0;
|
||||
out_2173118791568555829[8] = 0;
|
||||
}
|
||||
void h_29(double *state, double *unused, double *out_2671551803644006019) {
|
||||
out_2671551803644006019[0] = state[1];
|
||||
void h_29(double *state, double *unused, double *out_6982675176999014474) {
|
||||
out_6982675176999014474[0] = state[1];
|
||||
}
|
||||
void H_29(double *state, double *unused, double *out_5957687693331318447) {
|
||||
out_5957687693331318447[0] = 0;
|
||||
out_5957687693331318447[1] = 1;
|
||||
out_5957687693331318447[2] = 0;
|
||||
out_5957687693331318447[3] = 0;
|
||||
out_5957687693331318447[4] = 0;
|
||||
out_5957687693331318447[5] = 0;
|
||||
out_5957687693331318447[6] = 0;
|
||||
out_5957687693331318447[7] = 0;
|
||||
out_5957687693331318447[8] = 0;
|
||||
void H_29(double *state, double *unused, double *out_511875864546261266) {
|
||||
out_511875864546261266[0] = 0;
|
||||
out_511875864546261266[1] = 1;
|
||||
out_511875864546261266[2] = 0;
|
||||
out_511875864546261266[3] = 0;
|
||||
out_511875864546261266[4] = 0;
|
||||
out_511875864546261266[5] = 0;
|
||||
out_511875864546261266[6] = 0;
|
||||
out_511875864546261266[7] = 0;
|
||||
out_511875864546261266[8] = 0;
|
||||
}
|
||||
void h_28(double *state, double *unused, double *out_3845497107703219871) {
|
||||
out_3845497107703219871[0] = state[0];
|
||||
void h_28(double *state, double *unused, double *out_2400127593349653796) {
|
||||
out_2400127593349653796[0] = state[0];
|
||||
}
|
||||
void H_28(double *state, double *unused, double *out_7406657363308702595) {
|
||||
out_7406657363308702595[0] = 1;
|
||||
out_7406657363308702595[1] = 0;
|
||||
out_7406657363308702595[2] = 0;
|
||||
out_7406657363308702595[3] = 0;
|
||||
out_7406657363308702595[4] = 0;
|
||||
out_7406657363308702595[5] = 0;
|
||||
out_7406657363308702595[6] = 0;
|
||||
out_7406657363308702595[7] = 0;
|
||||
out_7406657363308702595[8] = 0;
|
||||
void H_28(double *state, double *unused, double *out_4570523152523269308) {
|
||||
out_4570523152523269308[0] = 1;
|
||||
out_4570523152523269308[1] = 0;
|
||||
out_4570523152523269308[2] = 0;
|
||||
out_4570523152523269308[3] = 0;
|
||||
out_4570523152523269308[4] = 0;
|
||||
out_4570523152523269308[5] = 0;
|
||||
out_4570523152523269308[6] = 0;
|
||||
out_4570523152523269308[7] = 0;
|
||||
out_4570523152523269308[8] = 0;
|
||||
}
|
||||
void h_31(double *state, double *unused, double *out_1981073584941794741) {
|
||||
out_1981073584941794741[0] = state[8];
|
||||
void h_31(double *state, double *unused, double *out_1304974248711835435) {
|
||||
out_1304974248711835435[0] = state[8];
|
||||
}
|
||||
void H_31(double *state, double *unused, double *out_8955606034275998830) {
|
||||
out_8955606034275998830[0] = 0;
|
||||
out_8955606034275998830[1] = 0;
|
||||
out_8955606034275998830[2] = 0;
|
||||
out_8955606034275998830[3] = 0;
|
||||
out_8955606034275998830[4] = 0;
|
||||
out_8955606034275998830[5] = 0;
|
||||
out_8955606034275998830[6] = 0;
|
||||
out_8955606034275998830[7] = 0;
|
||||
out_8955606034275998830[8] = 1;
|
||||
void H_31(double *state, double *unused, double *out_6884399859382787245) {
|
||||
out_6884399859382787245[0] = 0;
|
||||
out_6884399859382787245[1] = 0;
|
||||
out_6884399859382787245[2] = 0;
|
||||
out_6884399859382787245[3] = 0;
|
||||
out_6884399859382787245[4] = 0;
|
||||
out_6884399859382787245[5] = 0;
|
||||
out_6884399859382787245[6] = 0;
|
||||
out_6884399859382787245[7] = 0;
|
||||
out_6884399859382787245[8] = 1;
|
||||
}
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <iostream>
|
||||
@@ -518,68 +518,68 @@ void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) {
|
||||
update<1, 3, 0>(in_x, in_P, h_31, H_31, NULL, in_z, in_R, in_ea, MAHA_THRESH_31);
|
||||
}
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7353202191454035649) {
|
||||
err_fun(nom_x, delta_x, out_7353202191454035649);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7567495437456270106) {
|
||||
err_fun(nom_x, delta_x, out_7567495437456270106);
|
||||
}
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_5273149315250963980) {
|
||||
inv_err_fun(nom_x, true_x, out_5273149315250963980);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_6277676899296321355) {
|
||||
inv_err_fun(nom_x, true_x, out_6277676899296321355);
|
||||
}
|
||||
void car_H_mod_fun(double *state, double *out_5463203357507594110) {
|
||||
H_mod_fun(state, out_5463203357507594110);
|
||||
void car_H_mod_fun(double *state, double *out_7420380263460044385) {
|
||||
H_mod_fun(state, out_7420380263460044385);
|
||||
}
|
||||
void car_f_fun(double *state, double dt, double *out_6125045598488451197) {
|
||||
f_fun(state, dt, out_6125045598488451197);
|
||||
void car_f_fun(double *state, double dt, double *out_3045596088569131513) {
|
||||
f_fun(state, dt, out_3045596088569131513);
|
||||
}
|
||||
void car_F_fun(double *state, double dt, double *out_548031277288491442) {
|
||||
F_fun(state, dt, out_548031277288491442);
|
||||
void car_F_fun(double *state, double dt, double *out_8700545601531255316) {
|
||||
F_fun(state, dt, out_8700545601531255316);
|
||||
}
|
||||
void car_h_25(double *state, double *unused, double *out_7825793012049214991) {
|
||||
h_25(state, unused, out_7825793012049214991);
|
||||
void car_h_25(double *state, double *unused, double *out_2824946100795669147) {
|
||||
h_25(state, unused, out_2824946100795669147);
|
||||
}
|
||||
void car_H_25(double *state, double *unused, double *out_8986251996152959258) {
|
||||
H_25(state, unused, out_8986251996152959258);
|
||||
void car_H_25(double *state, double *unused, double *out_2516688438275379545) {
|
||||
H_25(state, unused, out_2516688438275379545);
|
||||
}
|
||||
void car_h_24(double *state, double *unused, double *out_3863850671382452828) {
|
||||
h_24(state, unused, out_3863850671382452828);
|
||||
void car_h_24(double *state, double *unused, double *out_3049085865514909722) {
|
||||
h_24(state, unused, out_3049085865514909722);
|
||||
}
|
||||
void car_H_24(double *state, double *unused, double *out_5428326397350636122) {
|
||||
H_24(state, unused, out_5428326397350636122);
|
||||
void car_H_24(double *state, double *unused, double *out_339474014668229572) {
|
||||
H_24(state, unused, out_339474014668229572);
|
||||
}
|
||||
void car_h_30(double *state, double *unused, double *out_3653635896979304131) {
|
||||
h_30(state, unused, out_3653635896979304131);
|
||||
void car_h_30(double *state, double *unused, double *out_6312771183888182035) {
|
||||
h_30(state, unused, out_6312771183888182035);
|
||||
}
|
||||
void car_H_30(double *state, double *unused, double *out_6467919037645710631) {
|
||||
H_30(state, unused, out_6467919037645710631);
|
||||
void car_H_30(double *state, double *unused, double *out_1644520231869082) {
|
||||
H_30(state, unused, out_1644520231869082);
|
||||
}
|
||||
void car_h_26(double *state, double *unused, double *out_8655309164640149776) {
|
||||
h_26(state, unused, out_8655309164640149776);
|
||||
void car_h_26(double *state, double *unused, double *out_6908065673986044634) {
|
||||
h_26(state, unused, out_6908065673986044634);
|
||||
}
|
||||
void car_H_26(double *state, double *unused, double *out_5718988758682536134) {
|
||||
H_26(state, unused, out_5718988758682536134);
|
||||
void car_H_26(double *state, double *unused, double *out_6258191757149435769) {
|
||||
H_26(state, unused, out_6258191757149435769);
|
||||
}
|
||||
void car_h_27(double *state, double *unused, double *out_5968325673074535865) {
|
||||
h_27(state, unused, out_5968325673074535865);
|
||||
void car_h_27(double *state, double *unused, double *out_709679758613972836) {
|
||||
h_27(state, unused, out_709679758613972836);
|
||||
}
|
||||
void car_H_27(double *state, double *unused, double *out_4244324966461767414) {
|
||||
H_27(state, unused, out_4244324966461767414);
|
||||
void car_H_27(double *state, double *unused, double *out_2173118791568555829) {
|
||||
H_27(state, unused, out_2173118791568555829);
|
||||
}
|
||||
void car_h_29(double *state, double *unused, double *out_2671551803644006019) {
|
||||
h_29(state, unused, out_2671551803644006019);
|
||||
void car_h_29(double *state, double *unused, double *out_6982675176999014474) {
|
||||
h_29(state, unused, out_6982675176999014474);
|
||||
}
|
||||
void car_H_29(double *state, double *unused, double *out_5957687693331318447) {
|
||||
H_29(state, unused, out_5957687693331318447);
|
||||
void car_H_29(double *state, double *unused, double *out_511875864546261266) {
|
||||
H_29(state, unused, out_511875864546261266);
|
||||
}
|
||||
void car_h_28(double *state, double *unused, double *out_3845497107703219871) {
|
||||
h_28(state, unused, out_3845497107703219871);
|
||||
void car_h_28(double *state, double *unused, double *out_2400127593349653796) {
|
||||
h_28(state, unused, out_2400127593349653796);
|
||||
}
|
||||
void car_H_28(double *state, double *unused, double *out_7406657363308702595) {
|
||||
H_28(state, unused, out_7406657363308702595);
|
||||
void car_H_28(double *state, double *unused, double *out_4570523152523269308) {
|
||||
H_28(state, unused, out_4570523152523269308);
|
||||
}
|
||||
void car_h_31(double *state, double *unused, double *out_1981073584941794741) {
|
||||
h_31(state, unused, out_1981073584941794741);
|
||||
void car_h_31(double *state, double *unused, double *out_1304974248711835435) {
|
||||
h_31(state, unused, out_1304974248711835435);
|
||||
}
|
||||
void car_H_31(double *state, double *unused, double *out_8955606034275998830) {
|
||||
H_31(state, unused, out_8955606034275998830);
|
||||
void car_H_31(double *state, double *unused, double *out_6884399859382787245) {
|
||||
H_31(state, unused, out_6884399859382787245);
|
||||
}
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt) {
|
||||
predict(in_x, in_P, in_Q, dt);
|
||||
|
||||
@@ -9,27 +9,27 @@ void car_update_27(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_29(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7353202191454035649);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_5273149315250963980);
|
||||
void car_H_mod_fun(double *state, double *out_5463203357507594110);
|
||||
void car_f_fun(double *state, double dt, double *out_6125045598488451197);
|
||||
void car_F_fun(double *state, double dt, double *out_548031277288491442);
|
||||
void car_h_25(double *state, double *unused, double *out_7825793012049214991);
|
||||
void car_H_25(double *state, double *unused, double *out_8986251996152959258);
|
||||
void car_h_24(double *state, double *unused, double *out_3863850671382452828);
|
||||
void car_H_24(double *state, double *unused, double *out_5428326397350636122);
|
||||
void car_h_30(double *state, double *unused, double *out_3653635896979304131);
|
||||
void car_H_30(double *state, double *unused, double *out_6467919037645710631);
|
||||
void car_h_26(double *state, double *unused, double *out_8655309164640149776);
|
||||
void car_H_26(double *state, double *unused, double *out_5718988758682536134);
|
||||
void car_h_27(double *state, double *unused, double *out_5968325673074535865);
|
||||
void car_H_27(double *state, double *unused, double *out_4244324966461767414);
|
||||
void car_h_29(double *state, double *unused, double *out_2671551803644006019);
|
||||
void car_H_29(double *state, double *unused, double *out_5957687693331318447);
|
||||
void car_h_28(double *state, double *unused, double *out_3845497107703219871);
|
||||
void car_H_28(double *state, double *unused, double *out_7406657363308702595);
|
||||
void car_h_31(double *state, double *unused, double *out_1981073584941794741);
|
||||
void car_H_31(double *state, double *unused, double *out_8955606034275998830);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7567495437456270106);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_6277676899296321355);
|
||||
void car_H_mod_fun(double *state, double *out_7420380263460044385);
|
||||
void car_f_fun(double *state, double dt, double *out_3045596088569131513);
|
||||
void car_F_fun(double *state, double dt, double *out_8700545601531255316);
|
||||
void car_h_25(double *state, double *unused, double *out_2824946100795669147);
|
||||
void car_H_25(double *state, double *unused, double *out_2516688438275379545);
|
||||
void car_h_24(double *state, double *unused, double *out_3049085865514909722);
|
||||
void car_H_24(double *state, double *unused, double *out_339474014668229572);
|
||||
void car_h_30(double *state, double *unused, double *out_6312771183888182035);
|
||||
void car_H_30(double *state, double *unused, double *out_1644520231869082);
|
||||
void car_h_26(double *state, double *unused, double *out_6908065673986044634);
|
||||
void car_H_26(double *state, double *unused, double *out_6258191757149435769);
|
||||
void car_h_27(double *state, double *unused, double *out_709679758613972836);
|
||||
void car_H_27(double *state, double *unused, double *out_2173118791568555829);
|
||||
void car_h_29(double *state, double *unused, double *out_6982675176999014474);
|
||||
void car_H_29(double *state, double *unused, double *out_511875864546261266);
|
||||
void car_h_28(double *state, double *unused, double *out_2400127593349653796);
|
||||
void car_H_28(double *state, double *unused, double *out_4570523152523269308);
|
||||
void car_h_31(double *state, double *unused, double *out_1304974248711835435);
|
||||
void car_H_31(double *state, double *unused, double *out_6884399859382787245);
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
void car_set_mass(double x);
|
||||
void car_set_rotational_inertia(double x);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,18 @@ void pose_update_4(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void pose_update_10(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_13(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_14(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_2134324718959906218);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_7874553077816062449);
|
||||
void pose_H_mod_fun(double *state, double *out_8129875391940694982);
|
||||
void pose_f_fun(double *state, double dt, double *out_5920058179701921818);
|
||||
void pose_F_fun(double *state, double dt, double *out_534012189001621552);
|
||||
void pose_h_4(double *state, double *unused, double *out_6191552396506608484);
|
||||
void pose_H_4(double *state, double *unused, double *out_4468786875016897913);
|
||||
void pose_h_10(double *state, double *unused, double *out_8347226084972414573);
|
||||
void pose_H_10(double *state, double *unused, double *out_4111169058602364742);
|
||||
void pose_h_13(double *state, double *unused, double *out_2707578914264125574);
|
||||
void pose_H_13(double *state, double *unused, double *out_7681060700349230714);
|
||||
void pose_h_14(double *state, double *unused, double *out_4697260034120461682);
|
||||
void pose_H_14(double *state, double *unused, double *out_8432027731356382442);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_4830772321857990181);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_5200017052910787602);
|
||||
void pose_H_mod_fun(double *state, double *out_1712341249416054868);
|
||||
void pose_f_fun(double *state, double dt, double *out_3100560613383761185);
|
||||
void pose_F_fun(double *state, double dt, double *out_3350602915939425859);
|
||||
void pose_h_4(double *state, double *unused, double *out_2074712223981570498);
|
||||
void pose_H_4(double *state, double *unused, double *out_4135740557335903853);
|
||||
void pose_h_10(double *state, double *unused, double *out_5949398724887603410);
|
||||
void pose_H_10(double *state, double *unused, double *out_3347508424845693314);
|
||||
void pose_h_13(double *state, double *unused, double *out_9220727019356339608);
|
||||
void pose_H_13(double *state, double *unused, double *out_923466732003571052);
|
||||
void pose_h_14(double *state, double *unused, double *out_8529780658774629922);
|
||||
void pose_H_14(double *state, double *unused, double *out_172499700996419324);
|
||||
void pose_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,8 @@ GearShifter = car.CarState.GearShifter
|
||||
AOL_TOGGLE = 9
|
||||
PAUSE_LATERAL = 3
|
||||
PAUSE_LONGITUDINAL = 4
|
||||
# The only wheel action WheelPublisher delivers from cruise MAIN.
|
||||
ADOPT_SPEED_LIMIT = 10
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AolSettings:
|
||||
@@ -28,6 +30,7 @@ class AolSettings:
|
||||
mode_actions: tuple[int, int, int] = (0, 0, 0)
|
||||
custom_actions: tuple[int, int, int] = (0, 0, 0)
|
||||
default_physical_buttons: bool = True
|
||||
main_wheel_action: bool = False
|
||||
|
||||
|
||||
def _button_value(params, key: str) -> int | None:
|
||||
@@ -64,6 +67,7 @@ def read_settings(params) -> AolSettings:
|
||||
enabled=params.get_bool('AlwaysOnLateral') and valid_pause, pause_brake_mps=pause_mps if valid_pause else 0.0,
|
||||
lkas_action=_axis_action(lkas), main_action=_axis_action(main),
|
||||
default_physical_buttons=lkas == 0 and main == 0,
|
||||
main_wheel_action=main == ADOPT_SPEED_LIMIT,
|
||||
distance_actions=(_button_action(params, 'DistanceButtonControl'),
|
||||
_button_action(params, 'LongDistanceButtonControl'),
|
||||
_button_action(params, 'VeryLongDistanceButtonControl')),
|
||||
@@ -315,7 +319,8 @@ class AolCardIntent:
|
||||
independent_buttons = self.independent_cruise_buttons and (main_managed or lkas_managed)
|
||||
# The opt-in is bound to existing HKG torque owners. Other physical
|
||||
# owners keep their current event and native contracts.
|
||||
main_toggle = not independent_buttons or main_managed
|
||||
main_toggle = (not independent_buttons or main_managed) and not (
|
||||
self.independent_cruise_buttons and self.settings.main_wheel_action)
|
||||
lkas_toggle = not independent_buttons or lkas_managed
|
||||
previously_held = (self._main_held and main_toggle) or (self._lkas_held and lkas_toggle)
|
||||
cancel_pressed = False
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Cruise MAIN assigned to Adopt speed limit is not also the Hyundai physical lateral toggle."""
|
||||
import pytest
|
||||
from opendbc.car import structs
|
||||
from openpilot.starpilot.aol.intent import AolCardIntent, AolSettings, read_settings
|
||||
from openpilot.starpilot.aol.tests.test_ioniq6_stock_repair import stock
|
||||
from openpilot.starpilot.car.hyundai.aol import create_intent, qualified_ioniq6
|
||||
|
||||
|
||||
def long_cp():
|
||||
cp = stock(False)
|
||||
cp.pcmCruise = False
|
||||
cp.openpilotLongitudinalControl = True
|
||||
cp.safetyConfigs[0].safetyParam = 0x8815
|
||||
assert qualified_ioniq6(cp)
|
||||
return cp
|
||||
|
||||
|
||||
def press(owner, cs, button, now_ns, *, engaged=False):
|
||||
cs.buttonEvents = [structs.CarState.ButtonEvent(type=button, pressed=True)]
|
||||
owner.update(cs, now_ns=now_ns, fault_active=False, standard_enabled=engaged)
|
||||
cs.buttonEvents = [structs.CarState.ButtonEvent(type=button, pressed=False)]
|
||||
owner.update(cs, now_ns=now_ns + 1, fault_active=False, standard_enabled=engaged)
|
||||
|
||||
|
||||
def lateral(owner):
|
||||
return owner.allowed_latch and not owner.pause_lateral
|
||||
|
||||
|
||||
def armed(owner):
|
||||
cs = structs.CarState(canValid=True, gearShifter='drive')
|
||||
cs.cruiseState.available = True
|
||||
owner.update(cs, now_ns=1, fault_active=False)
|
||||
press(owner, cs, 'lkas', 2)
|
||||
assert lateral(owner)
|
||||
return cs
|
||||
|
||||
|
||||
@pytest.mark.parametrize('main_wheel_action', [False, True])
|
||||
def test_ioniq6_main_assigned_to_adopt_keeps_lkas_armed_lateral(main_wheel_action):
|
||||
owner = create_intent(long_cp(), AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0), main_wheel_action=main_wheel_action))
|
||||
assert owner.independent_cruise_buttons
|
||||
cs = armed(owner)
|
||||
press(owner, cs, 'mainCruise', 10, engaged=True)
|
||||
assert lateral(owner) == main_wheel_action
|
||||
press(owner, cs, 'lkas', 20, engaged=True)
|
||||
assert lateral(owner) != main_wheel_action
|
||||
|
||||
|
||||
def test_main_toggle_is_unchanged_without_independent_cruise_buttons():
|
||||
owner = AolCardIntent(AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0), main_wheel_action=True), explicit_latch=True)
|
||||
cs = armed(owner)
|
||||
press(owner, cs, 'mainCruise', 10, engaged=True)
|
||||
assert not lateral(owner)
|
||||
|
||||
|
||||
class SavedParams:
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
def get(self, key, return_default=False):
|
||||
return self.values.get(key)
|
||||
|
||||
def get_bool(self, key):
|
||||
return self.values.get(key) == b'1'
|
||||
|
||||
def get_param_path(self, key):
|
||||
return f'/nonexistent/{key}'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('lkas,main,main_wheel', [(b'0', b'10', True), (b'0', b'9', False), (b'10', b'0', False), (b'0', b'8', False)])
|
||||
def test_only_main_adopt_is_marked_as_a_main_wheel_action(lkas, main, main_wheel):
|
||||
settings = read_settings(SavedParams({'AlwaysOnLateral': b'1', 'LKASButtonControl': lkas, 'MainCruiseButtonControl': main}))
|
||||
assert settings.main_wheel_action == main_wheel
|
||||
+115
-1
@@ -81,7 +81,7 @@
|
||||
"discriminantOffset": 0
|
||||
},
|
||||
"new_storage": {
|
||||
"dataWordCount": 18,
|
||||
"dataWordCount": 21,
|
||||
"pointerCount": 22,
|
||||
"preferredListEncoding": "inlineComposite",
|
||||
"isGroup": false,
|
||||
@@ -232,6 +232,78 @@
|
||||
"ordinal": {
|
||||
"explicit": 58
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot": {
|
||||
"offset": 141,
|
||||
"type": {
|
||||
"bool": null
|
||||
},
|
||||
"defaultValue": {
|
||||
"bool": false
|
||||
},
|
||||
"hadExplicitDefault": false
|
||||
},
|
||||
"name": "acceptedIsPreview",
|
||||
"codeOrder": 59,
|
||||
"discriminantValue": 65535,
|
||||
"ordinal": {
|
||||
"explicit": 59
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot": {
|
||||
"offset": 18,
|
||||
"type": {
|
||||
"uint64": null
|
||||
},
|
||||
"defaultValue": {
|
||||
"uint64": 0
|
||||
},
|
||||
"hadExplicitDefault": false
|
||||
},
|
||||
"name": "wheelOverrideSince",
|
||||
"codeOrder": 60,
|
||||
"discriminantValue": 65535,
|
||||
"ordinal": {
|
||||
"explicit": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot": {
|
||||
"offset": 19,
|
||||
"type": {
|
||||
"uint64": null
|
||||
},
|
||||
"defaultValue": {
|
||||
"uint64": 0
|
||||
},
|
||||
"hadExplicitDefault": false
|
||||
},
|
||||
"name": "wheelAdoptMonoTime",
|
||||
"codeOrder": 61,
|
||||
"discriminantValue": 65535,
|
||||
"ordinal": {
|
||||
"explicit": 61
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot": {
|
||||
"offset": 40,
|
||||
"type": {
|
||||
"float32": null
|
||||
},
|
||||
"defaultValue": {
|
||||
"float32": 0.0
|
||||
},
|
||||
"hadExplicitDefault": false
|
||||
},
|
||||
"name": "wheelAdoptTargetMps",
|
||||
"codeOrder": 62,
|
||||
"discriminantValue": 65535,
|
||||
"ordinal": {
|
||||
"explicit": 62
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -652,6 +724,48 @@
|
||||
"codeOrder": 12
|
||||
}
|
||||
]
|
||||
},
|
||||
"0x9ccdc8676701b412": {
|
||||
"kind": "struct",
|
||||
"before_name": "SlcAction",
|
||||
"after_name": "SlcAction",
|
||||
"old_field_count": 8,
|
||||
"old_storage": {
|
||||
"dataWordCount": 4,
|
||||
"pointerCount": 4,
|
||||
"preferredListEncoding": "inlineComposite",
|
||||
"isGroup": false,
|
||||
"discriminantCount": 0,
|
||||
"discriminantOffset": 0
|
||||
},
|
||||
"new_storage": {
|
||||
"dataWordCount": 5,
|
||||
"pointerCount": 4,
|
||||
"preferredListEncoding": "inlineComposite",
|
||||
"isGroup": false,
|
||||
"discriminantCount": 0,
|
||||
"discriminantOffset": 0
|
||||
},
|
||||
"added_fields": [
|
||||
{
|
||||
"slot": {
|
||||
"offset": 4,
|
||||
"type": {
|
||||
"uint64": null
|
||||
},
|
||||
"defaultValue": {
|
||||
"uint64": 0
|
||||
},
|
||||
"hadExplicitDefault": false
|
||||
},
|
||||
"name": "wheelAdoptMonoTime",
|
||||
"codeOrder": 8,
|
||||
"discriminantValue": 65535,
|
||||
"ordinal": {
|
||||
"explicit": 8
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"published_addition_commits": {
|
||||
|
||||
@@ -6,9 +6,8 @@ CEM selects Experimental mode for configured speed, road, signal, curve, lead, s
|
||||
|
||||
`ConditionalPlannerHost` joins the current native MPC result, current model/car/radar observations, saved preferences, and qualified wheel-button events. It produces a short-lived proposal on `slcState.conditionalMode`; it does not own the effective driving mode. `selfdrived` independently checks the current vehicle capability, longitudinal axis, drive, saved configuration, and input freshness before accepting that proposal. Its ordinary `ExperimentalMode` request remains the fallback. The UI displays an active conditional reason only from a fresh selfdrived acknowledgment matching the exact current `selfdriveState` frame and active longitudinal axis; the saved family is displayed separately.
|
||||
|
||||
Speed-limit availability is independent of mode selection. The retired `SLCFallback=1`
|
||||
preference cannot request Experimental or veto Chill, including when Maps or Vision
|
||||
becomes unknown or stale. Other scene triggers, manual mode choices and committed
|
||||
Speed-limit availability is independent of mode selection. A missing or stale Maps or Vision limit does
|
||||
not request Experimental or veto Chill. Other scene triggers, manual mode choices and committed
|
||||
stop handling retain their existing behavior. Historical speed-limit reason labels
|
||||
remain readable in recorded routes.
|
||||
|
||||
@@ -49,6 +48,6 @@ Traffic assignment does not suppress ordinary profile tuning.
|
||||
|
||||
Scene inputs distinguish false from unknown. Model filters advance once per original model tick; the oldest required source bounds freshness separately, so a slower fresh radar cannot stall a model filter or renew stale data. Startup, clock discontinuities, and drive changes require fresh observations. Conditional Chill requires every applicable veto to be known false. Disabled build features can be known absent; an enabled producer without a qualified observation remains unknown.
|
||||
|
||||
Current producers cover native MPC follow time and stop/throttle intent, model lane geometry, qualified adjacent radar, SLC attribution, pedal override, and the effective mode reported by selfdrived. The exact Ioniq media path supplies source-stamped Traffic Mode to CEM and a bounded Traffic follow/jerk, acceleration and cruise-braking profile. The shared committed-turn evaluator feeds both the stop detector and slower-lead retuning from original car/model stamps. Traffic retains the existing 0.75-second effective headway floor; saved values below it remain unsupported and intact. The native planner shapes the cruise-only floor for a qualified SLC cap, a validated named Eco/Sport braking personality, or the explicit global Braking response. A saved Traffic braking category reverts to its 0.42 m/s² cruise baseline for a present radar lead or standstill, then recovers through the existing profile smoother; MPC and model braking retain priority. The saved lane-change close-gap preference and current lateral minimum-speed policy are separate from conditional scene selection. Frozen weather and other hazard-context braking inputs remain absent. Stop-sign, forced-stop, and dashboard stop-sign producers remain absent. The historical red-light name is already represented by the current stop-light observation, so it does not need a duplicate owner.
|
||||
Current producers cover native MPC follow time and stop/throttle intent, model lane geometry, qualified adjacent radar, SLC attribution, pedal override, and the effective mode reported by selfdrived. The exact Ioniq media path supplies source-stamped Traffic Mode to CEM and a bounded Traffic follow/jerk, acceleration and cruise-braking profile. The shared committed-turn evaluator feeds both the stop detector and slower-lead retuning from original car/model stamps. Traffic retains the existing 0.75-second effective headway floor; saved values below it remain unsupported and intact. The native planner retains the selected cruise-braking response for an SLC cap. Conditional speed comparisons and curve control receive the same-cycle SLC-capped cruise target while the driver's SET remains unchanged. A saved Traffic braking category reverts to its 0.42 m/s² cruise baseline for a present radar lead or standstill, then recovers through the existing profile smoother; MPC and model braking retain priority. The saved lane-change close-gap preference and current lateral minimum-speed policy are separate from conditional scene selection. Frozen weather and other hazard-context braking inputs remain absent. Stop-sign, forced-stop, and dashboard stop-sign producers remain absent. The historical red-light name is already represented by the current stop-light observation, so it does not need a duplicate owner.
|
||||
|
||||
The source contract includes frozen `starpilot/controls/lib/conditional_experimental_mode.py`, `conditional_chill_mode.py`, `starpilot/common/experimental_state.py`, and the planner's mode selection. Tests compare executable frozen policy/detector traces and exercise current serialized messages, native MPC/radar joins, lifecycle boundaries, manual controls, and settings concurrency. These are integration evidence, not vehicle qualification. Normal availability is limited to the exact tagged Ioniq 6 LONG profile; the separate ECU handoff and native safety acknowledgment must succeed first. Missing scene producers and road behavior still require evaluation.
|
||||
|
||||
@@ -59,6 +59,7 @@ class ConditionalModeHost:
|
||||
selected_t_follow_s: float | None = None,
|
||||
selected_t_follow_observed_mono_ns: int | None = None,
|
||||
settings_owner: ConditionalSettingsOwner | None = None,
|
||||
cruise_ceiling_mps: float | None = None,
|
||||
) -> HostProposal:
|
||||
signal_options = None
|
||||
settings_revision = None
|
||||
@@ -120,6 +121,7 @@ class ConditionalModeHost:
|
||||
selected_t_follow_observed_mono_ns=selected_t_follow_observed_mono_ns,
|
||||
owner_context=owner_context,
|
||||
signal_options=signal_options,
|
||||
cruise_ceiling_mps=cruise_ceiling_mps,
|
||||
)
|
||||
authority = projected.authority
|
||||
if authority is not None and authority.fresh and (not authority.system_long_capable or authority.safe_mode or
|
||||
|
||||
@@ -180,6 +180,7 @@ class ConditionalPlannerHost:
|
||||
drive_id: int, manual: ManualState | None = None,
|
||||
owner_context: ConditionalOwnerContext | None = None,
|
||||
native_plan_valid: bool = False,
|
||||
cruise_ceiling_mps: float | None = None,
|
||||
manual_event=None,
|
||||
ui_event=None,
|
||||
) -> tuple[HostProposal, SettingsSnapshot | None]:
|
||||
@@ -289,6 +290,7 @@ class ConditionalPlannerHost:
|
||||
manual=self.manual_state if manual is None else manual, owner_context=context,
|
||||
selected_t_follow_s=selected_follow_time(planner),
|
||||
selected_t_follow_observed_mono_ns=sm.logMonoTime.get('modelV2'),
|
||||
cruise_ceiling_mps=cruise_ceiling_mps,
|
||||
settings_owner=self.settings,
|
||||
)
|
||||
snapshot = self.settings.current
|
||||
|
||||
@@ -406,6 +406,7 @@ class SceneProjector:
|
||||
selected_t_follow_observed_mono_ns: int | None = None,
|
||||
owner_context: ConditionalOwnerContext | None = None,
|
||||
signal_options: CEMOptions | None = None,
|
||||
cruise_ceiling_mps: float | None = None,
|
||||
) -> ProjectedScene:
|
||||
if (
|
||||
type(now_mono_ns) is not int
|
||||
@@ -441,6 +442,9 @@ class SceneProjector:
|
||||
speed = _ego_speed(car) if car is not None and _field(car, 'canValid') is True and _field(car, 'canTimeout') is False else None
|
||||
cruise_kph = _number(_field(car, 'vCruise'), high=V_CRUISE_UNSET_KPH) if speed is not None else None
|
||||
cruise = cruise_kph * KPH_TO_MPS if cruise_kph is not None and 0 < cruise_kph < V_CRUISE_UNSET_KPH else None
|
||||
if cruise_ceiling_mps is not None:
|
||||
cap = _number(cruise_ceiling_mps, high=V_CRUISE_UNSET_KPH * KPH_TO_MPS)
|
||||
cruise = min(cruise, cap) if cruise is not None and cap is not None and cap > 0 else None
|
||||
curvature = _number(_field(controls, 'curvature'), low=-1.0, high=1.0) if controls is not None else None
|
||||
raw_curve = abs(speed * speed * curvature) >= FROZEN_CURVE_ACCEL_MPS2 if speed is not None and curvature is not None else None
|
||||
horizon = None
|
||||
|
||||
@@ -114,6 +114,20 @@ def owner_context(stamp: int, *, stop_time=8.0, slower=True, stopped=False):
|
||||
|
||||
|
||||
class TestSceneProjector(unittest.TestCase):
|
||||
def test_conditional_speed_comparisons_use_the_slc_cap_without_mutating_set(self):
|
||||
for cap, expected in ((None, 20.), (15., 15.), (25., 20.), (float('nan'), None)):
|
||||
with self.subTest(cap=cap):
|
||||
projector = SceneProjector()
|
||||
sm = FakeSubMaster(serialized_scene(), MONO-5_000_000)
|
||||
projector.project(sm, CP, now_mono_ns=MONO, now_boot_ns=BOOT, sample_skew_ns=1000)
|
||||
now = MONO+50_000_000
|
||||
sm.stamp(now)
|
||||
sm.payloads.update(serialized_scene(stamp=BOOT+50_000_000))
|
||||
result = projector.project(sm, CP, now_mono_ns=now, now_boot_ns=BOOT+50_000_000,
|
||||
sample_skew_ns=1000, safe_mode=False, owner_context=owner_context(now), cruise_ceiling_mps=cap)
|
||||
self.assertEqual(result.scene.set_speed_mps, expected)
|
||||
self.assertEqual(sm['carState'].vCruise, 72.)
|
||||
|
||||
def test_standstill_filter_undershoot_requires_fresh_physical_zero_speed(self):
|
||||
cases = ((-1.401298464324817e-45, True, 0., True),
|
||||
(-0.0008391447481699288, True, 0., True),
|
||||
|
||||
@@ -80,8 +80,14 @@ class CurveHost:
|
||||
self.previous_long_active = False
|
||||
self.previous_active_headway = None
|
||||
|
||||
def sample(self, sm, CP, now_ns: int, *, drive_id: int = 0, event=None, follow_time_s: float | None = None):
|
||||
def sample(self, sm, CP, now_ns: int, *, drive_id: int = 0, event=None, follow_time_s: float | None = None,
|
||||
cruise_ceiling_mps: float | None = None):
|
||||
frame = frame_from_submaster(sm, CP, now_ns, drive_id=drive_id)
|
||||
if cruise_ceiling_mps is not None:
|
||||
if not math.isfinite(cruise_ceiling_mps) or cruise_ceiling_mps <= 0:
|
||||
frame = replace(frame, inputs_fresh=False)
|
||||
else:
|
||||
frame = replace(frame, cruise_mps=min(frame.cruise_mps, cruise_ceiling_mps))
|
||||
if drive_id and self.lead_drive_id and drive_id != self.lead_drive_id:
|
||||
self._reset_leads()
|
||||
if drive_id:
|
||||
|
||||
@@ -44,6 +44,12 @@ class TestCurveHost(unittest.TestCase):
|
||||
self.sm.logMonoTime['radarState'] = NOW - 1_000_000_000
|
||||
self.assertIsNone(self.host.sample(self.sm, self.cp, NOW + 50_000_000).ceiling_mps)
|
||||
|
||||
def test_curve_uses_same_cycle_slc_capped_cruise_without_mutating_set(self):
|
||||
with patch.object(self.host.runtime, 'step', wraps=self.host.runtime.step) as step:
|
||||
self.host.sample(self.sm, self.cp, NOW, follow_time_s=1.45, cruise_ceiling_mps=20.)
|
||||
self.assertEqual(step.call_args.args[0].cruise_mps, 20.)
|
||||
self.assertEqual(self.sm['carState'].vCruise, 108.)
|
||||
|
||||
def test_stock_owner_and_malformed_field_fail_closed(self):
|
||||
self.cp.openpilotLongitudinalControl = False
|
||||
self.assertIsNone(self.host.sample(self.sm, self.cp, NOW).ceiling_mps)
|
||||
|
||||
@@ -21,7 +21,7 @@ CYCLE_PERSONALITY = "__starpilot_controller_action__:cycle_driving_personality"
|
||||
|
||||
# Saved preference shortcuts retain the current owner's authority and dependencies.
|
||||
FEATURE_KEYS = {
|
||||
"slc": ("SpeedLimitController", "ShowSpeedLimits", "SLCConfirmation", "SLCConfirmationHigher", "SLCConfirmationLower"),
|
||||
"slc": ("SpeedLimitController", "ShowSpeedLimits", "SLCConfirmation"),
|
||||
"lane": ("LaneCentering", "LaneCenteringPauseOnSignal"),
|
||||
"curve": ("CurveSpeedController", "CurveSpeedControllerNoLead", "ShowCSCStatus"),
|
||||
"aol": ("AlwaysOnLateral", "NostalgiaMode"),
|
||||
|
||||
@@ -61,12 +61,21 @@ DESCRIPTIONS = {
|
||||
'CurveSpeedControllerNoLead': 'Pauses curve-speed adjustments while following a lead vehicle.',
|
||||
'ShowCSCStatus': 'Shows when Curve Speed Controller is adjusting speed on the driving screen.',
|
||||
'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.',
|
||||
'SpeedLimitController': 'Follows accepted limits plus offsets up to SET speed. Missing data uses the previous accepted limit.',
|
||||
'SLCOnEngage': 'Starts software cruise SET at the SLC limit plus offset, including a saved limit when live data is missing. RES restores the previous SET.',
|
||||
'SLCOverrideMode': ('Cruise uses this target when you lift off the accelerator. ' +
|
||||
'Holding your speed uses the highest speed you reached, up to SET.'),
|
||||
'SLCWheelOverride': 'Press + to raise SET past the adjusted limit and use your new SET speed.',
|
||||
'ShowSpeedLimits': 'Shows recognized speed-limit signs on the driving screen.',
|
||||
'SLCConfirmation': 'Auto adopts new limits immediately. With Automatic adoption off, + accepts and − declines within 10 seconds.',
|
||||
'SLCAdvanceSeconds': 'Offers mapped limits early using road distance and current speed. Existing adoption and confirmation settings still apply.',
|
||||
'SLCConfirmation': ('Off adopts new limits automatically. Both, Higher only, and Lower only prompt relative to the last accepted limit. ' +
|
||||
'+ accepts and − declines within 30 seconds.'),
|
||||
'SLCAdvanceHigherSeconds': ('Offers higher mapped limits early using road distance and current speed. ' +
|
||||
'Existing adoption and confirmation settings still apply.'),
|
||||
'SLCAdvanceLowerSeconds': ('Offers lower mapped limits early using road distance and current speed. ' +
|
||||
'Existing adoption and confirmation settings still apply.'),
|
||||
'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.',
|
||||
'SLCPriority3': 'Chooses an optional third source when neither earlier source has a fresh speed limit.',
|
||||
'slc_reset': 'Resets saved speed-limit offsets while retaining valid configured speed ranges.',
|
||||
'reset_calibration': 'Clears saved camera and steering calibration. The device recalibrates on the next drive.',
|
||||
'reset_driver_monitoring': 'Clear saved wheel-side detection. Automatic detection restarts on the next drive.',
|
||||
|
||||
@@ -77,10 +77,15 @@
|
||||
"RelaxedJerkSpeedDecrease",
|
||||
"RemoteStartBootsComma",
|
||||
"ReverseCruise",
|
||||
"SLCAdvanceSeconds",
|
||||
"SLCAdvanceHigherSeconds",
|
||||
"SLCAdvanceLowerSeconds",
|
||||
"SLCConfirmation",
|
||||
"SLCOnEngage",
|
||||
"SLCOverrideMode",
|
||||
"SLCPriority1",
|
||||
"SLCPriority2",
|
||||
"SLCPriority3",
|
||||
"SLCWheelOverride",
|
||||
"ScreenBrightness",
|
||||
"ScreenBrightnessOnroad",
|
||||
"ScreenTimeout",
|
||||
|
||||
@@ -63,40 +63,23 @@ class TestCanonicalRecovery(TestCase):
|
||||
self.assertTrue(recovery.confirm(pending['intent'], self.session, self.generation)['saved'])
|
||||
self.assertEqual(recovery.snapshot(self.session, self.generation)['count'], 0)
|
||||
|
||||
def test_legacy_confirmation_flags_do_not_create_hidden_customization(self):
|
||||
self.params.put_bool('SLCConfirmation', False, block=True)
|
||||
self.params.put_bool('SLCConfirmationLower', True, block=True)
|
||||
self.params.put_bool('SLCConfirmationHigher', True, block=True)
|
||||
def test_directional_confirmation_resets_to_off(self):
|
||||
self.params.put('SLCConfirmation', 4, block=True)
|
||||
recovery = self.recovery()
|
||||
view = recovery.snapshot(self.session, self.generation)
|
||||
children = [row for row in view['rows'] if row['label'] in ('Confirm lower limits', 'Confirm higher limits')]
|
||||
self.assertEqual(children, [])
|
||||
self.assertFalse(self.params.get_bool('SLCConfirmation'))
|
||||
self.assertTrue(self.params.get_bool('SLCConfirmationLower'))
|
||||
self.assertTrue(self.params.get_bool('SLCConfirmationHigher'))
|
||||
|
||||
def test_reset_confirmation_master_preserves_ignored_legacy_bytes(self):
|
||||
for key in ('SLCConfirmation', 'SLCConfirmationLower', 'SLCConfirmationHigher'):
|
||||
self.params.put_bool(key, True, block=True)
|
||||
recovery = self.recovery()
|
||||
view = recovery.snapshot(self.session, self.generation)
|
||||
selected = [row['id'] for row in view['rows'] if row['label'] in
|
||||
('Automatic adoption', 'Confirm lower limits', 'Confirm higher limits')]
|
||||
self.assertEqual(len(selected), 1)
|
||||
pending = recovery.preview(view['view'], selected, self.session, self.generation)
|
||||
row = next(row for row in view['rows'] if row['label'] == 'Confirm limits')
|
||||
pending = recovery.preview(view['view'], [row['id']], self.session, self.generation)
|
||||
self.assertTrue(recovery.confirm(pending['intent'], self.session, self.generation)['saved'])
|
||||
self.assertFalse(self.params.get_bool('SLCConfirmation'))
|
||||
for key in ('SLCConfirmationLower', 'SLCConfirmationHigher'):
|
||||
self.assertTrue(self.params.get_bool(key))
|
||||
self.assertFalse(self.params.get('SLCConfirmation'))
|
||||
|
||||
def test_unsupported_rows_remain_visible_but_cannot_block_available_reset_all(self):
|
||||
from dataclasses import replace
|
||||
self.params.put_bool('SLCConfirmation', True, block=True)
|
||||
self.params.put('SLCConfirmation', 1, block=True)
|
||||
self.params.put_bool('ShowBrakeStatus', True, block=True)
|
||||
self.context.value = replace(self.context.value, cp=None, cp_raw=None)
|
||||
recovery = self.recovery()
|
||||
view = recovery.snapshot(self.session, self.generation)
|
||||
unsupported = next(row for row in view['rows'] if row['label'] == 'Automatic adoption')
|
||||
unsupported = next(row for row in view['rows'] if row['label'] == 'Confirm limits')
|
||||
self.assertFalse(unsupported['resetAvailable'])
|
||||
with self.assertRaises(SettingsChanged):
|
||||
recovery.preview(view['view'], [unsupported['id']], self.session, self.generation)
|
||||
@@ -104,7 +87,7 @@ class TestCanonicalRecovery(TestCase):
|
||||
self.assertTrue(selected)
|
||||
pending = recovery.preview(view['view'], selected, self.session, self.generation)
|
||||
self.assertTrue(recovery.confirm(pending['intent'], self.session, self.generation)['saved'])
|
||||
self.assertTrue(self.params.get_bool('SLCConfirmation'))
|
||||
self.assertTrue(self.params.get('SLCConfirmation'))
|
||||
self.assertFalse(self.params.get_bool('ShowBrakeStatus'))
|
||||
|
||||
def test_other_canonical_inactive_children_reset_without_turning_their_masters_on(self):
|
||||
|
||||
@@ -44,20 +44,17 @@ class TestRecoveryHttp(TestCase):
|
||||
rows = self.request('/api/settings/non-defaults', cookie=cookie)[1]['rows']
|
||||
self.assertNotIn('Wheel brake and acceleration colors', [row['label'] for row in rows])
|
||||
|
||||
def test_confirmation_reset_uses_only_master_and_preserves_legacy_flags(self):
|
||||
for key in ('SLCConfirmation', 'SLCConfirmationLower', 'SLCConfirmationHigher'):
|
||||
self.params.put_bool(key, True, block=True)
|
||||
def test_confirmation_reset_restores_off(self):
|
||||
self.params.put('SLCConfirmation', 1, block=True)
|
||||
cookie = self.login()
|
||||
payload = self.selection(cookie, ['Automatic adoption'])
|
||||
payload = self.selection(cookie, ['Confirm limits'])
|
||||
status, pending, _ = self.request('/api/settings/recovery/preview', cookie=cookie, payload=payload)
|
||||
self.assertEqual(status, 200)
|
||||
result = self.request('/api/settings/recovery/confirm', cookie=cookie,
|
||||
payload={'intent': pending['intent'], 'confirmed': True})
|
||||
self.assertEqual(result[0], 200)
|
||||
self.assertTrue(result[1]['saved'])
|
||||
self.assertFalse(self.params.get_bool('SLCConfirmation'))
|
||||
for key in ('SLCConfirmationLower', 'SLCConfirmationHigher'):
|
||||
self.assertTrue(self.params.get_bool(key))
|
||||
self.assertFalse(self.params.get('SLCConfirmation'))
|
||||
|
||||
def test_wrong_selection_and_unconfirmed_requests_are_rejected(self):
|
||||
self.params.put_bool('ShowBrakeStatus', True, block=True)
|
||||
@@ -81,13 +78,13 @@ class TestRecoveryHttp(TestCase):
|
||||
self.assertTrue(self.params.get_bool('ShowBrakeStatus'))
|
||||
|
||||
def test_unsupported_rows_are_reported_and_available_reset_all_does_not_touch_them(self):
|
||||
self.params.put_bool('SLCConfirmation', True, block=True)
|
||||
self.params.put('SLCConfirmation', 1, block=True)
|
||||
self.params.put_bool('ShowBrakeStatus', True, block=True)
|
||||
self.context.value = replace(self.context.value, cp=None, cp_raw=None)
|
||||
cookie = self.login()
|
||||
status, view, _ = self.request('/api/settings/non-defaults', cookie=cookie)
|
||||
self.assertEqual(status, 200)
|
||||
unsupported = next(row for row in view['rows'] if row['label'] == 'Automatic adoption')
|
||||
unsupported = next(row for row in view['rows'] if row['label'] == 'Confirm limits')
|
||||
self.assertFalse(unsupported['resetAvailable'])
|
||||
self.assertEqual(self.request('/api/settings/recovery/preview', cookie=cookie,
|
||||
payload={'view': view['view'], 'selected': [unsupported['id']]})[0], 409)
|
||||
@@ -99,4 +96,4 @@ class TestRecoveryHttp(TestCase):
|
||||
self.assertEqual(result[0], 200)
|
||||
self.assertTrue(result[1]['saved'])
|
||||
self.assertFalse(self.params.get_bool('ShowBrakeStatus'))
|
||||
self.assertTrue(self.params.get_bool('SLCConfirmation'))
|
||||
self.assertTrue(self.params.get('SLCConfirmation'))
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestOffroadConfigurationPages(unittest.TestCase):
|
||||
'conditional': ('Saved driving mode',),
|
||||
'conditional/cem': ('Speed threshold', 'Speed with lead', 'Stop lights'),
|
||||
'lane_change': ('Allow lane changes', 'Minimum lane-change speed', 'Automatic Lane Changes', 'Lane Change Speed'),
|
||||
'slc': ('Speed Limit Controller', 'Automatic adoption'),
|
||||
'slc': ('Speed Limit Controller', 'Confirm limits'),
|
||||
'profiles': ('Force Stop', 'Selected Acceleration Profile', 'Selected Deceleration Profile'),
|
||||
'torque': ('Steering Controller', 'Turn Assist', 'Lat Accel', 'Friction'),
|
||||
'wheel': ('Short press', 'Hold', 'Distance long press', 'MODE press', 'Star button'),
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestSavedTorquePage(unittest.TestCase):
|
||||
self.assertTrue(self.gateway._context('torque').editing_saved_vehicle)
|
||||
self.assertEqual(self.context.sample().cp.carFingerprint, 'HYUNDAI_IONIQ_6')
|
||||
self.assertEqual(self.gateway._context('slc').cp.carFingerprint, 'HYUNDAI_IONIQ_6')
|
||||
slc = next(row for row in self.page('slc')['rows'] if row['label'] == 'Automatic adoption')
|
||||
slc = next(row for row in self.page('slc')['rows'] if row['label'] == 'Confirm limits')
|
||||
self.assertTrue(slc['available'])
|
||||
self.offroad.return_value = False
|
||||
with patch.object(self.context, 'configuration_allowed', return_value=True):
|
||||
|
||||
@@ -461,9 +461,9 @@ class SettingsGatewayTest(unittest.TestCase):
|
||||
self.assertIn("Vision", primary["choices"])
|
||||
self.assertIn("Vision", secondary["choices"])
|
||||
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("immediately", confirmation["reason"])
|
||||
confirmation = next(row for row in rows if row["label"] == "Confirm limits")
|
||||
self.assertEqual(confirmation["value"], "Off")
|
||||
self.assertIn("automatically", confirmation["reason"])
|
||||
self.assertIn("accepts", confirmation["reason"])
|
||||
|
||||
with mock.patch.dict(os.environ, {"SLC_REPLAY_RUNTIME": "0", "SLC_VISION_DEVELOPMENT": "0"}):
|
||||
|
||||
@@ -12,15 +12,14 @@ Policy order matters. Target shaping precedes PID preparation; vehicle output sh
|
||||
|
||||
`LongitudinalPlanner.update(sm, *, cruise_ceiling=None, profile_tuning=None)` accepts an optional
|
||||
`CruiseCeiling(speed_mps, authority)` in addition to the existing driver cruise
|
||||
message. The live caller supplies no ceiling. The planner still reads
|
||||
`carState.vCruise` for the driver's selected ceiling and leaves every supplied
|
||||
Cap'n Proto message unchanged. [SLC](../speed_limits/README.md) adopts accepted
|
||||
limits into Card's ordinary cruise SET; it supplies no additional planner ceiling.
|
||||
The optional input remains available to explicit callers and replay tests.
|
||||
message. The live [SLC](../speed_limits/README.md) caller supplies a qualified cap.
|
||||
The planner still reads `carState.vCruise` as the driver's selected ceiling and
|
||||
leaves supplied messages unchanged. Accepted limits affect the planner cap, not SET.
|
||||
|
||||
The `CruiseCeiling` dataclass validates no source or acceptance history by
|
||||
construction. Its caller must first establish a fresh, accepted, normalized
|
||||
m/s ceiling in the correct speed coordinate. The planner validates its numeric
|
||||
construction. Its caller must establish an accepted, normalized m/s ceiling in
|
||||
the correct speed coordinate, including the fixed previous-accepted-limit fallback.
|
||||
Vehicle coordinates and authority remain fresh. The planner validates its numeric
|
||||
shape and the shared four-mode `Authority` contract. It applies the lower of
|
||||
the supplied ceiling and the existing driver ceiling only when native system
|
||||
longitudinal control is available, the supplied authority grants active system
|
||||
@@ -33,7 +32,7 @@ publish that diagnostic or alter the longitudinal plan schema.
|
||||
|
||||
This optional input changes only the cruise candidate before the planner's
|
||||
existing jerk limiting. SLC uses the ordinary cruise braking response and selected
|
||||
profile after its SET adoption. Lead-relevance checks still prevent cruise comfort
|
||||
profile for its capped target; a cap does not strengthen a softer cruise profile. Lead-relevance checks still prevent cruise comfort
|
||||
tuning from weakening braking for a close, slow or braking lead; malformed lead
|
||||
evidence also vetoes that tuning. The native MPC, lead, e2e, stop and acceleration
|
||||
arbitration remain in place. An inactive planner may still compute a nonzero
|
||||
@@ -111,7 +110,7 @@ different historical scales:
|
||||
| Sport (`sport`) | 2.4 m/s² | 2.0 m/s² |
|
||||
|
||||
These are positive deceleration magnitudes for ordinary cruise braking,
|
||||
including an SLC change to cruise SET. Global values preserve the historical
|
||||
including a changed SLC cap. Global values preserve the historical
|
||||
1.2 m/s² baseline with half/normal/double responses; explicit personality
|
||||
presets retain their original physical values. The distinction preserves
|
||||
existing tuning and saved-document compatibility.
|
||||
|
||||
@@ -7,6 +7,8 @@ from openpilot.common.realtime import DT_MDL
|
||||
|
||||
STANDSTILL_HOLD_NS = 2_000_000_000
|
||||
STANDSTILL_CLEAR_NS = 750_000_000
|
||||
STOP_COMMIT_TIME_S = .5
|
||||
SIGN_CONFIRM_STEP_S = 2 * DT_MDL
|
||||
SIGN_MAX_SPEED_MPS = 17.88
|
||||
SIGN_MODEL_AGREE_M = 50.0
|
||||
SIGN_SEED_M = 27.0
|
||||
@@ -102,6 +104,10 @@ class ForceStop:
|
||||
self.standstill_clear_ns = 0
|
||||
self.standstill_reason = None
|
||||
|
||||
@property
|
||||
def can_commit_next_frame(self) -> bool:
|
||||
return self.timer + SIGN_CONFIRM_STEP_S >= STOP_COMMIT_TIME_S
|
||||
|
||||
def step(self, frame: StopFrame, tune: StopTune, offset_ft: int = 0) -> StopPlan:
|
||||
now = frame.model_ns
|
||||
if (type(now) is not int or now <= 0 or now <= self.last_model_ns or
|
||||
@@ -177,16 +183,16 @@ class ForceStop:
|
||||
if not available and not self.forcing:
|
||||
self.timer = 0
|
||||
elif active and not frame.standstill:
|
||||
self.timer = min(2.0, self.timer + DT_MDL * (2 if frame.sign else 1))
|
||||
self.timer = min(2.0, self.timer + (SIGN_CONFIRM_STEP_S if frame.sign else DT_MDL))
|
||||
elif turn and not self.forcing and not frame.standstill:
|
||||
self.timer = 0
|
||||
elif held:
|
||||
self.timer = max(self.timer, .5)
|
||||
self.timer = max(self.timer, STOP_COMMIT_TIME_S)
|
||||
elif self.forcing and frame.standstill and not frame.sign and not frame.light and not frame.raw_model_stopped:
|
||||
self.timer = 0
|
||||
else:
|
||||
self.timer = max(0, self.timer - DT_MDL * .25)
|
||||
force = self.timer >= .5 or self.forcing and not frame.standstill or held
|
||||
force = self.timer >= STOP_COMMIT_TIME_S or self.forcing and not frame.standstill or held
|
||||
light_cleared = (not held and self.forcing and self.from_light and not frame.standstill and
|
||||
available and not frame.light and not frame.sign and go_scene)
|
||||
low_speed_hold = (light_cleared and tune.low_speed_hold_mps is not None and self.entry_speed is not None and
|
||||
|
||||
@@ -7,11 +7,12 @@ from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.values import CAR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
from openpilot.selfdrive.controls.plannerd import update_curve_frame
|
||||
from openpilot.selfdrive.controls.plannerd import update_curve_frame, wheel_override_blocked
|
||||
from openpilot.starpilot.curve_speed.host import CurveHost
|
||||
from openpilot.starpilot.longitudinal.force_stop import StopPlan
|
||||
from openpilot.starpilot.longitudinal.force_stop import StopPlan, vehicle_tune
|
||||
from openpilot.starpilot.longitudinal.force_stop_runtime import ForceStopRuntime, dashboard_stop_sign
|
||||
from openpilot.starpilot.longitudinal.tests.test_cruise_ceiling import messages, snapshot
|
||||
from openpilot.starpilot.longitudinal.tests.test_force_stop import frame as stop_frame
|
||||
from openpilot.starpilot.longitudinal.tests.test_stop_resume import event as button_event
|
||||
|
||||
BASE = 100_000_000_000
|
||||
@@ -61,6 +62,31 @@ class ForceStopRuntimeTests(unittest.TestCase):
|
||||
self.assertTrue(output.forcing)
|
||||
return output
|
||||
|
||||
def test_wheel_priority_requires_a_stop_cap_or_possible_commit_next_frame(self):
|
||||
tune = vehicle_tune(str(self.cp.carFingerprint))
|
||||
sign = {'light': False, 'sign': True, 'horizon_m': 40.}
|
||||
self.owner.plan = self.owner.policy.step(stop_frame(0, **sign), tune)
|
||||
self.assertIsNone(self.owner.plan.speed_ceiling_mps)
|
||||
self.assertFalse(wheel_override_blocked(self.owner, None))
|
||||
self.owner.plan = self.owner.policy.step(stop_frame(1, light=False, resume_requested=True), tune)
|
||||
self.assertGreater(self.owner.policy.timer, 0.)
|
||||
self.assertEqual(self.owner.policy.override_until_ns, 0)
|
||||
self.assertFalse(wheel_override_blocked(self.owner, None))
|
||||
|
||||
self.owner.reset()
|
||||
for tick in range(4):
|
||||
self.owner.plan = self.owner.policy.step(stop_frame(tick, **sign), tune)
|
||||
self.assertIsNone(self.owner.plan.speed_ceiling_mps)
|
||||
self.assertTrue(wheel_override_blocked(self.owner, None))
|
||||
self.owner.policy.step(stop_frame(4, resume_requested=True, **sign), tune)
|
||||
self.assertGreater(self.owner.policy.override_until_ns, 0)
|
||||
|
||||
self.owner.reset()
|
||||
self.owner.plan = self.owner.policy.step(stop_frame(0), tune)
|
||||
self.assertFalse(self.owner.policy.can_commit_next_frame)
|
||||
self.assertIsNotNone(self.owner.plan.speed_ceiling_mps)
|
||||
self.assertTrue(wheel_override_blocked(self.owner, None))
|
||||
|
||||
def test_committed_stop_survives_valid_mode_settings_and_traffic_changes(self):
|
||||
import json
|
||||
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
||||
|
||||
@@ -245,6 +245,48 @@ class TestVoltCc(unittest.TestCase):
|
||||
self.assertFalse(any(msg[0] in (0x2cb, 0x315, 0x200, 0x3d1) for msg in sent[-1][0]))
|
||||
|
||||
|
||||
def test_slc_cap_reaches_cruise_down_without_changing_driver_set(self):
|
||||
for metric in (False, True):
|
||||
with self.subTest(metric=metric):
|
||||
controls, card, ci, packer, sent, base, offset = fixture()
|
||||
card.is_metric, card.slc_control_enabled, card.slc_source_floor_ns = metric, True, 0
|
||||
state = messaging.new_message('slcState').slcState
|
||||
state.enabled, state.sessionId, state.hasCeiling, state.effectiveCap = True, 'drive', True, 45 * .44704
|
||||
controls.sm.data['slcState'] = state
|
||||
for flags in (controls.sm.seen, controls.sm.valid, controls.sm.alive):
|
||||
flags['slcState'] = True
|
||||
flags['carControl'] = True
|
||||
command = structs.CarControl(enabled=True, latActive=True, longActive=True)
|
||||
command.actuators.accel = -1.
|
||||
command.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid
|
||||
command.hudControl.setSpeed = 70 * .44704
|
||||
controls.sm.data['carControl'] = command
|
||||
controls.sm.logMonoTime['carControl'] = base
|
||||
by_phase = []
|
||||
for phase in ('active', 'stale'):
|
||||
directions = []
|
||||
for tick in range(100):
|
||||
now = base + (len(by_phase) * 100 + tick) * 10_000_000
|
||||
out = ci.update([(now + offset - 2_000_000,
|
||||
physical_frames(packer, speed=55 * .44704, target_speed=70 * .44704, counter=(tick // 3) % 4))])
|
||||
self.assertTrue(out.canValid)
|
||||
out.vCruise = out.vCruiseCluster = 70 * .44704 * 3.6
|
||||
for name in controls.sm.logMonoTime:
|
||||
controls.sm.logMonoTime[name] = now - 2_000_000
|
||||
controls.sm.recv_time[name] = (now - 1_000_000) / 1e9
|
||||
controls.sm.logMonoTime['slcState'] = state.frameMonoTime = now - (2_000_000 if phase == 'active' else 200_000_000)
|
||||
with patch('openpilot.selfdrive.car.card.clock_pair_ns', return_value=(now, now + offset)), \
|
||||
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
|
||||
patch('openpilot.selfdrive.car.card.REPLAY', False):
|
||||
card.controls_update(out, command.as_reader())
|
||||
directions.extend((data[5] >> 4) & 7 for address, data, _ in sent[-1][0] if address == 0x1e1)
|
||||
self.assertAlmostEqual(out.vCruise, 70 * .44704 * 3.6, places=4)
|
||||
self.assertAlmostEqual(command.hudControl.setSpeed, 70 * .44704, places=4)
|
||||
by_phase.append(directions)
|
||||
self.assertIn(3, by_phase[0])
|
||||
self.assertEqual(by_phase[1], [])
|
||||
self.assertIsNone(ci.CC.volt_cc_speed_ceiling_mps)
|
||||
|
||||
def test_actual_card_constructor_subscribes_selected_consumer(self):
|
||||
class Registered(Exception):
|
||||
pass
|
||||
|
||||
@@ -86,8 +86,9 @@ class TestPlannerSelection(unittest.TestCase):
|
||||
imports = [node.module for node in planner.body if isinstance(node, ast.ImportFrom)]
|
||||
self.assertIn('openpilot.starpilot.longitudinal.upstream.mpc', imports)
|
||||
self.assertNotIn('openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc', imports)
|
||||
self.assertFalse(any(value and value.startswith('openpilot.starpilot.') and value !=
|
||||
'openpilot.starpilot.longitudinal.upstream.mpc' for value in imports))
|
||||
self.assertEqual({value for value in imports if value and value.startswith('openpilot.starpilot.')}, {
|
||||
'openpilot.starpilot.longitudinal.upstream.mpc', 'openpilot.starpilot.longitudinal.cruise_ceiling',
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -60,7 +60,7 @@ class TrafficAccelerationPlannerTests(unittest.TestCase):
|
||||
self.assertLessEqual(traffic.output_a_target, 0.44 + 1e-8)
|
||||
self.assertLess(traffic.a_cruise, baseline.a_cruise)
|
||||
|
||||
def test_soft_saved_traffic_keeps_slc_and_force_decel_braking(self):
|
||||
def test_slc_cap_preserves_soft_traffic_response_and_native_force_decel(self):
|
||||
target = self.tuning(acceleration=0.6, braking=0.35)
|
||||
self.assertAlmostEqual(target.acceleration_max, 0.6)
|
||||
self.assertAlmostEqual(target.cruise_brake_magnitude, 0.35)
|
||||
@@ -72,14 +72,15 @@ class TrafficAccelerationPlannerTests(unittest.TestCase):
|
||||
for _ in range(60):
|
||||
sm, _ = messages(force=force)
|
||||
sm['carControl'].longActive = True
|
||||
baseline.update(sm, cruise_ceiling=ceiling)
|
||||
traffic.update(sm, cruise_ceiling=ceiling, profile_tuning=target, traffic_mode=True)
|
||||
sm['carState'].vCruise = 15.0 * 3.6
|
||||
baseline.update(sm, profile_tuning=target, traffic_mode=True)
|
||||
self.assertEqual(traffic.mpc.solution_status, 0)
|
||||
self.assertLessEqual(traffic.a_cruise, baseline.a_cruise + 1e-8)
|
||||
self.assertEqual(traffic.last_cruise_ceiling_status, 'force_decel' if force else 'applied')
|
||||
self.assertIsNotNone(traffic.last_profile)
|
||||
self.assertAlmostEqual(traffic.last_profile.cruise_brake_magnitude, 0.35)
|
||||
self.assertLessEqual(traffic.a_cruise, -0.5 + 1e-8)
|
||||
self.assertLessEqual(traffic.a_cruise, (-0.5 if force else -0.35) + 1e-8)
|
||||
|
||||
def test_soft_saved_traffic_preserves_lead_and_model_stop_candidates(self):
|
||||
target = self.tuning(acceleration=0.6, braking=0.5)
|
||||
@@ -215,8 +216,10 @@ class TrafficAccelerationPlannerTests(unittest.TestCase):
|
||||
cruise_ceiling=cruise_ceiling, curve_ceiling=curve_ceiling)
|
||||
self.assertEqual(planner.mpc.solution_status, 0)
|
||||
self.assertAlmostEqual(planner.output_a_target, baseline.output_a_target, places=5)
|
||||
if case in ('slc', 'curve'):
|
||||
if case == 'curve':
|
||||
self.assertLessEqual(planner.a_cruise, -0.5 + 1e-8)
|
||||
elif case == 'slc':
|
||||
self.assertLessEqual(planner.a_cruise, -0.42 + 1e-8)
|
||||
elif case == 'force_decel':
|
||||
self.assertLessEqual(planner.a_cruise, -0.5 + 1e-8)
|
||||
elif case == 'model_stop':
|
||||
|
||||
@@ -23,7 +23,7 @@ class EndLoop(Exception):
|
||||
|
||||
|
||||
class TestUpstreamConditional(unittest.TestCase):
|
||||
def run_loop(self, *, safe=False, stale=False, clocks=True, replay=False):
|
||||
def run_loop(self, *, safe=False, stale=False, clocks=True, replay=False, show_speed_limits=False):
|
||||
directory = self.enterContext(tempfile.TemporaryDirectory())
|
||||
params = Params(directory)
|
||||
cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC_BOSCH)
|
||||
@@ -32,6 +32,9 @@ class TestUpstreamConditional(unittest.TestCase):
|
||||
params.put_bool('UseStarPilotLongitudinalPlanner', False, block=True)
|
||||
params.put_bool('SafeMode', safe, block=True)
|
||||
params.put_bool('ExperimentalMode', False, block=True)
|
||||
params.put_bool('ShowSpeedLimits', show_speed_limits, block=True)
|
||||
if show_speed_limits:
|
||||
params.put('SLCPriority1', 'Dashboard', block=True)
|
||||
params.put('ConditionalModeConfig', json.loads(encode_preferences(SavedPreferences(
|
||||
mode=ModeChoice.CEM, cem=CEMOptions(speed_mps=20., stop_lights=False, lead=False, slower_lead=False)))), block=True)
|
||||
settings = ConditionalSettingsOwner(params)
|
||||
@@ -82,6 +85,15 @@ class TestUpstreamConditional(unittest.TestCase):
|
||||
self.payloads[name] = getattr(messaging.log_from_bytes(event.to_bytes()), name)
|
||||
if stale:
|
||||
self.logMonoTime['modelV2'] -= 1_000_000_000
|
||||
if show_speed_limits:
|
||||
dashboard = messaging.new_message('slcDashboardObservation', valid=True).slcDashboardObservation
|
||||
dashboard.carStateLogMonoTime = dashboard.observedMonoTime = stamp
|
||||
dashboard.validUntilMonoTime = stamp + 100_000_000
|
||||
dashboard.status, dashboard.speedMps, dashboard.episode = 'valid', 15., 1
|
||||
dashboard.producerSessionId = 'card'
|
||||
self.payloads['slcDashboardObservation'] = dashboard
|
||||
self.logMonoTime['slcDashboardObservation'] = stamp
|
||||
self.valid['slcDashboardObservation'] = self.alive['slcDashboardObservation'] = True
|
||||
|
||||
sm = Scene()
|
||||
|
||||
@@ -133,10 +145,19 @@ class TestUpstreamConditional(unittest.TestCase):
|
||||
self.assertFalse(any(result.accepted for _, result in results))
|
||||
self.assertFalse(any(str(plan.longitudinalPlanSource) == 'e2e' for plan in plans))
|
||||
|
||||
def test_cem_and_speed_limit_share_one_status_per_model_frame(self):
|
||||
results, statuses, plans, subscriptions = self.run_loop(show_speed_limits=True)
|
||||
self.assertEqual(subscriptions.count('deviceState'), 1)
|
||||
self.assertEqual(len(statuses), len(plans))
|
||||
self.assertTrue(any(result.accepted and result.experimental for _, result in results))
|
||||
self.assertTrue(all(status.slcState.displayOnly and status.slcState.speedLimit == 15. and
|
||||
status.slcState.conditionalMode.version == 1 for status in statuses))
|
||||
|
||||
def test_safe_mode_retains_upstream_without_conditional_transport(self):
|
||||
results, statuses, plans, subscriptions = self.run_loop(safe=True)
|
||||
self.assertFalse(results or statuses)
|
||||
self.assertNotIn('deviceState', subscriptions)
|
||||
self.assertNotIn('starpilotRadarState', subscriptions)
|
||||
self.assertNotIn('starpilotNavigation', subscriptions)
|
||||
self.assertEqual(len(plans), 100)
|
||||
|
||||
def test_missing_live_clock_does_not_publish_conditional_proposals(self):
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
from openpilot.cereal import log
|
||||
import pytest
|
||||
from openpilot.cereal import log, messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls import plannerd as starpilot_plannerd
|
||||
from openpilot.starpilot.longitudinal.upstream import mpc, planner, plannerd
|
||||
from openpilot.starpilot.speed_limits.tests import test_runtime_replay as replay
|
||||
|
||||
|
||||
class Solver:
|
||||
@@ -51,9 +55,9 @@ class TestUpstreamPlanner(unittest.TestCase):
|
||||
self.assertEqual(vars(cp), before)
|
||||
self.assertFalse(hasattr(stock, 'profile_tuning'))
|
||||
|
||||
def test_upstream_process_constructs_only_native_publishers_and_planner(self):
|
||||
def test_upstream_process_without_slc_transport_uses_only_native_services(self):
|
||||
calls = []
|
||||
cp = SimpleNamespace(brand='honda', openpilotLongitudinalControl=True)
|
||||
cp = SimpleNamespace(brand='honda', openpilotLongitudinalControl=True, passive=True)
|
||||
class SM:
|
||||
def update(self):
|
||||
raise InterruptedError('bounded end of mocked loop')
|
||||
@@ -61,7 +65,8 @@ class TestUpstreamPlanner(unittest.TestCase):
|
||||
calls.append(('subscribe', services, kwargs))
|
||||
return SM()
|
||||
fake_params = SimpleNamespace(get=lambda key, **kwargs: b'CP')
|
||||
with patch.object(plannerd, 'config_realtime_process'), patch.object(plannerd, 'Params', return_value=fake_params), \
|
||||
with patch.dict('os.environ', {}, clear=True), patch.object(plannerd, 'config_realtime_process'), \
|
||||
patch.object(plannerd, 'Params', return_value=fake_params), \
|
||||
patch.object(plannerd.messaging, 'log_from_bytes', return_value=cp), \
|
||||
patch.object(plannerd, 'LongitudinalPlanner', side_effect=lambda cp: calls.append(('planner', cp))), \
|
||||
patch.object(plannerd, 'LaneDepartureWarning', return_value=object()), \
|
||||
@@ -77,5 +82,87 @@ class TestUpstreamPlanner(unittest.TestCase):
|
||||
self.assertEqual(Path(plannerd.__file__).resolve(), (root / 'plannerd.py').resolve())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('choice', ['wheel', 'screen', 'stock_cruise'])
|
||||
def test_upstream_planner_applies_confirmed_slc_with_op_long(tmp_path, monkeypatch, choice):
|
||||
fixture = replay.RuntimeReplayTests()
|
||||
fixture.setUp()
|
||||
cp, sm = fixture.cp, fixture.sm
|
||||
cp.openpilotLongitudinalControl = choice != 'stock_cruise'
|
||||
sm['slcDashboardObservation'].speedMps = 15.
|
||||
sm.data['deviceState'] = messaging.new_message('deviceState').deviceState
|
||||
sm.valid['deviceState'] = sm.alive['deviceState'] = False
|
||||
params = Params(str(tmp_path))
|
||||
params.put('CarParams', cp.to_bytes(), block=True)
|
||||
params.put_bool('UseStarPilotLongitudinalPlanner', False, block=True)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put('SLCPriority1', 'Dashboard', block=True)
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
|
||||
publisher = Mock()
|
||||
subscriber = Mock(return_value=sm)
|
||||
outputs = []
|
||||
events = {}
|
||||
start = 2_000_000_000
|
||||
|
||||
class EndLoop(Exception):
|
||||
pass
|
||||
|
||||
def update():
|
||||
sm.frame = len(outputs) + 1
|
||||
if sm.frame == 3:
|
||||
raise EndLoop
|
||||
sm.advance(start + (sm.frame - 1) * 50_000_000)
|
||||
if sm.frame != 2 or choice == 'stock_cruise':
|
||||
return
|
||||
shown = outputs[0].slcState
|
||||
assert shown.hasPending and not shown.hasCeiling
|
||||
service = 'slcCruiseEvent' if choice == 'wheel' else 'slcAction'
|
||||
event = messaging.new_message(service, valid=True)
|
||||
event.logMonoTime = sm.logMonoTime['modelV2']
|
||||
action = getattr(event, service)
|
||||
action.sessionId, action.decisionId, action.presentationId = shown.sessionId, shown.decisionId, shown.presentationId
|
||||
if choice == 'wheel':
|
||||
action.kind, action.button = 'confirmationAccept', 'accel'
|
||||
action.eventId, action.producerSessionId, action.observedMonoTime = 1, 'card-producer', event.logMonoTime
|
||||
else:
|
||||
action.kind, action.sequenceId = 'accept', 1
|
||||
events[service] = event
|
||||
|
||||
def send(service, message):
|
||||
if service == 'slcState':
|
||||
outputs.append(message)
|
||||
|
||||
sm.all_checks = lambda services=None: True
|
||||
monkeypatch.setattr(sm, 'update', update)
|
||||
monkeypatch.setenv('REPLAY', '1')
|
||||
monkeypatch.delenv('SLC_REPLAY_RUNTIME', raising=False)
|
||||
monkeypatch.setattr(starpilot_plannerd, 'Params', lambda: params)
|
||||
monkeypatch.setattr(plannerd, 'Params', lambda: params)
|
||||
monkeypatch.setattr(plannerd, 'config_realtime_process', Mock())
|
||||
monkeypatch.setattr(plannerd, 'LaneDepartureWarning', Mock(return_value=Mock(left=False, right=False)))
|
||||
monkeypatch.setattr(messaging, 'SubMaster', subscriber)
|
||||
monkeypatch.setattr(messaging, 'PubMaster', Mock(return_value=publisher))
|
||||
monkeypatch.setattr(messaging, 'sub_sock', lambda service, **kwargs: service)
|
||||
monkeypatch.setattr(messaging, 'recv_one_or_none', lambda service: events.pop(service, None))
|
||||
publisher.send.side_effect = send
|
||||
cruise = Mock(wraps=planner.get_cruise_accel)
|
||||
monkeypatch.setattr(planner, 'get_cruise_accel', cruise)
|
||||
|
||||
with pytest.raises(EndLoop):
|
||||
starpilot_plannerd.main()
|
||||
|
||||
assert 'slcDashboardObservation' in subscriber.call_args.kwargs['ignore_valid']
|
||||
assert cruise.call_args_list[0].args[1] == pytest.approx(100. / 3.6)
|
||||
assert sm['carState'].vCruise == 100.
|
||||
state = outputs[1].slcState
|
||||
if choice == 'stock_cruise':
|
||||
assert state.displayOnly and not state.hasPending and not state.hasCeiling
|
||||
assert cruise.call_args.args[1] == pytest.approx(100. / 3.6)
|
||||
else:
|
||||
assert state.hasCeiling and state.actionStatus == 'accept'
|
||||
assert cruise.call_args.args[1] == 15.
|
||||
assert params.get('SLCQualifiedHistory')['speed_mps'] == 15.
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -14,6 +14,7 @@ from openpilot.starpilot.longitudinal.upstream.mpc import T_IDXS as T_IDXS_MPC
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan, should_stop
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.starpilot.longitudinal.cruise_ceiling import select_cruise_ceiling
|
||||
|
||||
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
|
||||
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
|
||||
@@ -70,7 +71,7 @@ class LongitudinalPlanner:
|
||||
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
||||
self.j_desired_trajectory = np.zeros(CONTROL_N)
|
||||
|
||||
def update(self, sm):
|
||||
def update(self, sm, *, cruise_ceiling=None):
|
||||
if len(sm['carControl'].orientationNED) == 3:
|
||||
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
||||
else:
|
||||
@@ -79,11 +80,17 @@ class LongitudinalPlanner:
|
||||
v_ego = sm['carState'].vEgo
|
||||
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
|
||||
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
||||
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
||||
ceiling = select_cruise_ceiling(
|
||||
cruise_ceiling, driver_v_cruise_kph=sm['carState'].vCruise, driver_cruise_mps=v_cruise,
|
||||
system_long_available=self.CP.openpilotLongitudinalControl, long_control_active=not long_control_off,
|
||||
selfdrive_enabled=sm['selfdriveState'].enabled, force_decel=sm['controlsState'].forceDecel,
|
||||
)
|
||||
if ceiling.effective_mps is not None:
|
||||
v_cruise = ceiling.effective_mps
|
||||
if sm['controlsState'].forceDecel:
|
||||
v_cruise = 0.0
|
||||
|
||||
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
||||
|
||||
# Reset current state when not engaged, or user is controlling the speed
|
||||
reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled
|
||||
# PCM cruise speed may be updated a few cycles later, check if initialized
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from opendbc.car.structs import car
|
||||
@@ -11,7 +12,11 @@ from openpilot.selfdrive.controls.plannerd import current_manual_event, current_
|
||||
from openpilot.starpilot.conditional_mode.planner_host import ConditionalPlannerHost
|
||||
from openpilot.starpilot.conditional_mode.projection import paired_clocks_ns
|
||||
from openpilot.starpilot.feature_runtime import enabled as feature_enabled
|
||||
from openpilot.starpilot.feature_runtime import slc_transport_capable
|
||||
from openpilot.starpilot.longitudinal.upstream.planner import LongitudinalPlanner
|
||||
from openpilot.starpilot.speed_limits.planner_host import (
|
||||
SLC_ACTIONS, SLC_CRUISE_EVENTS, SavedSlcLifecycle, action_from_message, cruise_event_from_message, receive_slc_events,
|
||||
)
|
||||
import openpilot.cereal.messaging as messaging
|
||||
|
||||
|
||||
@@ -25,50 +30,76 @@ def main():
|
||||
|
||||
ldw = LaneDepartureWarning()
|
||||
longitudinal_planner = LongitudinalPlanner(CP)
|
||||
replay = 'REPLAY' in os.environ
|
||||
conditional = ConditionalPlannerHost(params) if feature_enabled(params, CP, 'conditional', os.environ) else None
|
||||
slc = SavedSlcLifecycle(params, CP, os.environ) if slc_transport_capable(CP, os.environ) else None
|
||||
native_inputs = ['carControl', 'carState', 'controlsState', 'vehicleParameters', 'radarState', 'modelV2', 'selfdriveState']
|
||||
optional = ['deviceState', 'longitudinalPlan', 'starpilotRadarState', 'starpilotNavigation'] if conditional is not None else []
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance'] + (['slcState'] if conditional is not None else []))
|
||||
optional = ['deviceState'] if slc is not None or conditional is not None else []
|
||||
if slc is not None:
|
||||
slc.refresh(0)
|
||||
optional.extend(slc.source_services())
|
||||
if conditional is not None:
|
||||
optional.extend(['longitudinalPlan', 'starpilotRadarState', 'starpilotNavigation'])
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance'] + (['slcState'] if optional else []))
|
||||
sm = messaging.SubMaster(native_inputs + optional, poll='modelV2',
|
||||
ignore_alive=optional, ignore_valid=optional, ignore_avg_freq=optional)
|
||||
cruise_sock = messaging.sub_sock('slcCruiseEvent', conflate=False) if conditional is not None else None
|
||||
action_sock = messaging.sub_sock('slcAction', conflate=False) if conditional is not None else None
|
||||
cruise_sock = messaging.sub_sock('slcCruiseEvent', conflate=False) if optional else None
|
||||
action_sock = messaging.sub_sock('slcAction', conflate=False) if optional else None
|
||||
pending_actions, pending_cruise = deque(maxlen=64), deque(maxlen=64)
|
||||
pending_manual, pending_ui = deque(maxlen=64), deque(maxlen=64)
|
||||
drive_start_ns = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
if slc is not None and not replay:
|
||||
slc.refresh(time.monotonic_ns(), pending_actions=pending_actions, pending_cruise=pending_cruise)
|
||||
sm.update()
|
||||
if sm.updated['modelV2']:
|
||||
longitudinal_planner.update(sm)
|
||||
if conditional is not None and 'REPLAY' not in os.environ:
|
||||
clocks = paired_clocks_ns()
|
||||
if clocks is not None:
|
||||
now_ns, boot_ns, skew_ns = clocks
|
||||
drive_id = (int(sm['deviceState'].startedMonoTime) if sm.valid['deviceState'] and sm.alive['deviceState'] and
|
||||
0 < sm.logMonoTime['deviceState'] <= now_ns else 0)
|
||||
if drive_id > 0 and drive_start_ns > 0 and drive_id != drive_start_ns:
|
||||
conditional.reset()
|
||||
pending_manual.clear()
|
||||
pending_ui.clear()
|
||||
for sock, service, kind, pending in ((cruise_sock, 'slcCruiseEvent', 'conditionalMode', pending_manual),
|
||||
(action_sock, 'slcAction', 'conditionalModeCycle', pending_ui)):
|
||||
for _ in range(8):
|
||||
event = messaging.recv_one_or_none(sock)
|
||||
if event is None:
|
||||
break
|
||||
if event.valid and str(getattr(event, service).kind) == kind:
|
||||
pending.append(event)
|
||||
proposal, snapshot = conditional.sample(
|
||||
sm, CP, longitudinal_planner, now_mono_ns=now_ns, now_boot_ns=boot_ns, sample_skew_ns=skew_ns,
|
||||
drive_id=drive_id, native_plan_valid=bool(sm.all_checks(native_inputs)),
|
||||
manual_event=current_manual_event(pending_manual, now_ns, int(sm.logMonoTime['carState'])),
|
||||
ui_event=current_ui_manual_event(pending_ui, now_ns))
|
||||
status = conditional.status.attach(None, proposal, snapshot, now_ns=now_ns, drive_id=drive_id,
|
||||
model_ns=int(sm.logMonoTime['modelV2']), car_state_ns=int(sm.logMonoTime['carState']))
|
||||
pm.send('slcState', status)
|
||||
if drive_id > 0:
|
||||
drive_start_ns = drive_id
|
||||
clocks = paired_clocks_ns() if conditional is not None and not replay else None
|
||||
now_ns = int(sm.logMonoTime['modelV2']) if replay else clocks[0] if clocks is not None else time.monotonic_ns()
|
||||
drive_id = (int(sm['deviceState'].startedMonoTime) if optional and sm.valid['deviceState'] and sm.alive['deviceState'] and
|
||||
0 < sm.logMonoTime['deviceState'] <= now_ns else 0)
|
||||
drive_changed = drive_id > 0 and drive_start_ns > 0 and drive_id != drive_start_ns
|
||||
if drive_changed and conditional is not None and not replay:
|
||||
conditional.reset()
|
||||
pending_manual.clear()
|
||||
pending_ui.clear()
|
||||
|
||||
if slc is not None or clocks is not None:
|
||||
if slc is not None and replay:
|
||||
slc.refresh(now_ns, pending_actions=pending_actions, pending_cruise=pending_cruise)
|
||||
for event in receive_slc_events(cruise_sock):
|
||||
if event.valid:
|
||||
kind = str(event.slcCruiseEvent.kind)
|
||||
if slc is not None and kind in SLC_CRUISE_EVENTS:
|
||||
pending_cruise.append(cruise_event_from_message(event))
|
||||
elif clocks is not None and kind == 'conditionalMode':
|
||||
pending_manual.append(event)
|
||||
for event in receive_slc_events(action_sock):
|
||||
if event.valid:
|
||||
kind = str(event.slcAction.kind)
|
||||
if slc is not None and kind in SLC_ACTIONS:
|
||||
pending_actions.append(action_from_message(event))
|
||||
elif clocks is not None and kind == 'conditionalModeCycle':
|
||||
pending_ui.append(event)
|
||||
|
||||
output = slc.step(sm, now_ns, pending_actions, pending_cruise, drive_changed=drive_changed) if slc is not None else None
|
||||
ceiling = output.result.ceiling if output is not None else None
|
||||
status = output.message if output is not None else None
|
||||
longitudinal_planner.update(sm, cruise_ceiling=ceiling)
|
||||
if conditional is not None and clocks is not None:
|
||||
_, boot_ns, skew_ns = clocks
|
||||
proposal, snapshot = conditional.sample(
|
||||
sm, CP, longitudinal_planner, now_mono_ns=now_ns, now_boot_ns=boot_ns, sample_skew_ns=skew_ns,
|
||||
drive_id=drive_id, native_plan_valid=bool(sm.all_checks(native_inputs)),
|
||||
cruise_ceiling_mps=ceiling.speed_mps if ceiling is not None else None,
|
||||
manual_event=current_manual_event(pending_manual, now_ns, int(sm.logMonoTime['carState'])),
|
||||
ui_event=current_ui_manual_event(pending_ui, now_ns))
|
||||
status = conditional.status.attach(status, proposal, snapshot, now_ns=now_ns, drive_id=drive_id,
|
||||
model_ns=int(sm.logMonoTime['modelV2']), car_state_ns=int(sm.logMonoTime['carState']))
|
||||
if status is not None:
|
||||
pm.send('slcState', status)
|
||||
drive_start_ns = drive_id or drive_start_ns
|
||||
longitudinal_planner.publish(sm, pm)
|
||||
|
||||
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
"plannerd.py": {
|
||||
"upstream_path": "openpilot/selfdrive/controls/plannerd.py",
|
||||
"upstream_sha256": "9c1591c78522e0f89794eb7301113fc616ec6ac079e576a3c9f9ebec64bcbfde",
|
||||
"adapted_sha256": "0af78b0030a2219f3224e28592715a50bc5620a1e644b75c13eeb0cbeeb2afd5",
|
||||
"adaptation": "Planner/MPC imports redirected; independent existing conditional-mode owner and bounded transport connected outside unchanged upstream planner/MPC arithmetic"
|
||||
"adapted_sha256": "c0a305cd934673706aa29166ba3b08ae82894b1d7ee41e5348c4b995ee9a4db2",
|
||||
"adaptation": "Redirected planner imports and connected independent conditional mode and shared SLC through bounded event transport and a shared status publisher"
|
||||
},
|
||||
"planner.py": {
|
||||
"upstream_path": "openpilot/selfdrive/controls/lib/longitudinal_planner.py",
|
||||
"upstream_sha256": "d81425700cf7152ef4f617ec1517fb4137fc5af9188047f48b31aff62dc652f4",
|
||||
"adapted_sha256": "7f79b61e095928100cb1d1d0b5e5e6ee66676d461c2274fc5a542cec2c841e8f",
|
||||
"adaptation": "Only planner/MPC import paths redirected; no arithmetic/state/publisher changes"
|
||||
"adapted_sha256": "47967154441baa073e777e9664f3383705ddeaa779da8dc31c3b2909b9ad49b0",
|
||||
"adaptation": "Redirected MPC imports and applied the optional, authority-checked SLC cap to the cruise target"
|
||||
},
|
||||
"mpc.py": {
|
||||
"upstream_path": "openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py",
|
||||
|
||||
@@ -218,18 +218,17 @@ def enabled(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
# Selecting Maps owns producer demand directly. Do not rewrite the separate
|
||||
# diagnostic opt-in, and do not start control inside the Go shadow provider.
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import LABELS, read_params
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import LABELS, SOURCE_PRIORITY_DEFAULTS, SOURCE_PRIORITY_KEYS, SOURCE_PRIORITY_MAX_BYTES, read_params
|
||||
from openpilot.starpilot.speed_limits.selection import SelectionMode, Source
|
||||
# Default Vision/Dashboard needs no Maps producer or full settings scan.
|
||||
# These bounded reads are demand hints only; canonical parsing below owns
|
||||
# the enabled/display state and rejects malformed complete preferences.
|
||||
labels = []
|
||||
for key in ('SLCPriority1', 'SLCPriority2'):
|
||||
raw, readable = read_saved(params, key, 32)
|
||||
for key, default in zip(SOURCE_PRIORITY_KEYS, SOURCE_PRIORITY_DEFAULTS, strict=True):
|
||||
raw, readable = read_saved(params, key, SOURCE_PRIORITY_MAX_BYTES)
|
||||
if not readable:
|
||||
return False
|
||||
try:
|
||||
labels.append(raw.decode('utf-8') if raw is not None else None)
|
||||
labels.append(raw.decode('utf-8') if raw is not None else default)
|
||||
except UnicodeError:
|
||||
return False
|
||||
if not any(LABELS.get(label) is Source.MAP for label in labels) and labels[0] not in ('Highest', 'Lowest'):
|
||||
|
||||
@@ -16,6 +16,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.starpilot.maps.artifact import elf_arm64_static, source_digest
|
||||
from openpilot.starpilot.maps import shadow_lifecycle as shadow
|
||||
from openpilot.starpilot.maps import package_shadow
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import SOURCE_PRIORITY_KEYS
|
||||
|
||||
|
||||
def fake_static_elf() -> bytes:
|
||||
@@ -35,7 +36,7 @@ class FakeParams(Params):
|
||||
self.path = path
|
||||
|
||||
def get_param_path(self, key: str = '') -> str:
|
||||
assert key in (shadow.OPT_IN_KEY, 'SLCPriority1', 'SLCPriority2')
|
||||
assert key in (shadow.OPT_IN_KEY, *SOURCE_PRIORITY_KEYS)
|
||||
return str(self.path if key == shadow.OPT_IN_KEY else self.path.parent / key)
|
||||
|
||||
|
||||
@@ -173,6 +174,7 @@ class TestShadowLifecycle(unittest.TestCase):
|
||||
path = self.base / 'MapdShadowEnabled'
|
||||
params = FakeParams(path)
|
||||
cp = FakeCar()
|
||||
(self.base / 'SLCPriority2').write_bytes(b'Dashboard')
|
||||
self.assertFalse(shadow.enabled(True, params, cp))
|
||||
path.write_bytes(b'1')
|
||||
self.assertTrue(shadow.enabled(True, params, cp))
|
||||
@@ -196,6 +198,7 @@ class TestShadowLifecycle(unittest.TestCase):
|
||||
with mock.patch.dict(os.environ, {'PARAMS_ROOT': str(self.base / 'params')}):
|
||||
params = Params()
|
||||
self.assertFalse(params.get_default_value(shadow.OPT_IN_KEY))
|
||||
params.put('SLCPriority2', 'Dashboard', block=True)
|
||||
self.assertFalse(shadow.enabled(True, params, FakeCar()))
|
||||
params.put_bool(shadow.OPT_IN_KEY, True, block=True)
|
||||
self.assertTrue(shadow.enabled(True, params, FakeCar()))
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Speed-limit composition
|
||||
|
||||
`composition.step` joins current source selection, adoption decisions, configured
|
||||
offsets, selected cruise coordinates and qualified driver intent. It preserves
|
||||
validation of sessions, clocks, source observations, current authority and the causal
|
||||
action ledger. Malformed or contradictory input cannot authorize a command.
|
||||
`composition.step` joins source selection, acceptance, offsets and qualified raw/cluster
|
||||
speed pairs into `Result.ceiling`. The planner caps the driver's SET with this value;
|
||||
SLC never writes SET. Source, action, session, clock and authority validation remain
|
||||
at their existing boundaries. Ordinary driver SET changes do not create an SLC bypass.
|
||||
|
||||
The resulting accepted adjusted limit is adoption data. Runtime sends it to card
|
||||
as a bounded set-speed command, retrying a failed application only while the same
|
||||
adoption and expected set speed remain current. `Result.ceiling` is always `None`; SLC does
|
||||
not compose another speed ceiling with the driver's set speed. The planner uses
|
||||
card's published set speed and its ordinary selected braking profile.
|
||||
The previous accepted limit plus the current offset remains the cap through source
|
||||
gaps and later engagements. Validated saved history supplies the same fallback
|
||||
after a restart. It never creates a fresh source observation or accepts a pending
|
||||
proposal. Current vehicle coordinates and system-longitudinal authority remain
|
||||
required. There is no separate SLC override reducer or retained pedal target.
|
||||
|
||||
Lead following, curves, stopping and actuator restrictions retain their independent planner paths.
|
||||
See the [current controller contract](README.md).
|
||||
Lead following, curves, stopping, cruise profiles and actuator restrictions retain
|
||||
their existing owners. See the [controller contract](README.md).
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
# Set-speed override
|
||||
# Driver inputs
|
||||
|
||||
Only a qualified driver set-speed change can create an SLC override. Either an
|
||||
increase or decrease qualifies when the resulting speed differs from the most
|
||||
recently applied set speed. Card's qualified application receipt records the
|
||||
rounded value; only a 0.001 m/s transport tolerance applies. The causal ledger
|
||||
excludes automatic changes, consumed
|
||||
confirmation presses, duplicated effects and foreign or obsolete contexts.
|
||||
Ordinary cruise +/- changes the driver's SET ceiling. With **Override speed limit
|
||||
with +** enabled, a qualifying physical tap past the adjusted limit also lets cruise
|
||||
use SET. The accelerator override has its own policy. See [SET speed and accelerator](README.md#set-speed-and-accelerator).
|
||||
|
||||
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 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
|
||||
choice. Authority transitions still suppress commands and invalidate action origins;
|
||||
reversed clocks and malformed evidence remain fail-closed. See [README](README.md).
|
||||
Source loss uses the previous accepted limit plus the current offset, up to SET.
|
||||
That remembered limit survives re-engagement and restarts; applying it still needs
|
||||
current vehicle coordinates and system-longitudinal authority. See [README](README.md).
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
# Speed Limit Controller
|
||||
|
||||
SLC uses the ordinary cruise set speed as its only vehicle-speed target. A qualified
|
||||
limit from the selected Dashboard, Maps, or Vision source is an input to adoption.
|
||||
Acceptance applies the configured offset within the existing 8–145 km/h cruise
|
||||
bounds and sends a qualified command to card,
|
||||
which owns `vCruise` and `vCruiseCluster`. SLC supplies no separate planner ceiling.
|
||||
Lead, curve, stop and actuator safety constraints retain their existing owners.
|
||||
SLC supplies a speed-limit cap beneath the driver's cruise SET. The planner uses
|
||||
the lower of SET and the accepted limit plus its configured offset, converted into
|
||||
physical-speed coordinates once. Accepting a limit never rewrites SET. Native PCM
|
||||
cruise owns its setpoint; non-PCM software cruise and the dedicated Subaru software
|
||||
cruise mode retain their existing owners. Automatic SLC requires system longitudinal
|
||||
control. Lead, curve, stop and actuator safety constraints retain their existing owners.
|
||||
SLC works with either the StarPilot or upstream longitudinal planner.
|
||||
|
||||
## New limits and adoption
|
||||
|
||||
On SET or RES engagement, a currently qualified limit from the selected source
|
||||
initializes SET with its existing offset and mph/km/h rounding, even with Automatic
|
||||
adoption off. Sources keep updating while disengaged. This explicit adoption clears
|
||||
the matching prompt, decline or set-speed override; later limits keep the normal
|
||||
confirmation policy. Without a qualified current limit, RESUME restores the previous
|
||||
SET and normal SET initialization applies. An early Maps proposal does not replace
|
||||
the current road's limit at engagement. Toyota still requires native cruise capacity
|
||||
and release approval before applying the target.
|
||||
**Match Speed Limit on Engage** is Off by default. When enabled, a new software SET
|
||||
engagement uses the SLC limit plus its offset, including a saved accepted limit
|
||||
during a source gap. A fresh reading takes priority over history from another
|
||||
drive. An unanswered proposal cannot replace a limit already adopted this drive.
|
||||
An accepted early Maps preview applies to the SLC ceiling only. A new SET
|
||||
uses the qualified current limit when available, or ordinary SET initialization
|
||||
if only the preview is available.
|
||||
RES restores the previous SET when one exists. Factory cruise keeps its own SET.
|
||||
|
||||
A different numeric limit from the selected source starts a new decision, including
|
||||
while a set-speed override is active. A source/road identity change at the same
|
||||
numeric limit does not start another decision. A decline remains effective across
|
||||
a source gap or same-value provider change; a later different limit starts a new
|
||||
window, including when an earlier numeric limit returns.
|
||||
A different numeric limit from the selected source can start a new decision, including
|
||||
after the driver changes SET. A source or road identity change at the same numeric
|
||||
limit does not start another decision. The most recent decline survives source
|
||||
gaps and other readings until a different limit is adopted or another limit is
|
||||
declined. The sign continues to show the road reading.
|
||||
|
||||
A different provider proposing a different numeric limit must remain the proposed
|
||||
replacement for two seconds before confirmation or automatic adoption. A competing
|
||||
qualified reading cancels that check; missing readings do not restart it. Adoption
|
||||
still requires fresh evidence at the end. Same-provider limit changes, explicit
|
||||
driver adoption and latched early Maps proposals keep their existing timing.
|
||||
A qualified limit from a different provider enters the same confirmation or automatic
|
||||
adoption flow immediately, without an extra source-switch delay.
|
||||
|
||||
While StarPilot owns speed control, a proposed limit whose adjusted, rounded target
|
||||
already matches both current SET values is recorded without another confirmation
|
||||
or cruise command. The comparison uses the same mph/km/h rounding as Card, not
|
||||
actual traveling speed. A presented Accept or Decline takes priority, and a decline
|
||||
remains effective for that proposal. If SET reaches the target during a prompt,
|
||||
the prompt closes without changing speed. An earlier command must first settle,
|
||||
and SET must be observed after its receipt or expiry, before alignment is trusted.
|
||||
Received-limit memory still manages proposal changes and declined choices.
|
||||
An expired confirmation stays declined even if SET now matches. Changing only
|
||||
display units renews pending actions while preserving accepted limits, declines, manual
|
||||
overrides and the already applied SET.
|
||||
A proposed adjusted limit matching both displayed SET values is recorded without
|
||||
another confirmation. The comparison uses the existing mph/km/h display rounding,
|
||||
not actual traveling speed. A presented Accept or Decline takes priority; an expired
|
||||
confirmation remains declined. Unit changes renew pending actions while preserving
|
||||
accepted limits, declines, SET and the active physical-speed cap.
|
||||
|
||||
With Automatic adoption on, qualified new limits are adopted immediately.
|
||||
With it off, the stationary confirmation card gives two gentle purple pulses
|
||||
With **Confirm limits** set to **Off**, qualified new limits are adopted immediately.
|
||||
**Both**, **Higher only**, and **Lower only** ask relative to the previous
|
||||
accepted posted limit. An unselected direction is automatic.
|
||||
Without a previous limit, any manual policy asks for the first one unless it
|
||||
already matches SET.
|
||||
The stationary confirmation card gives two gentle purple pulses
|
||||
(700 ms each, separated by 200 ms), then rests until the 3.2-second cycle repeats.
|
||||
Only the lower part of the card pulses; the speed and source remain steady.
|
||||
The arrow remains relative to vehicle speed.
|
||||
@@ -53,8 +49,13 @@ US signs share a cached antialiased white outline with a transparent interior;
|
||||
the offset fits inside the fixed sign height on both screens.
|
||||
Both sign styles identify the displayed source: DASH for Dashboard, OSM for offline
|
||||
Maps, and VISION for the vision model. Explicit Mapbox readings in historical
|
||||
replay use NAV; unidentified sources keep SPEED. Retained readings keep their
|
||||
original source label.
|
||||
replay use NAV; unidentified live sources show SPEED. A retained road reading
|
||||
shows LAST alongside its source when known. For the same source and speed, the
|
||||
label clears after 150 ms of live updates. Vision keeps a confirmed sign current
|
||||
after it leaves view while the camera pipeline stays healthy. DASH follows the
|
||||
current OEM reading, and Maps keeps the current road limit separate from its
|
||||
upcoming preview. The binding SLC speed is shown separately beside SET, including
|
||||
during a new-limit confirmation.
|
||||
Both styles place only this source above the number, beside any pending arrow.
|
||||
The source and arrow share a centered row; all sign contents use the same geometry
|
||||
on both screens. Small arrows are baked at double resolution and text is aligned
|
||||
@@ -65,25 +66,29 @@ while system longitudinal control is engaged, including a temporary accelerator
|
||||
override. Disengaged and AOL lateral-only modes keep displaying limits without
|
||||
prompting. Losing longitudinal engagement cancels the pending choice without
|
||||
declining it; re-engaging starts a fresh choice if needed. A pending manual choice
|
||||
expires after ten monotonic seconds while engaged, including source gaps. The same
|
||||
decision, card and expanded controls remain during a gap, with Accept disabled and
|
||||
an unavailable label. Reject still dismisses that decision. Recovery renews the
|
||||
action presentation without restarting the decision, deadline or attention cycle.
|
||||
expires after thirty monotonic seconds while engaged, including source gaps. The same
|
||||
decision, presentation, card and expanded controls remain actionable during a gap.
|
||||
Accept applies that remembered proposal and Reject dismisses it. Recovery of the
|
||||
same proposal preserves the interaction, deadline and attention cycle.
|
||||
Wheel confirmation presses remain consumed during a gap, so they cannot become
|
||||
ordinary SET adjustments. An unavailable Accept is never queued for recovery.
|
||||
Decline and timeout leave set speed unchanged. An action at the exact deadline
|
||||
can still complete its presented decision; a later action cannot accept it.
|
||||
ordinary SET adjustments. Acceptance takes effect immediately and is never queued.
|
||||
Decline and timeout leave set speed unchanged. An action captured by the deadline
|
||||
can complete its presented decision within the input freshness window. The prompt
|
||||
closes at expiry, and an action captured after the deadline cannot accept it.
|
||||
|
||||
The existing saved `SLCConfirmation` bit is retained: `0` means Automatic adoption
|
||||
on and `1` means off. Both native settings layouts show the inverse as Automatic
|
||||
adoption. Legacy directional confirmation and override preferences do not change
|
||||
this confirmation policy.
|
||||
The saved `SLCConfirmation` mode is `0` for Off, `1` for Both, `3` for Higher
|
||||
only, or `4` for Lower only.
|
||||
|
||||
## Optional early adoption
|
||||
|
||||
The existing SLC settings page adds **Adopt limits early**: Off by default, or
|
||||
1–10 whole seconds (`SLCAdvanceSeconds`). It applies to both increases and
|
||||
decreases from offline Maps. Other sources keep their current-limit behavior.
|
||||
The existing SLC settings page offers **Adopt higher limits early** and
|
||||
**Adopt lower limits early**: each is Off by default, or 1–10 whole seconds
|
||||
(`SLCAdvanceHigherSeconds` and `SLCAdvanceLowerSeconds`). Direction follows the
|
||||
posted upcoming limit relative to the current mapped limit, before offsets.
|
||||
Other sources keep their current-limit behavior.
|
||||
An ordered source list without Maps ignores map readings and previews. Removing
|
||||
Maps retires an in-progress preview and returns to the selected sources' normal
|
||||
adoption and previous-limit fallback rules.
|
||||
The upcoming Map proposal must win the configured source-selection policy.
|
||||
There is no new onroad widget, caption, or countdown. The existing New Limit
|
||||
confirmation presents the proposed limit and its own offset; the ordinary sign
|
||||
@@ -96,47 +101,69 @@ estimate arrival. A first offer requires motion and active system speed control.
|
||||
This is approximate timing, not a promise to reach the target speed at the sign.
|
||||
|
||||
The first offer latches one boundary encounter. Slowing or stopping does not
|
||||
retract it or restart the existing ten-second confirmation deadline. Source loss
|
||||
removes authority and retains the pending choice; fresh recovery of the same
|
||||
boundary uses the original deadline and a current presentation. Losing longitudinal
|
||||
retract it or restart the existing thirty-second confirmation deadline. Source loss
|
||||
retains the pending choice and its Accept / Reject interaction; recovery of the same
|
||||
boundary uses the original deadline and presentation. Losing longitudinal
|
||||
engagement cancels an unanswered choice; re-engaging starts a fresh deadline.
|
||||
Decline, expiry, manual override and completed application survive the boundary
|
||||
becoming current.
|
||||
A fresh preview with a different destination way or limit closes an unanswered
|
||||
prompt, even before the replacement is close enough to offer. Closing a prompt
|
||||
preserves the rollback point for an earlier applied preview.
|
||||
Decline, expiry and the matching decision survive the previewed limit becoming current.
|
||||
An older preview cap ends at arrival if the selected current reading differs.
|
||||
Changing the driver's SET does not withdraw an accepted early limit.
|
||||
An unchanged approach limit cannot be offered again solely because the preview
|
||||
is lost or disabled. Current-source evidence, command freshness, expected SET,
|
||||
driver-button checks and Toyota's native capacity/release checks still apply.
|
||||
is lost or disabled. Current-source evidence, active system-longitudinal authority and driver-button checks still apply.
|
||||
A fresh road match that contradicts the preview retires the encounter, even if
|
||||
Maps knows the road but has no posted speed for it.
|
||||
A newly selected current Maps limit gets its own decision before the next exit
|
||||
can be offered, including the first Maps reading of a drive.
|
||||
The same source-selection policy used to offer a preview can retire it when another
|
||||
qualified source wins. That approach cannot re-offer the same destination and
|
||||
limit solely because the winning source disappears or a tile refresh changes the ID.
|
||||
If the preferred live source matches the accepted preview speed, the cap continues.
|
||||
Otherwise the previous acceptance is restored and the selected source follows normal
|
||||
confirmation rules.
|
||||
The configured adopt wheel action continues to adopt the displayed current limit
|
||||
when no confirmation is pending.
|
||||
|
||||
Timing-only edits preserve the runtime session and driver decisions. Off cancels
|
||||
unapplied advance work and leaves an applied SET intact; normal current-limit
|
||||
behavior resumes at the boundary. Offset and unit edits retain their existing
|
||||
unapplied advance work only for that direction and leaves an applied SLC cap
|
||||
intact; normal current-limit behavior resumes at the boundary. Offset and unit edits retain their existing
|
||||
meaning. A short lower-limit zone can immediately qualify its exit increase;
|
||||
there is no hidden minimum dwell time. GPS uncertainty, missing map data and an
|
||||
unresolved continuation can prevent advance adoption; ordinary current-limit SLC
|
||||
remains the fallback. No accepted SET is rolled back just because data vanishes.
|
||||
remains the fallback. An applied cap is retained when source data vanishes during the engagement.
|
||||
|
||||
On the wire, `slcState.speedLimit` remains the current selected observation.
|
||||
`upcomingSpeedLimit` and `upcomingBoundaryId` identify a qualified early proposal;
|
||||
an early `slcCruiseCommand` must carry the same boundary ID. The boundary record
|
||||
early acceptance remains bound to that encounter. The boundary record
|
||||
is in memory for this runtime session, not a saved decision database.
|
||||
|
||||
Only a finite positive source observation can propose a limit. Missing, stale and
|
||||
unknown observations stay distinct. Source loss retains an engaged manual choice
|
||||
until its original deadline and creates no authority to change set speed. Automatic
|
||||
adoption retains accepted history and does not reapply an unchanged limit on
|
||||
recovery. While the publisher remains fresh, the widget retains the latest
|
||||
received limit, offset and original source with an unavailable status, independently
|
||||
of accepted history. Cruise continues using its current SET, including a driver's
|
||||
manual adjustment. Source loss creates no speed target and cannot select a
|
||||
different Conditional Experimental or Conditional Chill mode. Normal mode triggers
|
||||
and live braking safeguards remain with their existing owners.
|
||||
Only a finite positive qualified observation can propose a new limit. Missing,
|
||||
stale and unknown observations remain distinct. Source loss retains an engaged
|
||||
manual choice and its interaction until the original deadline. Only an explicit
|
||||
Accept can adopt that remembered proposal. Missing data uses the previous accepted limit plus the current offset,
|
||||
bounded by SET. A pending or declined new limit cannot replace that remembered value.
|
||||
Without a previous accepted limit, ordinary SET remains the target.
|
||||
|
||||
Previous accepted limit and its legacy Experimental fallback are retired.
|
||||
`SLCFallback` remains registered without a default solely to preserve existing
|
||||
saved bytes for older versions; current code neither reads nor writes it.
|
||||
Missing, 0, 1, 2 and malformed old values all use the same behavior above.
|
||||
Old Quick Select assignments remain saved but unavailable.
|
||||
The previous accepted limit survives disengagement and runtime restarts. The
|
||||
existing `SLCQualifiedHistory` record stores it with format and schema validation;
|
||||
restoration retains only the value and source label, never an old source episode
|
||||
as fresh evidence. An early preview can cap speed during the drive, but only a
|
||||
qualified current reading can confirm that preview for history. This remembered
|
||||
value may describe a different road. Current
|
||||
vehicle coordinates and active system-longitudinal authority are still required
|
||||
to apply its cap. Disabled SLC and stock longitudinal control apply no cap.
|
||||
Returning to the already accepted numeric limit clears an obsolete confirmation.
|
||||
|
||||
The widget retains the latest received limit, offset and source with an unavailable
|
||||
status. If the active SLC target differs from the displayed limit with its visible
|
||||
offset, the steady sign shows both values and labels the active target SLC.
|
||||
Normal conditional-mode triggers and live braking safeguards retain their
|
||||
owners; missing limit data cannot select an Experimental or Chill fallback mode.
|
||||
|
||||
Previous accepted limit is the fixed fallback. There is no fallback selector or
|
||||
Experimental fallback. Old Quick Select assignments remain saved but unavailable.
|
||||
|
||||
Offsets use only the saved `SLCOffsetSchedule`, with fixed physical speed ranges
|
||||
and adjustments. Without a saved schedule, all seven ranges start at zero and
|
||||
@@ -147,42 +174,49 @@ unit changes. There is no separate offset-adoption step.
|
||||
Converted offsets display as whole mph or km/h, and edits use whole units.
|
||||
Unit switches retain the saved precision so repeated conversions do not drift.
|
||||
|
||||
Offset-only edits preserve accepted and
|
||||
rejected state and renew action identities. A changed offset can update the set
|
||||
speed of the accepted current limit; source loss defers this command until that
|
||||
limit is qualified again.
|
||||
Offset-only edits preserve accepted and rejected state and renew action identities.
|
||||
A changed offset updates the previous accepted SLC cap, including during source loss.
|
||||
Valid source and confirmation edits also preserve the accepted limit and applied
|
||||
cap while renewing pending actions. Disabling SLC still clears that constraint.
|
||||
|
||||
## Set speed and accelerator
|
||||
## SET speed and accelerator
|
||||
|
||||
A qualified manual change to a different set speed marks an override, for increases
|
||||
and decreases. An X crosses the sign while override is active. Returning to the
|
||||
applied set speed clears it, using card's receipt to account for whole-unit rounding. Adopting
|
||||
a new limit also clears it. Adoption retains the ordinary buttons' 0.1 km/h internal
|
||||
precision, including for mph targets. New-limit decisions continue during override.
|
||||
Cruise + and − change SET. Confirmation presses remain consumed through hold and
|
||||
release and do not also apply a software cruise step. Native PCM button effects remain native;
|
||||
StarPilot does not pretend it can suppress the vehicle's own SET adjustment.
|
||||
Native standstill RES takes precedence over confirmation.
|
||||
|
||||
Ordinary software SET adjustments use the saved short-press and held-press intervals
|
||||
on every make, including PCM vehicles while SLC owns SET. Toyota uses Swap Toyota
|
||||
Cruise Steps for both native cruise and software SET: off selects short 1 / hold 5,
|
||||
and on selects short 5 / hold 1. Other makes use CustomCruise and CustomCruiseLong.
|
||||
Confirmation presses and native resume keep their existing meanings; they do not
|
||||
also apply an interval step. Toyota's native cruise ceiling still limits software SET.
|
||||
Software cruise keeps its saved short/held intervals. PCM vehicles retain their
|
||||
native steps, including the Toyota Swap Cruise Steps preference. The dedicated
|
||||
Subaru software-cruise mode remains independent of SLC.
|
||||
The Volt conventional-cruise controller uses the fresh SLC cap beneath SET for its
|
||||
cruise-button speed boundary, with its existing physical-source and timing checks.
|
||||
|
||||
The accelerator temporarily allows higher vehicle speed without changing set speed
|
||||
or creating an SLC override. Adoption and confirmation can update software set
|
||||
speed while accelerator override temporarily suspends actuator output. On release the ordinary planner approaches set speed
|
||||
using the currently selected braking profile. Existing lead and stopping constraints
|
||||
continue to apply.
|
||||
**After an accelerator override** defaults to **Hold the speed you reached**. Pressing
|
||||
the gas above the accepted limit plus offset lets cruise hold the fastest speed you
|
||||
reach, up to SET. **Use your SET speed** targets SET. **Resume the adjusted limit**
|
||||
returns to the accepted limit plus offset on release. The override clears when cruise disengages,
|
||||
the adjusted limit catches up, or the driver explicitly accepts or adopts a limit.
|
||||
After an explicit choice, the current accelerator press cannot start another
|
||||
override. Releasing and pressing again can start one.
|
||||
SET and the accepted limit are separate from the override. Lead, curve and
|
||||
stopping constraints still apply.
|
||||
|
||||
The onroad screen does not offer a separate button to reapply the displayed limit.
|
||||
The configured wheel action to adopt a limit remains available, as do the normal
|
||||
set-speed buttons and confirmation for a different limit.
|
||||
**Override speed limit with +** defaults to Off and is available when StarPilot
|
||||
owns SET. A tap must move the displayed SET from at or below the adjusted limit
|
||||
to above it. Cruise then uses SET until it returns to the adjusted limit, a different
|
||||
limit is accepted, or engagement ends. A display-unit change or a stop while
|
||||
cruise stays engaged preserves an active override. Confirmation, Curve Speed and
|
||||
Force Stop take priority over this gesture.
|
||||
|
||||
With SLC enabled and system longitudinal control available, card owns software set
|
||||
speed even on PCM vehicles; PCM still owns engagement. Native PCM setpoint changes
|
||||
cannot overwrite an adopted software set speed. When SLC is off, the original PCM
|
||||
set-speed path is restored. Stock longitudinal control never gains SLC commands.
|
||||
The configured wheel action can explicitly adopt the displayed current limit. Once
|
||||
the planner approves that specific press, card moves software SET to the approved
|
||||
adjusted limit while cruise is engaged. A newer SET change cancels the write. If
|
||||
card misses the approval frame, only SLC acceptance applies. On Hyundai
|
||||
independent-button cars, cruise MAIN assigned to adopt is no longer also the
|
||||
physical AOL lateral toggle. The onroad screen adds no new control.
|
||||
|
||||
## Source and command qualification
|
||||
## Source and action qualification
|
||||
|
||||
Source selection, registered vehicle capabilities, strict saved-setting decoding,
|
||||
Dashboard parser freshness, Maps road qualification, and Vision clock/inference
|
||||
@@ -192,33 +226,89 @@ sources. See [selection](SELECTION.md), [Maps](MAP_SOURCE.md), and the pinned ca
|
||||
[model provenance](vision/assets/PROVENANCE.md).
|
||||
|
||||
Vision interprets sign numerals using the existing Metric setting: km/h when on,
|
||||
mph when off. The producer converts once to m/s for both UI layouts and cruise SET.
|
||||
mph when off. The producer converts once to m/s for both UI layouts and the SLC cap.
|
||||
Changing Metric clears Vision's retained sign and supporting reads, including
|
||||
in-flight results and cached heartbeats; fresh evidence must confirm the next limit.
|
||||
The current applied SET remains unchanged by this reset.
|
||||
SET and the current engagement's applied cap remain unchanged by this reset.
|
||||
|
||||
Every action binds the current session, decision and presentation. The causal ledger
|
||||
keeps consumed confirmation presses out of the ordinary cruise-button path, through
|
||||
held repeats and release. Accept and Reject cannot follow a replaced source
|
||||
candidate, including a replacement at the same numeric speed in the action's
|
||||
planner cycle. Such a replacement preserves the decision and its deadline but
|
||||
renews its presentation. Card checks active system-longitudinal authority, command
|
||||
and state age, matching presentation and source, expected current set speed, legal
|
||||
speed bounds, and intervening buttons before applying a command once. Dashboard
|
||||
commands also match the local OEM episode. Maps and Vision commands bind the fresh
|
||||
qualified planner presentation. A command receipt distinguishes application from
|
||||
limit acceptance. Rejected or expired commands may retry with current evidence
|
||||
while the accepted limit and expected set speed remain current, with no pending
|
||||
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.
|
||||
keeps consumed confirmation presses out of ordinary software cruise, through held
|
||||
repeats and release. Accept and Reject cannot follow a replaced source candidate.
|
||||
Source recovery keeps the presentation for an unchanged proposal and its original deadline.
|
||||
The planner independently checks longitudinal authority, a valid initialized SET
|
||||
and the numeric cap before applying the lower speed.
|
||||
|
||||
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.
|
||||
`SlcState.hasCeiling` and `effectiveCap` report the active physical-speed constraint;
|
||||
`isLimitingMaxSet` identifies when it is below the driver's ceiling. Observations,
|
||||
accepted limits and the effective constraint remain separate. The existing custom
|
||||
wire layout and historical command fields remain reserved for compatibility; live
|
||||
SLC no longer publishes or consumes `slcCruiseCommand`. Official vehicle schemas
|
||||
are unchanged. Shared curve, conditional-mode and controller messages remain intact.
|
||||
|
||||
Host tests cover decisions, source validation, commands, cruise buttons, profile
|
||||
## Numeric comparison investigation
|
||||
|
||||
Native replay confirms a source-conversion cause of repeated confirmations. This
|
||||
is a synthetic reproduction through real cereal packets, source qualification and
|
||||
the runtime, not a diagnosis of a particular recorded drive.
|
||||
|
||||
The offline generator's [`maps.ParseMaxSpeed`](../../../mapd_repo/maps/generate_offline.go)
|
||||
uses `0.277778 * posted_kph`, while Dashboard decoding and Vision use
|
||||
`posted_kph * (1 / 3.6)`. The generated tile stores this result as Float64.
|
||||
`CurrentWay.EffectiveMaxSpeed` reaches `State.MatchedLimitMps` and `mapdOut` as
|
||||
Float32; the discrepancy survives that conversion. Dashboard and Vision also
|
||||
publish Float32, so ordinary equivalent floating-point arithmetic often collapses
|
||||
to an identical wire value instead of causing a new decision.
|
||||
|
||||
For the same posted **50 km/h**:
|
||||
|
||||
| Reading | Float32 m/s seen by SLC |
|
||||
| --- | --- |
|
||||
| Dashboard using `1 / 3.6` | `13.88888931274414` |
|
||||
| Map using `0.277778` | `13.888899803161621` |
|
||||
| Difference | `0.00001049041748046875` |
|
||||
|
||||
Both display as 50 km/h or 31 mph. Before the repair, switching between these
|
||||
readings could open another confirmation even though the displayed number agreed.
|
||||
StarPilot now checks current and upcoming Maps readings and restored Maps history
|
||||
for the exact legacy Float32 encoding of a whole km/h limit through 250 km/h.
|
||||
Matches are converted to the standard Float32 value before SLC uses them.
|
||||
|
||||
[`test_numeric_decisions.py`](tests/test_numeric_decisions.py) records the
|
||||
following controls against the actual runtime:
|
||||
|
||||
- For 30, 50, 80, 100, 130 and 250 km/h, a switch to the legacy Maps value keeps
|
||||
the accepted cap without another confirmation in either display-unit mode.
|
||||
- Over 40 seconds at 20 Hz, switching Dashboard availability every half-second
|
||||
with the two 50 km/h encodings now preserves one decision and its original
|
||||
timeout. The same replay created 81 decisions before the repair.
|
||||
- Equivalent mph arithmetic and a Float64 `nextafter` difference disappear at
|
||||
the shared Float32 wire boundary and cause no new numerical decision.
|
||||
- A same-number producer restart renews the presentation and retires an old
|
||||
press, while keeping the decision and deadline. A plain source gap preserves
|
||||
the presentation as well and counts toward the original deadline.
|
||||
- Genuine 1 km/h and 1 mph changes remain decisions. In particular, both 40 and
|
||||
41 km/h display as 25 mph, so current display rounding is insufficient to
|
||||
establish that two limits mean the same thing.
|
||||
|
||||
A separate arithmetic sweep of integer 4–250 km/h found distinct Float32 values
|
||||
for every shortened conversion; the largest difference was
|
||||
`0.00006103515625 m/s`. This measures the current converter formula, not installed
|
||||
map coverage or onroad prompt frequency. Conditional speed rules call the same
|
||||
Maps parser. The separate `main.ParseMaxSpeed` helper in `speed_limit.go` has no
|
||||
callers in the reviewed tree and is not the live producer path identified here.
|
||||
|
||||
The reference FrogPilot revision checked on 2026-11-03 was
|
||||
[`98ac7448`](https://github.com/FrogAi/FrogPilot/blob/98ac7448ab542e9fdff72d1ec4d7de2cd31460be/frogpilot/controls/lib/speed_limit_controller.py).
|
||||
Its controller and reviewed callers matched the earlier `bb8ffa7` source bytes.
|
||||
Running its actual decision methods with provider/network work isolated confirmed
|
||||
that its `< 1 m/s` rule updates the target for conversion noise, real 1 km/h and
|
||||
real 1–2 mph changes without another confirmation; a 3 mph change asks. At 20 Hz,
|
||||
the alternating-noise scenario retains its nominal timeout instead of restarting
|
||||
it. These were decision-function checks, not a complete FrogPilot runtime test.
|
||||
|
||||
Repairing the value at SLC input covers existing tiles and saved Maps history.
|
||||
Source identity and driver-action guards still apply after numeric repair.
|
||||
|
||||
Host tests cover decisions, source validation, planner caps, cruise buttons, profile
|
||||
selection and both UI layouts. Synthetic captures and host tests do not establish
|
||||
camera accuracy, device timing under load, or onroad behavior.
|
||||
|
||||
@@ -15,15 +15,16 @@ Geographic, producer-episode and session-value identities pass through unchanged
|
||||
Selection checks their structure but owns no drive session; the acceptance reducer
|
||||
must verify session-value applicability against its current session before use.
|
||||
|
||||
`SelectionPolicy` has an explicit mode, exactly two primary source slots and an
|
||||
online-fallback Boolean. A slot may be empty or repeat another slot. Online is
|
||||
`SelectionPolicy` has an explicit mode, up to three primary source slots and an
|
||||
online-fallback Boolean. The default order is Vision, Maps, then Off. The saved
|
||||
second and third slots can be Off. A slot may repeat another slot. Online is
|
||||
never a primary slot. This module does not activate or translate preferences.
|
||||
|
||||
| Mode | Primary candidates | Choice |
|
||||
| --- | --- | --- |
|
||||
| Ordered | Only the configured slots | First eligible valid observation |
|
||||
| Highest | Dashboard and map; vision only if present in either slot | Highest eligible value |
|
||||
| Lowest | Dashboard and map; vision only if present in either slot | Lowest eligible value |
|
||||
| Highest | Dashboard and map, plus vision if present in a slot | Highest eligible value |
|
||||
| Lowest | Dashboard and map, plus vision if present in a slot | Lowest eligible value |
|
||||
|
||||
Equal extrema choose dashboard, then map, then vision, independent of input map
|
||||
iteration order. The minimum eligible limit is explicitly 1 m/s, inclusive.
|
||||
@@ -42,14 +43,14 @@ If no eligible alternative exists, the result retains unknown or stale state.
|
||||
Unknown takes precedence over stale, which takes precedence over absence; the
|
||||
full per-source statuses remain available. Explicit absence, exclusion below the
|
||||
minimum or an empty configured source set produces an absent result. Acceptance
|
||||
keeps accepted history but supplies no control target during source loss.
|
||||
uses the previous accepted limit during source loss, without inventing a current observation.
|
||||
Malformed input produces `invalid_input`, errors and
|
||||
an unknown observation without selecting a source.
|
||||
|
||||
The tests include composition through the acceptance reducer: accepted 65 mph,
|
||||
proposed 45, driver rejection, then source loss and reconnect. Last accepted
|
||||
history remains 65 for both longitudinal-only and combined authority. Source loss
|
||||
does not change the already applied cruise SET or authorize a new command.
|
||||
does not change the driver's cruise SET or accept the rejected proposal.
|
||||
Lateral-only or stock ownership cannot turn a selected
|
||||
speed into a system longitudinal target. These are
|
||||
deterministic policy tests, not native producer, planner, CAN or vehicle evidence.
|
||||
|
||||
@@ -9,34 +9,25 @@ contains explicit half-open bands with signed m/s offsets;
|
||||
it supplies no settings defaults or source freshness policy. A missing band or
|
||||
unavailable target/pair produces `UNAVAILABLE`; malformed values produce
|
||||
`INVALID`. `SpeedPair` contains measured cluster speed, which may be below raw
|
||||
speed; resolution clamps each delta to `max(cluster - raw, 0)`. A nonvalid
|
||||
resolution with a context is malformed and triggers the override session-reset
|
||||
contract. Only a `VALID` context may enter `overrides.step(domain=...)`.
|
||||
speed; resolution clamps each delta to `max(cluster - raw, 0)`. Only a `VALID`
|
||||
resolution supplies a planner coordinate.
|
||||
|
||||
A finite negative offset that would make the target nonpositive in cluster or
|
||||
planner coordinates is ignored for that limit. The unadjusted accepted limit
|
||||
is used instead; the result stays within the ordinary cruise-speed bounds.
|
||||
Malformed schedules still fail validation.
|
||||
|
||||
Call order is acceptance, domain resolution, override reduction, then
|
||||
`to_planner_coordinate(context, contribution_raw_mps, basis)`. The reducer
|
||||
compares current selected and retained intent against `accepted raw + offset`
|
||||
in cluster coordinates. It keeps retained intent in raw selected-speed units,
|
||||
even when the current selected value later changes for an unrelated reason.
|
||||
The conversion adds the *current qualified selected cluster delta* to that
|
||||
retained raw speed. A current pedal contribution uses the current ego cluster
|
||||
delta. The larger of limit plus offset and override is converted to planner
|
||||
coordinates by subtracting the ego cluster delta exactly once. A nonpositive
|
||||
result gives no coordinate. The coordinate validates the composition; it is not
|
||||
a separate live planner ceiling. Runtime commands use the accepted adjusted
|
||||
cluster target to update Card's SET speed. See the [controller contract](README.md).
|
||||
Call order is acceptance, domain resolution, then
|
||||
`to_planner_coordinate(context)`. The accepted limit plus offset is
|
||||
in cluster coordinates; subtracting the current ego cluster delta once produces
|
||||
the planner cap. A nonpositive result supplies no coordinate. The planner compares
|
||||
this cap with the independent driver's SET and applies the lower target.
|
||||
|
||||
These helpers are pure; the live composition supplies the qualified inputs.
|
||||
They have no Params access, source freshness policy or vehicle output. Current
|
||||
SLC overrides come from qualified SET changes. Accelerator use does not create
|
||||
an SLC override; ordinary actuator gating owns the temporary pedal override.
|
||||
Ordinary SET changes do not supply an override contribution. During source loss,
|
||||
the previous accepted limit uses the current offset and fresh vehicle coordinates.
|
||||
It can be restored from validated saved history, without representing its old
|
||||
source identity as current evidence. Pending and declined proposals cannot replace it.
|
||||
|
||||
The tests compose actual acceptance and causal override decisions, including
|
||||
retained raw intent with a changed current selected value, offset changes,
|
||||
negative offsets, missing coordinates, and one native planner/MPC case. These
|
||||
are deterministic software contracts, not on-road qualification.
|
||||
These helpers remain pure and have no Params, source-freshness or vehicle-output
|
||||
ownership. Tests cover units, signed offsets, malformed and missing coordinates,
|
||||
source-gap continuation, unchanged SET, authority and the native planner.
|
||||
|
||||
@@ -6,9 +6,7 @@ import math
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum
|
||||
|
||||
CONFIRMATION_NS = 10_000_000_000
|
||||
SOURCE_SWITCH_NS = 2_000_000_000
|
||||
MAX_REJECTIONS = 256
|
||||
CONFIRMATION_NS = 30_000_000_000
|
||||
|
||||
|
||||
class ObservationKind(Enum):
|
||||
@@ -69,7 +67,7 @@ class Authority:
|
||||
|
||||
@property
|
||||
def can_control(self) -> bool:
|
||||
"""Permission to adopt a software set speed, including accelerator override."""
|
||||
"""Permission to adopt an SLC limit during a system-longitudinal engagement."""
|
||||
return (self.mode in (Mode.LONGITUDINAL_ONLY, Mode.COMBINED) and
|
||||
self.owner is LongitudinalOwner.SYSTEM and (self.longitudinal_active or self.accelerator_override))
|
||||
|
||||
@@ -92,6 +90,7 @@ class DriverAction:
|
||||
sequence_id: int
|
||||
decision_id: int
|
||||
kind: ActionKind
|
||||
observed_ns: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -131,16 +130,6 @@ class Presentation:
|
||||
candidate: Candidate
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdoptionProposal:
|
||||
session_id: str
|
||||
action_sequence_id: int
|
||||
presentation_id: int
|
||||
accepted: AcceptedLimit
|
||||
reconciliation_speed_mps: float
|
||||
clear_override: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class State:
|
||||
session_id: str
|
||||
@@ -148,18 +137,14 @@ class State:
|
||||
# A historical adoption may match a returned proposal the driver now declines.
|
||||
accepted: AcceptedLimit | None = None
|
||||
pending: PendingDecision | None = None
|
||||
rejected: frozenset[Candidate] = frozenset()
|
||||
rejected: Candidate | None = None
|
||||
last_timestamp_ns: int | None = None
|
||||
timer_running: bool = False
|
||||
last_action_sequence: int = -1
|
||||
next_decision_id: int = 1
|
||||
rejection_capacity_exhausted: bool = False
|
||||
presentation: Presentation | None = None
|
||||
next_presentation_id: int = 1
|
||||
received_speed_mps: float | None = None
|
||||
received_source: str | None = None
|
||||
switch_candidate: Candidate | None = None
|
||||
switch_started_ns: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -172,7 +157,6 @@ class Decision:
|
||||
action_receipt: ActionReceipt | None = None
|
||||
errors: tuple[str, ...] = ()
|
||||
adoption_receipt: ActionReceipt | None = None
|
||||
adoption: AdoptionProposal | None = None
|
||||
authority: Authority | None = None
|
||||
policy: Policy | None = None
|
||||
|
||||
@@ -260,21 +244,12 @@ def _state_errors(state: State) -> list[str]:
|
||||
errors.append("invalid state clock")
|
||||
if state.received_speed_mps is not None and not _speed_valid(state.received_speed_mps):
|
||||
errors.append("invalid received speed")
|
||||
if state.received_source is not None and not _token(state.received_source):
|
||||
errors.append("invalid received source")
|
||||
if state.switch_candidate is not None:
|
||||
if (not _candidate_valid(state.switch_candidate, state.session_id) or state.switch_started_ns is None or not _integer(state.switch_started_ns) or
|
||||
state.last_timestamp_ns is None or state.switch_started_ns > state.last_timestamp_ns):
|
||||
errors.append("invalid source switch")
|
||||
elif state.switch_started_ns is not None:
|
||||
errors.append("source switch lacks candidate")
|
||||
if not _integer(state.last_action_sequence, -1) or not _integer(state.next_decision_id, 1) or not _integer(state.next_presentation_id, 1):
|
||||
errors.append("invalid state sequence")
|
||||
if type(state.timer_running) is not bool or type(state.rejection_capacity_exhausted) is not bool:
|
||||
if type(state.timer_running) is not bool:
|
||||
errors.append("invalid state flags")
|
||||
rejections_valid = (type(state.rejected) is frozenset and len(state.rejected) <= MAX_REJECTIONS and
|
||||
all(_candidate_valid(c, state.session_id) for c in state.rejected))
|
||||
if not rejections_valid:
|
||||
rejection_valid = state.rejected is None or _candidate_valid(state.rejected, state.session_id)
|
||||
if not rejection_valid:
|
||||
errors.append("invalid rejection history")
|
||||
if state.pending is not None:
|
||||
pending = state.pending
|
||||
@@ -284,7 +259,7 @@ def _state_errors(state: State) -> list[str]:
|
||||
pending.candidate.speed_mps != state.received_speed_mps or
|
||||
pending.automatic and state.observation != Observation(ObservationKind.VALID, pending.candidate)):
|
||||
errors.append("invalid pending decision")
|
||||
elif rejections_valid and pending.candidate in state.rejected:
|
||||
elif rejection_valid and state.rejected is not None and pending.candidate.speed_mps == state.rejected.speed_mps:
|
||||
errors.append("pending candidate is already rejected")
|
||||
if state.timer_running and (state.pending is None or state.last_timestamp_ns is None):
|
||||
errors.append("timer lacks pending decision or clock")
|
||||
@@ -293,7 +268,10 @@ def _state_errors(state: State) -> list[str]:
|
||||
if (not isinstance(shown, Presentation) or not _integer(shown.presentation_id, 1) or
|
||||
not _integer(state.next_presentation_id, 1) or shown.presentation_id >= state.next_presentation_id or
|
||||
not _candidate_valid(shown.candidate, state.session_id) or state.last_timestamp_ns is None or
|
||||
state.observation != Observation(ObservationKind.VALID, shown.candidate)):
|
||||
state.observation != Observation(ObservationKind.VALID, shown.candidate) and not (
|
||||
isinstance(state.pending, PendingDecision) and not state.pending.automatic and
|
||||
state.pending.candidate == shown.candidate and isinstance(state.observation, Observation) and
|
||||
state.observation.kind is not ObservationKind.VALID)):
|
||||
errors.append("invalid presentation context")
|
||||
elif isinstance(state.observation, Observation) and state.observation.kind is ObservationKind.VALID:
|
||||
errors.append("valid observation lacks presentation context")
|
||||
@@ -311,7 +289,13 @@ def new_session(session_id: str, accepted: AcceptedLimit | None = None) -> State
|
||||
|
||||
def _action_valid(action: object) -> bool:
|
||||
return (isinstance(action, DriverAction) and _token(action.session_id) and _integer(action.sequence_id) and
|
||||
_integer(action.decision_id, 1) and isinstance(action.kind, ActionKind))
|
||||
_integer(action.decision_id, 1) and isinstance(action.kind, ActionKind) and
|
||||
(action.observed_ns is None or _integer(action.observed_ns, 1)))
|
||||
|
||||
|
||||
def _timely(action: DriverAction, pending: PendingDecision, now_ns: int) -> bool:
|
||||
observed_ns = now_ns if action.observed_ns is None else action.observed_ns
|
||||
return 0 <= pending.active_elapsed_ns - (now_ns - observed_ns) <= CONFIRMATION_NS
|
||||
|
||||
|
||||
def _adopt_valid(adopt: object) -> bool:
|
||||
@@ -330,6 +314,9 @@ def _acknowledge(state: State, action: DriverAction | AdoptRequest | None) -> tu
|
||||
|
||||
|
||||
def _accept(state: State, candidate: Candidate, now_ns: int) -> tuple[State, AcceptedLimit | None]:
|
||||
if state.rejected is not None and (state.accepted is None or state.accepted.candidate.speed_mps != candidate.speed_mps or
|
||||
state.rejected.speed_mps == candidate.speed_mps):
|
||||
state = replace(state, rejected=None)
|
||||
if state.accepted is not None and state.accepted.candidate == candidate and state.pending is None:
|
||||
return replace(state, pending=None, timer_running=False), None
|
||||
accepted = AcceptedLimit(candidate, state.session_id, now_ns)
|
||||
@@ -337,14 +324,12 @@ def _accept(state: State, candidate: Candidate, now_ns: int) -> tuple[State, Acc
|
||||
|
||||
|
||||
def _reject(state: State, candidate: Candidate) -> State:
|
||||
if candidate not in state.rejected and len(state.rejected) == MAX_REJECTIONS:
|
||||
return replace(state, pending=None, timer_running=False, rejection_capacity_exhausted=True)
|
||||
return replace(state, rejected=state.rejected | {candidate}, pending=None, timer_running=False)
|
||||
return replace(state, rejected=candidate, pending=None, timer_running=False)
|
||||
|
||||
|
||||
def step(state: State, observation: Observation, authority: Authority, policy: Policy, *, now_ns: int,
|
||||
action: DriverAction | None = None, adopt: AdoptRequest | None = None, set_speed_matches: bool = False,
|
||||
confirmation_started_ns: int | None = None) -> Decision:
|
||||
confirmation_started_ns: int | None = None, confirmation_grace_ns: int = 0) -> Decision:
|
||||
"""Return a decision, never a cruise command. Invalid input produces no control target."""
|
||||
state_errors = _state_errors(state)
|
||||
if state_errors:
|
||||
@@ -357,7 +342,7 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
errors.append("invalid policy")
|
||||
if not _integer(now_ns) or (state.last_timestamp_ns is not None and now_ns < state.last_timestamp_ns):
|
||||
errors.append("invalid or reversed monotonic clock")
|
||||
if action is not None and not _action_valid(action):
|
||||
if action is not None and (not _action_valid(action) or action.observed_ns is not None and action.observed_ns > now_ns):
|
||||
errors.append("invalid driver action")
|
||||
if adopt is not None and not _adopt_valid(adopt):
|
||||
errors.append("invalid adopt request")
|
||||
@@ -365,6 +350,8 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
errors.append("ambiguous action and adopt request")
|
||||
if type(set_speed_matches) is not bool:
|
||||
errors.append("invalid SET alignment")
|
||||
if not _integer(confirmation_grace_ns):
|
||||
errors.append("invalid confirmation grace")
|
||||
if confirmation_started_ns is not None and (not _integer(confirmation_started_ns) or not _integer(now_ns) or confirmation_started_ns > now_ns):
|
||||
errors.append('invalid confirmation start')
|
||||
if errors:
|
||||
@@ -387,43 +374,47 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
state = replace(state, pending=replace(old.pending,
|
||||
active_elapsed_ns=old.pending.active_elapsed_ns + now_ns - old.last_timestamp_ns))
|
||||
candidate = observation.candidate
|
||||
if candidate is None:
|
||||
if candidate is None and (state.pending is None or state.pending.automatic):
|
||||
state = replace(state, presentation=None)
|
||||
elif old.presentation is None or old.presentation.candidate != candidate:
|
||||
elif candidate is not None and (old.presentation is None or old.presentation.candidate != candidate):
|
||||
state = replace(state, presentation=Presentation(state.next_presentation_id, candidate), next_presentation_id=state.next_presentation_id + 1)
|
||||
|
||||
def finish(basis: str, *, target: float | None = None, history: AcceptedLimit | None = None,
|
||||
adoption: AdoptionProposal | None = None) -> Decision:
|
||||
def finish(basis: str, *, target: float | None = None, history: AcceptedLimit | None = None) -> Decision:
|
||||
nonlocal state
|
||||
if observation.kind is not ObservationKind.VALID and state.pending is None:
|
||||
state = replace(state, presentation=None)
|
||||
final_receipt = replace(receipt, status=basis) if receipt is not None and receipt.status == "seen" else receipt
|
||||
final_adopt_receipt = replace(adopt_receipt, status=basis) if adopt_receipt is not None and adopt_receipt.status == "seen" else adopt_receipt
|
||||
return Decision(state, candidate, target, basis, history, final_receipt, adoption_receipt=final_adopt_receipt,
|
||||
adoption=adoption, authority=authority, policy=policy)
|
||||
authority=authority, policy=policy)
|
||||
|
||||
def previous_target() -> float | None:
|
||||
return state.accepted.candidate.speed_mps if state.accepted is not None and authority.can_control else None
|
||||
|
||||
def unavailable(basis: str) -> Decision:
|
||||
nonlocal state, receipt
|
||||
nonlocal state, receipt, candidate
|
||||
pending = state.pending
|
||||
if (pending is None or pending.automatic or not authority.can_control or
|
||||
not (policy.confirm_lower or policy.confirm_higher)):
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish(basis)
|
||||
# The driver's choice survives missing evidence; its authority does not.
|
||||
return finish(basis, target=previous_target())
|
||||
candidate = pending.candidate
|
||||
# A presented manual choice stays actionable until its original deadline.
|
||||
if (receipt is not None and receipt.status == 'seen' and action is not None and
|
||||
action.kind is ActionKind.REJECT and action.decision_id == pending.decision_id and
|
||||
pending.active_elapsed_ns <= CONFIRMATION_NS):
|
||||
receipt = replace(receipt, consumed=True, status='reject')
|
||||
action.decision_id == pending.decision_id and old.presentation is not None and
|
||||
old.presentation.candidate == pending.candidate and
|
||||
_timely(action, pending, now_ns)):
|
||||
receipt = replace(receipt, consumed=True, status=action.kind.value)
|
||||
if action.kind is ActionKind.ACCEPT:
|
||||
state, history = _accept(state, pending.candidate, now_ns)
|
||||
return finish('driver_accept', target=pending.candidate.speed_mps, history=history)
|
||||
state = _reject(state, pending.candidate)
|
||||
return finish('driver_reject')
|
||||
if pending.active_elapsed_ns >= CONFIRMATION_NS:
|
||||
return finish('driver_reject', target=previous_target())
|
||||
if pending.active_elapsed_ns >= CONFIRMATION_NS + confirmation_grace_ns:
|
||||
state = _reject(state, pending.candidate)
|
||||
return finish('timeout_reject')
|
||||
return finish(basis)
|
||||
return finish('timeout_reject', target=previous_target())
|
||||
return finish(basis, target=previous_target())
|
||||
|
||||
if state.rejection_capacity_exhausted:
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish("session_reset_required")
|
||||
if policy.display_only:
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish("display_only")
|
||||
@@ -435,41 +426,32 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
explicit_adopt = (adopt is not None and adopt_receipt is not None and adopt_receipt.status == 'seen' and
|
||||
old.presentation is not None and old.presentation.presentation_id == adopt.presentation_id and
|
||||
old.presentation.candidate == candidate == adopt.candidate)
|
||||
if (new_limit and state.received_source is not None and candidate.source != state.received_source and
|
||||
authority.can_control and not explicit_adopt and confirmation_started_ns is None):
|
||||
switching = state.switch_candidate
|
||||
if switching is None or (switching.source, switching.speed_mps) != (candidate.source, candidate.speed_mps):
|
||||
state = replace(state, switch_candidate=candidate, switch_started_ns=now_ns)
|
||||
assert state.switch_started_ns is not None
|
||||
if now_ns - state.switch_started_ns < SOURCE_SWITCH_NS:
|
||||
return unavailable('source_switch_pending')
|
||||
state = replace(state, received_source=candidate.source, switch_candidate=None, switch_started_ns=None)
|
||||
if new_limit:
|
||||
state = replace(state, received_speed_mps=candidate.speed_mps, pending=None, rejected=frozenset(), timer_running=False)
|
||||
state = replace(state, received_speed_mps=candidate.speed_mps, pending=None, timer_running=False)
|
||||
if adopt_receipt is not None and adopt_receipt.status == "seen":
|
||||
assert adopt is not None
|
||||
if not explicit_adopt:
|
||||
adopt_receipt = replace(adopt_receipt, status="different_presentation")
|
||||
else:
|
||||
# Recorded for any authority; only an owner of longitudinal control commands a target.
|
||||
state, history = _accept(replace(state, rejected=frozenset(c for c in state.rejected if c.speed_mps != candidate.speed_mps)), candidate, now_ns)
|
||||
# Remember explicit adoption even when longitudinal control is inactive.
|
||||
state, history = _accept(state, candidate, now_ns)
|
||||
assert state.accepted is not None
|
||||
adopt_receipt = replace(adopt_receipt, consumed=True, status="adopt")
|
||||
if not authority.can_control:
|
||||
return finish("driver_adopt", history=history)
|
||||
proposal = AdoptionProposal(state.session_id, adopt.sequence_id, adopt.presentation_id, state.accepted, candidate.speed_mps)
|
||||
return finish("driver_adopt", target=candidate.speed_mps, history=history, adoption=proposal)
|
||||
return finish("driver_adopt", target=candidate.speed_mps, history=history)
|
||||
|
||||
if any(rejected.speed_mps == candidate.speed_mps for rejected in state.rejected):
|
||||
if state.rejected is not None and state.rejected.speed_mps == candidate.speed_mps:
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish("rejected", target=previous_target())
|
||||
|
||||
previous_speed = state.accepted.candidate.speed_mps if state.accepted is not None else None
|
||||
if not new_limit and old.pending is None and candidate.speed_mps == previous_speed:
|
||||
if candidate.speed_mps == previous_speed:
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish("accepted", target=previous_target())
|
||||
confirmation = policy.confirm_higher if previous_speed is None or candidate.speed_mps > previous_speed else (
|
||||
policy.confirm_lower if candidate.speed_mps < previous_speed else policy.confirm_lower or policy.confirm_higher)
|
||||
confirmation = policy.confirm_lower or policy.confirm_higher
|
||||
if previous_speed is not None:
|
||||
confirmation = policy.confirm_higher if candidate.speed_mps > previous_speed else policy.confirm_lower
|
||||
if confirmation and not authority.can_control:
|
||||
state = replace(state, pending=None, timer_running=False)
|
||||
return finish("inactive")
|
||||
@@ -490,8 +472,7 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
target=candidate.speed_mps if authority.can_control else None, history=history)
|
||||
return finish("automatic_pending", target=previous_target())
|
||||
|
||||
if receipt is not None and receipt.status == "seen" and pending.active_elapsed_ns <= CONFIRMATION_NS:
|
||||
assert action is not None
|
||||
if receipt is not None and receipt.status == "seen" and action is not None and _timely(action, pending, now_ns):
|
||||
if action.decision_id != pending.decision_id:
|
||||
receipt = replace(receipt, status="different_decision")
|
||||
elif old.pending is None or old.pending.decision_id != pending.decision_id:
|
||||
@@ -506,14 +487,14 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
state, history = _accept(state, candidate, now_ns)
|
||||
return finish("driver_accept", target=candidate.speed_mps if authority.can_control else None, history=history)
|
||||
state = _reject(state, candidate)
|
||||
return finish("driver_reject", target=None if state.rejection_capacity_exhausted else previous_target())
|
||||
return finish("driver_reject", target=previous_target())
|
||||
|
||||
if pending.active_elapsed_ns >= CONFIRMATION_NS:
|
||||
if pending.active_elapsed_ns >= CONFIRMATION_NS + confirmation_grace_ns:
|
||||
state = _reject(state, candidate)
|
||||
return finish("timeout_reject", target=None if state.rejection_capacity_exhausted else previous_target())
|
||||
return finish("timeout_reject", target=previous_target())
|
||||
|
||||
# An explicit choice above wins; alignment is approval only while we own speed control.
|
||||
if set_speed_matches and authority.can_control:
|
||||
if set_speed_matches and authority.can_control and pending.active_elapsed_ns < CONFIRMATION_NS:
|
||||
state, history = _accept(state, candidate, now_ns)
|
||||
return finish("accepted", target=candidate.speed_mps, history=history)
|
||||
|
||||
|
||||
@@ -13,11 +13,14 @@ class Encounter:
|
||||
speed_mps: float
|
||||
way_id: int
|
||||
started_ns: int | None
|
||||
approach_way_id: int
|
||||
crossed: bool = False
|
||||
declined: bool = False
|
||||
withdrawn: bool = False
|
||||
disabled: bool = False
|
||||
accepted_ns: int | None = None
|
||||
applied_preview: acc.AcceptedLimit | None = None
|
||||
retired: bool = False
|
||||
|
||||
|
||||
class Advance:
|
||||
@@ -25,6 +28,8 @@ class Advance:
|
||||
self.encounter: Encounter | None = None
|
||||
self.boundary_id = ''
|
||||
self.started_ns: int | None = None
|
||||
self.retired_cap: acc.AcceptedLimit | None = None
|
||||
self.retired_pending_speed_mps: float | None = None
|
||||
|
||||
def disable(self):
|
||||
if self.encounter is not None and not self.encounter.crossed:
|
||||
@@ -32,6 +37,8 @@ class Advance:
|
||||
|
||||
def use_current(self, candidate):
|
||||
self.boundary_id, self.started_ns = '', None
|
||||
self.retired_cap = None
|
||||
self.retired_pending_speed_mps = None
|
||||
if self.encounter is not None:
|
||||
self.encounter.disabled = True
|
||||
self.encounter.withdrawn = False
|
||||
@@ -40,41 +47,114 @@ class Advance:
|
||||
self.encounter.accepted_ns = None
|
||||
|
||||
def propose(self, observations, policy, upcoming: Upcoming | None, *, current_way_id: int,
|
||||
seconds: int, speed_mps: float | None, standstill: bool, can_offer: bool,
|
||||
confirmation_active: bool, now_ns: int):
|
||||
higher_seconds: int, lower_seconds: int, speed_mps: float | None, standstill: bool, can_offer: bool,
|
||||
confirmation_active: bool, now_ns: int, pending: acc.PendingDecision | None, live: acc.Candidate | None,
|
||||
current_unresolved: bool):
|
||||
self.boundary_id, self.started_ns = '', None
|
||||
self.retired_cap = None
|
||||
self.retired_pending_speed_mps = None
|
||||
current = observations[sel.Source.MAP].candidate
|
||||
encounter = self.encounter
|
||||
pending_preview = (pending is not None and encounter is not None and pending.candidate.source == 'map' and
|
||||
pending.candidate.speed_mps == encounter.speed_mps)
|
||||
if encounter is not None and current_way_id:
|
||||
if not encounter.crossed and current_way_id == encounter.way_id and current is not None:
|
||||
self.retired_cap = encounter.applied_preview
|
||||
if encounter.retired or current.speed_mps != encounter.speed_mps:
|
||||
if pending_preview:
|
||||
self.retired_pending_speed_mps = encounter.speed_mps
|
||||
self.encounter = None
|
||||
return None
|
||||
encounter.crossed = True
|
||||
elif not encounter.crossed and (
|
||||
current_way_id not in (encounter.approach_way_id, encounter.way_id) or
|
||||
current is not None and not encounter.retired and current.speed_mps not in (encounter.from_mps, encounter.speed_mps)):
|
||||
if (current is not None and upcoming is not None and upcoming.way_id == encounter.way_id and
|
||||
upcoming.observation.candidate.speed_mps == encounter.speed_mps and current.speed_mps == encounter.from_mps):
|
||||
encounter.approach_way_id = current_way_id
|
||||
else:
|
||||
self.retired_cap = encounter.applied_preview
|
||||
if pending_preview:
|
||||
self.retired_pending_speed_mps = encounter.speed_mps
|
||||
if current_way_id == encounter.approach_way_id:
|
||||
encounter.applied_preview = None
|
||||
encounter.retired = True
|
||||
else:
|
||||
self.encounter = None
|
||||
return None
|
||||
elif encounter.crossed and current is not None and current.speed_mps != encounter.speed_mps:
|
||||
self.encounter = encounter = None
|
||||
|
||||
held_wins = False
|
||||
if encounter is not None and not encounter.crossed and (not encounter.retired or encounter.applied_preview is not None):
|
||||
held = (encounter.applied_preview.candidate if encounter.applied_preview is not None else
|
||||
pending.candidate if pending_preview and not encounter.retired else None)
|
||||
if held is not None:
|
||||
# Compare projected Maps candidates without publishing them as fresh observations.
|
||||
comparison = dict(observations)
|
||||
comparison[sel.Source.MAP] = acc.Observation(acc.ObservationKind.VALID, held)
|
||||
fallback = current is None and live is not None and live.speed_mps != encounter.from_mps
|
||||
held_wins = not fallback and sel.select_limit(comparison, policy).selected_source is sel.Source.MAP
|
||||
pending_wins = held_wins
|
||||
if pending_preview and pending.candidate != held:
|
||||
comparison[sel.Source.MAP] = acc.Observation(acc.ObservationKind.VALID, pending.candidate)
|
||||
pending_wins = not fallback and sel.select_limit(comparison, policy).selected_source is sel.Source.MAP
|
||||
if not held_wins:
|
||||
self.retired_cap = encounter.applied_preview
|
||||
encounter.applied_preview = None
|
||||
if not pending_wins:
|
||||
encounter.retired = True
|
||||
if pending_preview and (live is None or live.speed_mps != encounter.speed_mps or held_wins):
|
||||
self.retired_pending_speed_mps = encounter.speed_mps
|
||||
if encounter.retired and not held_wins:
|
||||
return None
|
||||
if encounter is not None and not encounter.declined and encounter.accepted_ns is None:
|
||||
if not confirmation_active:
|
||||
encounter.started_ns = None
|
||||
elif encounter.started_ns is None:
|
||||
encounter.started_ns = now_ns
|
||||
if current is None:
|
||||
if encounter is not None and not encounter.crossed:
|
||||
if encounter is not None and not encounter.crossed and (not encounter.retired or held_wins):
|
||||
return {source: acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
if observation.candidate is not None and observation.candidate.speed_mps == encounter.from_mps
|
||||
else observation for source, observation in observations.items()}
|
||||
return None
|
||||
if encounter is not None:
|
||||
if current.speed_mps == encounter.speed_mps and (encounter.crossed or current_way_id == encounter.way_id):
|
||||
encounter.crossed = True
|
||||
elif current.speed_mps not in (encounter.from_mps, encounter.speed_mps):
|
||||
# A tile refresh can change the boundary ID for the same road and limit.
|
||||
replaced = (encounter is not None and upcoming is not None and
|
||||
(upcoming.way_id != encounter.way_id or upcoming.observation.candidate.speed_mps != encounter.speed_mps))
|
||||
previous = encounter if replaced and not encounter.crossed else None
|
||||
if replaced and not (encounter.crossed or encounter.declined or encounter.disabled or encounter.retired) and encounter.accepted_ns is None:
|
||||
if pending_preview:
|
||||
self.retired_pending_speed_mps = encounter.speed_mps
|
||||
if encounter.applied_preview is None:
|
||||
self.encounter = encounter = None
|
||||
else:
|
||||
encounter.started_ns = None
|
||||
|
||||
seconds = (higher_seconds if upcoming is not None and upcoming.observation.candidate.speed_mps > current.speed_mps
|
||||
else lower_seconds)
|
||||
pending_current = (pending is not None and live is not None and
|
||||
pending.candidate.source == live.source and pending.candidate.speed_mps == live.speed_mps)
|
||||
# shortcut: use fresh GPS distance without propagation; revisit only if replay shows material timing error.
|
||||
eligible = (can_offer and type(seconds) is int and 0 < seconds <= 10 and not standstill and
|
||||
current_first = current_unresolved and (encounter is None or encounter.crossed)
|
||||
eligible = (can_offer and not (pending_current or current_first) and type(seconds) is int and 0 < seconds <= 10 and not standstill and
|
||||
speed_mps is not None and type(speed_mps) in (int, float) and math.isfinite(speed_mps) and speed_mps > 0 and
|
||||
upcoming is not None and upcoming.distance_m <= speed_mps * seconds)
|
||||
if eligible and (encounter is None or upcoming.boundary_id != encounter.boundary_id):
|
||||
proposed = dict(observations)
|
||||
proposed[sel.Source.MAP] = upcoming.observation
|
||||
if sel.select_limit(proposed, policy).selected_source is sel.Source.MAP:
|
||||
assert upcoming.observation.candidate is not None
|
||||
encounter = self.encounter = Encounter(upcoming.boundary_id, current.speed_mps,
|
||||
upcoming.observation.candidate.speed_mps, upcoming.way_id, now_ns)
|
||||
if eligible:
|
||||
if encounter is None or replaced and not (encounter.crossed and pending_preview):
|
||||
proposed = dict(observations)
|
||||
proposed[sel.Source.MAP] = upcoming.observation
|
||||
if sel.select_limit(proposed, policy).selected_source is sel.Source.MAP:
|
||||
assert upcoming.observation.candidate is not None
|
||||
encounter = self.encounter = Encounter(upcoming.boundary_id, current.speed_mps,
|
||||
upcoming.observation.candidate.speed_mps, upcoming.way_id, now_ns, current_way_id,
|
||||
applied_preview=previous.applied_preview if previous is not None else None)
|
||||
elif not encounter.crossed:
|
||||
encounter.boundary_id = upcoming.boundary_id
|
||||
if encounter is None:
|
||||
return None
|
||||
if encounter.retired:
|
||||
return dict.fromkeys(sel.Source, acc.Observation(acc.ObservationKind.UNKNOWN)) if held_wins else None
|
||||
|
||||
proposed = dict(observations)
|
||||
blocked = encounter.declined or encounter.withdrawn
|
||||
@@ -87,15 +167,20 @@ class Advance:
|
||||
if not encounter.disabled and sel.select_limit(observations, policy).selected_source is sel.Source.MAP:
|
||||
self.started_ns = encounter.started_ns
|
||||
return None
|
||||
elif (seconds > 0 and not blocked and not encounter.disabled and upcoming is not None and
|
||||
elif (not blocked and not encounter.disabled and upcoming is not None and
|
||||
not (replaced and encounter is previous) and
|
||||
upcoming.boundary_id == encounter.boundary_id and
|
||||
upcoming.observation.candidate.speed_mps == encounter.speed_mps):
|
||||
proposed[sel.Source.MAP] = upcoming.observation
|
||||
if sel.select_limit(proposed, policy).selected_source is sel.Source.MAP:
|
||||
self.boundary_id, self.started_ns = encounter.boundary_id, encounter.started_ns
|
||||
return proposed
|
||||
else:
|
||||
# Losing a preview must not offer the still-current approach limit again.
|
||||
proposed[sel.Source.MAP] = acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
if held_wins:
|
||||
# A losing rival is neither an adoption candidate nor fresh evidence for a held preview.
|
||||
return dict.fromkeys(sel.Source, acc.Observation(acc.ObservationKind.UNKNOWN))
|
||||
chosen = sel.select_limit(proposed, policy).observation.candidate
|
||||
if not encounter.crossed and chosen is not None and chosen.speed_mps == encounter.from_mps:
|
||||
encounter.withdrawn = True
|
||||
@@ -110,18 +195,22 @@ class Advance:
|
||||
if encounter is None or result.acceptance is None:
|
||||
return
|
||||
decision = result.acceptance
|
||||
accepted = decision.state.accepted
|
||||
if encounter.crossed or accepted != encounter.applied_preview:
|
||||
encounter.applied_preview = None
|
||||
if encounter.retired:
|
||||
return
|
||||
selected = result.selection.observation.candidate if result.selection is not None else None
|
||||
selected = selected or decision.display_candidate
|
||||
if selected is not None and selected.speed_mps == encounter.speed_mps and selected.source == 'map':
|
||||
if decision.basis in ('driver_reject', 'timeout_reject', 'rejected'):
|
||||
encounter.declined = True
|
||||
accepted = decision.state.accepted
|
||||
if accepted is not None and accepted.candidate.speed_mps == encounter.speed_mps and decision.state.pending is None:
|
||||
if encounter.accepted_ns is None:
|
||||
encounter.accepted_ns = accepted.accepted_at_ns
|
||||
if (not encounter.crossed and decision.history_write == accepted and decision.basis != 'driver_adopt'):
|
||||
encounter.applied_preview = accepted
|
||||
elif selected is not None and not encounter.crossed and not encounter.disabled:
|
||||
encounter.withdrawn = True
|
||||
if encounter.accepted_ns is not None and result.state.override.persistent_selected_mps is not None:
|
||||
encounter.withdrawn = True
|
||||
accepted = decision.state.accepted
|
||||
if not encounter.disabled and encounter.accepted_ns is not None and accepted is not None and accepted.accepted_at_ns != encounter.accepted_ns:
|
||||
encounter.withdrawn = True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Pure source, adoption, offset and driver-intent decisions for card's set speed."""
|
||||
"""Qualified SLC decisions supply a planner cap beneath the driver's SET."""
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
@@ -7,9 +7,9 @@ from dataclasses import dataclass, replace
|
||||
from openpilot.starpilot.longitudinal.cruise_ceiling import CruiseCeiling
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import action_arbitration as arb
|
||||
from openpilot.starpilot.speed_limits import overrides as ov
|
||||
from openpilot.starpilot.speed_limits import selection as sel
|
||||
from openpilot.starpilot.speed_limits import speed_domain as sd
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import PedalOverride
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -18,7 +18,6 @@ class HostState:
|
||||
long_control_active: bool
|
||||
selfdrive_enabled: bool
|
||||
force_decel: bool
|
||||
force_stop_active: bool
|
||||
driver_v_cruise_kph: float
|
||||
selected_cluster_kph: float | None
|
||||
|
||||
@@ -39,22 +38,26 @@ class Frame:
|
||||
adopt: acc.AdoptRequest | None = None
|
||||
classified_cruise: arb.Result | None = None
|
||||
is_metric: bool | None = None
|
||||
set_speed_ready: bool = True
|
||||
adoption_observations: Mapping[sel.Source, acc.Observation] | None = None
|
||||
confirmation_started_ns: int | None = None
|
||||
pedal_override: PedalOverride = PedalOverride.OFF
|
||||
preview_reverted: bool = False
|
||||
confirmation_grace_ns: int = 0
|
||||
wheel_override_enabled: bool = False
|
||||
wheel_override_blocked: bool = False
|
||||
wheel_override_since_ns: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class State:
|
||||
session_id: str
|
||||
acceptance: acc.State
|
||||
override: ov.State
|
||||
control_generation: int = 0
|
||||
last_now_ns: int | None = None
|
||||
previous_mode: acc.Mode | None = None
|
||||
previous_owner: acc.LongitudinalOwner | None = None
|
||||
was_active_path: bool = False
|
||||
reset_required: bool = False
|
||||
pedal_target_mps: float = 0.
|
||||
pedal_blocked_until_release: bool = False
|
||||
wheel_override_metric: bool | None = None
|
||||
wheel_override_since_ns: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -63,7 +66,6 @@ class Result:
|
||||
selection: sel.Selection | None = None
|
||||
acceptance: acc.Decision | None = None
|
||||
domain: sd.DomainResolution | None = None
|
||||
override: ov.Decision | None = None
|
||||
coordinate: sd.PlannerCoordinate | None = None
|
||||
ceiling: CruiseCeiling | None = None
|
||||
status: str = "none"
|
||||
@@ -85,36 +87,33 @@ def _number(value: object, *, positive: bool = False) -> bool:
|
||||
|
||||
|
||||
def _state_valid(state: object) -> bool:
|
||||
if not isinstance(state, State) or not isinstance(state.acceptance, acc.State) or not isinstance(state.override, ov.State):
|
||||
if not isinstance(state, State) or not isinstance(state.acceptance, acc.State):
|
||||
return False
|
||||
if (type(state.session_id) is not str or not state.session_id or state.acceptance.session_id != state.session_id or
|
||||
state.override.session_id != state.session_id or not _integer(state.control_generation) or
|
||||
(state.last_now_ns is not None and not _integer(state.last_now_ns)) or
|
||||
type(state.was_active_path) is not bool or type(state.reset_required) is not bool):
|
||||
return False
|
||||
if ((state.previous_mode is None) != (state.previous_owner is None) or
|
||||
(state.previous_mode is not None and not isinstance(state.previous_mode, acc.Mode)) or
|
||||
(state.previous_owner is not None and not isinstance(state.previous_owner, acc.LongitudinalOwner))):
|
||||
return False
|
||||
return True
|
||||
return (type(state.session_id) is str and bool(state.session_id) and state.acceptance.session_id == state.session_id and
|
||||
_number(state.pedal_target_mps) and (state.last_now_ns is None or _integer(state.last_now_ns)) and
|
||||
type(state.reset_required) is bool and type(state.pedal_blocked_until_release) is bool and
|
||||
(state.wheel_override_metric is None or type(state.wheel_override_metric) is bool) and
|
||||
_integer(state.wheel_override_since_ns))
|
||||
|
||||
|
||||
def _host_valid(host: object) -> bool:
|
||||
return (isinstance(host, HostState) and
|
||||
all(type(value) is bool for value in (host.system_long_available, host.long_control_active,
|
||||
host.selfdrive_enabled, host.force_decel, host.force_stop_active)) and
|
||||
host.selfdrive_enabled, host.force_decel)) and
|
||||
_number(host.driver_v_cruise_kph) and
|
||||
(host.selected_cluster_kph is None or _number(host.selected_cluster_kph)))
|
||||
|
||||
|
||||
def _override_evidence(kind: sd.DomainStatus) -> ov.EvidenceKind:
|
||||
return {sd.DomainStatus.VALID: ov.EvidenceKind.VALID,
|
||||
sd.DomainStatus.ABSENT: ov.EvidenceKind.ABSENT,
|
||||
sd.DomainStatus.STALE: ov.EvidenceKind.STALE}.get(kind, ov.EvidenceKind.UNKNOWN)
|
||||
|
||||
|
||||
def new_session(session_id: str, qualified_history: acc.AcceptedLimit | None = None) -> State:
|
||||
return State(session_id, acc.new_session(session_id, qualified_history), ov.new_session(session_id))
|
||||
def new_session(session_id: str, previous_limit: acc.AcceptedLimit | None = None) -> State:
|
||||
if previous_limit is not None:
|
||||
if not isinstance(previous_limit, acc.AcceptedLimit):
|
||||
raise ValueError('invalid previous speed limit')
|
||||
acc.new_session(previous_limit.session_id, previous_limit)
|
||||
# Remember the value without carrying an old source episode into this session.
|
||||
candidate = replace(previous_limit.candidate,
|
||||
observation_identity=acc.ObservationIdentity(acc.IdentityKind.SESSION_VALUE, session_id=session_id))
|
||||
previous_limit = acc.AcceptedLimit(candidate, session_id, 0)
|
||||
return State(session_id, acc.new_session(session_id, previous_limit))
|
||||
|
||||
|
||||
def step(state: State, frame: Frame) -> Result:
|
||||
@@ -126,11 +125,14 @@ def step(state: State, frame: Frame) -> Result:
|
||||
(state.last_now_ns is not None and frame.now_ns < state.last_now_ns) or
|
||||
not _host_valid(frame.host) or not acc.authority_is_valid(frame.authority) or
|
||||
not isinstance(frame.acceptance_policy, acc.Policy) or
|
||||
not isinstance(frame.pedal_override, PedalOverride) or
|
||||
not _integer(frame.wheel_override_since_ns) or
|
||||
any(type(value) is not bool for value in (frame.acceptance_policy.confirm_lower,
|
||||
frame.acceptance_policy.confirm_higher,
|
||||
frame.acceptance_policy.display_only)) or
|
||||
frame.acceptance_policy.display_only,
|
||||
frame.wheel_override_enabled, frame.wheel_override_blocked)) or
|
||||
not isinstance(frame.selected_kind, sd.DomainStatus) or not isinstance(frame.ego_pair, sd.SpeedPair) or
|
||||
(frame.is_metric is not None and type(frame.is_metric) is not bool) or type(frame.set_speed_ready) is not bool):
|
||||
(frame.is_metric is not None and type(frame.is_metric) is not bool)):
|
||||
return Result(replace(state, reset_required=True), status="invalid_frame", errors=("invalid frame or clock",))
|
||||
host = frame.host
|
||||
selected_initialized = host.driver_v_cruise_kph > 0 and host.driver_v_cruise_kph != 255.0
|
||||
@@ -140,13 +142,10 @@ def step(state: State, frame: Frame) -> Result:
|
||||
frame.authority.accelerator_override and not (host.system_long_available and host.selfdrive_enabled and frame.pedal_pressed is True)):
|
||||
return Result(replace(state, last_now_ns=frame.now_ns, reset_required=True),
|
||||
status="host_authority_disagreement", errors=("host and system-long authority disagree",))
|
||||
active_path = (frame.authority.can_control and (host_long or frame.authority.accelerator_override) and selected_initialized and
|
||||
not host.force_decel and not host.force_stop_active and not frame.acceptance_policy.display_only)
|
||||
mode_changed = state.previous_mode is not None and (state.previous_mode != frame.authority.mode or
|
||||
state.previous_owner != frame.authority.owner)
|
||||
generation = state.control_generation + int(active_path and (not state.was_active_path or mode_changed))
|
||||
base = replace(state, control_generation=generation, last_now_ns=frame.now_ns,
|
||||
previous_mode=frame.authority.mode, previous_owner=frame.authority.owner, was_active_path=active_path)
|
||||
engaged_path = (frame.authority.can_control and (host_long or frame.authority.accelerator_override) and
|
||||
selected_initialized and not frame.acceptance_policy.display_only)
|
||||
active_path = engaged_path and not host.force_decel
|
||||
base = replace(state, last_now_ns=frame.now_ns, pedal_target_mps=0., wheel_override_metric=None, wheel_override_since_ns=0)
|
||||
|
||||
if frame.classified_cruise is not None:
|
||||
classified = frame.classified_cruise
|
||||
@@ -166,7 +165,7 @@ def step(state: State, frame: Frame) -> Result:
|
||||
ego = frame.ego_pair
|
||||
candidate = selected.observation.candidate
|
||||
matches_set = False
|
||||
if (active_path and frame.set_speed_ready and candidate is not None and frame.selected_kind is sd.DomainStatus.VALID and
|
||||
if (active_path and candidate is not None and frame.selected_kind is sd.DomainStatus.VALID and
|
||||
ego.status is sd.DomainStatus.VALID and ego.session_id == state.session_id and ego.timestamp_ns == frame.now_ns and
|
||||
_number(ego.raw_mps) and _number(ego.cluster_mps)):
|
||||
assert ego.raw_mps is not None and ego.cluster_mps is not None
|
||||
@@ -176,13 +175,13 @@ def step(state: State, frame: Frame) -> Result:
|
||||
target, host.driver_v_cruise_kph, host.selected_cluster_kph, frame.is_metric)
|
||||
acceptance = acc.step(base.acceptance, selected.observation, frame.authority, frame.acceptance_policy,
|
||||
now_ns=frame.now_ns, action=frame.action, adopt=frame.adopt, set_speed_matches=matches_set,
|
||||
confirmation_started_ns=frame.confirmation_started_ns)
|
||||
confirmation_started_ns=frame.confirmation_started_ns,
|
||||
confirmation_grace_ns=frame.confirmation_grace_ns)
|
||||
base = replace(base, acceptance=acceptance.state)
|
||||
if acceptance.errors:
|
||||
return Result(replace(base, reset_required=True), selected, acceptance,
|
||||
status="invalid_acceptance", errors=acceptance.errors)
|
||||
|
||||
# The raw selected set speed is derived solely from the actual driver vCruise kph.
|
||||
if frame.selected_kind is sd.DomainStatus.VALID and selected_initialized and host.selected_cluster_kph is not None:
|
||||
pair = sd.selected_pair_from_kph(host.driver_v_cruise_kph / 3.6, host.selected_cluster_kph,
|
||||
session_id=state.session_id, timestamp_ns=frame.now_ns)
|
||||
@@ -191,30 +190,62 @@ def step(state: State, frame: Frame) -> Result:
|
||||
else:
|
||||
pair = sd.SpeedPair(sd.DomainStatus.ABSENT)
|
||||
domain = sd.resolve(acceptance, frame.offset_schedule, pair, frame.ego_pair)
|
||||
selected_evidence = ov.SelectedEvidence(_override_evidence(pair.status), pair.raw_mps if pair.status is sd.DomainStatus.VALID else None)
|
||||
pedal_evidence = ov.PedalEvidence(_override_evidence(frame.ego_pair.status),
|
||||
frame.pedal_pressed if frame.ego_pair.status is sd.DomainStatus.VALID else None,
|
||||
frame.ego_pair.raw_mps if frame.ego_pair.status is sd.DomainStatus.VALID else None)
|
||||
change = frame.classified_cruise.change if frame.classified_cruise is not None else None
|
||||
override = ov.step(base.override, acceptance, frame.authority, frame.acceptance_policy,
|
||||
selected_evidence, pedal_evidence, now_ns=frame.now_ns, change=change, domain=domain)
|
||||
base = replace(base, override=override.state)
|
||||
if override.errors or domain.status is sd.DomainStatus.INVALID:
|
||||
return Result(replace(base, reset_required=True), selected, acceptance, domain, override,
|
||||
status="invalid_domain_or_override", errors=override.errors or (domain.reason,))
|
||||
if domain.status is sd.DomainStatus.INVALID:
|
||||
return Result(replace(base, reset_required=True), selected, acceptance, domain,
|
||||
status="invalid_domain", errors=(domain.reason,))
|
||||
clear_override = frame.preview_reverted or acceptance.basis in ('driver_accept', 'driver_adopt')
|
||||
clear_wheel = clear_override or state.acceptance.accepted != acceptance.state.accepted
|
||||
wheel_metric = state.wheel_override_metric if frame.wheel_override_enabled and not clear_wheel else None
|
||||
previous = 0. if clear_override else state.pedal_target_mps
|
||||
pedal_blocked = (state.pedal_blocked_until_release or clear_override) and (
|
||||
frame.pedal_pressed is not False or ego.status is not sd.DomainStatus.VALID)
|
||||
base = replace(base, pedal_blocked_until_release=pedal_blocked)
|
||||
if not active_path or domain.status is not sd.DomainStatus.VALID:
|
||||
return Result(base, selected, acceptance, domain, override,
|
||||
status=acceptance.basis if acceptance.basis == 'source_switch_pending' else 'inactive_or_unavailable')
|
||||
# Longitudinal output can resume one frame after the driver releases the pedal.
|
||||
if (system_mode and host.selfdrive_enabled and not engaged_path and selected_initialized and
|
||||
not (host.force_decel or frame.acceptance_policy.display_only) and
|
||||
ego.status is sd.DomainStatus.VALID and pair.status is sd.DomainStatus.VALID):
|
||||
base = replace(base, pedal_target_mps=previous if frame.pedal_override is not PedalOverride.OFF else 0.,
|
||||
wheel_override_metric=wheel_metric)
|
||||
return Result(base, selected, acceptance, domain, status='inactive_or_unavailable')
|
||||
context = domain.context
|
||||
if (not isinstance(context, sd.DomainContext) or override.state.session_id != state.session_id or
|
||||
override.state.last_timestamp_ns != frame.now_ns or override.state.accepted_mps != context.accepted_raw_mps or
|
||||
override.state.effective_target_mps != context.effective_cluster_mps):
|
||||
return Result(replace(base, reset_required=True), selected, acceptance, domain, override,
|
||||
status="invalid_join", errors=("override and speed domain mismatch",))
|
||||
coordinate = sd.to_planner_coordinate(context, override.contribution_mps, override.basis)
|
||||
assert context is not None
|
||||
selected_cluster = context.selected_raw_mps + context.selected_delta_mps
|
||||
if frame.wheel_override_enabled and frame.is_metric is not None:
|
||||
since = 0 if clear_wheel else state.wheel_override_since_ns
|
||||
pending = acceptance.state.pending
|
||||
ready = (host_long and not frame.pedal_pressed and not frame.wheel_override_blocked and
|
||||
(pending is None or pending.automatic))
|
||||
adjusted_limit = context.effective_cluster_mps
|
||||
change = frame.classified_cruise.change if frame.classified_cruise is not None else None
|
||||
# A tap belongs to the eligibility interval at button-down, even if SET changes later.
|
||||
if (wheel_metric is None and ready and since > 0 and frame.wheel_override_since_ns == since and change is not None and
|
||||
change.disposition is arb.Disposition.DRIVER_INTENT and change.previous_mps is not None and
|
||||
not sd.set_above_limit(change.previous_mps, adjusted_limit, frame.is_metric)):
|
||||
wheel_metric = frame.is_metric
|
||||
# A display-unit edit can round two different speeds to the same number.
|
||||
if wheel_metric is not None and not sd.set_above_limit(selected_cluster, adjusted_limit, wheel_metric):
|
||||
wheel_metric = None
|
||||
base = replace(base, wheel_override_metric=wheel_metric,
|
||||
wheel_override_since_ns=(since or frame.now_ns) if ready else 0)
|
||||
if wheel_metric is not None:
|
||||
coordinate = sd.PlannerCoordinate(None, 'wheel', selected_cluster, context.ego_delta_mps)
|
||||
return Result(base, selected, acceptance, domain, coordinate, status=acceptance.basis)
|
||||
|
||||
pedal_target = 0.
|
||||
if frame.pedal_override is not PedalOverride.OFF:
|
||||
ego_cluster = context.ego_raw_mps + context.ego_delta_mps
|
||||
gas_above_limit = not pedal_blocked and frame.authority.accelerator_override and ego_cluster > context.effective_cluster_mps
|
||||
if gas_above_limit or previous > context.effective_cluster_mps:
|
||||
pedal_target = (selected_cluster if frame.pedal_override is PedalOverride.SET_SPEED else
|
||||
min(selected_cluster, max(previous, ego_cluster if gas_above_limit else 0.)))
|
||||
if pedal_target <= context.effective_cluster_mps:
|
||||
pedal_target = 0.
|
||||
base = replace(base, pedal_target_mps=pedal_target)
|
||||
coordinate = sd.to_planner_coordinate(context, override_mps=pedal_target)
|
||||
if coordinate.cap_mps is None:
|
||||
return Result(replace(base, reset_required=True), selected, acceptance, domain, override, coordinate,
|
||||
return Result(replace(base, reset_required=True), selected, acceptance, domain, coordinate,
|
||||
status="invalid_coordinate", errors=("planner coordinate unavailable",))
|
||||
# Adoption changes card's set speed; SLC never supplies a second planner ceiling.
|
||||
return Result(base, selected, acceptance, domain, override, coordinate,
|
||||
return Result(base, selected, acceptance, domain, coordinate,
|
||||
ceiling=CruiseCeiling(coordinate.cap_mps, frame.authority),
|
||||
status=acceptance.basis)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Versioned accepted-limit persistence with explicit identity applicability."""
|
||||
"""Remember the last accepted limit as a fallback, never as fresh source evidence."""
|
||||
|
||||
import json
|
||||
import math
|
||||
@@ -6,9 +6,11 @@ import math
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.starpilot.schema_cache import schema_fingerprint
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits.map_observation import repair_legacy_map_speed
|
||||
|
||||
FORMAT = "starpilot-slc-accepted"
|
||||
VERSION = 1
|
||||
MAX_BYTES = 16_384
|
||||
|
||||
|
||||
def encode(accepted: acc.AcceptedLimit, saved_wall_ns: int) -> str:
|
||||
@@ -24,16 +26,8 @@ def encode(accepted: acc.AcceptedLimit, saved_wall_ns: int) -> str:
|
||||
"origin_session": accepted.session_id}, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def decode(raw: str | bytes | dict | None, *, session_id: str, current: acc.Observation,
|
||||
now_wall_ns: int, max_age_ns: int) -> acc.AcceptedLimit | None:
|
||||
"""Only durable geographic identity can be carried into a new drive.
|
||||
|
||||
The caller sets an applicability age from its source contract. Dashboard TSR
|
||||
producer episodes are drive-local and cannot be restored across a new session.
|
||||
"""
|
||||
if raw is None or type(session_id) is not str or not session_id or type(now_wall_ns) is not int or type(max_age_ns) is not int:
|
||||
return None
|
||||
if max_age_ns < 0 or now_wall_ns <= 0:
|
||||
def decode(raw: str | bytes | dict | None, *, session_id: str) -> acc.AcceptedLimit | None:
|
||||
if raw is None or type(session_id) is not str or not session_id:
|
||||
return None
|
||||
try:
|
||||
value = raw if isinstance(raw, dict) else json.loads(raw)
|
||||
@@ -41,18 +35,19 @@ def decode(raw: str | bytes | dict | None, *, session_id: str, current: acc.Obse
|
||||
"speed_mps", "identity_kind", "identity_value", "origin_session"} or
|
||||
value["format"] != FORMAT or type(value["version"]) is not int or value["version"] != VERSION or
|
||||
value["source_schema"] != schema_fingerprint(custom.SlcDashboardObservation.schema) or
|
||||
type(value["saved_wall_ns"]) is not int or value["saved_wall_ns"] > now_wall_ns or
|
||||
now_wall_ns - value["saved_wall_ns"] > max_age_ns or
|
||||
value["identity_kind"] != acc.IdentityKind.GEOGRAPHIC.value or
|
||||
not isinstance(value["identity_value"], str) or not value["identity_value"] or
|
||||
type(value["saved_wall_ns"]) is not int or value["saved_wall_ns"] <= 0 or
|
||||
value['source'] not in ('dashboard', 'map', 'vision') or
|
||||
not isinstance(value["origin_session"], str) or not value["origin_session"] or
|
||||
type(value["speed_mps"]) not in (int, float) or not math.isfinite(value["speed_mps"])):
|
||||
return None
|
||||
candidate = acc.Candidate(value["source"], acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value["identity_value"]),
|
||||
float(value["speed_mps"]))
|
||||
if (not acc.observation_is_valid(acc.Observation(acc.ObservationKind.VALID, candidate), session_id=session_id) or
|
||||
current != acc.Observation(acc.ObservationKind.VALID, candidate)):
|
||||
return None
|
||||
return acc.AcceptedLimit(candidate, session_id, 0)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
kind = acc.IdentityKind(value['identity_kind'])
|
||||
identity = acc.ObservationIdentity(kind, value['identity_value'],
|
||||
value['origin_session'] if kind is acc.IdentityKind.SESSION_VALUE else None)
|
||||
candidate = acc.Candidate(value['source'], identity, float(value['speed_mps']))
|
||||
acc.new_session(value['origin_session'], acc.AcceptedLimit(candidate, value['origin_session'], 0))
|
||||
speed = repair_legacy_map_speed(candidate.speed_mps) if candidate.source == 'map' else candidate.speed_mps
|
||||
remembered = acc.Candidate(candidate.source,
|
||||
acc.ObservationIdentity(acc.IdentityKind.SESSION_VALUE, session_id=session_id), speed)
|
||||
return acc.new_session(session_id, acc.AcceptedLimit(remembered, session_id, 0)).accepted
|
||||
except (TypeError, ValueError, OverflowError, RecursionError):
|
||||
return None
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
@@ -9,6 +10,26 @@ from openpilot.starpilot.speed_limits import map_source as ms
|
||||
from openpilot.starpilot.speed_limits.map_source import copy_frame as copy_frame
|
||||
|
||||
|
||||
_LEGACY_KPH_TO_MPS = .277778
|
||||
|
||||
|
||||
def _float32(value: float) -> float:
|
||||
return struct.unpack('f', struct.pack('f', value))[0]
|
||||
|
||||
|
||||
_MAX_LEGACY_SPEED = _float32(250 * _LEGACY_KPH_TO_MPS)
|
||||
|
||||
|
||||
def repair_legacy_map_speed(speed_mps: float) -> float:
|
||||
# shortcut: match the legacy converter through 250 km/h, revisit if higher posted limits need repair.
|
||||
if not 1 <= speed_mps <= _MAX_LEGACY_SPEED:
|
||||
return speed_mps
|
||||
posted_kph = round(speed_mps / _LEGACY_KPH_TO_MPS)
|
||||
if speed_mps == _float32(posted_kph * _LEGACY_KPH_TO_MPS):
|
||||
return _float32(posted_kph / 3.6)
|
||||
return speed_mps
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Upcoming:
|
||||
observation: acc.Observation
|
||||
@@ -39,25 +60,28 @@ class MapSource:
|
||||
if evidence.observation.kind is acc.ObservationKind.STALE:
|
||||
return evidence.observation
|
||||
accepted = self.tracker.diagnostic(now_boot_ns=now_boot_ns).frame
|
||||
if (evidence.kind != 'matched_limit_unqualified' or accepted is None or
|
||||
if (evidence.kind not in ('matched_limit_unqualified', 'matched_no_limit_unqualified') or accepted is None or
|
||||
accepted.way_selection != 'current' or
|
||||
any(type(value) is not str or not 0 < len(value) <= 256 or not value.strip() or
|
||||
any(ord(c) < 32 for c in value) for value in (drive_session, car_fingerprint))):
|
||||
# Missing speed metadata, coverage/GPS loss and ambiguous/lookahead ways
|
||||
# are incomplete evidence, not a new limit or regulatory absence.
|
||||
return acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
self.way_id = accepted.way_id
|
||||
if evidence.kind == 'matched_no_limit_unqualified':
|
||||
return evidence.observation
|
||||
identity = hashlib.sha256(json.dumps((drive_session, car_fingerprint, accepted.producer_session,
|
||||
accepted.generation, accepted.source, accepted.way_id), separators=(',', ':')).encode()).hexdigest()
|
||||
self.way_id = accepted.way_id
|
||||
boundary = accepted.preview_boundary_id
|
||||
speed = repair_legacy_map_speed(accepted.speed_mps)
|
||||
preview_speed = repair_legacy_map_speed(accepted.preview_speed_mps) if type(accepted.preview_speed_mps) is float else 0.
|
||||
if (type(boundary) is str and len(boundary) == 64 and all(c in '0123456789abcdef' for c in boundary) and
|
||||
type(accepted.preview_speed_mps) is float and math.isfinite(accepted.preview_speed_mps) and
|
||||
accepted.preview_speed_mps >= 1.0 and abs(accepted.preview_speed_mps - accepted.speed_mps) > .02 and
|
||||
math.isfinite(preview_speed) and preview_speed >= 1.0 and abs(preview_speed - speed) > .02 and
|
||||
type(accepted.preview_distance_m) is float and math.isfinite(accepted.preview_distance_m) and
|
||||
0 < accepted.preview_distance_m <= 500 and type(accepted.preview_way_id) is int and accepted.preview_way_id > 0):
|
||||
preview_identity = hashlib.sha256(f'{identity}:{boundary}'.encode()).hexdigest()
|
||||
observation = acc.Observation(acc.ObservationKind.VALID, acc.Candidate('map',
|
||||
acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value=preview_identity), accepted.preview_speed_mps))
|
||||
acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value=preview_identity), preview_speed))
|
||||
self.upcoming = Upcoming(observation, accepted.preview_distance_m, boundary, accepted.preview_way_id)
|
||||
return acc.Observation(acc.ObservationKind.VALID, acc.Candidate('map',
|
||||
acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value=identity), accepted.speed_mps))
|
||||
acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value=identity), speed))
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
"""Pure SLC override contribution from qualified driver intent and current pedal evidence."""
|
||||
from typing import cast
|
||||
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum
|
||||
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import action_arbitration as arb
|
||||
from openpilot.starpilot.speed_limits import speed_domain as sd
|
||||
|
||||
SUSPENSION_NS = 750_000_000
|
||||
|
||||
|
||||
class EvidenceKind(Enum):
|
||||
VALID = "valid"
|
||||
ABSENT = "absent"
|
||||
UNKNOWN = "unknown"
|
||||
STALE = "stale"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SelectedEvidence:
|
||||
kind: EvidenceKind
|
||||
speed_mps: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PedalEvidence:
|
||||
kind: EvidenceKind
|
||||
pressed: bool | None = None
|
||||
ego_mps: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Context:
|
||||
context_id: str
|
||||
issued_at_ns: int
|
||||
accepted_target_mps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class State:
|
||||
session_id: str
|
||||
persistent_selected_mps: float | None = None
|
||||
accepted_mps: float | None = None
|
||||
context: Context | None = None
|
||||
next_context_id: int = 1
|
||||
active: bool = False
|
||||
mode: acc.Mode | None = None
|
||||
owner: acc.LongitudinalOwner | None = None
|
||||
suspension_started_ns: int | None = None
|
||||
last_effect_id: int = -1
|
||||
last_adoption_sequence: int = -1
|
||||
last_timestamp_ns: int | None = None
|
||||
reset_required: bool = False
|
||||
effective_target_mps: float | None = None
|
||||
applied_selected_mps: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventReceipt:
|
||||
effect_id: int
|
||||
acknowledged: bool
|
||||
consumed: bool
|
||||
status: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Decision:
|
||||
state: State
|
||||
contribution_mps: float | None = None
|
||||
basis: str = "none"
|
||||
context: Context | None = None
|
||||
event_receipt: EventReceipt | None = None
|
||||
errors: tuple[str, ...] = ()
|
||||
|
||||
|
||||
def _token(value: object) -> bool:
|
||||
return type(value) is str and 0 < len(value) <= 256 and bool(value.strip()) and not any(ord(c) < 32 for c in value)
|
||||
|
||||
|
||||
def _integer(value: object, minimum: int = 0) -> bool:
|
||||
return type(value) is int and value >= minimum
|
||||
|
||||
|
||||
def _speed(value: object) -> bool:
|
||||
if type(value) not in (int, float):
|
||||
return False
|
||||
assert isinstance(value, (int, float))
|
||||
try:
|
||||
return math.isfinite(value) and value > 0
|
||||
except OverflowError:
|
||||
return False
|
||||
|
||||
|
||||
def _ego_speed(value: object) -> bool:
|
||||
if type(value) not in (int, float):
|
||||
return False
|
||||
assert isinstance(value, (int, float))
|
||||
try:
|
||||
return math.isfinite(value) and value >= 0
|
||||
except OverflowError:
|
||||
return False
|
||||
|
||||
|
||||
def _state_valid(state: object) -> bool:
|
||||
if not isinstance(state, State) or not _token(state.session_id):
|
||||
return False
|
||||
if any(v is not None and not _speed(v) for v in (state.persistent_selected_mps, state.accepted_mps,
|
||||
state.effective_target_mps, state.applied_selected_mps)):
|
||||
return False
|
||||
if (not _integer(state.next_context_id, 1) or type(state.active) is not bool or
|
||||
not _integer(state.last_effect_id, -1) or not _integer(state.last_adoption_sequence, -1) or
|
||||
type(state.reset_required) is not bool or
|
||||
any(v is not None and not _integer(v) for v in (state.suspension_started_ns, state.last_timestamp_ns))):
|
||||
return False
|
||||
if (state.mode is not None and not isinstance(state.mode, acc.Mode)) or (state.owner is not None and not isinstance(state.owner, acc.LongitudinalOwner)):
|
||||
return False
|
||||
if (state.mode is None) != (state.owner is None):
|
||||
return False
|
||||
if state.mode is not None and state.last_timestamp_ns is None:
|
||||
return False
|
||||
if state.reset_required and (state.persistent_selected_mps is not None or state.active or state.context is not None or
|
||||
state.suspension_started_ns is not None):
|
||||
return False
|
||||
if state.suspension_started_ns is not None and (state.active or state.last_timestamp_ns is None or
|
||||
state.suspension_started_ns > state.last_timestamp_ns or
|
||||
state.mode not in (acc.Mode.LONGITUDINAL_ONLY, acc.Mode.COMBINED) or
|
||||
state.owner is not acc.LongitudinalOwner.SYSTEM):
|
||||
return False
|
||||
# Retained intent is raw selected speed; a negative offset can make it lower
|
||||
# than the raw accepted limit while still exceeding the effective threshold.
|
||||
if state.context is not None:
|
||||
c = state.context
|
||||
if (not isinstance(c, Context) or not _token(c.context_id) or not _integer(c.issued_at_ns) or
|
||||
not _speed(c.accepted_target_mps) or state.last_timestamp_ns is None or c.issued_at_ns > state.last_timestamp_ns or
|
||||
not state.active or state.accepted_mps != c.accepted_target_mps or state.next_context_id <= 1 or
|
||||
c.context_id != f"override:{state.next_context_id - 1}" or
|
||||
state.mode not in (acc.Mode.LONGITUDINAL_ONLY, acc.Mode.COMBINED) or state.owner is not acc.LongitudinalOwner.SYSTEM):
|
||||
return False
|
||||
if state.active and (state.context is None or state.suspension_started_ns is not None):
|
||||
return False
|
||||
if state.active and state.effective_target_mps is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def new_session(session_id: str) -> State:
|
||||
state = State(session_id)
|
||||
if not _state_valid(state):
|
||||
raise ValueError("invalid session ID")
|
||||
return state
|
||||
|
||||
|
||||
def _selected_valid(selected: object) -> bool:
|
||||
return isinstance(selected, SelectedEvidence) and isinstance(selected.kind, EvidenceKind) and (
|
||||
_speed(selected.speed_mps) if selected.kind is EvidenceKind.VALID else selected.speed_mps is None)
|
||||
|
||||
|
||||
def _pedal_valid(pedal: object) -> bool:
|
||||
return isinstance(pedal, PedalEvidence) and isinstance(pedal.kind, EvidenceKind) and (
|
||||
(type(pedal.pressed) is bool and ((pedal.ego_mps is None or _ego_speed(pedal.ego_mps)) if not pedal.pressed else _ego_speed(pedal.ego_mps)))
|
||||
if pedal.kind is EvidenceKind.VALID else pedal.pressed is None and pedal.ego_mps is None)
|
||||
|
||||
|
||||
def _change_valid(change: object) -> bool:
|
||||
return (isinstance(change, arb.ClassifiedChange) and _token(change.session_id) and _integer(change.effect_id) and
|
||||
(change.action_id is None or _integer(change.action_id)) and
|
||||
(change.context_id is None or _token(change.context_id)) and
|
||||
(change.started_at_ns is None or _integer(change.started_at_ns)) and
|
||||
(change.previous_mps is None or _speed(change.previous_mps)) and _speed(change.selected_mps) and
|
||||
isinstance(change.disposition, arb.Disposition))
|
||||
|
||||
|
||||
def _acceptance_valid(decision: object, authority: object, policy: object, session_id: str, now_ns: int) -> bool:
|
||||
if (not isinstance(decision, acc.Decision) or not isinstance(decision.state, acc.State) or decision.errors or
|
||||
decision.state.session_id != session_id or decision.state.last_timestamp_ns != now_ns or
|
||||
not isinstance(authority, acc.Authority) or not isinstance(policy, acc.Policy) or
|
||||
decision.authority != authority or decision.policy != policy or
|
||||
not acc.observation_is_valid(decision.state.observation, session_id=session_id)):
|
||||
return False
|
||||
if policy.display_only or not authority.can_control:
|
||||
return decision.control_target_mps is None
|
||||
if decision.control_target_mps is None:
|
||||
return True
|
||||
accepted = decision.state.accepted
|
||||
if (accepted is None or not _token(accepted.session_id) or not _speed(accepted.candidate.speed_mps) or
|
||||
decision.control_target_mps != accepted.candidate.speed_mps):
|
||||
return False
|
||||
return decision.state.observation.kind is acc.ObservationKind.VALID
|
||||
|
||||
|
||||
def step(state: State, acceptance: acc.Decision, authority: acc.Authority, policy: acc.Policy,
|
||||
selected: SelectedEvidence, pedal: PedalEvidence, *, now_ns: int,
|
||||
change: arb.ClassifiedChange | None = None, domain: sd.DomainResolution | None = None) -> Decision:
|
||||
"""Propose an SLC contribution, never an actuator or stock-button command."""
|
||||
if not _state_valid(state):
|
||||
return Decision(state, errors=("invalid override state",))
|
||||
if state.reset_required:
|
||||
return Decision(state, basis="session_reset_required", errors=("new session required",))
|
||||
if (not _integer(now_ns) or (state.last_timestamp_ns is not None and now_ns < state.last_timestamp_ns) or
|
||||
not _selected_valid(selected) or not _pedal_valid(pedal) or
|
||||
(change is not None and not _change_valid(change)) or
|
||||
(domain is not None and (not isinstance(domain, sd.DomainResolution) or not isinstance(domain.status, sd.DomainStatus)))):
|
||||
failed = replace(state, persistent_selected_mps=None, context=None, active=False, suspension_started_ns=None,
|
||||
reset_required=True)
|
||||
return Decision(failed, basis="session_reset_required", errors=("invalid evidence or monotonic clock",))
|
||||
old = state
|
||||
state = replace(state, last_timestamp_ns=now_ns)
|
||||
receipt = None
|
||||
if change is not None:
|
||||
if change.session_id != state.session_id:
|
||||
receipt = EventReceipt(change.effect_id, True, False, "foreign_session")
|
||||
elif change.effect_id <= state.last_effect_id:
|
||||
receipt = EventReceipt(change.effect_id, True, False, "already_seen")
|
||||
else:
|
||||
state = replace(state, last_effect_id=change.effect_id)
|
||||
receipt = EventReceipt(change.effect_id, True, False, "ineligible")
|
||||
|
||||
if not _acceptance_valid(acceptance, authority, policy, state.session_id, now_ns):
|
||||
failed = replace(state, persistent_selected_mps=None, active=False, context=None, suspension_started_ns=None,
|
||||
reset_required=True)
|
||||
return Decision(failed, basis="session_reset_required", event_receipt=receipt,
|
||||
errors=("acceptance authority, policy, target or clock mismatch",))
|
||||
|
||||
assert isinstance(acceptance, acc.Decision) and isinstance(acceptance.state, acc.State)
|
||||
assert isinstance(authority, acc.Authority) and isinstance(policy, acc.Policy)
|
||||
accepted = acceptance.state.accepted
|
||||
accepted_speed = accepted.candidate.speed_mps if accepted is not None else None
|
||||
domain_unavailable = False
|
||||
selected_delta = 0.0
|
||||
effective_target = (old.effective_target_mps if acceptance.control_target_mps is None and accepted_speed == old.accepted_mps
|
||||
else acceptance.control_target_mps)
|
||||
if domain is not None:
|
||||
if domain.status is sd.DomainStatus.INVALID:
|
||||
failed = replace(state, persistent_selected_mps=None, active=False, context=None, suspension_started_ns=None,
|
||||
reset_required=True)
|
||||
return Decision(failed, basis="session_reset_required", event_receipt=receipt, errors=("invalid speed domain",))
|
||||
if domain.status is not sd.DomainStatus.VALID:
|
||||
if domain.context is not None:
|
||||
failed = replace(state, persistent_selected_mps=None, active=False, context=None, suspension_started_ns=None,
|
||||
reset_required=True)
|
||||
return Decision(failed, basis="session_reset_required", event_receipt=receipt, errors=("invalid speed domain",))
|
||||
domain_unavailable = True
|
||||
else:
|
||||
c = domain.context
|
||||
if (not isinstance(c, sd.DomainContext) or not sd.context_is_valid(c) or
|
||||
c.session_id != state.session_id or c.timestamp_ns != now_ns or
|
||||
c.accepted_raw_mps != acceptance.control_target_mps or selected.speed_mps != c.selected_raw_mps or
|
||||
(pedal.pressed and pedal.ego_mps != c.ego_raw_mps) or
|
||||
not _speed(c.effective_cluster_mps)):
|
||||
failed = replace(state, persistent_selected_mps=None, active=False, context=None, suspension_started_ns=None,
|
||||
reset_required=True)
|
||||
return Decision(failed, basis="session_reset_required", event_receipt=receipt, errors=("speed domain mismatch",))
|
||||
selected_delta = c.selected_delta_mps
|
||||
effective_target = c.effective_cluster_mps
|
||||
mode_changed = old.mode is not None and (old.mode != authority.mode or old.owner != authority.owner)
|
||||
if acceptance.history_write is not None:
|
||||
state = replace(state, persistent_selected_mps=None, applied_selected_mps=None)
|
||||
comparison_target = state.applied_selected_mps + selected_delta if state.applied_selected_mps is not None else effective_target
|
||||
if (not domain_unavailable and state.persistent_selected_mps is not None and effective_target is not None and
|
||||
selected.speed_mps is not None and math.isclose(cast(float, comparison_target), selected.speed_mps + selected_delta, abs_tol=0.001, rel_tol=0)):
|
||||
state = replace(state, persistent_selected_mps=None)
|
||||
state = replace(state, accepted_mps=accepted_speed, effective_target_mps=effective_target,
|
||||
mode=authority.mode, owner=authority.owner)
|
||||
|
||||
adoption = acceptance.adoption
|
||||
adopted_now = False
|
||||
if adoption is not None and adoption.session_id == state.session_id and adoption.clear_override and acceptance.adoption_receipt is not None:
|
||||
if (acceptance.adoption_receipt.consumed and acceptance.adoption_receipt.sequence_id == adoption.action_sequence_id and
|
||||
adoption.action_sequence_id > state.last_adoption_sequence):
|
||||
state = replace(state, persistent_selected_mps=None, last_adoption_sequence=adoption.action_sequence_id)
|
||||
adopted_now = True
|
||||
|
||||
eligible = (authority.can_control and not policy.display_only and accepted_speed is not None and effective_target is not None and
|
||||
selected.kind is EvidenceKind.VALID and pedal.kind is EvidenceKind.VALID and
|
||||
(not domain_unavailable or acceptance.control_target_mps is None))
|
||||
if not eligible:
|
||||
state = replace(state, active=False, context=None, suspension_started_ns=None)
|
||||
return Decision(state, basis="display_only" if policy.display_only else "inactive_or_missing_evidence", event_receipt=receipt)
|
||||
|
||||
target = accepted_speed
|
||||
assert target is not None and _speed(target) and selected.speed_mps is not None and pedal.pressed is not None
|
||||
state = replace(state, suspension_started_ns=None)
|
||||
rotate = (not old.active or old.context is None or old.context.accepted_target_mps != target or
|
||||
old.effective_target_mps != effective_target or
|
||||
mode_changed or adopted_now)
|
||||
if rotate:
|
||||
context = Context(f"override:{state.next_context_id}", now_ns, target)
|
||||
state = replace(state, context=context, next_context_id=state.next_context_id + 1, active=True)
|
||||
else:
|
||||
state = replace(state, active=True)
|
||||
|
||||
if receipt is not None and receipt.status == "ineligible" and change is not None:
|
||||
if policy.display_only:
|
||||
receipt = replace(receipt, status="display_only")
|
||||
elif (change.disposition is arb.Disposition.DRIVER_INTENT and change.action_id is not None and
|
||||
change.previous_mps is not None and change.previous_mps != change.selected_mps and
|
||||
old.active and old.context is not None and not rotate and change.context_id == old.context.context_id and
|
||||
change.started_at_ns is not None and old.context.issued_at_ns <= change.started_at_ns <= now_ns and
|
||||
change.selected_mps == selected.speed_mps):
|
||||
state = replace(state, persistent_selected_mps=(change.selected_mps if comparison_target is not None and
|
||||
not math.isclose(change.selected_mps + selected_delta, comparison_target, abs_tol=0.001, rel_tol=0) else None))
|
||||
receipt = replace(receipt, consumed=True, status="driver_intent")
|
||||
else:
|
||||
receipt = replace(receipt, status="unqualified_effect")
|
||||
|
||||
if state.persistent_selected_mps is not None:
|
||||
return Decision(state, state.persistent_selected_mps, "persistent", state.context, receipt)
|
||||
return Decision(state, basis="adoption_clear" if adopted_now else "none", context=state.context, event_receipt=receipt)
|
||||
@@ -1,15 +1,15 @@
|
||||
"""Card-side qualification for SLC buttons and selected-speed commands."""
|
||||
"""Card-side qualification for SLC choices, software SET engagement and active caps."""
|
||||
|
||||
import math
|
||||
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.selfdrive.car.cruise import SlcPendingConfirmation, V_CRUISE_MIN, V_CRUISE_MAX
|
||||
from openpilot.selfdrive.car.cruise import SlcPendingConfirmation, SlcWheelContext, V_CRUISE_MIN, V_CRUISE_MAX
|
||||
from openpilot.starpilot.speed_limits.acceptance import CONFIRMATION_NS
|
||||
from openpilot.starpilot.speed_limits.selection import Source
|
||||
from openpilot.starpilot.speed_limits.speed_domain import set_above_limit
|
||||
|
||||
|
||||
STATE_MAX_AGE_NS = int(2e9 / SERVICE_LIST['slcState'].frequency)
|
||||
COMMAND_MAX_AGE_NS = int(2e9 / SERVICE_LIST['slcState'].frequency)
|
||||
|
||||
|
||||
def finite_speed(value: object) -> bool:
|
||||
@@ -30,13 +30,17 @@ def adoption_speed(state):
|
||||
|
||||
|
||||
def source_matches(state, observation, producer_session: str, now_ns: int) -> bool:
|
||||
observed_ns = int(state.sourceObservedMonoTime)
|
||||
expiry_ns = int(state.sourceValidUntilMonoTime)
|
||||
# Card can have a newer packet for the same episode before the planner publishes again.
|
||||
return (str(state.source) == 'dashboard' and str(state.observationKind) == 'valid' and
|
||||
str(state.sourceProducerSessionId) == producer_session and producer_session != '' and
|
||||
getattr(observation.status, 'value', observation.status) == 'valid' and int(state.sourceEpisode) > 0 and
|
||||
int(state.sourceEpisode) == int(observation.episode) and
|
||||
int(state.sourceObservedMonoTime) == int(observation.observed_ns) and
|
||||
int(state.sourceValidUntilMonoTime) == int(observation.valid_until_ns) and
|
||||
0 < int(observation.observed_ns) <= now_ns <= int(observation.valid_until_ns) and
|
||||
0 < observed_ns <= state.frameMonoTime and observed_ns <= observation.observed_ns <= now_ns and
|
||||
observed_ns < expiry_ns and observation.observed_ns < observation.valid_until_ns and
|
||||
now_ns <= min(expiry_ns, observation.valid_until_ns) and
|
||||
(observed_ns != observation.observed_ns or expiry_ns == observation.valid_until_ns) and
|
||||
finite_speed(state.speedLimit) and finite_speed(observation.speed_mps) and
|
||||
abs(float(state.speedLimit) - float(observation.speed_mps)) <= 0.02)
|
||||
|
||||
@@ -60,14 +64,23 @@ def _current_state(sm, now_ns: int):
|
||||
str(state.source) in tuple(Source) and finite_speed(state.speedLimit) and adoption_speed(state) is not None) else None
|
||||
|
||||
|
||||
def state_current(sm, observation, producer_session: str, now_ns: int):
|
||||
state = _current_state(sm, now_ns)
|
||||
if state is None:
|
||||
def active_ceiling(sm, now_ns: int, *, after_ns: int = 0) -> float | None:
|
||||
state = _state(sm, now_ns)
|
||||
if state is None or int(state.frameMonoTime) <= after_ns or not state.hasCeiling:
|
||||
return None
|
||||
if (not getattr(state, 'upcomingBoundaryId', '') and str(state.source) == 'dashboard' and
|
||||
(observation is None or not source_matches(state, observation, producer_session, now_ns))):
|
||||
cap = float(state.effectiveCap)
|
||||
return cap if finite_speed(cap) and cap <= V_CRUISE_MAX / 3.6 + 1e-5 else None
|
||||
|
||||
|
||||
def wheel_override_context(sm, now_ns: int, *, selected_kph: float, is_metric: bool,
|
||||
after_ns: int = 0) -> SlcWheelContext | None:
|
||||
state = _state(sm, now_ns)
|
||||
if (state is None or int(state.frameMonoTime) <= after_ns or not state.hasAcceptedAdjustedLimit or
|
||||
not finite_speed(state.acceptedAdjustedLimit) or not V_CRUISE_MIN <= selected_kph <= V_CRUISE_MAX or
|
||||
set_above_limit(selected_kph / 3.6, float(state.acceptedAdjustedLimit), is_metric)):
|
||||
return None
|
||||
return state
|
||||
since = int(state.wheelOverrideSince)
|
||||
return (str(state.sessionId), since) if 0 < since <= int(state.frameMonoTime) else None
|
||||
|
||||
|
||||
def engagement_state(sm, observation, producer_session: str, now_ns: int, *, after_ns: int = 0):
|
||||
@@ -76,7 +89,7 @@ def engagement_state(sm, observation, producer_session: str, now_ns: int, *, aft
|
||||
str(state.source) == 'online' or not state.hasPresentationAdjustedLimit):
|
||||
return None
|
||||
if str(state.source) == 'dashboard' and (
|
||||
observation is None or observation.observed_ns <= after_ns or not source_matches(state, observation, producer_session, now_ns)):
|
||||
observation is None or int(state.sourceObservedMonoTime) <= after_ns or not source_matches(state, observation, producer_session, now_ns)):
|
||||
return None
|
||||
if str(state.source) == 'vision' and not (
|
||||
state.sourceProducerSessionId and state.sourceEpisode > 0 and
|
||||
@@ -86,58 +99,40 @@ def engagement_state(sm, observation, producer_session: str, now_ns: int, *, aft
|
||||
return state if finite_speed(target) and V_CRUISE_MIN / 3.6 - 1e-5 <= target <= V_CRUISE_MAX / 3.6 + 1e-5 else None
|
||||
|
||||
|
||||
def engagement_target(sm, observation, producer_session: str, now_ns: int, *, after_ns: int = 0):
|
||||
current = engagement_state(sm, observation, producer_session, now_ns, after_ns=after_ns)
|
||||
state = current if current is not None else _state(sm, now_ns)
|
||||
if state is None or int(state.frameMonoTime) <= after_ns:
|
||||
return None, None
|
||||
# A live reading outranks restored history, whose adoption time is zero.
|
||||
if state.hasAccepted and not state.acceptedIsPreview and state.hasAcceptedAdjustedLimit and (
|
||||
current is None or state.acceptedMonoTime > 0 and state.acceptedSpeedLimit != current.speedLimit):
|
||||
# Use the accepted target without confirming a different pending proposal.
|
||||
return float(state.acceptedAdjustedLimit), None
|
||||
return (float(current.presentationAdjustedLimit), current) if current is not None else (None, None)
|
||||
|
||||
|
||||
def adopted_target(sm, now_ns: int, *, wheel_ns: int) -> float | None:
|
||||
"""Read the one-frame SET approval for this Card's still-pending wheel event."""
|
||||
state = _state(sm, now_ns)
|
||||
if (state is None or int(state.wheelAdoptMonoTime) != wheel_ns or
|
||||
not 0 < wheel_ns <= int(state.frameMonoTime)):
|
||||
return None
|
||||
target = float(state.wheelAdoptTargetMps)
|
||||
return target if finite_speed(target) and V_CRUISE_MIN / 3.6 - 1e-5 <= target <= V_CRUISE_MAX / 3.6 + 1e-5 else None
|
||||
|
||||
|
||||
def pending_confirmation(sm, now_ns: int, *, long_active: bool,
|
||||
after_ns: int = 0) -> SlcPendingConfirmation | None:
|
||||
if not long_active:
|
||||
return None
|
||||
# The planner owns pending choices for every source; Dashboard commands also bind local OEM evidence.
|
||||
state = _state(sm, now_ns)
|
||||
if (state is None or int(state.frameMonoTime) <= after_ns or not state.hasPending or state.automaticAdoptionPending or
|
||||
int(state.decisionId) <= 0 or int(state.presentationId) < 0):
|
||||
int(state.decisionId) <= 0 or int(state.presentationId) <= 0):
|
||||
return None
|
||||
if not finite_speed(state.pendingSpeedLimit):
|
||||
return None
|
||||
if int(state.presentationId) == 0:
|
||||
started = int(state.pendingSinceMonoTime)
|
||||
# Consume the displayed choice even if its deadline passes in transit.
|
||||
if str(state.pendingSource) not in tuple(Source) or not 0 < started <= int(state.frameMonoTime) <= started + CONFIRMATION_NS:
|
||||
return None
|
||||
elif (_current_state(sm, now_ns) is None or
|
||||
abs(float(state.pendingSpeedLimit) - adoption_speed(state)) > 0.02):
|
||||
started = int(state.pendingSinceMonoTime)
|
||||
# Use the published prompt and its frame time so a late press cannot change SET.
|
||||
if (str(state.pendingSource) not in tuple(Source) or not finite_speed(state.pendingSpeedLimit) or
|
||||
not 0 < started <= int(state.frameMonoTime) <= started + CONFIRMATION_NS):
|
||||
return None
|
||||
return SlcPendingConfirmation(str(state.sessionId), int(state.decisionId), int(state.presentationId))
|
||||
|
||||
|
||||
def command_applicable(command, state, observation, producer_session: str, now_ns: int,
|
||||
selected_mps: float, *, long_active: bool, button_event: bool) -> bool:
|
||||
if state is None or button_event or not long_active or not state.hasAccepted:
|
||||
return False
|
||||
boundary = str(getattr(state, 'upcomingBoundaryId', ''))
|
||||
speed = adoption_speed(state)
|
||||
if speed is None or str(getattr(command, 'upcomingBoundaryId', '')) != boundary:
|
||||
return False
|
||||
source = 'map' if boundary else str(state.source)
|
||||
producer, episode, observed, valid_until = ('', 0, 0, 0) if boundary else (
|
||||
str(state.sourceProducerSessionId), int(state.sourceEpisode), int(state.sourceObservedMonoTime), int(state.sourceValidUntilMonoTime))
|
||||
if (str(command.kind) != 'adoptAcceptedLimit' or not str(command.sessionId) or
|
||||
str(command.sessionId) != str(state.sessionId) or int(command.commandId) <= 0 or
|
||||
int(command.actionId) < 0 or int(command.presentationId) <= 0 or
|
||||
int(command.commandId) != int(state.commandId) or
|
||||
int(command.presentationId) != int(state.presentationId) or
|
||||
int(command.sourceEpisode) != episode or
|
||||
str(command.source) != source or
|
||||
str(command.sourceProducerSessionId) != producer or
|
||||
int(command.sourceObservedMonoTime) != observed or
|
||||
int(command.sourceValidUntilMonoTime) != valid_until or
|
||||
(source == 'dashboard' and (observation is None or not source_matches(state, observation, producer_session, now_ns)))):
|
||||
return False
|
||||
issued, expires = int(command.issuedMonoTime), int(command.expiresMonoTime)
|
||||
if not (0 < issued <= now_ns <= expires and expires - issued <= COMMAND_MAX_AGE_NS):
|
||||
return False
|
||||
expected, target = float(command.expectedSelectedMps), float(command.targetMps)
|
||||
# Float32 transport can round a legal boundary just outside its double-precision value.
|
||||
return (finite_speed(expected) and finite_speed(target) and finite_speed(selected_mps) and
|
||||
abs(expected - selected_mps) <= 0.1 and
|
||||
abs(float(state.acceptedSpeedLimit) - speed) <= 0.02 and
|
||||
abs(target - (float(state.acceptedSpeedLimit) + float(state.offset))) <= 0.1 and
|
||||
V_CRUISE_MIN / 3.6 - 1e-5 <= target <= V_CRUISE_MAX / 3.6 + 1e-5)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Saved SLC lifecycle and model-frame integration shared by both planners."""
|
||||
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import replace
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.starpilot.feature_runtime import enabled as feature_enabled, slc_runtime_settings, vision_control_enabled
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.speed_limits import history
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime as SlcRuntime
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import read_params as read_slc_settings
|
||||
|
||||
ACTION_MAX_AGE_NS = int(2e9 / SERVICE_LIST['modelV2'].frequency)
|
||||
SLC_ACTIONS = ('accept', 'reject', 'adopt')
|
||||
SLC_CRUISE_EVENTS = ('driverChange', 'confirmationAccept', 'confirmationReject', 'engagementAdopt')
|
||||
|
||||
|
||||
class SavedSlcLifecycle:
|
||||
"""Refresh saved admission; offsets and display units preserve driver choices."""
|
||||
|
||||
def __init__(self, params, cp, environment):
|
||||
self.params, self.cp, self.environment = params, cp, dict(environment)
|
||||
self.runtime = None
|
||||
self.configuration = None
|
||||
self.last_refresh_ns = None
|
||||
|
||||
def source_services(self):
|
||||
services = ['slcDashboardObservation', 'slcVisionObservation', 'mapdOut']
|
||||
if feature_enabled(self.params, self.cp, 'aol', self.environment):
|
||||
services.append('aolAxisState')
|
||||
return services
|
||||
|
||||
def refresh(self, now_ns: int, *, pending_actions=None, pending_cruise=None):
|
||||
if self.last_refresh_ns is not None and 0 <= now_ns - self.last_refresh_ns < 1_000_000_000:
|
||||
return self.runtime, False
|
||||
self.last_refresh_ns = now_ns
|
||||
# One immutable parsed snapshot owns this refresh. Repeated qualification
|
||||
# used to reopen the same saved keys five times in a due model iteration.
|
||||
saved_slc = read_slc_settings(self.params)
|
||||
admitted = feature_enabled(self.params, self.cp, 'slc', self.environment, saved_slc=saved_slc)
|
||||
settings = slc_runtime_settings(self.params, self.cp, self.environment, saved_slc=saved_slc)
|
||||
vision = admitted and feature_enabled(self.params, self.cp, 'vision', self.environment, saved_slc=saved_slc)
|
||||
configuration = (admitted and not settings.errors, settings, vision,
|
||||
vision_control_enabled(self.params, self.cp, saved_slc=saved_slc),
|
||||
feature_enabled(self.params, self.cp, 'vision', {}, saved_slc=saved_slc),
|
||||
self.params.get_bool('IsMetric'))
|
||||
if configuration == self.configuration:
|
||||
return self.runtime, False
|
||||
previous_runtime = self.runtime
|
||||
continuing_control = bool(self.runtime is not None and self.runtime.settings.enabled and settings.enabled and configuration[0])
|
||||
timing_only = False
|
||||
preserve_runtime = continuing_control
|
||||
if self.runtime is not None and self.configuration is not None:
|
||||
old_admitted, old_settings, *old_display = self.configuration
|
||||
retimed = replace(old_settings, advance_higher_seconds=settings.advance_higher_seconds,
|
||||
advance_lower_seconds=settings.advance_lower_seconds)
|
||||
timing_only = configuration == (old_admitted, retimed, *old_display)
|
||||
preserve_runtime |= configuration[:-1] == (old_admitted, replace(retimed, offsets=settings.offsets), *old_display[:-1])
|
||||
if preserve_runtime:
|
||||
assert self.runtime is not None and self.configuration is not None
|
||||
# Policy/source edits renew choices without releasing the engaged speed cap.
|
||||
choices_changed = (settings.selection != self.runtime.settings.selection or
|
||||
settings.acceptance != self.runtime.settings.acceptance or configuration[2:5] != self.configuration[2:5])
|
||||
if choices_changed or settings.offsets != self.runtime.settings.offsets or configuration[5] != self.configuration[5]:
|
||||
self.runtime.update_offsets(settings.offsets, is_metric=configuration[5])
|
||||
if (settings.advance_higher_seconds != self.runtime.settings.advance_higher_seconds or
|
||||
settings.advance_lower_seconds != self.runtime.settings.advance_lower_seconds):
|
||||
self.runtime.update_advance(settings.advance_higher_seconds, settings.advance_lower_seconds)
|
||||
if settings.wheel_override != self.runtime.settings.wheel_override:
|
||||
self.runtime.state = replace(self.runtime.state, wheel_override_metric=None, wheel_override_since_ns=0)
|
||||
self.runtime.settings = settings
|
||||
self.runtime.vision_enabled = vision
|
||||
self.runtime.vision_control_qualified = vision and configuration[3]
|
||||
self.runtime.vision_display_qualified = bool(vision and configuration[4] and settings.display and not settings.enabled)
|
||||
else:
|
||||
if self.runtime is not None:
|
||||
self.runtime.reset()
|
||||
session_id = secrets.token_hex(16)
|
||||
saved_history, _ = read_saved(self.params, 'SLCQualifiedHistory', history.MAX_BYTES)
|
||||
self.runtime = (SlcRuntime(settings, vision_enabled=vision,
|
||||
vision_control_qualified=configuration[3],
|
||||
vision_display_qualified=configuration[4], is_metric=configuration[5],
|
||||
session_id=session_id, previous_limit=history.decode(saved_history, session_id=session_id))
|
||||
if configuration[0] else None)
|
||||
self.configuration = configuration
|
||||
if pending_actions is not None and not timing_only:
|
||||
pending_actions.clear()
|
||||
if pending_cruise is not None and (self.runtime is None or self.runtime is not previous_runtime):
|
||||
pending_cruise.clear()
|
||||
return self.runtime, True
|
||||
|
||||
def step(self, sm, now_ns, pending_actions, pending_cruise, *, drive_changed, wheel_override_blocked=False):
|
||||
runtime = self.runtime
|
||||
if runtime is None:
|
||||
return None
|
||||
if drive_changed or runtime.state.reset_required or runtime.ledger.reset_required:
|
||||
runtime.reset()
|
||||
pending_actions.clear()
|
||||
pending_cruise.clear()
|
||||
while pending_actions and now_ns - pending_actions[0][0] > ACTION_MAX_AGE_NS:
|
||||
pending_actions.popleft()
|
||||
cruise_event = current_cruise_event(pending_cruise, now_ns, car_state_ns=sm.logMonoTime['carState'])
|
||||
request = (pending_actions.popleft()[1] if cruise_event is None and pending_actions and
|
||||
pending_actions[0][0] <= now_ns else None)
|
||||
previous_accepted = runtime.history_limit
|
||||
output = runtime.step(sm, self.cp, now_ns=now_ns, request=request, cruise_event=cruise_event,
|
||||
confirmation_grace_ns=ACTION_MAX_AGE_NS, wheel_override_blocked=wheel_override_blocked)
|
||||
accepted = runtime.history_limit
|
||||
if accepted != previous_accepted:
|
||||
self.params.put("SLCQualifiedHistory", json.loads(history.encode(accepted, time.time_ns())) if accepted is not None else {})
|
||||
return output
|
||||
|
||||
|
||||
def receive_slc_events(sock):
|
||||
for _ in range(8):
|
||||
event = messaging.recv_one_or_none(sock)
|
||||
if event is None:
|
||||
break
|
||||
yield event
|
||||
|
||||
|
||||
def cruise_event_from_message(event):
|
||||
cruise = event.slcCruiseEvent
|
||||
return CruiseEvent(int(cruise.eventId), int(cruise.observedMonoTime), float(cruise.previousMps),
|
||||
float(cruise.selectedMps), str(cruise.button), str(cruise.producerSessionId),
|
||||
int(event.logMonoTime), str(cruise.kind), str(cruise.sessionId), int(cruise.decisionId),
|
||||
int(cruise.presentationId), int(cruise.commandId), int(cruise.wheelOverrideSince), bool(cruise.longPress))
|
||||
|
||||
|
||||
def action_from_message(event):
|
||||
request = event.slcAction
|
||||
observed_ns = int(event.logMonoTime)
|
||||
return observed_ns, Action(str(request.sessionId), int(request.sequenceId), int(request.decisionId),
|
||||
int(request.presentationId), str(request.kind), observed_ns, int(request.wheelAdoptMonoTime))
|
||||
|
||||
|
||||
def current_cruise_event(events, now_ns: int, *, car_state_ns: int | None = None):
|
||||
while events and now_ns - events[0].observed_ns > ACTION_MAX_AGE_NS:
|
||||
events.popleft()
|
||||
if events and events[0].observed_ns <= now_ns and (car_state_ns is None or events[0].car_state_log_ns <= car_state_ns):
|
||||
return events.popleft()
|
||||
return None
|
||||
@@ -8,12 +8,13 @@ import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MIN, V_CRUISE_MAX
|
||||
from openpilot.starpilot.aol.runtime import current_axis
|
||||
from openpilot.starpilot.controllers.wheel_actions import LIFETIME_NS as WHEEL_ACTION_MAX_AGE_NS
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import action_arbitration as arb
|
||||
from openpilot.starpilot.speed_limits import composition as comp
|
||||
from openpilot.starpilot.speed_limits import selection as sel
|
||||
from openpilot.starpilot.speed_limits import speed_domain as sd
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import Settings
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import ADVANCE_SECONDS, Settings
|
||||
from openpilot.starpilot.speed_limits.map_observation import MapSource, copy_frame
|
||||
from openpilot.starpilot.speed_limits.advance import Advance
|
||||
from openpilot.starpilot.speed_limits.vision.observation import clock_pair_ns, vision_observation
|
||||
@@ -84,6 +85,8 @@ class Action:
|
||||
decision_id: int
|
||||
presentation_id: int
|
||||
kind: str
|
||||
observed_ns: int | None = None
|
||||
wheel_observed_ns: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -100,6 +103,8 @@ class CruiseEvent:
|
||||
decision_id: int = 0
|
||||
presentation_id: int = 0
|
||||
command_id: int = 0
|
||||
wheel_override_since_ns: int = 0
|
||||
long_press: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -107,15 +112,14 @@ class Output:
|
||||
result: comp.Result
|
||||
message: object
|
||||
action_result: arb.Result | None = None
|
||||
command: object | None = None
|
||||
settings: Settings | None = None
|
||||
|
||||
|
||||
class Runtime:
|
||||
"""Own a unique drive session, action ledger and current-frame UI receipt."""
|
||||
|
||||
def __init__(self, settings: Settings, *, session_id: str | None = None, vision_enabled: bool = False,
|
||||
vision_control_qualified: bool = False, vision_display_qualified: bool = False, is_metric: bool = False):
|
||||
vision_control_qualified: bool = False, vision_display_qualified: bool = False, is_metric: bool = False,
|
||||
previous_limit: acc.AcceptedLimit | None = None):
|
||||
if type(is_metric) is not bool:
|
||||
raise ValueError('SLC display units must be Boolean')
|
||||
self.settings = settings
|
||||
@@ -133,30 +137,18 @@ class Runtime:
|
||||
self._last_vision_eof_boot = 0
|
||||
self._last_vision_episode = 0
|
||||
self.session_id = session_id or uuid.uuid4().hex
|
||||
self.state = comp.new_session(self.session_id)
|
||||
self.state = comp.new_session(self.session_id, previous_limit)
|
||||
self.history_limit = self.state.acceptance.accepted
|
||||
self.ledger = arb.new_ledger(self.session_id)
|
||||
self.last_action_result: arb.Result | None = None
|
||||
self.last_ui_sequence = -1
|
||||
self.last_cruise_event = -1
|
||||
self.last_cruise_producer: str | None = None
|
||||
self.last_command_id = 0
|
||||
self.command_status = ''
|
||||
self._command_ui_sequence = 0
|
||||
self._ui_receipt = None
|
||||
self.command_issued_ns = 0
|
||||
self.command_expiry_ns = 0
|
||||
self.set_speed_after_ns = 0
|
||||
self.command_expected_mps = 0.0
|
||||
self.command_target_mps = 0.0
|
||||
self.command_card_producer = ''
|
||||
self.commanded_acceptance: acc.AcceptedLimit | None = None
|
||||
self.last_received: acc.Candidate | None = None
|
||||
self.current_presentation: acc.Presentation | None = None
|
||||
self.toyota_eligible = False
|
||||
self.toyota_projection = None
|
||||
self.commanded_generation = -1
|
||||
|
||||
def reset(self) -> None:
|
||||
previous_limit = self.history_limit
|
||||
self.map_source.reset()
|
||||
self.advance = Advance()
|
||||
self.vision_diagnostic = acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
@@ -165,53 +157,39 @@ class Runtime:
|
||||
self._last_vision_eof_boot = 0
|
||||
self._last_vision_episode = 0
|
||||
self.session_id = uuid.uuid4().hex
|
||||
self.state = comp.new_session(self.session_id)
|
||||
try:
|
||||
self.state = comp.new_session(self.session_id, previous_limit)
|
||||
except ValueError:
|
||||
self.state = comp.new_session(self.session_id)
|
||||
self.history_limit = self.state.acceptance.accepted
|
||||
self.ledger = arb.new_ledger(self.session_id)
|
||||
self.last_action_result = None
|
||||
self.last_ui_sequence = -1
|
||||
self.last_cruise_event = -1
|
||||
self.last_cruise_producer = None
|
||||
# Keep the SET deadline: the previous session's command may still be in flight.
|
||||
self.last_command_id = 0
|
||||
self.command_status = ''
|
||||
self._command_ui_sequence = 0
|
||||
self._ui_receipt = None
|
||||
self.command_issued_ns = 0
|
||||
self.command_expiry_ns = 0
|
||||
self.command_expected_mps = 0.0
|
||||
self.command_target_mps = 0.0
|
||||
self.command_card_producer = ''
|
||||
self.commanded_acceptance = None
|
||||
self.last_received = None
|
||||
self.current_presentation = None
|
||||
self.toyota_projection = None
|
||||
self.commanded_generation = -1
|
||||
|
||||
def update_offsets(self, offsets: sd.OffsetSchedule, *, is_metric: bool | None = None) -> None:
|
||||
if is_metric is not None:
|
||||
if type(is_metric) is not bool:
|
||||
raise ValueError('SLC display units must be Boolean')
|
||||
self.is_metric = is_metric
|
||||
changed_offsets = offsets != self.settings.offsets
|
||||
self.settings = replace(self.settings, offsets=offsets)
|
||||
# Renew actions for new offsets/units without forgetting driver choices.
|
||||
acceptance = replace(self.state.acceptance, observation=acc.Observation(acc.ObservationKind.UNKNOWN),
|
||||
pending=None, presentation=None, timer_running=False)
|
||||
self.current_presentation = None
|
||||
self.state = replace(self.state, acceptance=acceptance)
|
||||
self.state = replace(self.state, acceptance=acceptance, wheel_override_since_ns=0)
|
||||
self._ui_receipt = None
|
||||
# Display units alone must not reapply an accepted SET in different rounding.
|
||||
if changed_offsets:
|
||||
self.command_status = ''
|
||||
self.commanded_acceptance = None
|
||||
|
||||
def update_advance(self, seconds: int) -> None:
|
||||
if type(seconds) is not int or not 0 <= seconds <= 10:
|
||||
def update_advance(self, higher_seconds: int, lower_seconds: int) -> None:
|
||||
if any(type(seconds) is not int or seconds not in ADVANCE_SECONDS for seconds in (higher_seconds, lower_seconds)):
|
||||
raise ValueError('invalid SLC advance time')
|
||||
self.settings = replace(self.settings, advance_seconds=seconds)
|
||||
if seconds == 0:
|
||||
encounter, pending = self.advance.encounter, self.state.acceptance.pending
|
||||
if (encounter is not None and not encounter.crossed and pending is not None and
|
||||
self.settings = replace(self.settings, advance_higher_seconds=higher_seconds, advance_lower_seconds=lower_seconds)
|
||||
encounter, pending = self.advance.encounter, self.state.acceptance.pending
|
||||
if encounter is not None and (higher_seconds if encounter.speed_mps > encounter.from_mps else lower_seconds) == 0:
|
||||
if (not encounter.crossed and pending is not None and
|
||||
pending.candidate.source == 'map' and pending.candidate.speed_mps == encounter.speed_mps):
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance,
|
||||
observation=acc.Observation(acc.ObservationKind.UNKNOWN), pending=None, presentation=None, timer_running=False))
|
||||
@@ -237,11 +215,7 @@ class Runtime:
|
||||
return None
|
||||
self._mark_event(event)
|
||||
action_id = self.ledger.last_action_id + 1
|
||||
context = self.state.override.context
|
||||
if context is not None and context.issued_at_ns > event.observed_ns:
|
||||
return None
|
||||
context_id = context.context_id if context is not None else 'no-slc-context'
|
||||
begun = arb.step(self.ledger, arb.BeginAction(self.session_id, action_id, arb.Origin.DRIVER_CRUISE, context_id), now_ns=now_ns)
|
||||
begun = arb.step(self.ledger, arb.BeginAction(self.session_id, action_id, arb.Origin.DRIVER_CRUISE, 'driver-set'), now_ns=now_ns)
|
||||
self.ledger = begun.state
|
||||
if begun.errors or begun.status != 'begun':
|
||||
return None
|
||||
@@ -267,13 +241,11 @@ class Runtime:
|
||||
engagement = event.kind == 'engagementAdopt'
|
||||
pending = self.state.acceptance.pending
|
||||
shown = self.state.acceptance.presentation
|
||||
retained_reject = (event.kind == 'confirmationReject' and event.presentation_id == 0 and
|
||||
pending is not None and event.decision_id == pending.decision_id)
|
||||
if shown is None and engagement:
|
||||
shown = self.current_presentation
|
||||
if (event.session_id != self.session_id or (shown is None and not retained_reject) or
|
||||
if (event.session_id != self.session_id or shown is None or
|
||||
event.producer_session != str(sm['slcDashboardObservation'].producerSessionId) or
|
||||
(not retained_reject and event.presentation_id != shown.presentation_id) or
|
||||
event.presentation_id != shown.presentation_id or
|
||||
event.button != ('accel' if event.kind == 'confirmationAccept' else 'decel')):
|
||||
return None, None, None
|
||||
if engagement:
|
||||
@@ -281,7 +253,7 @@ class Runtime:
|
||||
not _number(float(sm['carState'].vCruise), positive=True) or
|
||||
abs(float(sm['carState'].vCruise) / 3.6 - event.selected_mps) > 0.1):
|
||||
return None, None, None
|
||||
elif pending is None or event.decision_id != pending.decision_id or (not retained_reject and pending.candidate != shown.candidate):
|
||||
elif pending is None or event.decision_id != pending.decision_id or pending.candidate != shown.candidate:
|
||||
return None, None, None
|
||||
action_id = self.ledger.last_action_id + 1
|
||||
context = f'presentation:{shown.presentation_id}' if engagement else f'pending:{pending.decision_id}'
|
||||
@@ -293,33 +265,12 @@ class Runtime:
|
||||
return None, acc.AdoptRequest(self.session_id, action_id, shown.presentation_id, shown.candidate), action_id
|
||||
assert pending is not None
|
||||
kind = acc.ActionKind.ACCEPT if event.kind == 'confirmationAccept' else acc.ActionKind.REJECT
|
||||
return acc.DriverAction(self.session_id, action_id, pending.decision_id, kind), None, action_id
|
||||
return acc.DriverAction(self.session_id, action_id, pending.decision_id, kind, event.observed_ns), None, action_id
|
||||
|
||||
def _command_receipt(self, event: CruiseEvent, sm, now_ns: int) -> None:
|
||||
if (event.kind not in ('commandApplied', 'commandRejected') or
|
||||
not self._event_current(event, sm, now_ns)):
|
||||
return
|
||||
self._mark_event(event)
|
||||
if (event.session_id == self.session_id and event.command_id == self.last_command_id and
|
||||
event.producer_session == self.command_card_producer and self.command_status in ('issued', 'expired') and
|
||||
self.command_issued_ns <= event.observed_ns <= self.command_expiry_ns):
|
||||
if event.kind == 'commandApplied':
|
||||
if (not _number(event.previous_mps, positive=True) or
|
||||
abs(event.previous_mps - self.command_expected_mps) > 0.1 or
|
||||
not _number(event.selected_mps, positive=True) or
|
||||
abs(event.selected_mps - self.command_target_mps) > 0.5):
|
||||
return
|
||||
self.command_status = 'applied'
|
||||
self.set_speed_after_ns = event.observed_ns
|
||||
if self.commanded_acceptance == self.state.acceptance.accepted:
|
||||
self.state = replace(self.state, override=replace(self.state.override, applied_selected_mps=event.selected_mps))
|
||||
else:
|
||||
self.command_status = 'rejected'
|
||||
self.set_speed_after_ns = event.observed_ns
|
||||
|
||||
def _set_speed_ready(self, sm) -> bool:
|
||||
# A prior command may apply after the SET sample, even if its receipt has arrived.
|
||||
return int(sm.logMonoTime.get('carState', 0)) > self.set_speed_after_ns
|
||||
@property
|
||||
def _maps_selected(self) -> bool:
|
||||
policy = self.settings.selection
|
||||
return sel.Source.MAP in policy.slots or policy.mode is not sel.SelectionMode.ORDERED
|
||||
|
||||
def _observations(self, sm, CP, now_ns: int, now_boot_ns: int | None = None) -> dict[sel.Source, acc.Observation]:
|
||||
unknown = acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
@@ -362,8 +313,7 @@ class Runtime:
|
||||
self.vision_diagnostic = acc.Observation(acc.ObservationKind.STALE)
|
||||
else:
|
||||
self.vision_diagnostic = acc.Observation(acc.ObservationKind.UNKNOWN)
|
||||
if (sel.Source.MAP in self.settings.selection.slots or
|
||||
self.settings.selection.mode is not sel.SelectionMode.ORDERED):
|
||||
if self._maps_selected:
|
||||
pair = clock_pair_ns() if now_boot_ns is None else (now_ns, now_boot_ns)
|
||||
if pair is not None:
|
||||
received = bool(sm.updated.get('mapdOut', False))
|
||||
@@ -382,7 +332,7 @@ class Runtime:
|
||||
return values
|
||||
|
||||
def _frame(self, sm, CP, now_ns: int, now_boot_ns: int | None, action: acc.DriverAction | None, adopt: acc.AdoptRequest | None,
|
||||
classified: arb.Result | None) -> comp.Frame:
|
||||
classified: arb.Result | None, *, wheel_override_blocked: bool) -> comp.Frame:
|
||||
car_fresh = _fresh(sm, "carState", now_ns) and not sm["carState"].canTimeout
|
||||
controls_fresh = _fresh(sm, "controlsState", now_ns)
|
||||
selfdrive_fresh = _fresh(sm, "selfdriveState", now_ns)
|
||||
@@ -398,6 +348,7 @@ class Runtime:
|
||||
long_enabled = long_enabled and bool(axis is not None and axis.nativeAcknowledged and axis.desiredLongitudinal)
|
||||
lateral = bool(car_control_fresh and sm['carControl'].latActive and selfdrive_fresh)
|
||||
authority = _authority(CP, sm, host_long, lateral, long_enabled=long_enabled)
|
||||
# Native pedal handoff keeps the engagement while the driver controls acceleration.
|
||||
authority = replace(authority, accelerator_override=bool(CP.openpilotLongitudinalControl and long_enabled and car_state.gasPressed))
|
||||
selected = float(car_state.vCruise) if car_fresh and _number(car_state.vCruise) else 0.0
|
||||
selected_cluster = float(car_state.vCruiseCluster) if car_fresh and _number(car_state.vCruiseCluster) else None
|
||||
@@ -405,31 +356,67 @@ class Runtime:
|
||||
ego = (sd.SpeedPair(sd.DomainStatus.VALID, float(car_state.vEgo), float(car_state.vEgoCluster), self.session_id, now_ns)
|
||||
if ego_valid else sd.SpeedPair(sd.DomainStatus.STALE if sm.logMonoTime.get("carState", 0) else sd.DomainStatus.UNKNOWN))
|
||||
host = comp.HostState(bool(CP.openpilotLongitudinalControl), host_long, bool(selfdrive.enabled and selfdrive_fresh),
|
||||
bool(controls.forceDecel) if controls_fresh else True, False, selected, selected_cluster)
|
||||
bool(controls.forceDecel) if controls_fresh else True, selected, selected_cluster)
|
||||
observations = self._observations(sm, CP, now_ns, now_boot_ns)
|
||||
wheel_enabled = (self.settings.wheel_override and not CP.pcmCruise and car_fresh and car_state.canValid and
|
||||
V_CRUISE_MIN <= selected <= V_CRUISE_MAX and selected_cluster == selected and
|
||||
not car_state.brakePressed)
|
||||
wheel_blocked = wheel_override_blocked or car_state.standstill or car_state.cruiseState.standstill
|
||||
frame = comp.Frame(now_ns, observations, self.settings.selection, authority,
|
||||
self.settings.acceptance, self.settings.offsets,
|
||||
sd.DomainStatus.VALID if car_fresh and selected_cluster is not None else sd.DomainStatus.STALE,
|
||||
ego, bool(car_state.gasPressed) if car_fresh else False, host, action=action, adopt=adopt,
|
||||
classified_cruise=classified, is_metric=self.is_metric, set_speed_ready=self._set_speed_ready(sm))
|
||||
classified_cruise=classified, is_metric=self.is_metric, pedal_override=self.settings.pedal_override,
|
||||
wheel_override_enabled=wheel_enabled, wheel_override_blocked=wheel_blocked)
|
||||
maps_selected = self._maps_selected
|
||||
encounter = self.advance.encounter
|
||||
if not maps_selected and encounter is None:
|
||||
return frame
|
||||
current = sel.select_limit(observations, self.settings.selection).observation.candidate
|
||||
accepted = self.state.acceptance.accepted
|
||||
# Confirm the old cap before a short zone can offer its exit preview.
|
||||
if (encounter is not None and accepted is not None and accepted == encounter.applied_preview and
|
||||
current is not None and current.speed_mps == accepted.candidate.speed_mps):
|
||||
self.history_limit = accepted
|
||||
shown = self.state.acceptance.presentation
|
||||
if self.advance.encounter is not None and shown is not None and current != self.last_received:
|
||||
if encounter is not None and shown is not None and current is not None and current != self.last_received:
|
||||
identity = self.state.acceptance.next_presentation_id
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance,
|
||||
presentation=acc.Presentation(identity, shown.candidate), next_presentation_id=identity+1))
|
||||
if (adopt is not None and self.advance.encounter is not None and
|
||||
if not maps_selected:
|
||||
self.advance = Advance()
|
||||
self.map_source.reset()
|
||||
self.current_presentation = None
|
||||
self.advance.retired_cap = encounter.applied_preview
|
||||
self.advance.retired_pending_speed_mps = encounter.speed_mps
|
||||
proposals = None
|
||||
elif (adopt is not None and encounter is not None and
|
||||
adopt.candidate == self.last_received == current):
|
||||
self.advance.use_current(adopt.candidate)
|
||||
proposals = None
|
||||
else:
|
||||
rejected = self.state.acceptance.rejected
|
||||
# Give the current Maps limit its own decision before previewing the exit.
|
||||
current_unresolved = (current is not None and current.source == 'map' and current.speed_mps not in (
|
||||
accepted.candidate.speed_mps if accepted is not None else None,
|
||||
rejected.speed_mps if rejected is not None else None))
|
||||
proposals = self.advance.propose(observations, self.settings.selection, self.map_source.upcoming,
|
||||
current_way_id=self.map_source.way_id, seconds=self.settings.advance_seconds,
|
||||
current_way_id=self.map_source.way_id, higher_seconds=self.settings.advance_higher_seconds,
|
||||
lower_seconds=self.settings.advance_lower_seconds,
|
||||
speed_mps=float(car_state.vEgo) if ego_valid else None,
|
||||
standstill=bool(getattr(car_state, 'standstill', False)) or str(getattr(car_state, 'gearShifter', '')) == 'reverse',
|
||||
can_offer=self.settings.enabled and authority.can_control and not host.force_decel,
|
||||
confirmation_active=authority.can_control, now_ns=now_ns)
|
||||
return replace(frame, adoption_observations=proposals, confirmation_started_ns=self.advance.started_ns)
|
||||
confirmation_active=authority.can_control, now_ns=now_ns, pending=self.state.acceptance.pending, live=current,
|
||||
current_unresolved=current_unresolved)
|
||||
pending = self.state.acceptance.pending
|
||||
if pending is not None and pending.candidate.source == 'map' and pending.candidate.speed_mps == self.advance.retired_pending_speed_mps:
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance,
|
||||
observation=acc.Observation(acc.ObservationKind.UNKNOWN), pending=None, presentation=None, timer_running=False))
|
||||
preview_reverted = accepted is not None and accepted == self.advance.retired_cap and accepted != self.history_limit
|
||||
if preview_reverted:
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance, accepted=self.history_limit))
|
||||
return replace(frame, adoption_observations=proposals, confirmation_started_ns=self.advance.started_ns,
|
||||
preview_reverted=preview_reverted)
|
||||
|
||||
def _action(self, request: Action | None, now_ns: int) -> tuple[acc.DriverAction | None, acc.AdoptRequest | None, int | None]:
|
||||
if request is None or request.session_id != self.session_id or request.kind not in ("accept", "reject", "adopt"):
|
||||
@@ -440,15 +427,10 @@ class Runtime:
|
||||
shown = self.state.acceptance.presentation
|
||||
if shown is None and request.kind == 'adopt':
|
||||
shown = self.current_presentation
|
||||
pending = self.state.acceptance.pending
|
||||
retained_reject = (request.kind == 'reject' and request.presentation_id == 0 and
|
||||
pending is not None and request.decision_id == pending.decision_id)
|
||||
if not retained_reject and (shown is None or shown.presentation_id != request.presentation_id):
|
||||
if shown is None or shown.presentation_id != request.presentation_id:
|
||||
return None, None, None
|
||||
action_id = self.ledger.last_action_id + 1
|
||||
context = self.state.override.context
|
||||
context_id = context.context_id if context is not None else "slc-presentation"
|
||||
begun = arb.step(self.ledger, arb.BeginAction(self.session_id, action_id, arb.Origin.DRIVER_OTHER, context_id),
|
||||
begun = arb.step(self.ledger, arb.BeginAction(self.session_id, action_id, arb.Origin.DRIVER_OTHER, 'slc-presentation'),
|
||||
now_ns=now_ns)
|
||||
self.ledger = begun.state
|
||||
if begun.errors:
|
||||
@@ -457,33 +439,44 @@ class Runtime:
|
||||
assert shown is not None
|
||||
return None, acc.AdoptRequest(self.session_id, action_id, request.presentation_id, shown.candidate), action_id
|
||||
kind = acc.ActionKind.ACCEPT if request.kind == "accept" else acc.ActionKind.REJECT
|
||||
return acc.DriverAction(self.session_id, action_id, request.decision_id, kind), None, action_id
|
||||
return acc.DriverAction(self.session_id, action_id, request.decision_id, kind, request.observed_ns), None, action_id
|
||||
|
||||
def step(self, sm, CP, *, now_ns: int, now_boot_ns: int | None = None, request: Action | None = None,
|
||||
cruise_event: CruiseEvent | None = None) -> Output:
|
||||
from openpilot.starpilot.speed_limits.toyota import eligible, projection
|
||||
self.toyota_eligible = eligible(CP)
|
||||
self.toyota_projection = projection(sm, now_ns) if self.toyota_eligible else None
|
||||
old_pending = self.state.acceptance.pending
|
||||
if self.command_status == 'issued' and now_ns > self.command_expiry_ns:
|
||||
self.command_status = 'expired'
|
||||
cruise_event: CruiseEvent | None = None, confirmation_grace_ns: int = 0, wheel_override_blocked: bool = False) -> Output:
|
||||
classified = self._cruise(cruise_event, sm, now_ns) if request is None else None
|
||||
if cruise_event is not None and request is None:
|
||||
self._command_receipt(cruise_event, sm, now_ns)
|
||||
action, adopt, action_id = self._action(request, now_ns)
|
||||
if cruise_event is not None and request is None:
|
||||
action, adopt, action_id = self._physical_action(cruise_event, sm, now_ns)
|
||||
if (adopt is not None and self.advance.encounter is not None and self.last_received is not None and
|
||||
if (adopt is not None and self._maps_selected and self.advance.encounter is not None and self.last_received is not None and
|
||||
(request is None or self.state.acceptance.pending is None)):
|
||||
# Explicit adoption uses the current road, including SET over an old early prompt.
|
||||
shown = acc.Presentation(adopt.presentation_id, self.last_received)
|
||||
adopt = replace(adopt, candidate=shown.candidate)
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance, presentation=shown, pending=None, timer_running=False,
|
||||
observation=acc.Observation(acc.ObservationKind.VALID, shown.candidate)))
|
||||
frame = self._frame(sm, CP, now_ns, now_boot_ns, action, adopt, classified)
|
||||
wheel_since = 0
|
||||
if (classified is not None and cruise_event is not None and cruise_event.button == 'accel' and
|
||||
not cruise_event.long_press and cruise_event.selected_mps > cruise_event.previous_mps and
|
||||
abs(float(sm['carState'].vCruise) / 3.6 - cruise_event.selected_mps) <= 0.0001 and
|
||||
0 < cruise_event.wheel_override_since_ns <= cruise_event.observed_ns and cruise_event.session_id == self.session_id and
|
||||
_fresh(sm, 'slcDashboardObservation', now_ns) and
|
||||
cruise_event.producer_session == str(sm['slcDashboardObservation'].producerSessionId)):
|
||||
wheel_since = cruise_event.wheel_override_since_ns
|
||||
frame = self._frame(sm, CP, now_ns, now_boot_ns, action, adopt, classified, wheel_override_blocked=wheel_override_blocked)
|
||||
frame = replace(frame, confirmation_grace_ns=confirmation_grace_ns, wheel_override_since_ns=wheel_since)
|
||||
result = comp.step(self.state, frame)
|
||||
self.state = result.state
|
||||
self.advance.observe(result)
|
||||
if not result.errors and result.acceptance is not None:
|
||||
accepted = self.state.acceptance.accepted
|
||||
encounter = self.advance.encounter
|
||||
provisional = encounter is not None and not encounter.crossed and accepted == encounter.applied_preview
|
||||
if result.acceptance.history_write is not None and not provisional:
|
||||
self.history_limit = accepted
|
||||
elif accepted is not None and accepted != self.history_limit and result.selection is not None:
|
||||
current = sel.select_limit(frame.observations, self.settings.selection).observation.candidate
|
||||
if current is not None and current == result.selection.observation.candidate and current.speed_mps == accepted.candidate.speed_mps:
|
||||
self.history_limit = accepted
|
||||
if self.state.acceptance.presentation is None and self.advance.encounter is not None:
|
||||
current = sel.select_limit(frame.observations, self.settings.selection).observation.candidate
|
||||
if current is not None and (self.current_presentation is None or self.current_presentation.candidate != current):
|
||||
@@ -495,101 +488,22 @@ class Runtime:
|
||||
if result.acceptance is not None:
|
||||
action_result = arb.resolve_acceptance(self.ledger, action_id, result.acceptance, now_ns=now_ns)
|
||||
self.ledger = action_result.state
|
||||
self.last_action_result = action_result
|
||||
completed = arb.step(self.ledger, arb.CompleteAction(self.session_id, action_id), now_ns=now_ns)
|
||||
self.ledger = completed.state
|
||||
commanded_before = self.commanded_acceptance
|
||||
command = self._command(result, sm, now_ns, old_pending, action, adopt)
|
||||
result = replace(result, state=self.state)
|
||||
message = self._message(now_ns, result, sm, frame)
|
||||
if command is not None and (request is not None or commanded_before != self.commanded_acceptance):
|
||||
self._command_ui_sequence = request.sequence_id if request is not None else 0
|
||||
message.slcState.commandActionSequenceId = getattr(self, '_command_ui_sequence', 0)
|
||||
if action_result is not None and action_result.status == 'resolved':
|
||||
self._ui_receipt = (request.sequence_id, str(message.slcState.actionStatus)) if request is not None else None
|
||||
state = message.slcState
|
||||
if (request is not None and request.kind == 'adopt' and state.actionStatus == 'adopt' and
|
||||
0 < request.wheel_observed_ns <= (request.observed_ns or 0) <= now_ns <= request.wheel_observed_ns + WHEEL_ACTION_MAX_AGE_NS and
|
||||
frame.authority.can_control and not frame.host.force_decel and state.hasAcceptedAdjustedLimit and not state.acceptedIsPreview):
|
||||
state.wheelAdoptMonoTime = request.wheel_observed_ns
|
||||
state.wheelAdoptTargetMps = state.acceptedAdjustedLimit
|
||||
receipt = getattr(self, '_ui_receipt', None)
|
||||
if receipt is not None:
|
||||
message.slcState.actionSequenceId, message.slcState.actionStatus = receipt
|
||||
return Output(result, message, action_result, command, self.settings)
|
||||
|
||||
def _command(self, result: comp.Result, sm, now_ns: int, old_pending,
|
||||
action: acc.DriverAction | None, adopt: acc.AdoptRequest | None):
|
||||
decision = result.acceptance
|
||||
if (decision is None or decision.basis not in ('accepted', 'driver_accept', 'driver_adopt') or
|
||||
decision.state.accepted is None or decision.state.pending is not None or decision.authority is None or
|
||||
not decision.authority.can_control or result.coordinate is None or
|
||||
result.domain is None or result.domain.context is None or result.selection is None or
|
||||
result.selection.observation.candidate is None or
|
||||
result.selection.observation.candidate.speed_mps != decision.state.accepted.candidate.speed_mps or
|
||||
result.state.override.persistent_selected_mps is not None):
|
||||
return None
|
||||
target = result.domain.context.effective_cluster_mps
|
||||
if self.toyota_eligible:
|
||||
row = self.toyota_projection
|
||||
accepted = decision.state.accepted
|
||||
if (row is None or row.status != 'ready' or row.sessionId != self.session_id or
|
||||
int(row.acceptedMonoTime) != accepted.accepted_at_ns or
|
||||
abs(float(row.approvedTargetMps) - target) > 0.02):
|
||||
self.commanded_acceptance = None
|
||||
self.command_status = ''
|
||||
return None
|
||||
if self.commanded_generation != result.state.control_generation:
|
||||
self.commanded_acceptance = None
|
||||
self.commanded_generation = result.state.control_generation
|
||||
selected_kph = float(sm['carState'].vCruise)
|
||||
if (not _number(target, positive=True) or not _number(selected_kph, positive=True) or
|
||||
selected_kph >= 255 or not V_CRUISE_MIN / 3.6 <= target <= V_CRUISE_MAX / 3.6):
|
||||
return None
|
||||
if sd.set_speed_matches(target, selected_kph, float(sm['carState'].vCruiseCluster), self.is_metric):
|
||||
if not self._set_speed_ready(sm):
|
||||
return None
|
||||
self.commanded_acceptance = decision.state.accepted
|
||||
self.command_status = 'applied'
|
||||
self.state = replace(self.state, override=replace(self.state.override, applied_selected_mps=selected_kph / 3.6))
|
||||
return None
|
||||
if self.commanded_acceptance == decision.state.accepted and decision.adoption is None:
|
||||
if self.command_status in ('issued', 'applied') or abs(selected_kph / 3.6 - self.command_expected_mps) > 0.1:
|
||||
return None
|
||||
evidence = sm['slcDashboardObservation']
|
||||
expiry = now_ns + int(2e9 / SERVICE_LIST['slcState'].frequency)
|
||||
if result.selection.selected_source is sel.Source.DASHBOARD:
|
||||
expiry = min(expiry, int(evidence.validUntilMonoTime))
|
||||
elif result.selection.selected_source is sel.Source.VISION:
|
||||
evidence = sm['slcVisionObservation'].vision
|
||||
expiry = min(expiry, int(evidence.validUntilMonoTime))
|
||||
if expiry <= now_ns:
|
||||
return None
|
||||
self.commanded_acceptance = decision.state.accepted
|
||||
self.last_command_id += 1
|
||||
self.command_status = 'issued'
|
||||
self.command_issued_ns = now_ns
|
||||
self.command_expiry_ns = expiry
|
||||
self.set_speed_after_ns = max(self.set_speed_after_ns, expiry)
|
||||
self.command_expected_mps = selected_kph / 3.6
|
||||
self.command_target_mps = target
|
||||
self.command_card_producer = str(sm['slcDashboardObservation'].producerSessionId)
|
||||
command = messaging.new_message('slcCruiseCommand')
|
||||
command.valid = True
|
||||
command.logMonoTime = now_ns
|
||||
record = command.slcCruiseCommand
|
||||
record.kind = 'adoptAcceptedLimit'
|
||||
record.source = result.selection.selected_source.value
|
||||
record.upcomingBoundaryId = self.advance.boundary_id
|
||||
record.sessionId = self.session_id
|
||||
record.commandId = self.last_command_id
|
||||
record.actionId = action.sequence_id if action is not None else adopt.sequence_id if adopt is not None else 0
|
||||
record.decisionId = old_pending.decision_id if old_pending is not None else 0
|
||||
record.presentationId = self.state.acceptance.presentation.presentation_id if self.state.acceptance.presentation is not None else 0
|
||||
if result.selection.selected_source in (sel.Source.DASHBOARD, sel.Source.VISION):
|
||||
record.sourceProducerSessionId = str(evidence.producerSessionId)
|
||||
record.sourceEpisode = int(evidence.episode)
|
||||
record.sourceObservedMonoTime = int(evidence.observedMonoTime)
|
||||
record.sourceValidUntilMonoTime = int(evidence.validUntilMonoTime)
|
||||
record.issuedMonoTime = now_ns
|
||||
record.expiresMonoTime = expiry
|
||||
record.expectedSelectedMps = selected_kph / 3.6
|
||||
record.targetMps = target
|
||||
return command
|
||||
return Output(result, message, action_result)
|
||||
|
||||
def _message(self, now_ns: int, result: comp.Result, sm, frame: comp.Frame):
|
||||
message = messaging.new_message("slcState")
|
||||
@@ -628,16 +542,22 @@ class Runtime:
|
||||
state.acceptedMonoTime = accepted.accepted_at_ns
|
||||
state.acceptedSpeedLimit = accepted.candidate.speed_mps
|
||||
state.acceptedSource = accepted.candidate.source
|
||||
state.acceptedIsPreview = accepted != self.history_limit and self.advance.encounter is not None and accepted == self.advance.encounter.applied_preview
|
||||
if result.domain is not None and result.domain.context is not None:
|
||||
state.offset = result.domain.context.offset_mps
|
||||
# Publish the qualified domain coordinate; a raw cap is not a cluster value.
|
||||
context = result.domain.context if result.domain is not None else None
|
||||
if context is not None:
|
||||
state.hasEffectiveClusterTarget = True
|
||||
state.effectiveClusterTarget = context.effective_cluster_mps
|
||||
# Accelerator use never creates a set-speed override.
|
||||
state.driverOverrideActive = result.state.override.persistent_selected_mps is not None
|
||||
state.effectiveClusterTarget = result.coordinate.cluster_target_mps if result.coordinate is not None else context.effective_cluster_mps
|
||||
if result.ceiling is not None:
|
||||
state.hasCeiling = True
|
||||
state.effectiveCap = result.ceiling.speed_mps
|
||||
state.isLimitingMaxSet = result.ceiling.speed_mps < frame.host.driver_v_cruise_kph / 3.6
|
||||
pending = result.state.acceptance.pending
|
||||
# Only an already-captured response can finish an expired presentation.
|
||||
if pending is not None and not pending.automatic and pending.active_elapsed_ns >= acc.CONFIRMATION_NS:
|
||||
pending = None
|
||||
if pending is not None:
|
||||
state.hasPending = True
|
||||
state.pendingSpeedLimit = pending.candidate.speed_mps
|
||||
@@ -645,8 +565,11 @@ class Runtime:
|
||||
state.decisionId = pending.decision_id
|
||||
state.automaticAdoptionPending = pending.automatic
|
||||
state.pendingSinceMonoTime = now_ns - pending.active_elapsed_ns
|
||||
state.overrideBasis = 'persistent' if state.driverOverrideActive else 'none'
|
||||
state.retainedOverride = bool(result.state.override.persistent_selected_mps is not None)
|
||||
wheel_active = result.coordinate is not None and result.coordinate.basis == 'wheel'
|
||||
state.wheelOverrideSince = result.state.wheel_override_since_ns if not result.errors else 0
|
||||
state.driverOverrideActive = wheel_active or result.ceiling is not None and result.state.pedal_target_mps > 0.
|
||||
state.overrideBasis = 'wheel' if wheel_active else (
|
||||
('pedal' if frame.pedal_pressed else 'persistent') if state.driverOverrideActive else 'none')
|
||||
shown = result.state.acceptance.presentation
|
||||
ego = frame.ego_pair
|
||||
ego_delta = (max(0.0, ego.cluster_mps - ego.raw_mps)
|
||||
@@ -667,7 +590,7 @@ class Runtime:
|
||||
setattr(state, flag_field, True)
|
||||
if state.hasPresentationAdjustedLimit and not self.advance.boundary_id:
|
||||
state.offset = float(state.presentationAdjustedLimit) - float(state.speedLimit)
|
||||
# A retained choice gets an Accept presentation only when fresh evidence matches it.
|
||||
# A manual choice keeps its presentation through source gaps.
|
||||
if shown is not None and (pending is None or pending.candidate == shown.candidate):
|
||||
state.presentationId = shown.presentation_id
|
||||
elif pending is None and self.current_presentation is not None and self.current_presentation.candidate == current_selection.observation.candidate:
|
||||
@@ -685,8 +608,6 @@ class Runtime:
|
||||
state.sourceEpisode = int(evidence.episode)
|
||||
state.sourceObservedMonoTime = int(evidence.observedMonoTime)
|
||||
state.sourceValidUntilMonoTime = int(evidence.validUntilMonoTime)
|
||||
state.commandId = self.last_command_id
|
||||
state.commandStatus = self.command_status
|
||||
receipt = result.acceptance.action_receipt or result.acceptance.adoption_receipt if result.acceptance else None
|
||||
if receipt is not None:
|
||||
state.actionSequenceId = receipt.sequence_id
|
||||
|
||||
@@ -2,18 +2,35 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import offset_document as od
|
||||
from openpilot.starpilot.speed_limits import selection as sel
|
||||
from openpilot.starpilot.speed_limits import speed_domain as sd
|
||||
|
||||
MPH_TO_MPS = od.MPH_TO_MPS
|
||||
KPH_TO_MPS = od.KPH_TO_MPS
|
||||
IMPERIAL_BOUNDS = od.IMPERIAL_BOUNDS
|
||||
METRIC_BOUNDS = od.METRIC_BOUNDS
|
||||
LABELS = {"Dashboard": sel.Source.DASHBOARD, "Map Data": sel.Source.MAP, "Maps": sel.Source.MAP, "Vision": sel.Source.VISION}
|
||||
SOURCE_PRIORITY_KEYS = ('SLCPriority1', 'SLCPriority2', 'SLCPriority3')
|
||||
SOURCE_PRIORITY_DEFAULTS = ('Vision', 'Map Data', 'Off')
|
||||
SOURCE_PRIORITY_MAX_BYTES = 32
|
||||
CONFIRMATION_CHOICES = {0: 'Off', 1: 'Both', 3: 'Higher only', 4: 'Lower only'}
|
||||
CONFIRMATION_BYTES = tuple(str(mode).encode() for mode in CONFIRMATION_CHOICES)
|
||||
ADVANCE_KEYS = ('SLCAdvanceHigherSeconds', 'SLCAdvanceLowerSeconds')
|
||||
ADVANCE_SECONDS = range(11)
|
||||
ADVANCE_BYTES = tuple(str(seconds).encode() for seconds in ADVANCE_SECONDS)
|
||||
|
||||
PEDAL_OVERRIDE_KEY = 'SLCOverrideMode'
|
||||
PEDAL_OVERRIDE_CHOICES = ('Resume the adjusted limit', 'Hold the speed you reached', 'Use your SET speed')
|
||||
PEDAL_OVERRIDE_BYTES = (b'0', b'1', b'2')
|
||||
WHEEL_OVERRIDE_KEY = 'SLCWheelOverride'
|
||||
|
||||
|
||||
class PedalOverride(IntEnum):
|
||||
OFF = 0
|
||||
SPEED_REACHED = 1
|
||||
SET_SPEED = 2
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -26,7 +43,10 @@ class Settings:
|
||||
errors: tuple[str, ...] = ()
|
||||
# Saved intent must still retire pending work when vehicle capabilities disable control.
|
||||
requested: bool = False
|
||||
advance_seconds: int = 0
|
||||
advance_higher_seconds: int = 0
|
||||
advance_lower_seconds: int = 0
|
||||
pedal_override: PedalOverride = PedalOverride.OFF
|
||||
wheel_override: bool = False
|
||||
|
||||
|
||||
def _bool(values: Mapping[str, object], key: str, default: bool = False) -> bool:
|
||||
@@ -51,37 +71,50 @@ def parse(values: Mapping[str, object]) -> Settings:
|
||||
od.default_document(_bool(values, 'IsMetric')))
|
||||
if not isinstance(document, od.OffsetDocument):
|
||||
raise ValueError('saved SLC offsets need review')
|
||||
first = values.get("SLCPriority1", "Vision")
|
||||
second = values.get("SLCPriority2", "Dashboard")
|
||||
if type(first) is not str or type(second) is not str:
|
||||
first, second, third = (values.get(key, default) for key, default in
|
||||
zip(SOURCE_PRIORITY_KEYS, SOURCE_PRIORITY_DEFAULTS, strict=True))
|
||||
if any(type(source) is not str for source in (first, second, third)):
|
||||
raise ValueError("invalid saved SLC source priority")
|
||||
mode = {"Highest": sel.SelectionMode.HIGHEST, "Lowest": sel.SelectionMode.LOWEST}.get(first, sel.SelectionMode.ORDERED)
|
||||
if first not in (*LABELS, "Highest", "Lowest") or second not in LABELS:
|
||||
if first not in (*LABELS, "Highest", "Lowest") or any(source not in (*LABELS, 'Off') for source in (second, third)):
|
||||
raise ValueError("invalid saved SLC source priority")
|
||||
slots = (LABELS.get(first), LABELS[second])
|
||||
override = values.get("SLCOverride", 1)
|
||||
if type(override) is not int or override not in (0, 1, 2):
|
||||
raise ValueError("invalid saved SLC override")
|
||||
confirmation = _bool(values, "SLCConfirmation")
|
||||
advance = values.get('SLCAdvanceSeconds', 0)
|
||||
if type(advance) is not int or not 0 <= advance <= 10:
|
||||
advance = 0 # An invalid optional timer cannot enable early adoption.
|
||||
policy = acc.Policy(confirm_lower=confirmation, confirm_higher=confirmation,
|
||||
slots = (LABELS.get(first), LABELS.get(second), LABELS.get(third))
|
||||
confirmation = values.get('SLCConfirmation', 0)
|
||||
if type(confirmation) is not int or confirmation not in CONFIRMATION_CHOICES:
|
||||
raise ValueError('invalid saved SLC confirmation mode')
|
||||
higher = values.get('SLCAdvanceHigherSeconds', 0)
|
||||
lower = values.get('SLCAdvanceLowerSeconds', 0)
|
||||
if type(higher) is not int or higher not in ADVANCE_SECONDS:
|
||||
higher = 0
|
||||
if type(lower) is not int or lower not in ADVANCE_SECONDS:
|
||||
lower = 0
|
||||
policy = acc.Policy(confirm_lower=confirmation in (1, 4),
|
||||
confirm_higher=confirmation in (1, 3),
|
||||
display_only=not control)
|
||||
offsets = document.schedule()
|
||||
# SLCOverride remains a validated legacy preference; only set-speed changes override SLC.
|
||||
pedal_override = values.get(PEDAL_OVERRIDE_KEY, PedalOverride.SPEED_REACHED.value)
|
||||
if type(pedal_override) is not int or pedal_override not in (0, 1, 2):
|
||||
pedal_override = PedalOverride.OFF.value
|
||||
return Settings(control, display, sel.SelectionPolicy(mode, slots, False), policy, offsets,
|
||||
requested=control, advance_seconds=advance)
|
||||
requested=control, advance_higher_seconds=higher, advance_lower_seconds=lower,
|
||||
pedal_override=PedalOverride(pedal_override), wheel_override=values.get(WHEEL_OVERRIDE_KEY) is True)
|
||||
except (ValueError, OverflowError) as error:
|
||||
return _disabled(str(error))
|
||||
|
||||
|
||||
SAVED_KEYS = ("SpeedLimitController", "ShowSpeedLimits", "IsMetric", "SLCPriority1", "SLCPriority2",
|
||||
"SLCOverride", "SLCConfirmation", "SLCAdvanceSeconds")
|
||||
SAVED_KEYS = ("SpeedLimitController", "ShowSpeedLimits", "IsMetric", *SOURCE_PRIORITY_KEYS,
|
||||
"SLCConfirmation")
|
||||
|
||||
|
||||
def read_params(params) -> Settings:
|
||||
values = {}
|
||||
values = {WHEEL_OVERRIDE_KEY: read_saved(params, WHEEL_OVERRIDE_KEY, 1) == (b'1', True)}
|
||||
raw_override, readable_override = read_saved(params, PEDAL_OVERRIDE_KEY, 1)
|
||||
if raw_override is not None or not readable_override:
|
||||
values[PEDAL_OVERRIDE_KEY] = int(raw_override) if readable_override and raw_override in PEDAL_OVERRIDE_BYTES else 0
|
||||
for key in ADVANCE_KEYS:
|
||||
raw, readable = read_saved(params, key, 2)
|
||||
if readable and raw in ADVANCE_BYTES:
|
||||
values[key] = int(raw)
|
||||
try:
|
||||
with Path(params.get_param_path('SLCOffsetSchedule')).open('rb') as stream:
|
||||
raw_document = stream.read(od.MAX_DOCUMENT_BYTES + 1)
|
||||
@@ -97,20 +130,23 @@ def read_params(params) -> Settings:
|
||||
for key in SAVED_KEYS:
|
||||
if raw_document is not None and key == 'IsMetric':
|
||||
continue
|
||||
# Typed Params reads turn malformed numbers/strings into absence and accept
|
||||
# any non-"1" Boolean as false. Neither may silently alter confirmation or
|
||||
# units. Decode the saved bytes explicitly; only an absent file uses defaults.
|
||||
# Only missing files use defaults. Invalid bytes must not change confirmation or units.
|
||||
try:
|
||||
with Path(params.get_param_path(key)).open('rb') as stream:
|
||||
raw = stream.read(8 if key == 'SLCAdvanceSeconds' else -1)
|
||||
if key == 'SLCAdvanceSeconds':
|
||||
if raw not in tuple(str(seconds).encode() for seconds in range(11)):
|
||||
raise ValueError('invalid advance time')
|
||||
value = int(raw)
|
||||
elif key == 'SLCOverride':
|
||||
value = int(raw)
|
||||
elif key in ('SLCPriority1', 'SLCPriority2'):
|
||||
if key in (*SOURCE_PRIORITY_KEYS, 'SLCConfirmation'):
|
||||
raw, readable = read_saved(params, key, 1 if key == 'SLCConfirmation' else SOURCE_PRIORITY_MAX_BYTES)
|
||||
if not readable:
|
||||
return _disabled(f'Invalid saved {key}')
|
||||
if raw is None:
|
||||
continue
|
||||
else:
|
||||
with Path(params.get_param_path(key)).open('rb') as stream:
|
||||
raw = stream.read()
|
||||
if key in SOURCE_PRIORITY_KEYS:
|
||||
value = raw.decode('utf-8')
|
||||
elif key == 'SLCConfirmation':
|
||||
if raw not in CONFIRMATION_BYTES:
|
||||
raise ValueError("invalid confirmation mode")
|
||||
value = int(raw)
|
||||
else:
|
||||
if raw not in (b'0', b'1'):
|
||||
raise ValueError('invalid Boolean')
|
||||
@@ -119,8 +155,5 @@ def read_params(params) -> Settings:
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except (OSError, ValueError, OverflowError):
|
||||
if key == 'SLCAdvanceSeconds':
|
||||
values[key] = 0
|
||||
continue
|
||||
return _disabled(f'Invalid saved {key}')
|
||||
return parse(values)
|
||||
|
||||
@@ -22,7 +22,7 @@ class SelectionMode(StrEnum):
|
||||
@dataclass(frozen=True)
|
||||
class SelectionPolicy:
|
||||
mode: SelectionMode
|
||||
slots: tuple[Source | None, Source | None]
|
||||
slots: tuple[Source | None, ...]
|
||||
online_fallback: bool
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ def _input_errors(observations, policy):
|
||||
return ["policy must be SelectionPolicy"]
|
||||
if not isinstance(policy.mode, SelectionMode):
|
||||
errors.append("selection mode must be explicit")
|
||||
if (not isinstance(policy.slots, tuple) or len(policy.slots) != 2 or
|
||||
if (not isinstance(policy.slots, tuple) or len(policy.slots) not in (2, 3) or
|
||||
any(source is not None and (not isinstance(source, Source) or source == Source.ONLINE) for source in policy.slots)):
|
||||
errors.append("two primary source slots are required; online is fallback only")
|
||||
errors.append("two or three source slots are required; online is fallback only")
|
||||
if type(policy.online_fallback) is not bool:
|
||||
errors.append("online fallback must be Boolean")
|
||||
if not isinstance(observations, Mapping):
|
||||
|
||||
@@ -5,6 +5,7 @@ import struct
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MIN, V_CRUISE_MAX, slc_target_kph
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
|
||||
@@ -165,6 +166,15 @@ def set_speed_matches(target_mps: float, raw_kph: float, cluster_kph: float | No
|
||||
math.isclose(target_kph, cluster_kph, rel_tol=0, abs_tol=0.001))
|
||||
|
||||
|
||||
def set_above_limit(selected_mps: float, adjusted_limit_mps: float, is_metric: bool) -> bool:
|
||||
# Use the sign's wire precision and restore software SET's 0.1 km/h after transport.
|
||||
limit_mps = struct.unpack('f', struct.pack('f', adjusted_limit_mps))[0]
|
||||
selected_kph = round(selected_mps * CV.MS_TO_KPH, 1)
|
||||
if is_metric:
|
||||
return round(selected_kph) > round(limit_mps * CV.MS_TO_KPH)
|
||||
return round(selected_kph * CV.KPH_TO_MPH) > round(limit_mps * CV.MS_TO_MPH)
|
||||
|
||||
|
||||
def resolve(decision: acc.Decision, schedule: OffsetSchedule, selected: SpeedPair, ego: SpeedPair) -> DomainResolution:
|
||||
"""Bind an explicit offset and contemporaneous speed pairs to an acceptance output."""
|
||||
if (not isinstance(decision, acc.Decision) or not isinstance(decision.state, acc.State) or decision.errors or
|
||||
@@ -198,7 +208,8 @@ def resolve(decision: acc.Decision, schedule: OffsetSchedule, selected: SpeedPai
|
||||
if (not _number(target, positive=True) or accepted is None or
|
||||
accepted.candidate.speed_mps != target or not decision.authority.can_control or
|
||||
decision.policy.display_only or
|
||||
decision.state.observation.kind is not acc.ObservationKind.VALID):
|
||||
decision.state.observation.kind is not acc.ObservationKind.VALID and
|
||||
decision.basis not in ('absent', 'unknown', 'stale', 'driver_accept', 'driver_reject', 'timeout_reject')):
|
||||
return DomainResolution(DomainStatus.INVALID, reason="accepted target mismatch")
|
||||
assert selected.raw_mps is not None and selected.cluster_mps is not None
|
||||
assert ego.raw_mps is not None and ego.cluster_mps is not None
|
||||
@@ -216,22 +227,12 @@ def resolve(decision: acc.Decision, schedule: OffsetSchedule, selected: SpeedPai
|
||||
ego.raw_mps, ego_delta))
|
||||
|
||||
|
||||
def to_planner_coordinate(context: DomainContext, contribution_raw_mps: float | None, basis: str) -> PlannerCoordinate:
|
||||
"""Apply a raw override in cluster space, then remove the ego delta exactly once."""
|
||||
if (not context_is_valid(context) or basis not in ("none", "adoption_clear", "pedal", "persistent") or
|
||||
(basis in ("none", "adoption_clear")) != (contribution_raw_mps is None)):
|
||||
def to_planner_coordinate(context: DomainContext, *, override_mps: float = 0.) -> PlannerCoordinate:
|
||||
"""Remove the ego delta from the selected cluster target exactly once."""
|
||||
if not context_is_valid(context) or not _number(override_mps):
|
||||
return PlannerCoordinate(None, "invalid")
|
||||
if contribution_raw_mps is not None and not _number(contribution_raw_mps, positive=True):
|
||||
return PlannerCoordinate(None, "invalid")
|
||||
cluster_target = context.effective_cluster_mps
|
||||
if basis == "pedal":
|
||||
if contribution_raw_mps != context.ego_raw_mps:
|
||||
return PlannerCoordinate(None, "invalid")
|
||||
cluster_target = max(cluster_target, context.ego_raw_mps + context.ego_delta_mps)
|
||||
elif basis == "persistent":
|
||||
assert contribution_raw_mps is not None
|
||||
cluster_target = max(cluster_target, contribution_raw_mps + context.selected_delta_mps)
|
||||
cluster_target = max(context.effective_cluster_mps, override_mps)
|
||||
cap = cluster_target - context.ego_delta_mps
|
||||
if not _number(cap, positive=True):
|
||||
return PlannerCoordinate(None, "nonpositive", cluster_target, context.ego_delta_mps)
|
||||
return PlannerCoordinate(cap, basis, cluster_target, context.ego_delta_mps)
|
||||
return PlannerCoordinate(cap, 'none', cluster_target, context.ego_delta_mps)
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import FrozenInstanceError, replace
|
||||
from typing import Any, cast
|
||||
|
||||
from openpilot.starpilot.speed_limits.acceptance import (
|
||||
CONFIRMATION_NS, MAX_REJECTIONS, AcceptedLimit, ActionKind, AdoptRequest, Authority, Candidate, DriverAction,
|
||||
CONFIRMATION_NS, AcceptedLimit, ActionKind, AdoptRequest, Authority, Candidate, DriverAction,
|
||||
IdentityKind, LongitudinalOwner, Mode, Observation, ObservationIdentity, ObservationKind, Policy, State, new_session, step,
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
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)
|
||||
self.assertIsNone(lost.control_target_mps)
|
||||
self.assertEqual(lost.control_target_mps, candidate.speed_mps)
|
||||
returned = step(lost.state, valid(candidate), ACTIVE, policy, now_ns=6 * NS)
|
||||
self.assertIsNone(returned.state.pending)
|
||||
self.assertIsNone(returned.history_write)
|
||||
@@ -127,22 +127,22 @@ class TestAcceptance(unittest.TestCase):
|
||||
lost = step(rejected.state, ABSENT, ACTIVE, policy, now_ns=4 * NS)
|
||||
returned = step(lost.state, valid(LOW), ACTIVE, policy, now_ns=5 * NS)
|
||||
for decision in (rejected, lost, returned):
|
||||
self.assertEqual(decision.control_target_mps, None if decision is lost else HIGH.speed_mps)
|
||||
self.assertEqual(decision.control_target_mps, HIGH.speed_mps)
|
||||
self.assertEqual(decision.state.accepted, initial.history_write)
|
||||
self.assertIsNone(decision.state.pending)
|
||||
self.assertIsNone(decision.history_write)
|
||||
self.assertIn(LOW, decision.state.rejected)
|
||||
self.assertEqual(decision.state.rejected, LOW)
|
||||
self.assertTrue(rejected.action_receipt.consumed)
|
||||
self.assertEqual(returned.basis, "rejected")
|
||||
|
||||
def test_source_gaps_keep_accepted_history_without_a_control_target(self):
|
||||
def test_source_gaps_use_the_previous_accepted_limit_without_inventing_an_observation(self):
|
||||
for source, kind in itertools.product(('dashboard', 'map', 'vision', 'online', 'future-source'),
|
||||
(ObservationKind.ABSENT, ObservationKind.UNKNOWN, ObservationKind.STALE)):
|
||||
with self.subTest(source=source, kind=kind):
|
||||
prior = saved(replace(HIGH, source=source))
|
||||
policy = Policy()
|
||||
result = step(prior, Observation(kind), ACTIVE, policy, now_ns=300 * NS)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertEqual(result.control_target_mps, prior.accepted.candidate.speed_mps)
|
||||
self.assertEqual(result.state.accepted, prior.accepted)
|
||||
self.assertEqual(result.state.observation.kind, kind)
|
||||
self.assertIsNone(result.display_candidate)
|
||||
@@ -156,62 +156,79 @@ class TestAcceptance(unittest.TestCase):
|
||||
previous = saved(first)
|
||||
for kind in (ObservationKind.ABSENT, ObservationKind.UNKNOWN, ObservationKind.STALE):
|
||||
lost = step(previous, Observation(kind), ACTIVE, policy, now_ns=NS)
|
||||
self.assertIsNone(lost.control_target_mps)
|
||||
self.assertEqual(lost.control_target_mps, first.speed_mps)
|
||||
self.assertEqual(lost.state.accepted, previous.accepted)
|
||||
next_sign = replace(LOW, source='vision')
|
||||
pending = step(lost.state, valid(next_sign), ACTIVE, policy, now_ns=2 * NS)
|
||||
self.assertEqual(pending.control_target_mps, first.speed_mps)
|
||||
self.assertIsNotNone(pending.state.pending)
|
||||
recovered = step(pending.state, valid(first), ACTIVE, policy, now_ns=3 * NS)
|
||||
self.assertIsNotNone(recovered.state.pending)
|
||||
self.assertIsNone(recovered.state.pending)
|
||||
self.assertFalse(step(recovered.state, valid(first), ACTIVE, policy, now_ns=4 * NS).errors)
|
||||
for kind in ActionKind:
|
||||
action = DriverAction('drive', 1, pending.state.pending.decision_id, kind)
|
||||
acted = step(pending.state, valid(next_sign), ACTIVE, policy, now_ns=3 * NS, action=action)
|
||||
target = next_sign.speed_mps if kind is ActionKind.ACCEPT else first.speed_mps
|
||||
gap = step(acted.state, Observation(ObservationKind.UNKNOWN), ACTIVE, policy, now_ns=300 * NS)
|
||||
self.assertIsNone(gap.control_target_mps)
|
||||
self.assertEqual(gap.control_target_mps, target)
|
||||
self.assertEqual(gap.state.accepted.candidate.speed_mps, target)
|
||||
timeout = step(pending.state, valid(next_sign), ACTIVE, policy, now_ns=33 * NS)
|
||||
self.assertEqual(timeout.basis, 'timeout_reject')
|
||||
self.assertEqual(timeout.control_target_mps, first.speed_mps)
|
||||
|
||||
def test_source_gaps_keep_the_decision_and_original_deadline_without_accept_authority(self):
|
||||
def test_source_gaps_keep_the_decision_presentation_and_original_deadline(self):
|
||||
for kind in (ObservationKind.UNKNOWN, ObservationKind.STALE, ObservationKind.ABSENT):
|
||||
with self.subTest(kind=kind):
|
||||
before = self.pending()
|
||||
missing = step(before.state, Observation(kind), ACTIVE, Policy(), now_ns=NS)
|
||||
after = self.pending(state=missing.state, now_ns=2 * NS)
|
||||
self.assertEqual(missing.state.pending.decision_id, before.state.pending.decision_id)
|
||||
self.assertIsNone(missing.state.presentation)
|
||||
self.assertIsNone(missing.control_target_mps)
|
||||
self.assertEqual(missing.state.presentation, before.state.presentation)
|
||||
self.assertEqual(missing.control_target_mps, HIGH.speed_mps)
|
||||
self.assertEqual(before.state.pending.decision_id, after.state.pending.decision_id)
|
||||
self.assertEqual(after.state.pending.active_elapsed_ns, 2 * NS)
|
||||
late = DriverAction("drive", 1, before.state.pending.decision_id, ActionKind.ACCEPT)
|
||||
result = step(missing.state, valid(LOW), ACTIVE, Policy(), now_ns=3 * NS, action=late)
|
||||
self.assertFalse(result.action_receipt.consumed)
|
||||
self.assertEqual(result.action_receipt.status, "different_presentation")
|
||||
self.assertEqual(result.state.accepted.candidate, HIGH)
|
||||
self.assertTrue(result.action_receipt.consumed)
|
||||
self.assertEqual(result.action_receipt.status, "accept")
|
||||
self.assertEqual(result.state.accepted.candidate, LOW)
|
||||
expired = step(after.state, Observation(kind), ACTIVE, Policy(), now_ns=CONFIRMATION_NS)
|
||||
self.assertEqual(expired.basis, 'timeout_reject')
|
||||
self.assertIsNone(expired.state.pending)
|
||||
self.assertIsNone(expired.control_target_mps)
|
||||
returned = step(expired.state, valid(LOW), ACTIVE, Policy(), now_ns=11 * NS)
|
||||
self.assertEqual(expired.control_target_mps, HIGH.speed_mps)
|
||||
returned = step(expired.state, valid(LOW), ACTIVE, Policy(), now_ns=CONFIRMATION_NS + NS)
|
||||
self.assertEqual(returned.basis, 'rejected')
|
||||
|
||||
def test_retained_confirmation_can_be_rejected_but_not_accepted_without_a_source(self):
|
||||
def test_retained_confirmation_can_be_accepted_or_rejected_without_a_source(self):
|
||||
before = self.pending()
|
||||
for kind in (ActionKind.ACCEPT, ActionKind.REJECT):
|
||||
with self.subTest(kind=kind):
|
||||
action = DriverAction('drive', 1, before.state.pending.decision_id, kind)
|
||||
result = step(before.state, ABSENT, ACTIVE, Policy(), now_ns=NS, action=action)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertEqual(result.state.accepted, before.state.accepted)
|
||||
self.assertEqual(result.action_receipt.consumed, kind is ActionKind.REJECT)
|
||||
self.assertEqual(result.state.pending is None, kind is ActionKind.REJECT)
|
||||
expected = LOW if kind is ActionKind.ACCEPT else HIGH
|
||||
self.assertEqual(result.control_target_mps, expected.speed_mps)
|
||||
self.assertEqual(result.state.accepted.candidate, expected)
|
||||
self.assertTrue(result.action_receipt.consumed)
|
||||
self.assertIsNone(result.state.pending)
|
||||
self.assertIsNone(result.state.presentation)
|
||||
self.assertEqual(result.state.observation.kind, ObservationKind.ABSENT)
|
||||
held = step(result.state, ABSENT, ACTIVE, Policy(), now_ns=2 * NS)
|
||||
self.assertFalse(held.errors)
|
||||
self.assertEqual(held.control_target_mps, expected.speed_mps)
|
||||
inactive = step(before.state, ABSENT, PAUSED, Policy(), now_ns=NS)
|
||||
self.assertIsNone(inactive.state.pending)
|
||||
|
||||
def test_source_gap_actions_respect_the_original_deadline(self):
|
||||
for kind, late in itertools.product(ActionKind, (False, True)):
|
||||
with self.subTest(kind=kind, late=late):
|
||||
before = self.pending()
|
||||
action = DriverAction('drive', 1, before.state.pending.decision_id, kind)
|
||||
result = step(before.state, ABSENT, ACTIVE, Policy(), now_ns=CONFIRMATION_NS + int(late), action=action)
|
||||
self.assertEqual(result.action_receipt.consumed, not late)
|
||||
self.assertEqual(result.basis, 'timeout_reject' if late else f'driver_{kind.value}')
|
||||
self.assertEqual(result.control_target_mps, LOW.speed_mps if kind is ActionKind.ACCEPT and not late else HIGH.speed_mps)
|
||||
self.assertIsNone(result.state.pending)
|
||||
|
||||
def test_recorded_vision_expiry_burst_is_one_decision_or_one_automatic_adoption(self):
|
||||
# Route 00000048: decisions 52, 53 and 54 were the same 35 mph episode.
|
||||
previous = replace(LOW, source='vision', speed_mps=20 * .44704)
|
||||
@@ -233,57 +250,51 @@ class TestAcceptance(unittest.TestCase):
|
||||
if result.history_write is not None:
|
||||
writes.append(result.history_write)
|
||||
if not available:
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertEqual(result.control_target_mps, result.state.accepted.candidate.speed_mps)
|
||||
self.assertEqual(len(set(ids)), 1 if manual else 0)
|
||||
self.assertEqual(len(writes), 0 if manual else 1)
|
||||
|
||||
def test_different_provider_must_settle_before_replacing_the_received_limit(self):
|
||||
mapped = replace(LOW, source='map', speed_mps=30 * .44704)
|
||||
vision = replace(HIGH, source='vision', speed_mps=35 * .44704)
|
||||
def test_provider_changes_use_the_same_immediate_adoption_or_confirmation_flow(self):
|
||||
mapped = replace(HIGH, source='map')
|
||||
automatic = Policy(confirm_lower=False, confirm_higher=False)
|
||||
for manual in (False, True):
|
||||
with self.subTest(manual=manual):
|
||||
for source, speed, manual in itertools.product(('map', 'dashboard', 'vision'),
|
||||
(LOW.speed_mps, HIGH.speed_mps + 5 * .44704), (False, True)):
|
||||
with self.subTest(source=source, speed=speed, manual=manual):
|
||||
first = step(new_session('drive'), valid(mapped), ACTIVE, automatic, now_ns=0)
|
||||
state = first.state
|
||||
reading = replace(LOW, source=source, speed_mps=speed)
|
||||
policy = Policy(confirm_lower=manual, confirm_higher=manual)
|
||||
for timestamp, reading in ((NS, vision), (NS + NS // 2, mapped),
|
||||
(2 * NS, vision), (2 * NS + NS // 2, mapped)):
|
||||
result = step(state, valid(reading), ACTIVE, policy, now_ns=timestamp)
|
||||
self.assertFalse(result.errors)
|
||||
state = result.state
|
||||
self.assertEqual(state.accepted, first.state.accepted)
|
||||
self.assertIsNone(state.pending)
|
||||
self.assertIsNone(result.history_write)
|
||||
waiting = step(state, valid(vision), ACTIVE, policy, now_ns=3 * NS)
|
||||
self.assertEqual(waiting.basis, 'source_switch_pending')
|
||||
settled = step(waiting.state, valid(vision), ACTIVE, policy, now_ns=5 * NS)
|
||||
self.assertEqual(settled.state.pending is not None, manual)
|
||||
self.assertEqual(settled.history_write is not None, not manual)
|
||||
result = step(first.state, valid(reading), ACTIVE, policy, now_ns=NS)
|
||||
self.assertFalse(result.errors)
|
||||
self.assertEqual(result.basis, 'pending' if manual else 'accepted')
|
||||
self.assertEqual(result.control_target_mps, mapped.speed_mps if manual else speed)
|
||||
self.assertEqual(result.history_write is not None, not manual)
|
||||
if manual:
|
||||
self.assertEqual(result.state.accepted, first.state.accepted)
|
||||
self.assertEqual(result.state.pending.active_elapsed_ns, 0)
|
||||
expired = step(result.state, valid(reading), ACTIVE, policy, now_ns=NS + CONFIRMATION_NS)
|
||||
self.assertEqual(expired.basis, 'timeout_reject')
|
||||
self.assertEqual(expired.control_target_mps, mapped.speed_mps)
|
||||
else:
|
||||
self.assertIsNone(result.state.pending)
|
||||
self.assertEqual(result.state.accepted.candidate, reading)
|
||||
|
||||
def test_only_current_explicit_adoption_can_bypass_provider_verification(self):
|
||||
def test_explicit_adoption_of_a_replacement_still_requires_its_current_presentation(self):
|
||||
mapped = replace(LOW, source='map', speed_mps=30 * .44704)
|
||||
vision = replace(HIGH, source='vision', speed_mps=35 * .44704)
|
||||
automatic = Policy(confirm_lower=False, confirm_higher=False)
|
||||
first = step(new_session('drive'), valid(mapped), ACTIVE, automatic, now_ns=0)
|
||||
waiting = step(first.state, valid(vision), ACTIVE, automatic, now_ns=NS)
|
||||
request = AdoptRequest('drive', 1, waiting.state.presentation.presentation_id, vision)
|
||||
shown = step(first.state, valid(vision), ACTIVE, Policy(), now_ns=NS)
|
||||
request = AdoptRequest('drive', 1, shown.state.presentation.presentation_id, vision)
|
||||
for invalid in (replace(request, session_id='foreign'), replace(request, presentation_id=request.presentation_id + 1)):
|
||||
held = step(waiting.state, valid(vision), ACTIVE, automatic, now_ns=NS + 1, adopt=invalid)
|
||||
self.assertEqual(held.basis, 'source_switch_pending')
|
||||
held = step(shown.state, valid(vision), ACTIVE, Policy(), now_ns=NS + 1, adopt=invalid)
|
||||
self.assertEqual(held.basis, 'pending')
|
||||
self.assertFalse(held.adoption_receipt.consumed)
|
||||
self.assertIsNone(held.history_write)
|
||||
self.assertEqual(held.state.accepted, first.state.accepted)
|
||||
explicit = step(waiting.state, valid(vision), ACTIVE, automatic, now_ns=NS + 1, adopt=request)
|
||||
explicit = step(shown.state, valid(vision), ACTIVE, Policy(), now_ns=NS + 1, adopt=request)
|
||||
self.assertTrue(explicit.adoption_receipt.consumed)
|
||||
self.assertEqual(explicit.state.accepted.candidate, vision)
|
||||
|
||||
def test_changed_received_speed_restarts_confirmation_after_a_decline(self):
|
||||
rejected = self.act(self.pending().state, ActionKind.REJECT)
|
||||
other = replace(LOW, speed_mps=25.)
|
||||
changed = self.pending(candidate=other, state=rejected.state, now_ns=2 * NS)
|
||||
returned = self.pending(candidate=LOW, state=changed.state, now_ns=3 * NS)
|
||||
self.assertNotEqual(returned.state.pending.decision_id, rejected.state.next_decision_id - 1)
|
||||
self.assertFalse(returned.state.rejected)
|
||||
|
||||
def test_same_received_speed_does_not_prompt_again_for_a_new_zone(self):
|
||||
rejected = self.act(self.pending().state, ActionKind.REJECT)
|
||||
next_zone = replace(LOW, observation_identity=geographic('road-3'))
|
||||
@@ -306,13 +317,15 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertNotEqual(changed.state.pending.decision_id, first.state.pending.decision_id)
|
||||
self.assertEqual(changed.state.pending.active_elapsed_ns, 0)
|
||||
|
||||
def test_confirmation_expires_at_ten_seconds(self):
|
||||
def test_confirmation_expires_at_thirty_seconds(self):
|
||||
pending = self.pending()
|
||||
before = step(pending.state, valid(LOW), ACTIVE, Policy(), now_ns=CONFIRMATION_NS - 1)
|
||||
ten_seconds = step(pending.state, valid(LOW), ACTIVE, Policy(), now_ns=10 * NS)
|
||||
self.assertIsNotNone(ten_seconds.state.pending)
|
||||
before = step(ten_seconds.state, valid(LOW), ACTIVE, Policy(), now_ns=30 * NS - 1)
|
||||
self.assertIsNotNone(before.state.pending)
|
||||
expired = step(before.state, valid(LOW), ACTIVE, Policy(), now_ns=CONFIRMATION_NS)
|
||||
expired = step(before.state, valid(LOW), ACTIVE, Policy(), now_ns=30 * NS)
|
||||
self.assertEqual(expired.basis, "timeout_reject")
|
||||
self.assertIn(LOW, expired.state.rejected)
|
||||
self.assertEqual(expired.state.rejected, LOW)
|
||||
self.assertIsNone(expired.state.pending)
|
||||
self.assertIsNone(expired.history_write)
|
||||
self.assertEqual(expired.control_target_mps, HIGH.speed_mps)
|
||||
@@ -324,10 +337,10 @@ class TestAcceptance(unittest.TestCase):
|
||||
inactive = step(pause.state, valid(LOW), PAUSED, Policy(), now_ns=CONFIRMATION_NS)
|
||||
self.assertFalse(inactive.state.rejected)
|
||||
self.assertIsNone(inactive.control_target_mps)
|
||||
resumed = self.pending(state=inactive.state, now_ns=20 * NS)
|
||||
resumed = self.pending(state=inactive.state, now_ns=CONFIRMATION_NS + NS)
|
||||
self.assertNotEqual(resumed.state.pending.decision_id, start.state.pending.decision_id)
|
||||
self.assertEqual(resumed.state.pending.active_elapsed_ns, 0)
|
||||
expired = step(resumed.state, valid(LOW), ACTIVE, Policy(), now_ns=30 * NS)
|
||||
expired = step(resumed.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * CONFIRMATION_NS + NS)
|
||||
self.assertEqual(expired.basis, 'timeout_reject')
|
||||
self.assertEqual(expired.state.accepted.candidate, HIGH)
|
||||
|
||||
@@ -492,7 +505,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertFalse(result.action_receipt.consumed)
|
||||
self.assertEqual(result.action_receipt.status, "different_decision")
|
||||
self.assertEqual(result.state.last_action_sequence, 3)
|
||||
self.assertEqual(result.state.rejected, frozenset())
|
||||
self.assertIsNone(result.state.rejected)
|
||||
|
||||
def test_event_cannot_accept_a_decision_not_previously_presented(self):
|
||||
action = DriverAction("drive", 0, 1, ActionKind.ACCEPT)
|
||||
@@ -579,7 +592,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertIsNone(result.history_write)
|
||||
|
||||
def test_pending_candidate_cannot_also_be_rejected(self):
|
||||
state = replace(self.pending().state, rejected=frozenset({LOW}))
|
||||
state = replace(self.pending().state, rejected=LOW)
|
||||
result = step(state, valid(HIGH), ACTIVE, Policy(False, False), now_ns=NS)
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
@@ -587,26 +600,28 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertIs(result.state, state)
|
||||
self.assertEqual(result.state.accepted.candidate, HIGH)
|
||||
|
||||
def test_rejecting_a_returned_limit_preserves_its_earlier_accepted_history(self):
|
||||
def test_returning_to_the_previous_limit_clears_the_obsolete_choice(self):
|
||||
other = self.pending()
|
||||
returned = self.pending(candidate=HIGH, state=other.state, now_ns=NS)
|
||||
declined = self.act(returned.state, ActionKind.REJECT, now_ns=2 * NS)
|
||||
held = step(declined.state, valid(HIGH), ACTIVE, Policy(), now_ns=3 * NS)
|
||||
returned = step(other.state, valid(HIGH), ACTIVE, Policy(), now_ns=NS)
|
||||
stale = DriverAction('drive', 1, other.state.pending.decision_id, ActionKind.REJECT)
|
||||
held = step(returned.state, valid(HIGH), ACTIVE, Policy(), now_ns=2 * NS, action=stale)
|
||||
self.assertFalse(held.errors)
|
||||
self.assertEqual(held.basis, 'rejected')
|
||||
self.assertEqual(held.basis, 'accepted')
|
||||
self.assertIsNone(returned.state.pending)
|
||||
self.assertFalse(held.action_receipt.consumed)
|
||||
self.assertEqual(held.state.accepted, returned.state.accepted)
|
||||
self.assertIn(HIGH, held.state.rejected)
|
||||
self.assertFalse(held.state.rejected)
|
||||
self.assertIsNone(held.state.pending)
|
||||
|
||||
def test_changed_limits_do_not_accumulate_obsolete_rejections(self):
|
||||
state = saved()
|
||||
for index in range(MAX_REJECTIONS + 1):
|
||||
candidate = replace(LOW, speed_mps=15. + index / 100)
|
||||
pending = self.pending(candidate=candidate, state=state, now_ns=2 * index)
|
||||
result = self.act(pending.state, ActionKind.REJECT, sequence=index, now_ns=2 * index + 1)
|
||||
state = result.state
|
||||
self.assertEqual(state.rejected, frozenset({candidate}))
|
||||
self.assertFalse(state.rejection_capacity_exhausted)
|
||||
def test_new_decline_replaces_the_previous_decline(self):
|
||||
first = self.act(self.pending().state, ActionKind.REJECT)
|
||||
other = replace(LOW, speed_mps=19.)
|
||||
pending = self.pending(candidate=other, state=first.state, now_ns=2 * NS)
|
||||
self.assertEqual(pending.state.rejected, LOW)
|
||||
second = self.act(pending.state, ActionKind.REJECT, sequence=1, now_ns=3 * NS)
|
||||
self.assertEqual(second.state.rejected, other)
|
||||
returned = self.pending(state=second.state, now_ns=4 * NS)
|
||||
self.assertEqual(returned.control_target_mps, HIGH.speed_mps)
|
||||
|
||||
def test_state_and_inputs_are_immutable_and_new_session_validates_history(self):
|
||||
original = saved()
|
||||
@@ -638,7 +653,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
alternate = step(absent.state, valid(replace(candidate, source="map")), ACTIVE, Policy(), now_ns=3 * NS)
|
||||
returned = step(alternate.state, valid(Candidate("dashboard", ObservationIdentity(IdentityKind.SESSION_VALUE, session_id="drive"),
|
||||
LOW.speed_mps)), ACTIVE, Policy(), now_ns=4 * NS)
|
||||
self.assertIn(candidate, returned.state.rejected)
|
||||
self.assertEqual(returned.state.rejected, candidate)
|
||||
self.assertEqual(returned.basis, "rejected")
|
||||
self.assertEqual(returned.state.accepted.candidate, HIGH)
|
||||
self.assertIsNone(alternate.state.pending)
|
||||
@@ -666,7 +681,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
qualified = AcceptedLimit(HIGH, "old", 0)
|
||||
self.assertIs(new_session("drive", qualified).accepted, qualified)
|
||||
|
||||
def test_explicit_adopt_clears_only_matching_rejection(self):
|
||||
def test_explicit_adopt_of_different_limit_clears_rejection(self):
|
||||
first_reject = self.act(self.pending().state, ActionKind.REJECT)
|
||||
other = replace(LOW, speed_mps=19.0)
|
||||
second_pending = self.pending(candidate=other, state=first_reject.state, now_ns=2 * NS)
|
||||
@@ -677,29 +692,23 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertEqual(adopted.basis, "driver_adopt")
|
||||
self.assertTrue(adopted.adoption_receipt.consumed)
|
||||
self.assertIsNone(adopted.action_receipt)
|
||||
self.assertEqual(adopted.state.rejected, frozenset())
|
||||
self.assertIsNone(adopted.state.rejected)
|
||||
self.assertEqual(adopted.history_write.candidate, LOW)
|
||||
self.assertEqual(adopted.control_target_mps, LOW.speed_mps)
|
||||
self.assertIsNone(adopted.state.pending)
|
||||
self.assertTrue(adopted.adoption.clear_override)
|
||||
self.assertEqual(adopted.adoption.session_id, "drive")
|
||||
self.assertEqual(adopted.adoption.action_sequence_id, 2)
|
||||
self.assertEqual(adopted.adoption.presentation_id, request.presentation_id)
|
||||
self.assertEqual(adopted.adoption.accepted, adopted.state.accepted)
|
||||
self.assertEqual(adopted.adoption.reconciliation_speed_mps, LOW.speed_mps)
|
||||
still_rejected = step(adopted.state, valid(other), ACTIVE, Policy(), now_ns=6 * NS)
|
||||
self.assertEqual(still_rejected.basis, "pending")
|
||||
self.assertEqual(still_rejected.control_target_mps, LOW.speed_mps)
|
||||
self.assertEqual(adopted.adoption_receipt.sequence_id, request.sequence_id)
|
||||
self.assertEqual(adopted.state.accepted, adopted.history_write)
|
||||
next_offer = self.pending(candidate=other, state=adopted.state, now_ns=6 * NS)
|
||||
self.assertEqual(next_offer.control_target_mps, LOW.speed_mps)
|
||||
|
||||
def test_adopt_already_accepted_candidate_still_emits_reconciliation(self):
|
||||
def test_adopt_already_accepted_candidate_keeps_control_target(self):
|
||||
shown = step(saved(), valid(HIGH), ACTIVE, Policy(), now_ns=0)
|
||||
for authority in (ACTIVE, replace(ACTIVE, mode=Mode.COMBINED, lateral_active=True)):
|
||||
result = step(shown.state, valid(HIGH), authority, Policy(), now_ns=NS, adopt=self.adopt_request(shown.state))
|
||||
self.assertTrue(result.adoption_receipt.consumed)
|
||||
self.assertIsNone(result.history_write)
|
||||
self.assertEqual(result.state.accepted, shown.state.accepted)
|
||||
self.assertTrue(result.adoption.clear_override)
|
||||
self.assertEqual(result.adoption.reconciliation_speed_mps, HIGH.speed_mps)
|
||||
self.assertEqual(result.control_target_mps, HIGH.speed_mps)
|
||||
|
||||
def test_explicit_adopt_clears_same_speed_rejection_after_provider_change(self):
|
||||
rejected = self.act(self.pending().state, ActionKind.REJECT)
|
||||
@@ -718,7 +727,7 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(pending.state, valid(LOW), ACTIVE, Policy(), now_ns=CONFIRMATION_NS, adopt=self.adopt_request(pending.state))
|
||||
self.assertEqual(result.basis, "driver_adopt")
|
||||
self.assertEqual(result.state.accepted.candidate, LOW)
|
||||
self.assertNotIn(LOW, result.state.rejected)
|
||||
self.assertIsNone(result.state.rejected)
|
||||
|
||||
def test_adopt_requires_prior_presentation_even_when_new_id_is_guessed(self):
|
||||
guessed = AdoptRequest("drive", 0, 1, LOW)
|
||||
@@ -726,27 +735,30 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertEqual(unseen.state.presentation.presentation_id, guessed.presentation_id)
|
||||
self.assertFalse(unseen.adoption_receipt.consumed)
|
||||
self.assertEqual(unseen.adoption_receipt.status, "different_presentation")
|
||||
self.assertIsNone(unseen.adoption)
|
||||
self.assertIsNone(unseen.history_write)
|
||||
replay = step(unseen.state, valid(LOW), ACTIVE, Policy(), now_ns=NS, adopt=guessed)
|
||||
self.assertEqual(replay.adoption_receipt.status, "already_seen")
|
||||
self.assertIsNone(replay.adoption)
|
||||
fresh = step(replay.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * NS, adopt=replace(guessed, sequence_id=1))
|
||||
self.assertTrue(fresh.adoption_receipt.consumed)
|
||||
|
||||
def test_adopt_context_expires_on_source_change_or_loss(self):
|
||||
def test_adopt_requires_live_evidence_and_an_unchanged_presentation(self):
|
||||
shown = self.pending()
|
||||
request = self.adopt_request(shown.state)
|
||||
for observation in (valid(replace(LOW, source="map")), ABSENT, Observation(ObservationKind.UNKNOWN), Observation(ObservationKind.STALE)):
|
||||
with self.subTest(observation=observation):
|
||||
changed = step(shown.state, observation, ACTIVE, Policy(), now_ns=NS, adopt=request)
|
||||
self.assertFalse(changed.adoption_receipt.consumed)
|
||||
self.assertIsNone(changed.adoption)
|
||||
self.assertIsNone(changed.history_write)
|
||||
replayed = step(changed.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * NS, adopt=request)
|
||||
self.assertFalse(replayed.adoption_receipt.consumed)
|
||||
returned = step(changed.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * NS, adopt=replace(request, sequence_id=1))
|
||||
self.assertFalse(returned.adoption_receipt.consumed)
|
||||
self.assertIsNone(returned.adoption)
|
||||
self.assertNotEqual(returned.state.presentation.presentation_id, request.presentation_id)
|
||||
if observation.kind is ObservationKind.VALID:
|
||||
self.assertFalse(returned.adoption_receipt.consumed)
|
||||
self.assertNotEqual(returned.state.presentation.presentation_id, request.presentation_id)
|
||||
else:
|
||||
self.assertTrue(returned.adoption_receipt.consumed)
|
||||
self.assertEqual(returned.state.accepted.candidate, LOW)
|
||||
self.assertEqual(returned.state.presentation.presentation_id, request.presentation_id)
|
||||
|
||||
def test_adopt_exact_candidate_cannot_be_swapped_under_existing_context(self):
|
||||
shown = self.pending()
|
||||
@@ -756,7 +768,6 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(shown.state, valid(LOW), ACTIVE, Policy(), now_ns=NS, adopt=request)
|
||||
self.assertFalse(result.adoption_receipt.consumed)
|
||||
self.assertEqual(result.adoption_receipt.status, "different_presentation")
|
||||
self.assertIsNone(result.adoption)
|
||||
|
||||
def test_adopt_is_recorded_for_any_authority_but_only_commands_with_system_long(self):
|
||||
rejected = self.act(self.pending().state, ActionKind.REJECT)
|
||||
@@ -770,16 +781,14 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(rejected.state, valid(LOW), authority, Policy(), now_ns=2 * NS, adopt=request)
|
||||
self.assertTrue(result.adoption_receipt.consumed)
|
||||
self.assertEqual(result.adoption_receipt.status, "adopt")
|
||||
self.assertNotIn(LOW, result.state.rejected)
|
||||
self.assertIsNone(result.state.rejected)
|
||||
self.assertEqual(result.state.accepted.candidate, LOW)
|
||||
self.assertIsNone(result.adoption)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertEqual(result.state.last_action_sequence, 1)
|
||||
display = step(rejected.state, valid(LOW), ACTIVE, Policy(display_only=True), now_ns=2 * NS, adopt=request)
|
||||
self.assertFalse(display.adoption_receipt.consumed)
|
||||
self.assertEqual(display.adoption_receipt.status, "display_only")
|
||||
self.assertIn(LOW, display.state.rejected)
|
||||
self.assertIsNone(display.adoption)
|
||||
self.assertEqual(display.state.rejected, LOW)
|
||||
self.assertIsNone(display.history_write)
|
||||
|
||||
def test_adopt_foreign_session_does_not_advance_watermark(self):
|
||||
@@ -788,7 +797,6 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(shown.state, valid(LOW), ACTIVE, Policy(), now_ns=NS, adopt=foreign)
|
||||
self.assertEqual(result.adoption_receipt.status, "foreign_session")
|
||||
self.assertEqual(result.state.last_action_sequence, -1)
|
||||
self.assertIsNone(result.adoption)
|
||||
accepted = step(result.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * NS, adopt=self.adopt_request(result.state))
|
||||
self.assertTrue(accepted.adoption_receipt.consumed)
|
||||
|
||||
@@ -806,7 +814,6 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertTrue(fresh.action_receipt.consumed)
|
||||
old_adopt = step(fresh.state, valid(LOW), ACTIVE, Policy(), now_ns=4 * NS, adopt=request)
|
||||
self.assertFalse(old_adopt.adoption_receipt.consumed)
|
||||
self.assertIsNone(old_adopt.adoption)
|
||||
|
||||
def test_ambiguous_action_and_adopt_fail_closed_and_watermark_both(self):
|
||||
shown = self.pending()
|
||||
@@ -820,7 +827,6 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertFalse(result.adoption_receipt.consumed)
|
||||
self.assertEqual(result.state.last_action_sequence, 7)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertIsNone(result.adoption)
|
||||
self.assertIsNone(result.history_write)
|
||||
self.assertEqual(result.state.accepted.candidate, HIGH)
|
||||
self.assertIsNone(result.state.presentation)
|
||||
@@ -833,14 +839,12 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(shown.state, valid(LOW), ACTIVE, Policy(), now_ns=11 * NS, adopt=malformed)
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.adoption_receipt)
|
||||
self.assertIsNone(result.adoption)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertEqual(result.state.last_action_sequence, -1)
|
||||
reversed_clock = step(shown.state, valid(LOW), ACTIVE, Policy(), now_ns=9 * NS, adopt=request)
|
||||
self.assertTrue(reversed_clock.errors)
|
||||
self.assertFalse(reversed_clock.adoption_receipt.consumed)
|
||||
self.assertEqual(reversed_clock.state.last_action_sequence, 1)
|
||||
self.assertIsNone(reversed_clock.adoption)
|
||||
|
||||
def test_invalid_presentation_state_is_inert(self):
|
||||
shown = self.pending()
|
||||
@@ -849,7 +853,6 @@ class TestAcceptance(unittest.TestCase):
|
||||
result = step(state, valid(LOW), ACTIVE, Policy(), now_ns=NS, adopt=self.adopt_request(shown.state))
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertIsNone(result.adoption)
|
||||
self.assertIsNone(result.history_write)
|
||||
|
||||
|
||||
|
||||
@@ -41,14 +41,14 @@ class TestActionArbitration(unittest.TestCase):
|
||||
self.assertEqual(delayed.change.started_at_ns, 1)
|
||||
self.assertEqual(delayed.change.context_id, "accepted-context-1")
|
||||
|
||||
def test_adoption_proposal_and_effect_keep_originating_action(self):
|
||||
def test_adoption_receipt_and_effect_keep_originating_action(self):
|
||||
shown = accept.step(accept.new_session(SESSION), OBSERVATION, AUTHORITY, accept.Policy(), now_ns=0)
|
||||
request = accept.AdoptRequest(SESSION, 1, shown.state.presentation.presentation_id, CANDIDATE)
|
||||
adopted = accept.step(shown.state, OBSERVATION, AUTHORITY, accept.Policy(), now_ns=2, adopt=request)
|
||||
ledger = arb.resolve_acceptance(begin(), 1, adopted, now_ns=2).state
|
||||
result = change(ledger, previous=25.0, selected=adopted.adoption.reconciliation_speed_mps, now=10_000_000_000)
|
||||
result = change(ledger, previous=25.0, selected=adopted.control_target_mps, now=10_000_000_000)
|
||||
self.assertEqual(result.change.disposition, arb.Disposition.SLC_CONSUMED)
|
||||
self.assertEqual(result.change.action_id, adopted.adoption.action_sequence_id)
|
||||
self.assertEqual(result.change.action_id, adopted.adoption_receipt.sequence_id)
|
||||
|
||||
def test_final_unconsumed_receipt_does_not_swallow_cruise_input(self):
|
||||
shown = accept.step(accept.new_session(SESSION), OBSERVATION, AUTHORITY, accept.Policy(), now_ns=0)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Mapped previews through the actual runtime, confirmations and Card guards."""
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import base64
|
||||
@@ -6,19 +7,22 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc, physical_actions as pa
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc, physical_actions
|
||||
from openpilot.starpilot.speed_limits.map_source import decode_event
|
||||
from openpilot.starpilot.speed_limits.map_observation import MapSource, copy_frame
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import parse, read_params
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import ADVANCE_KEYS, parse, read_params
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
from openpilot.starpilot.speed_limits.tests.test_maps_runtime import packet
|
||||
from openpilot.starpilot.speed_limits.tests.test_runtime_replay import ReplaySM
|
||||
from openpilot.starpilot.speed_limits.vision.observation import MODEL_ID
|
||||
from openpilot.starpilot.ui.onroad_state import speed_limit_from_message
|
||||
from openpilot.starpilot.ui.tests.test_slc_ui_runtime import START, replay_inputs
|
||||
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
|
||||
from opendbc.car.structs import car
|
||||
|
||||
|
||||
BOUNDARY = 'a' * 64
|
||||
@@ -26,24 +30,26 @@ STEP = 50_000_000
|
||||
|
||||
|
||||
class Drive:
|
||||
def __init__(self, *, confirm=False, seconds=5):
|
||||
def __init__(self, *, confirm=False, higher=5, lower=5):
|
||||
self.sm, self.cp = replay_inputs()
|
||||
self.now = START
|
||||
self.sm['carState'].canValid = True
|
||||
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 79.2
|
||||
settings = replace(parse({'SpeedLimitController': True, 'SLCPriority1': 'Maps',
|
||||
'SLCPriority2': 'Dashboard', 'SLCAdvanceSeconds': seconds}), offsets=OffsetSchedule((OffsetBand(0., None, 2.),)))
|
||||
'SLCPriority2': 'Dashboard', 'SLCAdvanceHigherSeconds': higher, 'SLCAdvanceLowerSeconds': lower}),
|
||||
offsets=OffsetSchedule((OffsetBand(0., None, 2.),)))
|
||||
self.runtime = Runtime(settings, session_id='drive', is_metric=True)
|
||||
self.tick(target=0.)
|
||||
assert self.runtime.state.acceptance.accepted.candidate.speed_mps == 20.
|
||||
self.runtime.settings = replace(settings, acceptance=acc.Policy(confirm_lower=confirm, confirm_higher=confirm))
|
||||
|
||||
def tick(self, *, current=20., target=15., distance=60., way=1, target_way=2, speed=20., stopped=False,
|
||||
alive=True, producer=41, boundary=BOUNDARY, action=None, event=None, at=None, dashboard=None):
|
||||
alive=True, producer=41, boundary=BOUNDARY, action=None, event=None, at=None, dashboard=None, vision_speed=None,
|
||||
road_status='matchedLimit'):
|
||||
self.now = self.now + STEP if at is None else at
|
||||
self.sm.advance(self.now)
|
||||
boot = self.now + 4_000_000_000
|
||||
road = packet(speedLimit=current, previewSpeedLimit=target, previewDistance=distance,
|
||||
road = packet(roadStatus=road_status, speedLimit=current, previewSpeedLimit=target, previewDistance=distance,
|
||||
previewBoundaryId=boundary if target else '', previewWayId=target_way, wayId=way, producerSession=producer,
|
||||
gpsMonoTime=boot-50_000_000, computedMonoTime=boot)
|
||||
road.logMonoTime = boot
|
||||
@@ -56,53 +62,79 @@ class Drive:
|
||||
source = self.sm['slcDashboardObservation']
|
||||
source.status, source.speedMps = ('unknown', 0.) if dashboard is None else ('valid', dashboard)
|
||||
source.observedMonoTime, source.validUntilMonoTime = self.now, self.now+1_000_000_000
|
||||
if vision_speed is not None or 'slcVisionObservation' in self.sm.data:
|
||||
camera = messaging.new_message('slcVisionObservation').slcVisionObservation
|
||||
vision = camera.init('vision')
|
||||
vision.producerSessionId, vision.status, vision.stream, vision.modelId = 'camera', 'valid', 'road', MODEL_ID
|
||||
vision.speedMps, vision.confidence, vision.supportCount = vision_speed or 0., .9, 2
|
||||
vision.episode, vision.frameId = 1, self.now // STEP
|
||||
vision.observedMonoTime, vision.validUntilMonoTime, vision.cameraFrameEofBootTime = self.now, self.now + STEP, boot
|
||||
self.sm.data['slcVisionObservation'] = camera
|
||||
self.sm.logMonoTime['slcVisionObservation'] = self.now
|
||||
for values in (self.sm.valid, self.sm.alive, self.sm.updated):
|
||||
values['slcVisionObservation'] = vision_speed is not None
|
||||
return self.runtime.step(self.sm, self.cp, now_ns=self.now, now_boot_ns=boot, request=action, cruise_event=event)
|
||||
|
||||
def action(self, state, kind):
|
||||
return Action(self.runtime.session_id, self.now+STEP, state.decisionId, state.presentationId, kind)
|
||||
|
||||
def apply(self, output):
|
||||
command = output.command.slcCruiseCommand
|
||||
card_sm = ReplaySM({'slcState': output.message.slcState}, self.now)
|
||||
approved = pa.state_current(card_sm, None, 'card-producer', self.now)
|
||||
previous = self.sm['carState'].vCruise / 3.6
|
||||
assert pa.command_applicable(command, approved, None, 'card-producer', self.now,
|
||||
previous, long_active=True, button_event=False)
|
||||
helper = VCruiseHelper(self.cp)
|
||||
helper.v_cruise_kph = helper.v_cruise_cluster_kph = previous*3.6
|
||||
helper.apply_slc_target(command.targetMps, True)
|
||||
self.sm['carState'].vCruise = helper.v_cruise_kph
|
||||
self.sm['carState'].vCruiseCluster = helper.v_cruise_cluster_kph
|
||||
event = CruiseEvent(1, self.now+STEP, previous, helper.v_cruise_kph/3.6, 'unknown',
|
||||
'card-producer', self.now+STEP, 'commandApplied', self.runtime.session_id, command_id=command.commandId)
|
||||
return self.tick(event=event)
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
assert output.result.ceiling is not None
|
||||
selected = self.sm['carState'].vCruise
|
||||
LongitudinalPlanner(self.cp).update(self.sm, cruise_ceiling=output.result.ceiling)
|
||||
assert self.sm['carState'].vCruise == selected
|
||||
return self.tick()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target', [15., 25.])
|
||||
def test_threshold_uses_physical_speed_for_both_directions(target):
|
||||
drive = Drive()
|
||||
outside = drive.tick(target=target, distance=100.01)
|
||||
assert outside.command is None and not outside.message.slcState.upcomingBoundaryId
|
||||
assert not outside.message.slcState.upcomingBoundaryId
|
||||
inside = drive.tick(target=target, distance=100.)
|
||||
state = inside.message.slcState
|
||||
assert state.speedLimit == 20. and state.upcomingSpeedLimit == target
|
||||
assert state.acceptedSpeedLimit == target and state.acceptedAdjustedLimit == target+2.
|
||||
assert inside.command.slcCruiseCommand.targetMps == target+2.
|
||||
assert inside.result.ceiling.speed_mps == target+2.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target,higher,lower,threshold', [
|
||||
(15., 1, 7, 140.), (25., 1, 7, 20.), (15., 0, 7, 140.), (25., 7, 0, 140.),
|
||||
])
|
||||
def test_direction_selects_its_own_time_before_offsets(target, higher, lower, threshold):
|
||||
drive = Drive(higher=higher, lower=lower)
|
||||
# Reversed offset targets must not change the posted-limit direction.
|
||||
drive.runtime.update_offsets(OffsetSchedule((OffsetBand(0., 18., 10.), OffsetBand(18., None, -2.))))
|
||||
outside = drive.tick(target=target, distance=threshold+.01)
|
||||
assert not outside.message.slcState.upcomingBoundaryId
|
||||
inside = drive.tick(target=target, distance=threshold)
|
||||
assert inside.message.slcState.upcomingSpeedLimit == target
|
||||
assert inside.message.slcState.acceptedSpeedLimit == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target,higher,lower', [(15., 5, 0), (25., 0, 5)])
|
||||
def test_each_direction_can_be_off_without_disabling_current_limit_adoption(target, higher, lower):
|
||||
drive = Drive(higher=higher, lower=lower)
|
||||
before = drive.tick(target=target, distance=1.).message.slcState
|
||||
assert not before.upcomingBoundaryId and before.acceptedSpeedLimit == 20.
|
||||
crossed = drive.tick(current=target, target=0., way=2).message.slcState
|
||||
assert crossed.acceptedSpeedLimit == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize('speed,stopped', [(0., True), (0., False), (20., True)])
|
||||
def test_no_first_offer_at_standstill(speed, stopped):
|
||||
drive = Drive()
|
||||
output = drive.tick(speed=speed, stopped=stopped, distance=1.)
|
||||
assert output.command is None and not output.message.slcState.upcomingBoundaryId
|
||||
assert not output.message.slcState.upcomingBoundaryId
|
||||
|
||||
|
||||
def test_off_preserves_current_limit_behavior():
|
||||
drive = Drive(seconds=0)
|
||||
drive = Drive(higher=0, lower=0)
|
||||
assert drive.tick().message.slcState.acceptedSpeedLimit == 20.
|
||||
output = drive.tick(current=15., target=0., way=2)
|
||||
assert output.message.slcState.acceptedSpeedLimit == 15.
|
||||
assert output.command is not None
|
||||
assert output.result.ceiling is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('primary,dashboard,target,early', [
|
||||
@@ -116,10 +148,45 @@ def test_preview_respects_existing_source_selection(primary, dashboard, target,
|
||||
output = drive.tick(dashboard=dashboard, target=target)
|
||||
assert bool(output.message.slcState.upcomingBoundaryId) == early
|
||||
if early:
|
||||
command, state = output.command.slcCruiseCommand, output.message.slcState
|
||||
assert command.source == 'map'
|
||||
assert pa.command_applicable(command, state, None, 'card-producer', drive.now,
|
||||
22., long_active=True, button_event=False)
|
||||
assert output.message.slcState.acceptedSource == 'map'
|
||||
assert output.result.ceiling.speed_mps == target + 2.
|
||||
|
||||
|
||||
def vision_only_settings():
|
||||
return parse({'SpeedLimitController': True, 'SLCPriority1': 'Vision', 'SLCPriority2': 'Off',
|
||||
'SLCPriority3': 'Off', 'SLCAdvanceHigherSeconds': 5, 'SLCAdvanceLowerSeconds': 5})
|
||||
|
||||
|
||||
@pytest.mark.parametrize('maps_alive', [False, True])
|
||||
def test_vision_only_ignores_map_readings_previews_and_road_changes(maps_alive):
|
||||
drive = Drive()
|
||||
drive.runtime = Runtime(vision_only_settings(), vision_enabled=True, vision_control_qualified=True)
|
||||
for vision_speed, expected, source, road in ((16., 16., 'vision', {}),
|
||||
(None, 16., 'none', {'way': 3, 'target': 25.}),
|
||||
(18., 18., 'vision', {'way': 4, 'target': 0.})):
|
||||
output = drive.tick(vision_speed=vision_speed, alive=maps_alive, **road)
|
||||
state = output.message.slcState
|
||||
assert state.source == source and state.acceptedSource == 'vision'
|
||||
assert state.effectiveCap == expected
|
||||
assert not state.upcomingBoundaryId and drive.runtime.advance.encounter is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('vision_at_switch', [False, True])
|
||||
def test_removing_maps_retires_the_preview_and_resumes_vision_decisions(vision_at_switch):
|
||||
drive = Drive()
|
||||
early = drive.tick(target=25.).message.slcState
|
||||
assert early.upcomingBoundaryId and early.acceptedSpeedLimit == 25.
|
||||
settings = vision_only_settings()
|
||||
drive.runtime.update_offsets(settings.offsets)
|
||||
drive.runtime.settings = replace(settings, acceptance=acc.Policy())
|
||||
drive.runtime.vision_enabled = drive.runtime.vision_control_qualified = True
|
||||
output = drive.tick(vision_speed=20. if vision_at_switch else None, way=3, target=0.)
|
||||
state = output.message.slcState
|
||||
assert drive.runtime.advance.encounter is None and not state.upcomingBoundaryId
|
||||
assert not state.hasPending and output.result.ceiling.speed_mps == 20.
|
||||
output = drive.tick(vision_speed=20., way=3, target=0.)
|
||||
assert output.message.slcState.source == 'vision' and not output.message.slcState.hasPending
|
||||
assert output.result.ceiling.speed_mps == 20.
|
||||
|
||||
|
||||
def test_reverse_and_inactive_control_do_not_start_advance():
|
||||
@@ -145,7 +212,7 @@ def test_confirmation_latches_on_slowdown_and_stop_then_times_out_once():
|
||||
expired = drive.tick(at=start+acc.CONFIRMATION_NS, speed=0., stopped=True).message.slcState
|
||||
assert expired.actionStatus in ('', 'timeout_reject') and not expired.hasPending
|
||||
crossed = drive.tick(current=15., target=0., way=2)
|
||||
assert not crossed.message.slcState.hasPending and crossed.command is None
|
||||
assert not crossed.message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kind', ['accept', 'reject'])
|
||||
@@ -157,25 +224,63 @@ def test_exact_deadline_action_wins_and_crossing_does_not_repeat(kind):
|
||||
action = drive.action(state, kind)
|
||||
output = drive.tick(at=start+acc.CONFIRMATION_NS, action=action)
|
||||
assert output.message.slcState.actionStatus == kind
|
||||
assert (output.command is not None) == (kind == 'accept')
|
||||
if kind == 'accept':
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 61.2
|
||||
assert output.result.ceiling.speed_mps == (17. if kind == 'accept' else 22.)
|
||||
crossed = drive.tick(current=15., target=0., way=2)
|
||||
assert not crossed.message.slcState.hasPending and crossed.command is None
|
||||
assert not crossed.message.slcState.hasPending
|
||||
|
||||
|
||||
def test_recovery_renews_presentation_without_restarting_confirmation():
|
||||
@pytest.mark.parametrize('producer', [41, 42])
|
||||
def test_recovery_keeps_deadline_and_retires_actions_only_for_a_new_producer(producer):
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
start = drive.runtime.advance.encounter.started_ns
|
||||
assert start is not None
|
||||
lost = drive.tick(alive=False)
|
||||
assert lost.message.slcState.hasPending and lost.command is None
|
||||
assert lost.message.slcState.decisionId == first.decisionId and lost.message.slcState.presentationId == 0
|
||||
recovered = drive.tick(at=start+9_000_000_000, producer=42).message.slcState
|
||||
assert recovered.hasPending and recovered.presentationId != first.presentationId
|
||||
assert lost.message.slcState.hasPending
|
||||
assert lost.message.slcState.decisionId == first.decisionId and lost.message.slcState.presentationId == first.presentationId
|
||||
recovered = drive.tick(at=start+9_000_000_000, producer=producer).message.slcState
|
||||
assert recovered.hasPending and (recovered.presentationId == first.presentationId) == (producer == 41)
|
||||
expired = drive.tick(at=start+acc.CONFIRMATION_NS+1)
|
||||
assert not expired.message.slcState.hasPending and expired.command is None
|
||||
assert not expired.message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kind', ['accept', 'reject'])
|
||||
@pytest.mark.parametrize('gap', [{'alive': False}, {'target': 0.}], ids=['source-lost', 'preview-missing'])
|
||||
@pytest.mark.parametrize('wheel', [False, True], ids=['touch', 'wheel'])
|
||||
def test_source_gap_keeps_early_confirmation_actionable_and_does_not_repeat_at_the_boundary(kind, gap, wheel):
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
lost = drive.tick(**gap).message.slcState
|
||||
assert lost.presentationId == first.presentationId
|
||||
if wheel:
|
||||
pending = physical_actions.pending_confirmation(ReplaySM({'slcState': lost}, drive.now), drive.now, long_active=True)
|
||||
assert pending is not None and pending.presentation_id == first.presentationId
|
||||
drive.cp.pcmCruise = False
|
||||
helper = VCruiseHelper(drive.cp)
|
||||
helper.v_cruise_kph = drive.sm['carState'].vCruise
|
||||
button = 'accelCruise' if kind == 'accept' else 'decelCruise'
|
||||
cs = car.CarState(cruiseState={'available': True}, buttonEvents=[car.CarState.ButtonEvent(type=button, pressed=True)])
|
||||
helper.update_v_cruise(cs, True, True, pending, slc_enabled=True)
|
||||
consumed = helper.slc_consumed_button
|
||||
assert consumed is not None
|
||||
cs.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
|
||||
helper.update_v_cruise(cs, True, True, slc_enabled=True)
|
||||
assert helper.v_cruise_kph == drive.sm['carState'].vCruise
|
||||
stamp = drive.now + STEP
|
||||
event = CruiseEvent(1, stamp, 0., 0., consumed.button, 'card-producer', stamp,
|
||||
'confirmationAccept' if kind == 'accept' else 'confirmationReject',
|
||||
consumed.pending.session_id, consumed.pending.decision_id, consumed.pending.presentation_id)
|
||||
output = drive.tick(**gap, event=event)
|
||||
else:
|
||||
output = drive.tick(**gap, action=drive.action(first, kind))
|
||||
assert not output.result.errors and output.message.slcState.actionStatus == kind
|
||||
assert not output.message.slcState.hasPending
|
||||
assert output.result.ceiling.speed_mps == (17. if kind == 'accept' else 22.)
|
||||
encounter = drive.runtime.advance.encounter
|
||||
assert (encounter.accepted_ns is not None) == (kind == 'accept')
|
||||
assert encounter.declined == (kind == 'reject')
|
||||
crossed = drive.tick(current=15., target=0., way=2)
|
||||
assert not crossed.message.slcState.hasPending
|
||||
|
||||
|
||||
def test_disengagement_cancels_early_confirmation_and_reengagement_gets_a_fresh_deadline():
|
||||
@@ -186,7 +291,7 @@ def test_disengagement_cancels_early_confirmation_and_reengagement_gets_a_fresh_
|
||||
drive.sm['selfdriveState'].enabled = False
|
||||
drive.sm['controlsState'].longControlState = 'off'
|
||||
inactive = drive.tick(at=start+2_000_000_000)
|
||||
assert not inactive.message.slcState.hasPending and inactive.command is None
|
||||
assert not inactive.message.slcState.hasPending
|
||||
assert not drive.runtime.advance.encounter.declined
|
||||
drive.sm['carControl'].enabled = drive.sm['carControl'].longActive = True
|
||||
drive.sm['selfdriveState'].enabled = True
|
||||
@@ -196,18 +301,44 @@ def test_disengagement_cancels_early_confirmation_and_reengagement_gets_a_fresh_
|
||||
assert resumed.pendingSinceMonoTime == drive.now
|
||||
restarted = drive.now
|
||||
stale_action = drive.tick(action=drive.action(first, 'accept'))
|
||||
assert stale_action.message.slcState.hasPending and stale_action.command is None
|
||||
assert stale_action.message.slcState.hasPending
|
||||
assert drive.tick(at=restarted+9_000_000_000).message.slcState.hasPending
|
||||
assert not drive.tick(at=restarted+acc.CONFIRMATION_NS).message.slcState.hasPending
|
||||
assert drive.runtime.advance.encounter.declined
|
||||
|
||||
|
||||
def test_fresh_different_boundary_supersedes_without_accepting_stale_action():
|
||||
@pytest.mark.parametrize('distance', [60., 160.])
|
||||
def test_fresh_different_boundary_supersedes_without_accepting_stale_action(distance):
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
changed = drive.tick(boundary='b'*64, target=25., target_way=3, action=drive.action(first, 'accept'))
|
||||
assert changed.message.slcState.hasPending and changed.message.slcState.decisionId != first.decisionId
|
||||
assert changed.message.slcState.pendingSpeedLimit == 25. and changed.command is None
|
||||
changed = drive.tick(boundary='b'*64, target=25., target_way=3, distance=distance, action=drive.action(first, 'accept'))
|
||||
state = changed.message.slcState
|
||||
assert state.acceptedSpeedLimit == 20.
|
||||
assert state.hasPending == (distance == 60.)
|
||||
assert state.decisionId != first.decisionId
|
||||
if state.hasPending:
|
||||
assert state.pendingSpeedLimit == 25.
|
||||
else:
|
||||
nearer = drive.tick(boundary='b'*64, target=25., target_way=3).message.slcState
|
||||
assert nearer.hasPending and nearer.pendingSpeedLimit == 25.
|
||||
assert nearer.decisionId != first.decisionId
|
||||
|
||||
|
||||
def test_boundary_id_change_alone_does_not_disprove_the_latched_limit():
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
changed = drive.tick(boundary='b'*64, speed=5., distance=55.).message.slcState
|
||||
assert changed.hasPending and changed.decisionId == first.decisionId
|
||||
assert changed.pendingSinceMonoTime == first.pendingSinceMonoTime
|
||||
|
||||
|
||||
def test_far_replacement_does_not_reopen_a_declined_limit_if_it_returns():
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
declined = drive.tick(action=drive.action(first, 'reject')).message.slcState
|
||||
assert declined.actionStatus == 'reject'
|
||||
assert not drive.tick(boundary='b'*64, target=25., target_way=3, distance=160.).message.slcState.hasPending
|
||||
assert not drive.tick().message.slcState.hasPending
|
||||
|
||||
|
||||
def test_short_zone_can_qualify_exit_immediately_without_hidden_dwell():
|
||||
@@ -216,30 +347,36 @@ def test_short_zone_can_qualify_exit_immediately_without_hidden_dwell():
|
||||
exit_offer = drive.tick(current=15., way=2, target=20., target_way=3, boundary='b'*64, distance=10.)
|
||||
assert exit_offer.message.slcState.speedLimit == 15.
|
||||
assert exit_offer.message.slcState.acceptedSpeedLimit == 20.
|
||||
assert exit_offer.command.slcCruiseCommand.targetMps == 22.
|
||||
assert exit_offer.result.ceiling.speed_mps == 22.
|
||||
assert drive.runtime.history_limit.candidate.speed_mps == 15.
|
||||
drive.runtime.reset()
|
||||
assert drive.tick(alive=False).message.slcState.acceptedSpeedLimit == 15.
|
||||
|
||||
|
||||
def test_time_edits_keep_decision_and_off_withdraws_without_set_rollback():
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
drive.runtime.update_advance(1)
|
||||
drive.runtime.update_advance(5, 1)
|
||||
second = drive.tick(distance=60.).message.slcState
|
||||
assert second.decisionId == first.decisionId and second.pendingSinceMonoTime == first.pendingSinceMonoTime
|
||||
drive.runtime.update_advance(0)
|
||||
drive.runtime.update_advance(5, 0)
|
||||
assert not drive.tick().message.slcState.hasPending
|
||||
drive.runtime.update_advance(10)
|
||||
drive.runtime.update_advance(5, 10)
|
||||
assert not drive.tick().message.slcState.hasPending
|
||||
assert drive.tick(current=15., target=0., way=2).message.slcState.hasPending
|
||||
|
||||
|
||||
def test_applied_set_survives_off_source_loss_and_crossing_without_reissue():
|
||||
@pytest.mark.parametrize('target', [15., 25.])
|
||||
def test_applied_cap_survives_advance_off_source_loss_and_crossing(target):
|
||||
drive = Drive()
|
||||
applied = drive.apply(drive.tick())
|
||||
assert applied.command is None and applied.message.slcState.commandStatus == 'applied'
|
||||
drive.runtime.update_advance(0)
|
||||
for changes in ({}, {'alive': False}, {'current': 15., 'target': 0., 'way': 2}):
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 140.
|
||||
applied = drive.tick(target=target)
|
||||
assert applied.result.ceiling.speed_mps == target+2.
|
||||
drive.runtime.update_advance(0 if target > 20. else 5, 0 if target < 20. else 5)
|
||||
for changes in ({'target': target}, {'target': target, 'alive': False}, {'current': target, 'target': 0., 'way': 2}):
|
||||
output = drive.tick(**changes)
|
||||
assert output.command is None and drive.sm['carState'].vCruise == 61.
|
||||
assert drive.sm['carState'].vCruise == 140.
|
||||
assert output.result.ceiling.speed_mps == target+2.
|
||||
|
||||
|
||||
def test_wheel_adopt_keeps_meaning_the_current_displayed_limit():
|
||||
@@ -249,7 +386,7 @@ def test_wheel_adopt_keeps_meaning_the_current_displayed_limit():
|
||||
assert not adopted.result.errors
|
||||
assert adopted.message.slcState.acceptedSpeedLimit == 20.
|
||||
assert adopted.message.slcState.actionStatus == 'adopt'
|
||||
assert adopted.command.slcCruiseCommand.targetMps == 22.
|
||||
assert adopted.result.ceiling.speed_mps == 22.
|
||||
assert not adopted.message.slcState.upcomingBoundaryId
|
||||
|
||||
|
||||
@@ -257,20 +394,21 @@ def test_stale_current_adopt_cannot_cancel_or_replace_a_fresh_producer_proposal(
|
||||
drive = Drive()
|
||||
applied = drive.apply(drive.tick())
|
||||
rejected = drive.tick(producer=42, action=drive.action(applied.message.slcState, 'adopt'))
|
||||
assert not rejected.result.errors and rejected.command is None
|
||||
assert not rejected.result.errors
|
||||
assert rejected.message.slcState.acceptedSpeedLimit == 15.
|
||||
assert rejected.message.slcState.upcomingBoundaryId == BOUNDARY
|
||||
assert not drive.runtime.advance.encounter.disabled
|
||||
|
||||
|
||||
def test_manual_set_override_and_current_adoption_survive_boundary():
|
||||
def test_driver_ceiling_changes_keep_advance_and_explicit_current_adoption():
|
||||
drive = Drive()
|
||||
drive.apply(drive.tick())
|
||||
previous = drive.sm['carState'].vCruise / 3.6
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 80.
|
||||
event = CruiseEvent(2, drive.now+STEP, previous, 80./3.6, 'accel', 'card-producer', drive.now+STEP)
|
||||
overridden = drive.tick(event=event)
|
||||
assert overridden.message.slcState.driverOverrideActive and overridden.command is None
|
||||
assert not overridden.message.slcState.driverOverrideActive
|
||||
assert overridden.result.ceiling.speed_mps == 17.
|
||||
withdrawn = drive.tick()
|
||||
assert withdrawn.message.slcState.presentationId > 0
|
||||
current = drive.tick(action=drive.action(withdrawn.message.slcState, 'adopt'))
|
||||
@@ -289,25 +427,14 @@ def test_offset_edit_uses_upcoming_band_without_changing_decision_or_current_sig
|
||||
state = output.message.slcState
|
||||
assert state.acceptedMonoTime == accepted_at and not state.hasPending
|
||||
assert state.speedLimit == 20. and state.presentationAdjustedLimit == 23.
|
||||
assert state.acceptedAdjustedLimit == 16. and output.command.slcCruiseCommand.targetMps == 16.
|
||||
assert state.acceptedAdjustedLimit == 16. and output.result.ceiling.speed_mps == 16.
|
||||
|
||||
|
||||
def test_upcoming_commands_bind_boundary_and_preserve_card_fences():
|
||||
def test_upcoming_acceptance_never_commands_card_set_speed():
|
||||
drive = Drive()
|
||||
output = drive.tick()
|
||||
state, command = output.message.slcState, output.command.slcCruiseCommand
|
||||
def applies(**changes):
|
||||
return pa.command_applicable(command, state, None, 'card-producer', drive.now,
|
||||
changes.pop('selected', 22.), long_active=changes.pop('active', True), button_event=changes.pop('button', False))
|
||||
assert applies()
|
||||
assert not applies(selected=23.) and not applies(active=False) and not applies(button=True)
|
||||
command.upcomingBoundaryId = 'b' * 64
|
||||
assert not applies()
|
||||
command.upcomingBoundaryId = ''
|
||||
assert not applies()
|
||||
command.upcomingBoundaryId = BOUNDARY
|
||||
state.upcomingSpeedLimit = float('nan')
|
||||
assert not applies()
|
||||
assert output.result.ceiling.speed_mps == 17.
|
||||
assert drive.sm['carState'].vCruise == pytest.approx(79.2)
|
||||
|
||||
|
||||
def test_existing_confirmation_resolver_shows_proposal_without_changing_current_sign():
|
||||
@@ -349,48 +476,71 @@ def test_actual_go_fixture_qualifies_current_and_upcoming_fields_with_host_schem
|
||||
assert source.upcoming.boundary_id == BOUNDARY and source.upcoming.way_id == 2 and source.upcoming.distance_m == 60.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw', [b'0', b'5', b'10', b'-1', b'11', b'1.5', b'broken', b'\xff', b'5\n', b'05'])
|
||||
def test_saved_advance_values_are_optional_bounded_and_not_rewritten(tmp_path, raw):
|
||||
@pytest.mark.parametrize('key', ADVANCE_KEYS)
|
||||
@pytest.mark.parametrize('raw', [b'0', b'1', b'5', b'10', b'-1', b'11', b'1.5', b'broken', b'\xff', b'5\n', b'05'])
|
||||
def test_saved_advance_values_are_independent_bounded_and_not_rewritten(tmp_path, key, raw):
|
||||
params = Params(str(tmp_path))
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
path = Path(params.get_param_path('SLCAdvanceSeconds'))
|
||||
other = next(name for name in ADVANCE_KEYS if name != key)
|
||||
params.put(other, 7, block=True)
|
||||
path = Path(params.get_param_path(key))
|
||||
path.write_bytes(raw)
|
||||
settings = read_params(params)
|
||||
assert settings.enabled and not settings.errors
|
||||
assert settings.advance_seconds == ({b'5': 5, b'10': 10}.get(raw, 0))
|
||||
values = dict(zip(ADVANCE_KEYS, (settings.advance_higher_seconds, settings.advance_lower_seconds), strict=True))
|
||||
assert values[key] == ({b'1': 1, b'5': 5, b'10': 10}.get(raw, 0))
|
||||
assert values[other] == 7
|
||||
assert path.read_bytes() == raw
|
||||
|
||||
|
||||
def test_time_only_lifecycle_refresh_keeps_session_decision_deadline_and_decline(tmp_path):
|
||||
def test_timer_rejects_symlink_without_disabling_other_direction(tmp_path):
|
||||
params = Params(str(tmp_path))
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put('SLCAdvanceLowerSeconds', 6, block=True)
|
||||
source = tmp_path / 'linked-value'
|
||||
source.write_bytes(b'9')
|
||||
Path(params.get_param_path('SLCAdvanceHigherSeconds')).symlink_to(source)
|
||||
settings = read_params(params)
|
||||
assert settings.enabled
|
||||
assert (settings.advance_higher_seconds, settings.advance_lower_seconds) == (0, 6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target,key', [(15., 'SLCAdvanceLowerSeconds'), (25., 'SLCAdvanceHigherSeconds')])
|
||||
def test_time_only_lifecycle_refresh_keeps_session_decision_deadline_and_decline(tmp_path, target, key):
|
||||
from openpilot.selfdrive.controls.plannerd import SavedSlcLifecycle
|
||||
params = Params(str(tmp_path))
|
||||
for key, value in {'SpeedLimitController': True, 'SLCConfirmation': True, 'IsMetric': True}.items():
|
||||
params.put_bool(key, value, block=True)
|
||||
for name, value in {'SpeedLimitController': True, 'IsMetric': True}.items():
|
||||
params.put_bool(name, value, block=True)
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
params.put('SLCPriority1', 'Map Data', block=True)
|
||||
params.put('SLCAdvanceSeconds', 5, block=True)
|
||||
params.put(key, 5, block=True)
|
||||
drive = Drive()
|
||||
lifecycle = SavedSlcLifecycle(params, drive.cp, {'SLC_REPLAY_RUNTIME': '1'})
|
||||
runtime, _ = lifecycle.refresh(START)
|
||||
drive.runtime = runtime
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 72.
|
||||
drive.tick(target=0.)
|
||||
first = drive.tick().message.slcState
|
||||
first = drive.tick(target=target).message.slcState
|
||||
assert first.hasPending
|
||||
params.put('SLCAdvanceSeconds', 1, block=True)
|
||||
reject = drive.action(first, 'reject')
|
||||
queued = deque([reject])
|
||||
params.put(key, 1, block=True)
|
||||
now = drive.now+1_000_000_000
|
||||
assert lifecycle.refresh(now)[0] is runtime
|
||||
pending = drive.tick(at=now).message.slcState
|
||||
assert lifecycle.refresh(now, pending_actions=queued)[0] is runtime
|
||||
pending = drive.tick(at=now, target=target).message.slcState
|
||||
assert pending.decisionId == first.decisionId and pending.pendingSinceMonoTime == first.pendingSinceMonoTime
|
||||
drive.tick(action=drive.action(pending, 'reject'))
|
||||
params.put('SLCAdvanceSeconds', 10, block=True)
|
||||
assert list(queued) == [reject]
|
||||
drive.tick(target=target, action=queued.popleft())
|
||||
params.put(key, 10, block=True)
|
||||
now = drive.now+1_000_000_000
|
||||
assert lifecycle.refresh(now)[0] is runtime
|
||||
assert not drive.tick(at=now).message.slcState.hasPending
|
||||
crossed = drive.tick(current=15., target=0., way=2)
|
||||
assert not crossed.message.slcState.hasPending and crossed.command is None
|
||||
assert not drive.tick(at=now, target=target).message.slcState.hasPending
|
||||
crossed = drive.tick(current=target, target=0., way=2)
|
||||
assert not crossed.message.slcState.hasPending
|
||||
|
||||
|
||||
def test_existing_settings_owner_saves_guards_and_repairs_one_timing_option(tmp_path):
|
||||
@pytest.mark.parametrize('key', ADVANCE_KEYS)
|
||||
def test_existing_settings_owner_saves_guards_and_repairs_each_timing_option(tmp_path, key):
|
||||
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
||||
from openpilot.starpilot.ui.feature_settings_state import row_change, row_default
|
||||
params = Params(str(tmp_path))
|
||||
@@ -403,22 +553,422 @@ def test_existing_settings_owner_saves_guards_and_repairs_one_timing_option(tmp_
|
||||
def rows():
|
||||
return owner.snapshot('slc', parked=True, system_long=True, lateral_context=True, metric=True).rows
|
||||
def row():
|
||||
return next(item for item in rows() if item.key == 'SLCAdvanceSeconds')
|
||||
return next(item for item in rows() if item.key == key)
|
||||
keys = [item.key for item in rows()]
|
||||
assert keys.index('SLCAdvanceSeconds') == keys.index('SLCConfirmation')+1
|
||||
assert keys.index(ADVANCE_KEYS[0]) == keys.index('SLCConfirmation')+1
|
||||
assert keys.index(ADVANCE_KEYS[1]) == keys.index(ADVANCE_KEYS[0])+1
|
||||
assert 'SLCAdvanceSeconds' not in keys
|
||||
assert row().available and row().value == 'Off'
|
||||
request = row_change(row())
|
||||
assert request is not None
|
||||
assert owner.apply(request)
|
||||
assert read_params(params).advance_seconds == 1
|
||||
settings = read_params(params)
|
||||
assert (settings.advance_higher_seconds, settings.advance_lower_seconds) == ((1, 0) if key == ADVANCE_KEYS[0] else (0, 1))
|
||||
assert not owner.apply(request)
|
||||
request = row_change(row())
|
||||
assert request is not None
|
||||
allowed[0] = False
|
||||
assert not owner.apply(request)
|
||||
allowed[0] = True
|
||||
Path(params.get_param_path('SLCAdvanceSeconds')).write_bytes(b'broken')
|
||||
Path(params.get_param_path(key)).write_bytes(b'broken')
|
||||
assert row().choices == ('Off',)
|
||||
request = row_default(row())
|
||||
assert request is not None and owner.apply(request)
|
||||
assert read_params(params).advance_seconds == 0 and read_params(params).enabled
|
||||
settings = read_params(params)
|
||||
assert settings.advance_higher_seconds == settings.advance_lower_seconds == 0 and settings.enabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target', [15., 25.])
|
||||
def test_editing_the_other_direction_does_not_cancel_a_latched_choice(target):
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick(target=target).message.slcState
|
||||
drive.runtime.update_advance(5 if target > 20. else 0, 5 if target < 20. else 0)
|
||||
retained = drive.tick(target=target, speed=0., stopped=True).message.slcState
|
||||
assert retained.hasPending and retained.decisionId == first.decisionId
|
||||
assert retained.pendingSinceMonoTime == first.pendingSinceMonoTime
|
||||
drive.runtime.update_advance(0 if target > 20. else 5, 0 if target < 20. else 5)
|
||||
assert not drive.tick(target=target).message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('higher', [0, 5])
|
||||
def test_revised_limit_at_same_boundary_retires_old_choice(higher):
|
||||
drive = Drive(confirm=True, higher=higher, lower=5)
|
||||
first = drive.tick(target=15.).message.slcState
|
||||
assert first.hasPending
|
||||
revised = drive.tick(target=25., action=drive.action(first, 'accept')).message.slcState
|
||||
assert revised.acceptedSpeedLimit == 20.
|
||||
assert revised.hasPending == bool(higher)
|
||||
if higher:
|
||||
assert revised.pendingSpeedLimit == 25.
|
||||
assert revised.decisionId != first.decisionId
|
||||
|
||||
|
||||
@pytest.mark.parametrize('boundary,way', [(BOUNDARY, 2), ('b'*64, 3)])
|
||||
def test_ineligible_boundary_revision_keeps_the_applied_cap(boundary, way):
|
||||
drive = Drive(higher=0, lower=5)
|
||||
assert drive.tick(target=15.).message.slcState.acceptedSpeedLimit == 15.
|
||||
revised = drive.tick(target=25., boundary=boundary, target_way=way).message.slcState
|
||||
assert revised.acceptedSpeedLimit == 15.
|
||||
assert not revised.upcomingBoundaryId
|
||||
assert drive.tick(current=25., target=0., way=way).message.slcState.acceptedSpeedLimit == 25.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('target', [15., 25.])
|
||||
@pytest.mark.parametrize('confirm', [False, True])
|
||||
def test_confirmed_destination_corrects_an_early_cap_even_at_the_approach_speed(target, confirm):
|
||||
drive = Drive(confirm=confirm)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 108.
|
||||
early = drive.tick(target=target).message.slcState
|
||||
if confirm:
|
||||
early = drive.tick(target=target, action=drive.action(early, 'accept')).message.slcState
|
||||
assert early.acceptedSpeedLimit == target
|
||||
|
||||
for inputs in ({'alive': False}, {'target': 0.}):
|
||||
held = drive.tick(**inputs).message.slcState
|
||||
assert held.effectiveCap == target + 2.
|
||||
assert not held.hasPending
|
||||
|
||||
arrived = drive.tick(current=20., target=0., way=2).message.slcState
|
||||
assert not arrived.hasPending
|
||||
assert arrived.acceptedSpeedLimit == 20.
|
||||
assert arrived.effectiveCap == 22.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('confirm', [False, True])
|
||||
def test_fresh_other_road_reverts_applied_preview_even_at_the_same_approach_speed(confirm):
|
||||
drive = Drive(confirm=confirm)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 108.
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
if confirm:
|
||||
preview = drive.tick(target=25., action=drive.action(preview, 'accept')).message.slcState
|
||||
assert preview.effectiveCap == 27.
|
||||
|
||||
assert drive.tick(target=25., way=3).message.slcState.effectiveCap == 27.
|
||||
assert drive.tick(alive=False).message.slcState.effectiveCap == 27.
|
||||
assert drive.tick(target=0., way=3).message.slcState.effectiveCap == 27.
|
||||
departed = drive.tick(target=0., way=4)
|
||||
assert departed.message.slcState.effectiveCap == 22.
|
||||
assert not departed.message.slcState.hasPending
|
||||
assert departed.message.slcState.acceptedSpeedLimit == 20.
|
||||
|
||||
|
||||
def test_departure_keeps_the_preview_limit_when_a_preferred_live_source_confirms_it():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 108.
|
||||
drive.runtime.settings = replace(drive.runtime.settings,
|
||||
selection=parse({'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Maps'}).selection)
|
||||
preview = drive.tick(target=15.).message.slcState
|
||||
assert drive.tick(target=15., action=drive.action(preview, 'accept')).message.slcState.effectiveCap == 17.
|
||||
for _ in range(2):
|
||||
state = drive.tick(way=3, target=0., dashboard=15.).message.slcState
|
||||
assert state.source == 'dashboard' and state.acceptedSpeedLimit == 15.
|
||||
assert state.effectiveCap == 17. and not state.hasPending
|
||||
|
||||
|
||||
def test_departure_restores_the_previous_cap_while_a_different_road_limit_waits():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 108.
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
assert drive.tick(target=25., action=drive.action(preview, 'accept')).message.slcState.effectiveCap == 27.
|
||||
# The preview still describes the same approach after a tile refresh.
|
||||
assert drive.tick(target=25., way=3, boundary='b'*64).message.slcState.effectiveCap == 27.
|
||||
state = drive.tick(current=16., way=4, target=0.).message.slcState
|
||||
assert state.acceptedSpeedLimit == 20. and state.effectiveCap == 22.
|
||||
assert state.hasPending and state.pendingSpeedLimit == 16.
|
||||
accepted = drive.tick(current=16., way=4, target=0., action=drive.action(state, 'accept')).message.slcState
|
||||
assert accepted.acceptedSpeedLimit == 16. and accepted.effectiveCap == 18.
|
||||
|
||||
|
||||
def test_departure_without_previous_acceptance_drops_the_preview_cap():
|
||||
drive = Drive(confirm=True)
|
||||
drive.runtime = Runtime(drive.runtime.settings)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 108.
|
||||
current = drive.tick(target=0.).message.slcState
|
||||
declined = drive.tick(target=0., action=drive.action(current, 'reject')).message.slcState
|
||||
assert declined.actionStatus == 'reject' and not declined.hasAccepted
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
assert drive.tick(target=25., action=drive.action(preview, 'accept')).message.slcState.hasCeiling
|
||||
state = drive.tick(current=16., way=3, target=0.).message.slcState
|
||||
assert not state.hasAccepted and not state.hasCeiling
|
||||
assert state.hasPending and state.pendingSpeedLimit == 16.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('accept_replacement', [False, True], ids=['pending', 'accepted'])
|
||||
def test_departure_restores_72_after_90_preview_is_replaced_by_108(accept_replacement):
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.update_offsets(vision_only_settings().offsets)
|
||||
first = drive.tick(target=25.).message.slcState
|
||||
assert drive.tick(target=25., action=drive.action(first, 'accept')).message.slcState.effectiveCap == 25.
|
||||
revised = {'target': 30., 'target_way': 3, 'boundary': 'b'*64}
|
||||
replacement = drive.tick(**revised).message.slcState
|
||||
assert replacement.hasPending and replacement.pendingSpeedLimit == 30.
|
||||
if accept_replacement:
|
||||
assert drive.tick(**revised, action=drive.action(replacement, 'accept')).message.slcState.effectiveCap == 30.
|
||||
# A late response to the old preview must not accept the new road's limit.
|
||||
state = drive.tick(current=16., way=4, target=0., action=drive.action(replacement, 'accept')).message.slcState
|
||||
assert state.acceptedSpeedLimit == state.effectiveCap == 20.
|
||||
assert state.hasPending and state.pendingSpeedLimit == 16.
|
||||
assert drive.tick(current=16., way=4, target=0.).message.slcState.decisionId == state.decisionId
|
||||
|
||||
|
||||
def test_far_revision_retires_only_the_unanswered_preview():
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(first, 'accept'))
|
||||
pending = drive.tick(target=30., target_way=3, boundary='b'*64).message.slcState
|
||||
assert pending.hasPending
|
||||
far = drive.tick(target=32., target_way=5, boundary='c'*64, distance=160., action=drive.action(pending, 'accept')).message.slcState
|
||||
assert not far.hasPending and far.acceptedSpeedLimit == 25.
|
||||
departed = drive.tick(current=16., way=4, target=0.).message.slcState
|
||||
assert departed.acceptedSpeedLimit == 20. and departed.pendingSpeedLimit == 16.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('maps_alive', [False, True])
|
||||
def test_preferred_dashboard_can_restore_the_approach_limit(maps_alive):
|
||||
drive = Drive()
|
||||
drive.runtime.settings = replace(drive.runtime.settings,
|
||||
selection=parse({'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Maps'}).selection)
|
||||
assert drive.tick(target=25.).message.slcState.acceptedSpeedLimit == 25.
|
||||
assert drive.tick(alive=False).message.slcState.acceptedSpeedLimit == 25.
|
||||
state = drive.tick(alive=maps_alive, target=25., dashboard=20.).message.slcState
|
||||
assert state.source == 'dashboard' and state.acceptedSpeedLimit == 20.
|
||||
assert not state.hasPending and not state.upcomingBoundaryId
|
||||
|
||||
|
||||
@pytest.mark.parametrize('confirm_higher', [False, True])
|
||||
def test_corrected_arrival_compares_direction_to_the_accepted_road(confirm_higher):
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(preview, 'accept'))
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy(confirm_lower=not confirm_higher,
|
||||
confirm_higher=confirm_higher))
|
||||
arrived = drive.tick(current=22.5, way=2, target=0.).message.slcState
|
||||
assert arrived.hasPending == confirm_higher
|
||||
assert arrived.acceptedSpeedLimit == (20. if confirm_higher else 22.5)
|
||||
|
||||
|
||||
def test_same_approach_correction_does_not_reopen_the_old_preview():
|
||||
drive = Drive(confirm=True)
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(preview, 'accept'))
|
||||
corrected = drive.tick(current=16., target=25.).message.slcState
|
||||
assert corrected.effectiveCap == 22. and corrected.pendingSpeedLimit == 16.
|
||||
next_frame = drive.tick(current=16., target=25.).message.slcState
|
||||
assert next_frame.decisionId == corrected.decisionId and not next_frame.upcomingBoundaryId
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,preview', [('Dashboard', 25.), ('Lowest', 25.), ('Highest', 15.)])
|
||||
@pytest.mark.parametrize('confirm', [False, True])
|
||||
def test_source_recovery_does_not_reopen_the_same_early_offer(mode, preview, confirm):
|
||||
drive = Drive(confirm=confirm)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': mode, 'SLCPriority2': 'Maps'}).selection)
|
||||
first = drive.tick(target=preview).message.slcState
|
||||
assert first.hasPending == confirm and first.upcomingBoundaryId
|
||||
for dashboard, boundary in ((20., BOUNDARY), (None, 'b'*64), (20., 'b'*64), (None, BOUNDARY)):
|
||||
shown = drive.tick(target=preview, dashboard=dashboard, boundary=boundary).message.slcState
|
||||
assert not shown.hasPending and not shown.upcomingBoundaryId and shown.effectiveCap == 22.
|
||||
arrived = drive.tick(current=preview, way=2, target=0.).message.slcState
|
||||
assert arrived.hasPending == confirm
|
||||
assert arrived.acceptedSpeedLimit == (20. if confirm else preview)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,preview,rival', [('Lowest', 25., 26.), ('Highest', 15., 14.)])
|
||||
def test_extrema_keep_preview_when_maps_still_wins(mode, preview, rival):
|
||||
drive = Drive()
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': mode}).selection)
|
||||
assert drive.tick(target=preview).message.slcState.acceptedSpeedLimit == preview
|
||||
assert drive.tick(target=preview, dashboard=rival).message.slcState.acceptedSpeedLimit == preview
|
||||
|
||||
|
||||
def test_map_gap_allows_a_qualified_different_fallback_limit():
|
||||
drive = Drive()
|
||||
assert drive.tick(target=25.).message.slcState.acceptedSpeedLimit == 25.
|
||||
assert drive.tick(alive=False, dashboard=20.).message.slcState.acceptedSpeedLimit == 25.
|
||||
fallback = drive.tick(alive=False, dashboard=16.).message.slcState
|
||||
assert fallback.acceptedSpeedLimit == 16. and fallback.source == 'dashboard'
|
||||
|
||||
|
||||
def test_pending_replacement_keeps_its_deadline_on_arrival_and_retires_the_old_cap():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
first = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(first, 'accept'))
|
||||
replacement = drive.tick(target=30., target_way=3, boundary='b'*64).message.slcState
|
||||
arrived = drive.tick(current=30., way=3, target=20., target_way=4, boundary='c'*64, distance=10.).message.slcState
|
||||
assert arrived.effectiveCap == 22. and arrived.pendingSpeedLimit == 30.
|
||||
assert (arrived.decisionId, arrived.pendingSinceMonoTime) == (replacement.decisionId, replacement.pendingSinceMonoTime)
|
||||
|
||||
|
||||
def test_dashboard_only_ignores_maps_and_vision_during_a_source_gap():
|
||||
drive = Drive()
|
||||
drive.runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Off',
|
||||
'SLCAdvanceHigherSeconds': 5}), vision_enabled=True, vision_control_qualified=True)
|
||||
for dashboard, expected in ((18., 18.), (None, 18.), (16., 16.)):
|
||||
state = drive.tick(dashboard=dashboard, vision_speed=22., target=25., distance=10.).message.slcState
|
||||
assert state.effectiveCap == expected and state.acceptedSource == 'dashboard'
|
||||
assert not state.upcomingBoundaryId and drive.runtime.advance.encounter is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,preview,rival', [('Lowest', 25., 26.), ('Highest', 15., 14.)])
|
||||
def test_losing_rival_cannot_replace_a_held_cap_while_maps_is_current(mode, preview, rival):
|
||||
drive = Drive()
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': mode}).selection)
|
||||
assert drive.tick(target=preview).message.slcState.acceptedSpeedLimit == preview
|
||||
state = drive.tick(target=0., dashboard=rival).message.slcState
|
||||
assert state.source == 'map' and state.acceptedSpeedLimit == preview
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode', ['Highest', 'Lowest'])
|
||||
def test_applied_cap_and_pending_replacement_compete_independently(mode):
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': mode}).selection)
|
||||
first = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(first, 'accept'))
|
||||
revised = {'target': 15., 'target_way': 3, 'boundary': 'b'*64}
|
||||
pending = drive.tick(**revised).message.slcState
|
||||
assert pending.pendingSpeedLimit == 15.
|
||||
if mode == 'Highest':
|
||||
state = drive.tick(**revised, dashboard=20., action=drive.action(pending, 'accept')).message.slcState
|
||||
assert state.acceptedSpeedLimit == 25. and not state.hasPending
|
||||
assert not drive.tick(**revised).message.slcState.hasPending
|
||||
else:
|
||||
state = drive.tick(**revised, dashboard=20.).message.slcState
|
||||
assert state.acceptedSpeedLimit == 20. and state.pendingSpeedLimit == 15.
|
||||
assert (state.decisionId, state.pendingSinceMonoTime) == (pending.decisionId, pending.pendingSinceMonoTime)
|
||||
assert drive.tick(**revised, dashboard=20., action=drive.action(state, 'accept')).message.slcState.acceptedSpeedLimit == 15.
|
||||
|
||||
|
||||
def test_corrected_current_limit_prompt_precedes_the_exit_preview():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
preview = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(preview, 'accept'))
|
||||
zone = {'current': 16., 'way': 2, 'target': 30., 'target_way': 3, 'boundary': 'b'*64, 'distance': 10.}
|
||||
corrected = drive.tick(**zone).message.slcState
|
||||
next_frame = drive.tick(**zone).message.slcState
|
||||
assert next_frame.effectiveCap == 22. and next_frame.pendingSpeedLimit == 16.
|
||||
assert (next_frame.decisionId, next_frame.pendingSinceMonoTime) == (corrected.decisionId, corrected.pendingSinceMonoTime)
|
||||
accepted = drive.tick(**zone, action=drive.action(next_frame, 'accept')).message.slcState
|
||||
assert accepted.acceptedSpeedLimit == 16.
|
||||
assert drive.tick(**zone).message.slcState.pendingSpeedLimit == 30.
|
||||
|
||||
|
||||
def test_early_limit_replaces_history_only_after_current_road_confirms_it():
|
||||
drive = Drive()
|
||||
early = drive.tick(target=25.).message.slcState
|
||||
assert early.acceptedSpeedLimit == 25. and early.acceptedIsPreview
|
||||
assert drive.runtime.history_limit.candidate.speed_mps == 20.
|
||||
gap = drive.tick(alive=False).message.slcState
|
||||
assert gap.acceptedSpeedLimit == 25. and gap.acceptedIsPreview
|
||||
assert drive.runtime.history_limit.candidate.speed_mps == 20.
|
||||
drive.runtime.reset()
|
||||
assert drive.tick(alive=False).message.slcState.acceptedSpeedLimit == 20.
|
||||
|
||||
assert drive.tick(target=25.).message.slcState.acceptedSpeedLimit == 25.
|
||||
arrived = drive.tick(current=25., way=2, target=0.).message.slcState
|
||||
assert arrived.acceptedSpeedLimit == 25. and not arrived.acceptedIsPreview
|
||||
assert drive.runtime.history_limit.candidate.speed_mps == 25.
|
||||
drive.runtime.reset()
|
||||
assert drive.tick(alive=False).message.slcState.acceptedSpeedLimit == 25.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('choice', [None, 'accept', 'reject'])
|
||||
@pytest.mark.parametrize('cold_start', [False, True], ids=['known-limit', 'first-map-reading'])
|
||||
def test_first_current_road_decision_precedes_its_exit(choice, cold_start):
|
||||
drive = Drive(confirm=choice is not None)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
if cold_start:
|
||||
drive.runtime = Runtime(drive.runtime.settings)
|
||||
unavailable = drive.tick(alive=False).message.slcState
|
||||
assert not unavailable.hasAccepted and not unavailable.hasPending
|
||||
zone = {'current': 16., 'way': 2, 'target': 30., 'target_way': 3, 'distance': 10.}
|
||||
entered = drive.tick(**zone).message.slcState
|
||||
assert not entered.upcomingBoundaryId
|
||||
if choice is None:
|
||||
assert entered.acceptedSpeedLimit == 16.
|
||||
assert drive.tick(**zone).message.slcState.acceptedSpeedLimit == 30.
|
||||
else:
|
||||
assert entered.pendingSpeedLimit == 16.
|
||||
held = drive.tick(**zone).message.slcState
|
||||
assert held.decisionId == entered.decisionId and held.pendingSpeedLimit == 16.
|
||||
resolved = drive.tick(**zone, action=drive.action(held, choice)).message.slcState
|
||||
assert resolved.actionStatus == choice
|
||||
assert drive.tick(**zone).message.slcState.pendingSpeedLimit == 30.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('accepted', [False, True])
|
||||
def test_known_turn_away_without_posted_speed_retires_preview(accepted):
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
early = drive.tick(target=25.).message.slcState
|
||||
if accepted:
|
||||
drive.tick(target=25., action=drive.action(early, 'accept'))
|
||||
gap = drive.tick(alive=False).message.slcState
|
||||
assert gap.acceptedSpeedLimit == (25. if accepted else 20.) and gap.hasPending != accepted
|
||||
same_road = drive.tick(current=0., target=0., road_status='matchedNoLimit').message.slcState
|
||||
assert same_road.acceptedSpeedLimit == gap.acceptedSpeedLimit and same_road.hasPending == gap.hasPending
|
||||
turned = drive.tick(current=0., target=0., way=3, road_status='matchedNoLimit',
|
||||
action=drive.action(early, 'accept') if not accepted else None).message.slcState
|
||||
assert turned.observationKind == 'unknown'
|
||||
assert turned.acceptedSpeedLimit == 20. and not turned.hasPending
|
||||
|
||||
|
||||
def test_retired_cap_cannot_hide_later_current_limit_on_same_way():
|
||||
drive = Drive()
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
assert drive.tick(target=25.).message.slcState.acceptedSpeedLimit == 25.
|
||||
assert drive.tick(current=27., target=0.).message.slcState.acceptedSpeedLimit == 27.
|
||||
assert drive.tick(current=20., target=0.).message.slcState.acceptedSpeedLimit == 20.
|
||||
|
||||
|
||||
def test_current_reading_matching_the_older_cap_retires_the_replacement():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
first = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(first, 'accept'))
|
||||
assert drive.tick(target=30., target_way=3).message.slcState.pendingSpeedLimit == 30.
|
||||
current = drive.tick(current=25., target=0.).message.slcState
|
||||
assert current.acceptedSpeedLimit == 25. and not current.hasPending and not current.acceptedIsPreview
|
||||
assert drive.tick(current=20., target=0.).message.slcState.pendingSpeedLimit == 20.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,preview,dashboard', [('Highest', 25., 22.), ('Lowest', 15., 18.)])
|
||||
def test_adopt_current_releases_preview_priority(mode, preview, dashboard):
|
||||
drive = Drive()
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': mode}).selection)
|
||||
early = drive.tick(target=preview).message.slcState
|
||||
assert drive.tick(target=preview, action=drive.action(early, 'adopt')).message.slcState.acceptedSpeedLimit == 20.
|
||||
assert drive.tick(target=preview, dashboard=dashboard).message.slcState.acceptedSpeedLimit == dashboard
|
||||
|
||||
|
||||
def test_losing_replacement_does_not_reopen_approach_during_map_gap():
|
||||
drive = Drive(confirm=True)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 126.
|
||||
drive.runtime.settings = replace(drive.runtime.settings, selection=parse({'SLCPriority1': 'Highest'}).selection)
|
||||
early = drive.tick(target=25.).message.slcState
|
||||
drive.tick(target=25., action=drive.action(early, 'accept'))
|
||||
replacement = {'target': 15., 'target_way': 3, 'boundary': 'b'*64}
|
||||
assert drive.tick(**replacement).message.slcState.pendingSpeedLimit == 15.
|
||||
assert not drive.tick(**replacement, dashboard=20.).message.slcState.hasPending
|
||||
gap = drive.tick(alive=False, dashboard=20.).message.slcState
|
||||
assert gap.acceptedSpeedLimit == 25. and not gap.hasPending
|
||||
fallback = drive.tick(alive=False, dashboard=18.).message.slcState
|
||||
assert fallback.acceptedSpeedLimit == 20. and fallback.pendingSpeedLimit == 18.
|
||||
|
||||
|
||||
def test_same_speed_replacement_gets_a_fresh_decision_and_deadline():
|
||||
drive = Drive(confirm=True)
|
||||
first = drive.tick().message.slcState
|
||||
deadline = first.pendingSinceMonoTime + acc.CONFIRMATION_NS
|
||||
replacement = {'target_way': 3, 'boundary': 'b'*64}
|
||||
revised = drive.tick(**replacement, at=deadline-100_000_000, action=drive.action(first, 'accept')).message.slcState
|
||||
assert revised.pendingSpeedLimit == first.pendingSpeedLimit and revised.acceptedSpeedLimit == 20.
|
||||
assert revised.decisionId != first.decisionId and revised.pendingSinceMonoTime == drive.now
|
||||
assert drive.tick(**replacement, at=deadline+1).message.slcState.decisionId == revised.decisionId
|
||||
|
||||
@@ -14,7 +14,7 @@ ACTIVE = acc.Authority(acc.Mode.LONGITUDINAL_ONLY, acc.LongitudinalOwner.SYSTEM,
|
||||
POLICY = acc.Policy(confirm_lower=False, confirm_higher=False)
|
||||
SELECT = sel.SelectionPolicy(sel.SelectionMode.ORDERED, (sel.Source.DASHBOARD, sel.Source.MAP), False)
|
||||
OFFSET = sd.OffsetSchedule((sd.OffsetBand(0.0, None, 2.0),))
|
||||
HOST = comp.HostState(True, True, True, False, False, 100.0, 100.0)
|
||||
HOST = comp.HostState(True, True, True, False, 100.0, 100.0)
|
||||
|
||||
|
||||
def observations(speed: float | None = 15.0, *, kind: acc.ObservationKind = acc.ObservationKind.VALID):
|
||||
@@ -60,20 +60,19 @@ class CompositionTests(unittest.TestCase):
|
||||
self.assertEqual(result.acceptance.control_target_mps, 15.0)
|
||||
self.assertEqual(result.domain.context.offset_mps, 2.0)
|
||||
self.assertEqual(result.coordinate.cap_mps, 16.0)
|
||||
self.assertIsNone(result.ceiling)
|
||||
self.assertEqual(result.state.control_generation, 1)
|
||||
self.assertEqual(result.ceiling.speed_mps, 16.0)
|
||||
|
||||
def test_source_gaps_keep_history_without_a_control_target_or_ceiling(self):
|
||||
def test_source_gaps_keep_the_applied_ceiling_without_accepting_a_new_limit(self):
|
||||
self.run_frame(frame(0))
|
||||
accepted = self.state.acceptance.accepted
|
||||
for now, kind in enumerate((acc.ObservationKind.ABSENT, acc.ObservationKind.UNKNOWN, acc.ObservationKind.STALE), 1):
|
||||
result = self.run_frame(frame(now, source_kind=kind))
|
||||
self.assertEqual(result.acceptance.basis, kind.value)
|
||||
self.assertIsNone(result.acceptance.control_target_mps)
|
||||
self.assertEqual(result.acceptance.control_target_mps, 15.)
|
||||
self.assertEqual(result.state.acceptance.accepted, accepted)
|
||||
self.assertIsNone(result.domain.context)
|
||||
self.assertIsNone(result.coordinate)
|
||||
self.assertIsNone(result.ceiling)
|
||||
self.assertEqual(result.domain.context.accepted_raw_mps, 15.)
|
||||
self.assertEqual(result.coordinate.cap_mps, 17.)
|
||||
self.assertEqual(result.ceiling.speed_mps, 17.0)
|
||||
|
||||
def test_uninitialized_driver_and_missing_coordinates_suppress(self):
|
||||
unset = self.run_frame(frame(0, host=replace(HOST, driver_v_cruise_kph=255.0)))
|
||||
@@ -97,7 +96,7 @@ class CompositionTests(unittest.TestCase):
|
||||
self.state = comp.new_session(SESSION)
|
||||
host = HOST if authority.can_control else replace(HOST, system_long_available=False, long_control_active=False)
|
||||
result = self.run_frame(frame(0, authority=authority, host=host))
|
||||
self.assertIsNone(result.ceiling)
|
||||
self.assertEqual(result.ceiling is not None, authority.can_control)
|
||||
self.state = comp.new_session(SESSION)
|
||||
self.run_frame(frame(0))
|
||||
lost = self.run_frame(frame(1, authority=replace(ACTIVE, longitudinal_active=False),
|
||||
@@ -105,7 +104,7 @@ class CompositionTests(unittest.TestCase):
|
||||
self.assertIsNone(lost.ceiling)
|
||||
self.assertFalse(lost.state.reset_required)
|
||||
resumed = self.run_frame(frame(2))
|
||||
self.assertEqual(resumed.state.control_generation, 2)
|
||||
self.assertEqual(resumed.ceiling.speed_mps, 17.)
|
||||
|
||||
def test_host_authority_disagreement_does_not_advance_acceptance_time(self):
|
||||
self.run_frame(frame(0))
|
||||
@@ -134,7 +133,7 @@ class CompositionTests(unittest.TestCase):
|
||||
def test_rejected_lower_limit_survives_source_loss_and_reconnect(self):
|
||||
confirmation = replace(POLICY, confirm_lower=True)
|
||||
initial = self.run_frame(frame(0, speed=25.0, policy=confirmation))
|
||||
self.assertIsNone(initial.ceiling)
|
||||
self.assertEqual(initial.ceiling.speed_mps, 27.0)
|
||||
initial_accepted = acc.AcceptedLimit(observations(25.)[sel.Source.DASHBOARD].candidate, SESSION, 0)
|
||||
self.state = replace(self.state, acceptance=replace(self.state.acceptance, accepted=initial_accepted, pending=None, timer_running=False))
|
||||
pending = self.run_frame(frame(1, speed=15.0, policy=confirmation))
|
||||
@@ -149,15 +148,12 @@ class CompositionTests(unittest.TestCase):
|
||||
for result in (rejected, absent, reconnected):
|
||||
self.assertFalse(result.errors)
|
||||
self.assertEqual(result.acceptance.state.accepted.candidate.speed_mps, 25.0)
|
||||
if result is absent:
|
||||
self.assertIsNone(result.domain.context)
|
||||
else:
|
||||
self.assertEqual(result.domain.context.accepted_raw_mps, 25.0)
|
||||
self.assertIsNone(result.ceiling)
|
||||
self.assertEqual(result.domain.context.accepted_raw_mps, 25.0)
|
||||
self.assertEqual(result.ceiling.speed_mps, 27.0)
|
||||
|
||||
def test_reversed_monotonic_clock_latches_without_ceiling(self):
|
||||
first = self.run_frame(frame(10))
|
||||
self.assertIsNone(first.ceiling)
|
||||
self.assertEqual(first.ceiling.speed_mps, 17.0)
|
||||
prior_acceptance = self.state.acceptance
|
||||
reversed_time = self.run_frame(frame(9))
|
||||
self.assertEqual(reversed_time.status, "invalid_frame")
|
||||
@@ -168,32 +164,23 @@ class CompositionTests(unittest.TestCase):
|
||||
self.assertEqual(latched.status, "session_reset_required")
|
||||
self.assertIsNone(latched.ceiling)
|
||||
|
||||
def test_actual_causal_ledger_change_retains_raw_driver_intent(self):
|
||||
first = self.run_frame(frame(0))
|
||||
context = first.override.context
|
||||
assert context is not None
|
||||
def test_causal_driver_cruise_change_does_not_bypass_the_slc_cap(self):
|
||||
self.run_frame(frame(0))
|
||||
ledger = arb.step(arb.new_ledger(SESSION),
|
||||
arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE, context.context_id), now_ns=0).state
|
||||
arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE, 'driver-set'), now_ns=0).state
|
||||
ledger = arb.step(ledger, arb.ResolveAction(SESSION, 1, arb.Disposition.DRIVER_INTENT), now_ns=0).state
|
||||
changed = arb.step(ledger, arb.CruiseChange(SESSION, 1, 1, 100.0 / 3.6, 110.0 / 3.6), now_ns=1)
|
||||
second_frame = replace(frame(1, host=replace(HOST, driver_v_cruise_kph=110.0, selected_cluster_kph=110.0)),
|
||||
classified_cruise=changed)
|
||||
second = self.run_frame(second_frame)
|
||||
self.assertEqual(second.override.event_receipt.status, "driver_intent")
|
||||
self.assertTrue(second.override.event_receipt.consumed)
|
||||
assert second.override.state.persistent_selected_mps is not None
|
||||
self.assertAlmostEqual(second.override.state.persistent_selected_mps, 110.0 / 3.6)
|
||||
self.assertEqual(second.ceiling.speed_mps, 17.0)
|
||||
third = self.run_frame(frame(2, host=replace(HOST, driver_v_cruise_kph=105.0, selected_cluster_kph=106.0)))
|
||||
assert third.override.state.persistent_selected_mps is not None
|
||||
self.assertAlmostEqual(third.override.state.persistent_selected_mps, 110.0 / 3.6)
|
||||
self.assertIsNone(third.ceiling)
|
||||
self.assertEqual(third.ceiling.speed_mps, 17.0)
|
||||
|
||||
def test_old_ledger_result_does_not_become_a_current_speed_action(self):
|
||||
first = self.run_frame(frame(0))
|
||||
context = first.override.context
|
||||
assert context is not None
|
||||
self.run_frame(frame(0))
|
||||
ledger = arb.step(arb.new_ledger(SESSION),
|
||||
arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE, context.context_id), now_ns=0).state
|
||||
arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE, 'driver-set'), now_ns=0).state
|
||||
ledger = arb.step(ledger, arb.ResolveAction(SESSION, 1, arb.Disposition.DRIVER_INTENT), now_ns=0).state
|
||||
old = arb.step(ledger, arb.CruiseChange(SESSION, 1, 1, 100.0 / 3.6, 110.0 / 3.6), now_ns=1)
|
||||
rejected = self.run_frame(replace(frame(2, host=replace(HOST, driver_v_cruise_kph=110.0)),
|
||||
@@ -202,11 +189,11 @@ class CompositionTests(unittest.TestCase):
|
||||
self.assertIsNone(rejected.ceiling)
|
||||
self.assertEqual(rejected.state.acceptance.last_timestamp_ns, 0)
|
||||
|
||||
def test_new_limit_never_creates_a_hidden_planner_target(self):
|
||||
def test_new_limit_supplies_a_separate_planner_cap(self):
|
||||
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.assertEqual(accepted.ceiling.speed_mps, 22.)
|
||||
|
||||
def test_actual_native_planner_default_and_optional_cap_transitions(self):
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
@@ -260,8 +247,11 @@ class CompositionTests(unittest.TestCase):
|
||||
self.assertEqual(deviations, [])
|
||||
self.assertEqual(last[2], "absent")
|
||||
else:
|
||||
self.assertEqual(deviations, [])
|
||||
self.assertEqual(last[2], "absent")
|
||||
self.assertEqual(last[2], "applied")
|
||||
if scenario == 'ordinary':
|
||||
self.assertEqual(deviations[0], 12)
|
||||
else:
|
||||
self.assertEqual(last[0][0], last[1][0])
|
||||
if scenario == "lead":
|
||||
self.assertEqual(last[1][2], "1")
|
||||
elif scenario == "e2e":
|
||||
|
||||
@@ -18,18 +18,17 @@ def test_runtime_confirms_only_with_engaged_longitudinal_control(mode):
|
||||
sm['carControl'].latActive = mode in ('aol', 'combined')
|
||||
sm['controlsState'].longControlState = 'pid' if active else 'off'
|
||||
sm['carState'].gasPressed = mode == 'accelerator'
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': 1, 'SLCPriority1': 'Dashboard'}))
|
||||
output = runtime.step(sm, cp, now_ns=START)
|
||||
assert not output.result.errors
|
||||
assert output.message.slcState.hasPending == engaged
|
||||
assert output.message.slcState.observationKind == 'valid'
|
||||
assert output.message.slcState.presentationId > 0
|
||||
assert output.command is None
|
||||
|
||||
|
||||
def test_runtime_cancels_old_action_and_restarts_confirmation_after_disengagement():
|
||||
sm, cp = replay_inputs()
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': 1, 'SLCPriority1': 'Dashboard'}))
|
||||
first = runtime.step(sm, cp, now_ns=START).message.slcState
|
||||
action = Action(runtime.session_id, 1, first.decisionId, first.presentationId, 'accept')
|
||||
sm.advance(START+50_000_000)
|
||||
@@ -38,7 +37,7 @@ def test_runtime_cancels_old_action_and_restarts_confirmation_after_disengagemen
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
inactive = runtime.step(sm, cp, now_ns=START+50_000_000, request=action)
|
||||
assert not inactive.message.slcState.hasPending and not inactive.message.slcState.hasAccepted
|
||||
assert inactive.command is None and not inactive.action_result.change
|
||||
assert not inactive.action_result.change
|
||||
now = START+20_000_000_000
|
||||
sm.advance(now)
|
||||
sm['slcDashboardObservation'].observedMonoTime = now
|
||||
@@ -67,7 +66,7 @@ def test_aol_pedal_override_requires_current_longitudinal_engagement(desired, ac
|
||||
sm.seen = {'aolAxisState': True}
|
||||
sm.valid['aolAxisState'] = sm.alive['aolAxisState'] = True
|
||||
sm.logMonoTime['aolAxisState'] = START if fresh else START-100_000_000
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': 1, 'SLCPriority1': 'Dashboard'}))
|
||||
output = runtime.step(sm, cp, now_ns=START)
|
||||
assert not output.result.errors
|
||||
assert output.message.slcState.hasPending == (desired and acknowledged and fresh)
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
"""Live SLC caps the planner while the driver and native cruise retain SET."""
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import PedalOverride, parse
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
from openpilot.starpilot.speed_limits.tests import test_runtime_replay as replay
|
||||
|
||||
START = 2_000_000_000
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def drive():
|
||||
fixture = replay.RuntimeReplayTests()
|
||||
fixture.setUp()
|
||||
return fixture.sm, fixture.cp
|
||||
|
||||
|
||||
def tick(runtime, sm, cp, step, **kwargs):
|
||||
now = START + step * 50_000_000
|
||||
sm.advance(now)
|
||||
sm['slcDashboardObservation'].validUntilMonoTime = now + 1_000_000_000
|
||||
output = runtime.step(sm, cp, now_ns=now, **kwargs)
|
||||
assert not output.result.errors
|
||||
return output
|
||||
|
||||
|
||||
def controller(offset=0., *, confirm=False):
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Off', 'SLCConfirmation': int(confirm)})
|
||||
return Runtime(replace(settings, offsets=OffsetSchedule((OffsetBand(0., None, offset),))))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pcm', [False, True])
|
||||
@pytest.mark.parametrize('metric', [False, True])
|
||||
def test_limits_follow_below_driver_ceiling_without_rewriting_set(drive, pcm, metric):
|
||||
sm, cp = drive
|
||||
cp.pcmCruise = pcm
|
||||
unit = CV.KPH_TO_MS if metric else CV.MPH_TO_MS
|
||||
selected = 65 * unit * 3.6
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = selected
|
||||
selected = float(sm['carState'].vCruise)
|
||||
runtime = controller(5 * unit)
|
||||
planner = LongitudinalPlanner(cp)
|
||||
for step, limit, status in ((0, 55, 'applied'), (1, 70, 'above_driver'), (2, 40, 'applied')):
|
||||
sm['slcDashboardObservation'].speedMps = limit * unit
|
||||
output = tick(runtime, sm, cp, step)
|
||||
assert output.result.ceiling.speed_mps == pytest.approx((limit + 5) * unit)
|
||||
planner.update(sm, cruise_ceiling=output.result.ceiling)
|
||||
assert planner.last_cruise_ceiling_status == status
|
||||
assert sm['carState'].vCruise == sm['carState'].vCruiseCluster == selected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('selected', [50., 110.])
|
||||
def test_cruise_adjustments_change_only_the_driver_ceiling(drive, selected):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = selected
|
||||
event = CruiseEvent(1, START+50_000_000, 100/3.6, selected/3.6,
|
||||
'accel' if selected > 100 else 'decel', 'card-producer', START+50_000_000)
|
||||
changed = tick(runtime, sm, cp, 1, cruise_event=event)
|
||||
assert changed.result.ceiling.speed_mps == 25.
|
||||
assert not changed.message.slcState.driverOverrideActive
|
||||
assert sm['carState'].vCruise == selected
|
||||
|
||||
|
||||
def wheel_crossing(drive, *, metric=True, enabled=True, previous_kph=90., selected_kph=91., offset=0.):
|
||||
sm, cp = drive
|
||||
cp.pcmCruise = False
|
||||
car_state = sm['carState']
|
||||
car_state.canValid = True
|
||||
car_state.vCruise = car_state.vCruiseCluster = previous_kph
|
||||
settings = replace(controller(offset).settings, wheel_override=enabled)
|
||||
runtime = Runtime(settings, is_metric=metric)
|
||||
first = tick(runtime, sm, cp, 0).message.slcState
|
||||
car_state.vCruise = car_state.vCruiseCluster = selected_kph
|
||||
event = CruiseEvent(1, START + 50_000_000, previous_kph / 3.6, selected_kph / 3.6, 'accel',
|
||||
'card-producer', START + 50_000_000, session_id=runtime.session_id,
|
||||
wheel_override_since_ns=first.wheelOverrideSince)
|
||||
return runtime, event
|
||||
|
||||
|
||||
@pytest.mark.parametrize('metric,previous_kph,selected_kph,limit,offset', [
|
||||
(True, 90., 91., 25., 0.),
|
||||
(True, 54.5, 55.5, 54. / 3.6, 0.),
|
||||
(False, 89., 90.6, 55. * CV.MPH_TO_MS, 0.),
|
||||
(False, 86.9, 88.5, 55. * CV.MPH_TO_MS, -.5 * CV.MPH_TO_MS),
|
||||
])
|
||||
def test_wheel_tap_crosses_displayed_limit_and_uses_native_set(drive, metric, previous_kph, selected_kph, limit, offset):
|
||||
from struct import pack, unpack
|
||||
sm, cp = drive
|
||||
sm['slcDashboardObservation'].speedMps = limit
|
||||
sm['carState'].vEgoCluster += 1.0
|
||||
runtime, event = wheel_crossing(drive, metric=metric, previous_kph=previous_kph, selected_kph=selected_kph, offset=offset)
|
||||
event = replace(event, previous_mps=unpack('f', pack('f', event.previous_mps))[0])
|
||||
assert event.wheel_override_since_ns == START
|
||||
# Card publishes carState before the event. The new SET can be sampled first.
|
||||
assert tick(runtime, sm, cp, 1).result.ceiling is not None
|
||||
crossed = tick(runtime, sm, cp, 2, cruise_event=event)
|
||||
status = crossed.message.slcState
|
||||
assert crossed.result.ceiling is None and status.driverOverrideActive and status.overrideBasis == 'wheel'
|
||||
assert status.effectiveClusterTarget == pytest.approx(selected_kph / 3.6)
|
||||
assert not status.isLimitingMaxSet
|
||||
planner = LongitudinalPlanner(cp)
|
||||
planner.update(sm, cruise_ceiling=crossed.result.ceiling)
|
||||
assert planner.last_cruise_ceiling_status == 'absent'
|
||||
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = previous_kph
|
||||
lowered = tick(runtime, sm, cp, 3)
|
||||
assert lowered.result.ceiling is not None and not lowered.message.slcState.driverOverrideActive
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = selected_kph
|
||||
assert not tick(runtime, sm, cp, 4, cruise_event=event).message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('scenario', ['off', 'already above', 'controller', 'hold', 'foreign producer', 'foreign session',
|
||||
'different SET', 'stop or curve', 'confirmation', 'new limit', 'disengaged', 'pcm',
|
||||
'standstill', 'units changed'])
|
||||
def test_wheel_override_requires_current_unconsumed_physical_crossing(drive, scenario):
|
||||
sm, cp = drive
|
||||
prior, selected = (100., 101.) if scenario == 'already above' else (90., 91.)
|
||||
runtime, event = wheel_crossing(drive, enabled=scenario != 'off', previous_kph=prior, selected_kph=selected)
|
||||
changes = {
|
||||
'controller': {'wheel_override_since_ns': 0},
|
||||
'hold': {'long_press': True},
|
||||
'foreign producer': {'producer_session': 'old-card'},
|
||||
'foreign session': {'session_id': 'old-slc'},
|
||||
'different SET': {'selected_mps': event.selected_mps - .05},
|
||||
}
|
||||
event = replace(event, **changes.get(scenario, {}))
|
||||
if scenario in ('confirmation', 'new limit'):
|
||||
sm['slcDashboardObservation'].speedMps = 23.
|
||||
if scenario == 'confirmation':
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy(confirm_lower=True))
|
||||
if scenario == 'disengaged':
|
||||
sm['carControl'].enabled = sm['carControl'].longActive = sm['selfdriveState'].enabled = False
|
||||
if scenario == 'pcm':
|
||||
cp.pcmCruise = True
|
||||
if scenario == 'standstill':
|
||||
sm['carState'].standstill = True
|
||||
if scenario == 'units changed':
|
||||
runtime.update_offsets(runtime.settings.offsets, is_metric=False)
|
||||
output = tick(runtime, sm, cp, 1, cruise_event=event, wheel_override_blocked=scenario == 'stop or curve')
|
||||
assert not output.message.slcState.driverOverrideActive
|
||||
if scenario in ('stop or curve', 'standstill'):
|
||||
assert output.message.slcState.wheelOverrideSince == 0
|
||||
sm['carState'].standstill = False
|
||||
assert not tick(runtime, sm, cp, 2, cruise_event=replace(event, event_id=2)).message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('limit_kph', [51., 90.])
|
||||
def test_wheel_override_survives_units_change_until_set_returns_to_limit(drive, limit_kph):
|
||||
sm, cp = drive
|
||||
sm['slcDashboardObservation'].speedMps = limit_kph / 3.6
|
||||
runtime, event = wheel_crossing(drive, previous_kph=limit_kph, selected_kph=limit_kph + 1.)
|
||||
assert tick(runtime, sm, cp, 1, cruise_event=event).message.slcState.overrideBasis == 'wheel'
|
||||
|
||||
# At 51 km/h both speeds round to 32 mph, even though SET remains higher.
|
||||
runtime.update_offsets(runtime.settings.offsets, is_metric=False)
|
||||
changed = tick(runtime, sm, cp, 2)
|
||||
assert changed.result.ceiling is None
|
||||
assert changed.message.slcState.overrideBasis == 'wheel'
|
||||
assert changed.message.slcState.wheelOverrideSince != event.wheel_override_since_ns
|
||||
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = limit_kph
|
||||
returned = tick(runtime, sm, cp, 3)
|
||||
assert returned.result.ceiling.speed_mps == pytest.approx(limit_kph / 3.6)
|
||||
assert not returned.message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('cruise_standstill', [False, True])
|
||||
def test_wheel_override_survives_engaged_stop(drive, cruise_standstill):
|
||||
sm, cp = drive
|
||||
runtime, event = wheel_crossing(drive)
|
||||
assert tick(runtime, sm, cp, 1, cruise_event=event).message.slcState.overrideBasis == 'wheel'
|
||||
|
||||
stopped = sm['carState'].cruiseState if cruise_standstill else sm['carState']
|
||||
stopped.standstill = True
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 0.
|
||||
sm['controlsState'].longControlState = 'stopping'
|
||||
held = tick(runtime, sm, cp, 2)
|
||||
assert held.message.slcState.overrideBasis == 'wheel'
|
||||
assert held.message.slcState.wheelOverrideSince == 0
|
||||
|
||||
stopped.standstill = False
|
||||
sm['controlsState'].longControlState = 'pid'
|
||||
assert tick(runtime, sm, cp, 3).message.slcState.overrideBasis == 'wheel'
|
||||
|
||||
|
||||
def test_wheel_override_survives_gas_handoff_without_latching_pedal_speed(drive):
|
||||
sm, cp = drive
|
||||
runtime, event = wheel_crossing(drive)
|
||||
assert tick(runtime, sm, cp, 1, cruise_event=event).message.slcState.overrideBasis == 'wheel'
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 91. / 3.6
|
||||
sm['carState'].gasPressed = True
|
||||
sm['carControl'].longActive = False
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
assert tick(runtime, sm, cp, 2).result.state.pedal_target_mps == 0.
|
||||
sm['carState'].gasPressed = False
|
||||
assert tick(runtime, sm, cp, 3).result.state.wheel_override_metric is True
|
||||
sm['carControl'].longActive = True
|
||||
sm['controlsState'].longControlState = 'pid'
|
||||
assert tick(runtime, sm, cp, 4).message.slcState.overrideBasis == 'wheel'
|
||||
sm['slcDashboardObservation'].speedMps = 23.
|
||||
accepted = tick(runtime, sm, cp, 5)
|
||||
assert accepted.result.ceiling.speed_mps == 23. and not accepted.message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('missing', ['absent', 'unknown', 'stale'])
|
||||
def test_source_gap_uses_the_previous_limit_and_current_offset(drive, missing):
|
||||
sm, cp = drive
|
||||
runtime = controller(2.)
|
||||
first = tick(runtime, sm, cp, 0)
|
||||
assert first.result.ceiling.speed_mps == 27.
|
||||
sm['slcDashboardObservation'].status = missing
|
||||
runtime.update_offsets(OffsetSchedule((OffsetBand(0., None, 4.),)))
|
||||
gap = tick(runtime, sm, cp, 1)
|
||||
assert gap.result.ceiling.speed_mps == 29.
|
||||
assert gap.message.slcState.hasCeiling and gap.message.slcState.observationKind == missing
|
||||
sm['slcDashboardObservation'].status = 'valid'
|
||||
recovered = tick(runtime, sm, cp, 2)
|
||||
assert recovered.result.ceiling.speed_mps == 29.
|
||||
assert sm['carState'].vCruise == 100.
|
||||
|
||||
|
||||
def test_previous_limit_resumes_after_disengagement_and_session_reset(drive):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
sm['slcDashboardObservation'].status = 'unknown'
|
||||
sm['selfdriveState'].enabled = sm['carControl'].enabled = sm['carControl'].longActive = False
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
assert tick(runtime, sm, cp, 1).result.ceiling is None
|
||||
sm['selfdriveState'].enabled = sm['carControl'].enabled = sm['carControl'].longActive = True
|
||||
sm['controlsState'].longControlState = 'pid'
|
||||
assert tick(runtime, sm, cp, 2).result.ceiling.speed_mps == 25.
|
||||
runtime.reset()
|
||||
assert tick(runtime, sm, cp, 3).result.ceiling.speed_mps == 25.
|
||||
|
||||
|
||||
def test_force_deceleration_suppresses_a_cap_without_forgetting_source_gap_continuity(drive):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
sm['slcDashboardObservation'].status = 'unknown'
|
||||
sm['controlsState'].forceDecel = True
|
||||
assert tick(runtime, sm, cp, 1).result.ceiling is None
|
||||
sm['controlsState'].forceDecel = False
|
||||
assert tick(runtime, sm, cp, 2).result.ceiling.speed_mps == 25.
|
||||
assert sm['carState'].vCruise == 100.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('choice', ['accept', 'reject'])
|
||||
def test_confirmation_changes_the_slc_target_and_preserves_set(drive, choice):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy(confirm_lower=True, confirm_higher=True))
|
||||
sm['slcDashboardObservation'].speedMps = 15.
|
||||
pending = tick(runtime, sm, cp, 1)
|
||||
assert pending.result.ceiling.speed_mps == 25.
|
||||
state = pending.message.slcState
|
||||
action = Action(runtime.session_id, 1, state.decisionId, state.presentationId, choice)
|
||||
chosen = tick(runtime, sm, cp, 2, request=action)
|
||||
assert chosen.result.ceiling.speed_mps == (15. if choice == 'accept' else 25.)
|
||||
assert not chosen.message.slcState.hasPending
|
||||
assert sm['carState'].vCruise == 100.
|
||||
|
||||
|
||||
def test_pedal_release_reapplies_limit_without_a_sticky_override(drive):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
runtime.settings = replace(runtime.settings, pedal_override=PedalOverride.OFF)
|
||||
tick(runtime, sm, cp, 0)
|
||||
sm['carState'].gasPressed = True
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 30.
|
||||
sm['carControl'].longActive = False
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
pressed = tick(runtime, sm, cp, 1)
|
||||
assert not pressed.message.slcState.driverOverrideActive
|
||||
sm['carState'].gasPressed = False
|
||||
sm['carControl'].longActive = True
|
||||
sm['controlsState'].longControlState = 'pid'
|
||||
released = tick(runtime, sm, cp, 2)
|
||||
assert released.result.ceiling.speed_mps == 25.
|
||||
assert not released.message.slcState.driverOverrideActive
|
||||
assert sm['carState'].vCruise == 100.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,target', [(PedalOverride.SPEED_REACHED, 26.), (PedalOverride.SET_SPEED, 100. / 3.6)])
|
||||
def test_gas_above_limit_holds_the_selected_override_after_release(drive, mode, target):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
runtime.settings = replace(runtime.settings, pedal_override=mode)
|
||||
assert tick(runtime, sm, cp, 0).result.ceiling.speed_mps == 25.
|
||||
|
||||
sm['carState'].gasPressed = True
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 24.
|
||||
assert not tick(runtime, sm, cp, 1).message.slcState.driverOverrideActive
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 26.
|
||||
sm['carControl'].longActive = False
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
pressed = tick(runtime, sm, cp, 2).message.slcState
|
||||
assert pressed.overrideBasis == 'pedal'
|
||||
assert pressed.effectiveClusterTarget == pytest.approx(target)
|
||||
|
||||
sm['carState'].gasPressed = False
|
||||
assert tick(runtime, sm, cp, 3).result.ceiling is None
|
||||
sm['carControl'].longActive = True
|
||||
sm['controlsState'].longControlState = 'pid'
|
||||
released = tick(runtime, sm, cp, 4)
|
||||
assert released.message.slcState.overrideBasis == 'persistent'
|
||||
assert released.result.ceiling.speed_mps == pytest.approx(target)
|
||||
assert released.message.slcState.acceptedSpeedLimit == 25.
|
||||
assert released.message.slcState.speedLimit == 25.
|
||||
assert sm['carState'].vCruise == 100.
|
||||
planner = LongitudinalPlanner(cp)
|
||||
planner.update(sm, cruise_ceiling=released.result.ceiling)
|
||||
assert planner.last_cruise_ceiling_status == ('applied' if mode is PedalOverride.SPEED_REACHED else 'above_driver')
|
||||
|
||||
sm['carControl'].enabled = sm['carControl'].longActive = sm['selfdriveState'].enabled = False
|
||||
assert tick(runtime, sm, cp, 5).result.ceiling is None
|
||||
sm['carControl'].enabled = sm['carControl'].longActive = sm['selfdriveState'].enabled = True
|
||||
resumed = tick(runtime, sm, cp, 6)
|
||||
assert resumed.result.ceiling.speed_mps == 25.
|
||||
assert not resumed.message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('clear_with', ['lower SET', 'higher limit', 'explicit accept'])
|
||||
def test_override_clears_when_the_driver_or_limit_takes_back_control(drive, clear_with):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 29.
|
||||
sm['carState'].gasPressed = True
|
||||
assert tick(runtime, sm, cp, 1).result.ceiling.speed_mps == pytest.approx(100. / 3.6)
|
||||
sm['carState'].gasPressed = False
|
||||
action = None
|
||||
expected = 25.
|
||||
if clear_with == 'lower SET':
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 90.
|
||||
elif clear_with == 'higher limit':
|
||||
sm['slcDashboardObservation'].speedMps = expected = 28.
|
||||
else:
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy())
|
||||
sm['slcDashboardObservation'].speedMps = expected = 20.
|
||||
pending = tick(runtime, sm, cp, 2).message.slcState
|
||||
assert pending.hasPending and pending.driverOverrideActive
|
||||
action = Action(runtime.session_id, 1, pending.decisionId, pending.presentationId, 'accept')
|
||||
cleared = tick(runtime, sm, cp, 3, request=action)
|
||||
assert cleared.result.ceiling.speed_mps == expected
|
||||
assert not cleared.message.slcState.driverOverrideActive
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 100.
|
||||
assert tick(runtime, sm, cp, 4).result.ceiling.speed_mps == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode', [PedalOverride.SPEED_REACHED, PedalOverride.SET_SPEED])
|
||||
def test_explicit_accept_blocks_override_until_the_held_pedal_is_released(drive, mode):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy(), pedal_override=mode)
|
||||
sm['slcDashboardObservation'].speedMps = 15.
|
||||
sm['carState'].gasPressed = True
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 26.
|
||||
prompt = tick(runtime, sm, cp, 1).message.slcState
|
||||
action = Action(runtime.session_id, 1, prompt.decisionId, prompt.presentationId, 'accept')
|
||||
for step, request in ((2, action), (3, None)):
|
||||
accepted = tick(runtime, sm, cp, step, request=request).message.slcState
|
||||
assert accepted.acceptedSpeedLimit == accepted.effectiveCap == 15.
|
||||
assert not accepted.driverOverrideActive
|
||||
sm['carState'].gasPressed = False
|
||||
assert tick(runtime, sm, cp, 4).result.ceiling.speed_mps == 15.
|
||||
sm['carState'].gasPressed = True
|
||||
assert tick(runtime, sm, cp, 5).message.slcState.driverOverrideActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('wheel', [False, True])
|
||||
@pytest.mark.parametrize('in_time', [False, True])
|
||||
def test_host_keeps_in_flight_confirmation_bound_to_its_capture_time(drive, tmp_path, wheel, in_time):
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.speed_limits.planner_host import SavedSlcLifecycle
|
||||
|
||||
sm, cp = drive
|
||||
host = SavedSlcLifecycle(Params(str(tmp_path)), cp, {})
|
||||
host.runtime = runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy())
|
||||
sm['slcDashboardObservation'].speedMps = 15.
|
||||
prompt = tick(runtime, sm, cp, 1).message.slcState
|
||||
deadline = prompt.pendingSinceMonoTime + acc.CONFIRMATION_NS
|
||||
observed = deadline + (-5_000_000 if in_time else 1_000_000)
|
||||
actions, cruise = deque(), deque()
|
||||
if wheel:
|
||||
cruise.append(CruiseEvent(1, observed, 0., 0., 'accel', 'card-producer', deadline + 2_000_000,
|
||||
'confirmationAccept', runtime.session_id, prompt.decisionId, prompt.presentationId))
|
||||
|
||||
def host_tick(now, *, card_ns=None):
|
||||
sm.advance(card_ns if card_ns is not None else now)
|
||||
sm['slcDashboardObservation'].validUntilMonoTime = now + 1_000_000_000
|
||||
return host.step(sm, now, actions, cruise, drive_changed=False).message.slcState
|
||||
|
||||
expired = host_tick(deadline + 3_000_000, card_ns=deadline)
|
||||
assert not expired.hasPending and expired.acceptedSpeedLimit == 25.
|
||||
if wheel:
|
||||
assert len(cruise) == 1
|
||||
else:
|
||||
actions.append((observed, Action(runtime.session_id, 1, prompt.decisionId, prompt.presentationId, 'accept', observed)))
|
||||
joined = host_tick(deadline + 50_000_000)
|
||||
assert not cruise and not actions
|
||||
assert joined.acceptedSpeedLimit == (15. if in_time else 25.)
|
||||
if not in_time:
|
||||
assert host_tick(deadline + 100_000_000).status == 'timeout_reject'
|
||||
|
||||
|
||||
def test_offset_edit_applies_to_the_previous_limit_without_accepting_the_proposal(drive):
|
||||
sm, cp = drive
|
||||
runtime = controller()
|
||||
tick(runtime, sm, cp, 0)
|
||||
runtime.settings = replace(runtime.settings, acceptance=acc.Policy(confirm_lower=True, confirm_higher=True))
|
||||
sm['slcDashboardObservation'].speedMps = 30.
|
||||
assert tick(runtime, sm, cp, 1).message.slcState.hasPending
|
||||
runtime.update_offsets(OffsetSchedule((OffsetBand(0., None, 2.),)))
|
||||
assert tick(runtime, sm, cp, 2).result.ceiling.speed_mps == 27.
|
||||
sm['slcDashboardObservation'].speedMps = 25.
|
||||
assert tick(runtime, sm, cp, 3).result.ceiling.speed_mps == 27.
|
||||
|
||||
|
||||
def test_raw_cluster_conversion_is_applied_once_including_during_source_loss(drive):
|
||||
sm, cp = drive
|
||||
runtime = controller(2.)
|
||||
sm['carState'].vEgoCluster = sm['carState'].vEgo + 1.
|
||||
assert tick(runtime, sm, cp, 0).result.ceiling.speed_mps == 26.
|
||||
sm['slcDashboardObservation'].status = 'unknown'
|
||||
sm['carState'].vEgoCluster = sm['carState'].vEgo + .5
|
||||
assert tick(runtime, sm, cp, 1).result.ceiling.speed_mps == 26.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pcm', [False, True])
|
||||
def test_factory_longitudinal_never_gets_an_slc_cap(drive, pcm):
|
||||
sm, cp = drive
|
||||
cp.pcmCruise, cp.openpilotLongitudinalControl = pcm, False
|
||||
sm['carControl'].longActive = False
|
||||
sm['controlsState'].longControlState = 'off'
|
||||
output = tick(controller(), sm, cp, 0)
|
||||
assert output.result.ceiling is None
|
||||
assert not output.message.slcState.hasCeiling
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pcm', [False, True])
|
||||
def test_curve_press_remains_available_and_cannot_reuse_an_slc_confirmation(drive, pcm):
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.car.card import curve_press_receipt
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper, SlcPendingConfirmation
|
||||
sm, cp = drive
|
||||
cp.pcmCruise = pcm
|
||||
helper = VCruiseHelper(cp)
|
||||
cs = car.CarState(canValid=True, cruiseState={'available': True, 'enabled': True, 'speed': 25.},
|
||||
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=True)])
|
||||
helper.update_v_cruise(cs, True, True, slc_enabled=True)
|
||||
def receipt():
|
||||
return curve_press_receipt(cs, helper, sm, curve_replay=True, slc_replay=False,
|
||||
host_long_active=True, observed_ns=START)
|
||||
assert receipt()[0] == 'curveAccelPress'
|
||||
helper.update_v_cruise(cs, True, True, SlcPendingConfirmation('drive', 1, 1), slc_enabled=True)
|
||||
assert receipt() is None
|
||||
cs.buttonEvents = [car.CarState.ButtonEvent(type='accelCruise', pressed=False)]
|
||||
helper.update_v_cruise(cs, True, True, slc_enabled=False)
|
||||
assert receipt() is None
|
||||
cs.standstill = True
|
||||
cs.buttonEvents = [car.CarState.ButtonEvent(type='accelCruise', pressed=True)]
|
||||
assert receipt() is None
|
||||
@@ -1,19 +1,23 @@
|
||||
"""SET and RES adopt qualified current limits, with normal resume as fallback."""
|
||||
"""Fresh software SET can match a qualified limit; RES preserves the previous SET."""
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.dashboard_speed_limit import Status
|
||||
from opendbc.car.dashboard_speed_limit import Status, Tracker
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car import card, cruise
|
||||
from openpilot.selfdrive.controls.plannerd import queue_cruise_event
|
||||
from openpilot.starpilot.speed_limits.acceptance import AcceptedLimit, Candidate, IdentityKind, ObservationIdentity
|
||||
from openpilot.starpilot.speed_limits import physical_actions
|
||||
from openpilot.starpilot.speed_limits.planner_host import action_from_message
|
||||
from openpilot.starpilot.speed_limits.runtime import Action, CruiseEvent, Runtime
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import parse
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
@@ -35,7 +39,7 @@ def drive(*, metric=False, limit=45., offset=5., confirmation=True):
|
||||
sm['carControl'].enabled = sm['carControl'].longActive = False
|
||||
units = CV.KPH_TO_MS if metric else CV.MPH_TO_MS
|
||||
sm['slcDashboardObservation'].speedMps = limit * units
|
||||
settings = replace(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': confirmation}),
|
||||
settings = replace(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': int(confirmation)}),
|
||||
offsets=OffsetSchedule((OffsetBand(0., None, offset * units),)))
|
||||
runtime = Runtime(settings, is_metric=metric)
|
||||
state = runtime.step(sm, cp, now_ns=START).message.slcState
|
||||
@@ -46,13 +50,14 @@ def drive(*, metric=False, limit=45., offset=5., confirmation=True):
|
||||
return runtime, sm, cp
|
||||
|
||||
|
||||
def owner(sm, cp, *, metric=False, resume=None):
|
||||
def owner(sm, cp, *, metric=False, resume=None, match=True):
|
||||
instance = card.Car.__new__(card.Car)
|
||||
instance.CP, instance.sm = cp, sm
|
||||
instance.CC_prev = car.CarControl(enabled=False)
|
||||
instance.CS_prev = car.CarState(vEgo=sm['carState'].vEgo)
|
||||
instance.is_metric, instance.experimental_mode, instance.initial_cruise_speed = metric, False, 40
|
||||
instance.slc_control_enabled = instance.slc_replay = True
|
||||
instance.slc_match_on_engage = match
|
||||
instance.slc_source_floor_ns = 0
|
||||
instance.slc_producer_session, instance.slc_cruise_event_id = 'card-producer', 0
|
||||
instance.slc_receipts = []
|
||||
@@ -84,13 +89,75 @@ def receipt(instance, *, now=START + 10_000_000, event_id=1):
|
||||
kind, str(state.sessionId), int(state.decisionId), int(state.presentationId))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw,match', [(None, False), (b'0', False), (b'1', True), (b'yes', False)])
|
||||
def test_engagement_matching_requires_an_enabled_saved_toggle(tmp_path, raw, match):
|
||||
_, sm, cp = drive()
|
||||
instance = owner(sm, cp)
|
||||
instance.params = Params(str(tmp_path))
|
||||
assert not instance.params.get_default_value('SLCOnEngage')
|
||||
if raw is not None:
|
||||
Path(instance.params.get_param_path('SLCOnEngage')).write_bytes(raw)
|
||||
instance.slc_match_on_engage = instance.read_slc_match_on_engage()
|
||||
assert instance.slc_match_on_engage == match
|
||||
engage(instance)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == (80.5 if match else 72.)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('resume', [False, True])
|
||||
@pytest.mark.parametrize('live', [False, True])
|
||||
def test_match_on_engage_uses_saved_history_as_a_fallback(resume, live):
|
||||
runtime, sm, cp = drive()
|
||||
previous = AcceptedLimit(Candidate('map', ObservationIdentity(IdentityKind.GEOGRAPHIC, value='old-road'), 25 * CV.MPH_TO_MS),
|
||||
'previous-drive', 0)
|
||||
runtime = Runtime(runtime.settings, previous_limit=previous)
|
||||
if not live:
|
||||
sm['slcDashboardObservation'].status = 'unknown'
|
||||
prepared = runtime.step(sm, cp, now_ns=START).message.slcState
|
||||
assert prepared.hasAcceptedAdjustedLimit and prepared.hasPresentationAdjustedLimit == live
|
||||
sm.data['slcState'] = prepared
|
||||
instance = owner(sm, cp, resume=resume)
|
||||
if resume:
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 105.
|
||||
engage(instance)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == (105. if resume else 80.5 if live else 48.3)
|
||||
assert bool(instance.slc_receipts) == (live and not resume)
|
||||
|
||||
|
||||
def test_new_dashboard_packet_in_same_episode_matches_set_over_saved_history():
|
||||
runtime, sm, cp = drive(limit=45., offset=0.)
|
||||
previous = AcceptedLimit(Candidate('dashboard', ObservationIdentity(IdentityKind.PRODUCER_EPISODE, value='old-road'), 30 * CV.MPH_TO_MS),
|
||||
'previous-drive', 0)
|
||||
runtime = Runtime(runtime.settings, previous_limit=previous)
|
||||
state = runtime.step(sm, cp, now_ns=START).message.slcState
|
||||
sm.data['slcState'] = state
|
||||
assert state.hasPresentationAdjustedLimit and state.acceptedMonoTime == 0
|
||||
assert state.acceptedAdjustedLimit == pytest.approx(30 * CV.MPH_TO_MS)
|
||||
|
||||
tracker = Tracker()
|
||||
tracker.update(state.sourceObservedMonoTime, Status.VALID, 45 * CV.MPH_TO_MS, valid_until_ns=state.sourceValidUntilMonoTime)
|
||||
newer = tracker.update(START + 5_000_000, Status.VALID, 45 * CV.MPH_TO_MS, valid_until_ns=START + 300_000_000)
|
||||
assert newer.episode == state.sourceEpisode and newer.observed_ns > state.sourceObservedMonoTime
|
||||
instance = owner(sm, cp, resume=False)
|
||||
instance.CI.CS.dashboard_limit.observation = newer
|
||||
|
||||
engage(instance)
|
||||
|
||||
assert instance.v_cruise_helper.v_cruise_kph == cruise.slc_target_kph(45 * CV.MPH_TO_MS, False)
|
||||
event = receipt(instance)
|
||||
assert event.presentation_id == state.presentationId
|
||||
evidence = sm['slcDashboardObservation']
|
||||
evidence.observedMonoTime, evidence.validUntilMonoTime = newer.observed_ns, newer.valid_until_ns
|
||||
adopted = runtime.step(sm, cp, now_ns=event.observed_ns, cruise_event=event).message.slcState
|
||||
assert adopted.actionStatus == 'adopt' and adopted.acceptedSpeedLimit == pytest.approx(45 * CV.MPH_TO_MS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('confirmation', [False, True])
|
||||
@pytest.mark.parametrize('pcm', [False, True])
|
||||
@pytest.mark.parametrize('resume,previous', [(False, cruise.V_CRUISE_UNSET), (True, 30.), (None, 105.)],
|
||||
ids=['set', 'resume-hint', 'resume-button'])
|
||||
@pytest.mark.parametrize('metric,limit,offset', [(False, 45., 5.), (False, 20., -2.), (False, 55., -.4),
|
||||
(True, 72., 5.), (True, 30., -2.), (True, 72., -.4)])
|
||||
def test_prepared_limit_initializes_set_once_and_adopts_it(metric, limit, offset, confirmation, pcm, resume, previous):
|
||||
def test_limit_matches_only_new_software_set_and_resume_preserves_ceiling(metric, limit, offset, confirmation, pcm, resume, previous):
|
||||
runtime, sm, cp = drive(metric=metric, limit=limit, offset=offset, confirmation=confirmation)
|
||||
cp.pcmCruise = pcm
|
||||
prepared = sm['slcState']
|
||||
@@ -101,18 +168,22 @@ def test_prepared_limit_initializes_set_once_and_adopts_it(metric, limit, offset
|
||||
if resume is None:
|
||||
instance.CS_prev.buttonEvents = [car.CarState.ButtonEvent(type='accelCruise', pressed=False)]
|
||||
engage(instance)
|
||||
expected = cruise.slc_target_kph(float(prepared.presentationAdjustedLimit), metric)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == expected
|
||||
assert instance.v_cruise_helper.v_cruise_cluster_kph == expected
|
||||
adopted = runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=receipt(instance))
|
||||
matched = not pcm and resume is False
|
||||
expected = cruise.slc_target_kph(float(prepared.presentationAdjustedLimit), metric) if matched else 60. if pcm else previous
|
||||
assert instance.v_cruise_helper.v_cruise_kph == pytest.approx(expected)
|
||||
assert instance.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(expected)
|
||||
adopted = runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=receipt(instance) if matched else None)
|
||||
if not matched:
|
||||
assert not instance.slc_receipts
|
||||
return
|
||||
assert not adopted.result.errors and adopted.message.slcState.hasAccepted
|
||||
assert not adopted.message.slcState.hasPending and not adopted.message.slcState.driverOverrideActive
|
||||
assert adopted.message.slcState.actionStatus == 'adopt' and adopted.command is None
|
||||
assert adopted.message.slcState.actionStatus == 'adopt'
|
||||
assert runtime.ledger.transactions == ()
|
||||
instance.CC_prev.enabled = True
|
||||
instance.slc_receipts.clear()
|
||||
engage(instance, now=START + 20_000_000)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == expected and instance.slc_receipts == []
|
||||
assert instance.v_cruise_helper.v_cruise_kph == pytest.approx(expected) and instance.slc_receipts == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid', ['disabled', 'stale', 'unknown', 'stock-long', 'can-fault', 'wrong-episode', 'configuration'])
|
||||
@@ -182,12 +253,14 @@ def test_card_publication_routes_engagement_to_runtime_and_preserves_later_confi
|
||||
events = deque()
|
||||
queue_cruise_event(published.as_reader(), events, deque())
|
||||
assert len(events) == 1 and events[0].kind == 'engagementAdopt'
|
||||
assert events[0].command_id == 0
|
||||
first = runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=events.popleft())
|
||||
assert first.message.slcState.hasAccepted and first.command is None
|
||||
assert first.message.slcState.hasAccepted
|
||||
assert first.message.slcState.wheelAdoptMonoTime == 0
|
||||
sm.advance(START + 50_000_000)
|
||||
sm['slcDashboardObservation'].speedMps = 55 * CV.MPH_TO_MS
|
||||
later = runtime.step(sm, cp, now_ns=START + 50_000_000)
|
||||
assert later.message.slcState.hasPending and later.command is None
|
||||
assert later.message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('change', ['session', 'presentation', 'producer', 'replaced-source', 'changed-set', 'stale', 'replay'])
|
||||
@@ -212,7 +285,7 @@ def test_engagement_receipt_cannot_adopt_unrelated_or_stale_work(change):
|
||||
else:
|
||||
runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=event)
|
||||
output = runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=event)
|
||||
assert output.message.slcState.actionStatus != 'adopt' and output.command is None
|
||||
assert output.message.slcState.actionStatus != 'adopt'
|
||||
assert runtime.ledger.transactions == ()
|
||||
|
||||
|
||||
@@ -238,31 +311,13 @@ def test_reengagement_clears_previous_decline(resume):
|
||||
if resume:
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 105.
|
||||
engage(instance)
|
||||
if resume:
|
||||
assert instance.v_cruise_helper.v_cruise_kph == 105. and not instance.slc_receipts
|
||||
assert runtime.state.acceptance.rejected
|
||||
return
|
||||
adopted = runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=receipt(instance))
|
||||
assert adopted.message.slcState.actionStatus == 'adopt' and adopted.message.slcState.hasAccepted
|
||||
assert not adopted.message.slcState.hasPending and adopted.command is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('resume', [False, True])
|
||||
def test_reengagement_clears_override_even_after_previous_application(resume):
|
||||
runtime, sm, cp = drive()
|
||||
instance = owner(sm, cp, resume=resume)
|
||||
engage(instance)
|
||||
runtime.step(sm, cp, now_ns=START + 10_000_000, cruise_event=receipt(instance))
|
||||
previous = float(sm['carState'].vCruise) / 3.6
|
||||
sm.advance(START + 20_000_000)
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 105.
|
||||
overridden = runtime.step(sm, cp, now_ns=START + 20_000_000,
|
||||
cruise_event=CruiseEvent(2, START + 20_000_000, previous, 105 / 3.6, 'accel', 'card-producer', START + 20_000_000))
|
||||
assert overridden.message.slcState.driverOverrideActive
|
||||
sm.data['slcState'] = overridden.message.slcState
|
||||
instance.CC_prev.enabled = False
|
||||
instance.slc_receipts.clear()
|
||||
engage(instance, now=START + 30_000_000)
|
||||
adopted = runtime.step(sm, cp, now_ns=START + 30_000_000,
|
||||
cruise_event=receipt(instance, now=START + 30_000_000, event_id=3))
|
||||
assert adopted.message.slcState.actionStatus == 'adopt' and not adopted.message.slcState.driverOverrideActive
|
||||
assert adopted.message.slcState.commandStatus == 'applied' and adopted.command is None
|
||||
assert not adopted.message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source', ['Maps', 'Vision'])
|
||||
@@ -296,8 +351,11 @@ def test_engagement_uses_existing_priority_and_qualified_secondary_source(source
|
||||
if resume:
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 105.
|
||||
engage(instance)
|
||||
expected = cruise.slc_target_kph(float(prepared.presentationAdjustedLimit), False)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == expected
|
||||
expected = 105. if resume else cruise.slc_target_kph(float(prepared.presentationAdjustedLimit), False)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == pytest.approx(expected)
|
||||
if resume:
|
||||
assert not instance.slc_receipts
|
||||
return
|
||||
# ReplaySM.advance uses monotonic stamps for every service; mapd uses boottime.
|
||||
if source == 'Maps':
|
||||
sm.logMonoTime['mapdOut'] = boot
|
||||
@@ -307,14 +365,17 @@ def test_engagement_uses_existing_priority_and_qualified_secondary_source(source
|
||||
|
||||
@pytest.mark.parametrize('confirmation', [False, True])
|
||||
@pytest.mark.parametrize('resume', [False, True])
|
||||
def test_engagement_uses_current_limit_over_previous_early_proposal(confirmation, resume):
|
||||
@pytest.mark.parametrize('live', [False, True])
|
||||
def test_engagement_never_initializes_set_from_a_preview(confirmation, resume, live):
|
||||
from openpilot.starpilot.speed_limits.tests.test_advance import Drive
|
||||
preview = Drive(confirm=confirmation)
|
||||
output = preview.tick()
|
||||
assert output.message.slcState.upcomingBoundaryId
|
||||
preview.sm['carControl'].enabled = preview.sm['carControl'].longActive = False
|
||||
prepared = preview.tick().message.slcState
|
||||
prepared = preview.tick(alive=live).message.slcState
|
||||
assert prepared.acceptedIsPreview == (not confirmation)
|
||||
sm, cp = preview.sm, preview.cp
|
||||
cp.pcmCruise = False
|
||||
sm.data['slcState'] = prepared
|
||||
sm.valid['slcState'] = sm.alive['slcState'] = True
|
||||
sm.logMonoTime['slcState'] = preview.now
|
||||
@@ -323,11 +384,22 @@ def test_engagement_uses_current_limit_over_previous_early_proposal(confirmation
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 105.
|
||||
stamp = preview.now + 10_000_000
|
||||
engage(instance, now=stamp)
|
||||
if resume:
|
||||
assert instance.v_cruise_helper.v_cruise_kph == 105. and not instance.slc_receipts
|
||||
return
|
||||
if not live:
|
||||
assert instance.v_cruise_helper.v_cruise_kph == (79. if confirmation else 72.)
|
||||
assert not instance.slc_receipts
|
||||
return
|
||||
assert instance.v_cruise_helper.v_cruise_kph == 79.
|
||||
adopted = preview.tick(at=stamp, event=receipt(instance, now=stamp))
|
||||
assert adopted.message.slcState.acceptedSpeedLimit == 20.
|
||||
assert not adopted.message.slcState.hasPending and not adopted.message.slcState.upcomingBoundaryId
|
||||
assert adopted.message.slcState.actionStatus == 'adopt' and adopted.command is None
|
||||
assert adopted.message.slcState.actionStatus == 'adopt'
|
||||
|
||||
turned = preview.tick(way=3, target=0.).message.slcState
|
||||
assert not turned.acceptedIsPreview and turned.acceptedSpeedLimit == 20.
|
||||
assert sm['carState'].vCruise == 79.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('current', [20., 25.])
|
||||
@@ -339,6 +411,7 @@ def test_delayed_engagement_is_bound_to_current_limit(current, resume):
|
||||
preview.sm['carControl'].enabled = preview.sm['carControl'].longActive = False
|
||||
prepared = preview.tick().message.slcState
|
||||
sm, cp = preview.sm, preview.cp
|
||||
cp.pcmCruise = False
|
||||
sm.data['slcState'] = prepared
|
||||
sm.valid['slcState'] = sm.alive['slcState'] = True
|
||||
sm.logMonoTime['slcState'] = preview.now
|
||||
@@ -347,11 +420,14 @@ def test_delayed_engagement_is_bound_to_current_limit(current, resume):
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 105.
|
||||
stamp = preview.now + 10_000_000
|
||||
engage(instance, now=stamp)
|
||||
if resume:
|
||||
assert instance.v_cruise_helper.v_cruise_kph == 105. and not instance.slc_receipts
|
||||
return
|
||||
event = receipt(instance, now=stamp)
|
||||
pending = preview.tick(at=stamp + 40_000_000, current=current).message.slcState
|
||||
delayed = preview.tick(at=stamp + 90_000_000, current=current, event=event)
|
||||
state = delayed.message.slcState
|
||||
assert not delayed.result.errors and delayed.command is None
|
||||
assert not delayed.result.errors
|
||||
assert sm['carState'].vCruise == 79. and state.acceptedSpeedLimit == 20.
|
||||
assert (state.actionStatus == 'adopt') == (current == 20.)
|
||||
assert state.hasPending == (current != 20.)
|
||||
@@ -359,3 +435,94 @@ def test_delayed_engagement_is_bound_to_current_limit(current, resume):
|
||||
assert state.presentationId != event.presentation_id
|
||||
assert state.decisionId == pending.decisionId
|
||||
assert state.pendingSinceMonoTime == pending.pendingSinceMonoTime
|
||||
|
||||
|
||||
def wheel_approval(runtime, sm, cp, *, wheel_ns, now):
|
||||
request = messaging.new_message('slcAction', logMonoTime=now)
|
||||
request.slcAction = {'sessionId': runtime.session_id, 'sequenceId': now, 'kind': 'adopt',
|
||||
'presentationId': sm['slcState'].presentationId, 'wheelAdoptMonoTime': wheel_ns}
|
||||
sm.advance(now)
|
||||
sm['slcDashboardObservation'].validUntilMonoTime = now + 1_000_000_000
|
||||
state = runtime.step(sm, cp, now_ns=now, request=action_from_message(request)[1]).message.slcState
|
||||
assert state.actionStatus == 'adopt' and state.wheelAdoptMonoTime == wheel_ns
|
||||
return state
|
||||
|
||||
|
||||
@pytest.mark.parametrize('case', ['valid', 'disengaged', 'newer-set', 'held-set-button', 'expired', 'different-press', 'no-local-press'])
|
||||
def test_wheel_adopt_only_applies_to_its_own_eligible_press(case):
|
||||
runtime, sm, cp = drive(limit=45., offset=5., confirmation=True)
|
||||
instance = owner(sm, cp, match=False)
|
||||
engage(instance)
|
||||
helper = instance.v_cruise_helper
|
||||
wheel_ns = START + 20_000_000
|
||||
if case != 'no-local-press':
|
||||
instance.slc_wheel_adopt = (wheel_ns + int(case == 'different-press'), helper.v_cruise_kph)
|
||||
now = wheel_ns + (240_000_000 if case == 'expired' else 30_000_000)
|
||||
sm.data['slcState'] = wheel_approval(runtime, sm, cp, wheel_ns=wheel_ns, now=now)
|
||||
if case == 'newer-set':
|
||||
helper.v_cruise_kph -= 1.6
|
||||
elif case == 'held-set-button':
|
||||
sm['carState'].buttonEvents = [car.CarState.ButtonEvent(type='decelCruise', pressed=True)]
|
||||
helper.update_v_cruise(sm['carState'], True, False)
|
||||
before = helper.v_cruise_kph
|
||||
read_ns = wheel_ns + 250_000_001 if case == 'expired' else now
|
||||
instance.apply_slc_adopt(read_ns, host_control_enabled=case != 'disengaged')
|
||||
assert helper.v_cruise_kph == (cruise.slc_target_kph(50 * CV.MPH_TO_MS, False) if case == 'valid' else before)
|
||||
if case == 'valid':
|
||||
assert instance.slc_wheel_adopt is None
|
||||
helper.v_cruise_kph = before
|
||||
instance.apply_slc_adopt(read_ns, host_control_enabled=True)
|
||||
assert helper.v_cruise_kph == before
|
||||
|
||||
|
||||
def test_next_automatic_limit_cannot_reuse_wheel_approval():
|
||||
runtime, sm, cp = drive(limit=45., offset=5., confirmation=False)
|
||||
instance = owner(sm, cp, match=False)
|
||||
engage(instance)
|
||||
before = instance.v_cruise_helper.v_cruise_kph
|
||||
wheel_ns = START + 20_000_000
|
||||
instance.slc_wheel_adopt = (wheel_ns, before)
|
||||
approved = wheel_approval(runtime, sm, cp, wheel_ns=wheel_ns, now=START + 50_000_000)
|
||||
assert approved.wheelAdoptTargetMps == pytest.approx(50 * CV.MPH_TO_MS)
|
||||
|
||||
now = START + 100_000_000
|
||||
sm.advance(now)
|
||||
sm['slcDashboardObservation'].speedMps = 50 * CV.MPH_TO_MS
|
||||
sm['slcDashboardObservation'].episode += 1
|
||||
state = runtime.step(sm, cp, now_ns=now).message.slcState
|
||||
assert state.actionSequenceId == approved.actionSequenceId and state.actionStatus == 'adopt'
|
||||
assert state.acceptedAdjustedLimit == pytest.approx(55 * CV.MPH_TO_MS)
|
||||
assert state.wheelAdoptMonoTime == 0
|
||||
sm.data['slcState'] = state
|
||||
instance.apply_slc_adopt(now, host_control_enabled=True)
|
||||
assert instance.v_cruise_helper.v_cruise_kph == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize('proof', ['qualified', 'expired', 'unacknowledged', 'lateral-only', 'missing'])
|
||||
def test_stopped_set_matches_limit_before_longitudinal_activates(proof):
|
||||
_, sm, cp = drive(limit=40., offset=0., confirmation=False)
|
||||
assert sm['slcState'].hasAccepted
|
||||
now = START + 10_000_000
|
||||
if proof != 'missing':
|
||||
sm.data['aolAxisState'] = messaging.new_message('aolAxisState').aolAxisState
|
||||
axis = sm['aolAxisState']
|
||||
axis.sessionId, axis.qualified = 'axis', True
|
||||
axis.nativeAcknowledged = proof != 'unacknowledged'
|
||||
axis.desiredLongitudinal = proof != 'lateral-only'
|
||||
axis.observedMonoTime = START
|
||||
axis.validUntilMonoTime = now - 1 if proof == 'expired' else now + 10_000_000
|
||||
sm.valid['aolAxisState'] = sm.alive['aolAxisState'] = True
|
||||
sm.seen = {'aolAxisState': True}
|
||||
sm.advance(now)
|
||||
if proof != 'missing':
|
||||
sm.logMonoTime['aolAxisState'] = START
|
||||
sm['carControl'].enabled = True
|
||||
sm['carState'].vEgo = sm['carState'].vEgoCluster = 0.
|
||||
sm['carState'].standstill = sm['carState'].brakePressed = True
|
||||
instance = owner(sm, cp, resume=False)
|
||||
instance.v_cruise_helper.v_cruise_kph = instance.v_cruise_helper.v_cruise_cluster_kph = 64.4
|
||||
with patch.object(card.time, 'monotonic_ns', return_value=now):
|
||||
instance.update_cruise_speed(sm['carState'])
|
||||
expected = 64.4 if proof == 'qualified' else cruise.V_CRUISE_INITIAL
|
||||
assert instance.v_cruise_helper.v_cruise_kph == expected
|
||||
assert bool(instance.slc_receipts) == (proof == 'qualified')
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""The fixed previous-limit fallback survives restarts without inventing source evidence."""
|
||||
|
||||
import json
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.plannerd import SavedSlcLifecycle
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc, history
|
||||
from openpilot.starpilot.speed_limits.runtime import Action
|
||||
from openpilot.starpilot.ui.tests.test_slc_retention import MPH, NOW, RetentionDrive
|
||||
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source', ['dashboard', 'map', 'vision'])
|
||||
def test_saved_previous_limit_caps_native_planner_after_restart_without_a_current_source(tmp_path, source):
|
||||
drive = RetentionDrive(tmp_path)
|
||||
accepted, _, _ = drive.step(0)
|
||||
previous = accepted.result.state.acceptance.accepted
|
||||
previous = replace(previous, candidate=replace(previous.candidate, source=source))
|
||||
params = Params(str(tmp_path))
|
||||
params.put('SLCQualifiedHistory', json.loads(history.encode(previous, 1)), block=True)
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
lifecycle = SavedSlcLifecycle(params, drive.cp, {})
|
||||
drive.runtime, _ = lifecycle.refresh(NOW)
|
||||
drive.sm['slcDashboardObservation'].status = 'unknown'
|
||||
output, view, target = drive.step(1000, received=False)
|
||||
assert not output.result.errors
|
||||
assert target == pytest.approx(45, abs=1e-4)
|
||||
assert output.message.slcState.observationKind != 'valid'
|
||||
assert output.message.slcState.presentationId == 0
|
||||
assert output.message.slcState.hasCeiling and not output.message.slcState.hasPending
|
||||
shown = resolve_unified_speed(view)
|
||||
assert (shown.posted_text, shown.status) == ('45', 'Unavailable')
|
||||
assert shown.source == source
|
||||
|
||||
proposed, _, target = drive.step(1050, limit=50)
|
||||
assert proposed.message.slcState.hasPending
|
||||
assert target == pytest.approx(45, abs=1e-4)
|
||||
state = proposed.message.slcState
|
||||
action = Action(drive.runtime.session_id, 1, state.decisionId, state.presentationId, 'accept')
|
||||
adopted, _, target = drive.step(1100, limit=50, request=action)
|
||||
assert not adopted.message.slcState.hasPending
|
||||
assert target == pytest.approx(50, abs=1e-4)
|
||||
assert drive.sm['carState'].vCruise == pytest.approx(70 * MPH * 3.6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('source_schema', 'wrong'), ('version', True), ('version', 2), ('saved_wall_ns', 0),
|
||||
('source', 'online'), ('source', 'unknown'), ('speed_mps', 0), ('speed_mps', -1),
|
||||
('speed_mps', True), ('speed_mps', float('nan')), ('speed_mps', float('inf')),
|
||||
('identity_kind', 'invalid'), ('identity_value', ''), ('origin_session', ' '),
|
||||
])
|
||||
def test_invalid_saved_previous_limit_cannot_authorize_a_cap(field, value):
|
||||
candidate = acc.Candidate('dashboard', acc.ObservationIdentity(acc.IdentityKind.PRODUCER_EPISODE, 'card:1'), 20.)
|
||||
saved = json.loads(history.encode(acc.AcceptedLimit(candidate, 'drive', 1), 1))
|
||||
saved[field] = value
|
||||
assert history.decode(saved, session_id='next-drive') is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw', [None, b'', b'{', b'null', b'[]', b'{}', b'[' * 1500 + b']' * 1500])
|
||||
def test_unreadable_previous_limit_is_absent(raw):
|
||||
assert history.decode(raw, session_id='next-drive') is None
|
||||
|
||||
|
||||
def test_oversized_saved_previous_limit_does_not_stop_runtime_startup(tmp_path):
|
||||
drive = RetentionDrive(tmp_path)
|
||||
params = Params(str(tmp_path))
|
||||
with open(params.get_param_path('SLCQualifiedHistory'), 'wb') as stream:
|
||||
stream.write(b' ' * (history.MAX_BYTES + 1))
|
||||
drive.runtime, _ = SavedSlcLifecycle(params, drive.cp, {}).refresh(NOW)
|
||||
output, _, target = drive.step(1000, received=False)
|
||||
assert not output.result.errors
|
||||
assert not output.message.slcState.hasCeiling
|
||||
assert target == pytest.approx(70, abs=1e-4)
|
||||
@@ -261,7 +261,7 @@ class MapSourceTests(unittest.TestCase):
|
||||
authority = acc.Authority(acc.Mode.COMBINED, acc.LongitudinalOwner.SYSTEM, True, True, False, False)
|
||||
decision = acc.step(acc.new_session('drive', prior), chosen.observation, authority,
|
||||
acc.Policy(), now_ns=456_100_000_000)
|
||||
self.assertIsNone(decision.control_target_mps)
|
||||
self.assertEqual(decision.control_target_mps, prior.candidate.speed_mps)
|
||||
self.assertEqual(decision.basis, 'unknown')
|
||||
self.assertEqual(decision.state.accepted, prior)
|
||||
self.assertIsNone(decision.history_write)
|
||||
|
||||
@@ -54,18 +54,20 @@ class SM:
|
||||
def test_maps_priority_slots_and_defaults_use_actual_runtime_projection():
|
||||
sm = SM(packet())
|
||||
cp = NS(carFingerprint='vehicle')
|
||||
for first, second, expected in (('Maps', 'Dashboard', sel.Source.MAP),
|
||||
('Dashboard', 'Maps', sel.Source.DASHBOARD),
|
||||
('Vision', 'Maps', sel.Source.MAP)):
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': first, 'SLCPriority2': second})
|
||||
for first, second, third, expected in (('Maps', 'Dashboard', 'Off', sel.Source.MAP),
|
||||
('Dashboard', 'Maps', 'Off', sel.Source.DASHBOARD),
|
||||
('Vision', 'Maps', 'Off', sel.Source.MAP),
|
||||
('Vision', 'Off', 'Maps', sel.Source.MAP),
|
||||
('Vision', 'Off', 'Off', None)):
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': first, 'SLCPriority2': second, 'SLCPriority3': third})
|
||||
runtime = Runtime(settings, session_id='drive')
|
||||
observations = runtime._observations(sm, cp, MONO, BOOT)
|
||||
chosen = sel.select_limit(observations, settings.selection)
|
||||
assert chosen.selected_source is expected
|
||||
settings = parse({})
|
||||
assert settings.selection.slots == (sel.Source.VISION, sel.Source.DASHBOARD)
|
||||
assert settings.selection.slots == (sel.Source.VISION, sel.Source.MAP, None)
|
||||
chosen = sel.select_limit(Runtime(settings)._observations(sm, cp, MONO, BOOT), settings.selection)
|
||||
assert chosen.selected_source is sel.Source.DASHBOARD
|
||||
assert chosen.selected_source is sel.Source.MAP
|
||||
assert parse({'SLCPriority1': 'Map Data'}).selection.slots[0] is sel.Source.MAP
|
||||
|
||||
|
||||
@@ -83,21 +85,25 @@ def test_expired_map_falls_through_to_current_dashboard_without_renewal():
|
||||
assert values[sel.Source.MAP].candidate is None
|
||||
|
||||
|
||||
def test_map_loss_no_metadata_and_lookahead_keep_history_without_control_target():
|
||||
def test_map_loss_no_metadata_and_lookahead_reuse_only_the_previous_accepted_limit():
|
||||
authority = acc.Authority(acc.Mode.COMBINED, acc.LongitudinalOwner.SYSTEM, True, True, False, False)
|
||||
prior = acc.AcceptedLimit(acc.Candidate('map', acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value='previous-way'), 20.), 'drive', 0)
|
||||
frame = ms.decode_event(packet().to_bytes())
|
||||
without_limit = replace(frame, status='matchedNoLimit', speed_mps=0.)
|
||||
for changed in (replace(frame, way_selection='possible'), replace(frame, way_selection='predicted'),
|
||||
replace(frame, way_selection='extended'), replace(frame, version=1),
|
||||
replace(frame, tile_loaded=False), replace(frame, event_mono_ns=BOOT+1),
|
||||
replace(frame, status='matchedNoLimit', speed_mps=0.),
|
||||
without_limit, replace(without_limit, way_selection='possible'),
|
||||
replace(without_limit, event_valid=False), replace(without_limit, event_mono_ns=BOOT+1),
|
||||
replace(frame, status='noCoverage', event_valid=False, tile_loaded=False, way_id=0, speed_mps=0.)):
|
||||
observation = MapSource().step(changed, received=True, now_boot_ns=BOOT,
|
||||
source = MapSource()
|
||||
observation = source.step(changed, received=True, now_boot_ns=BOOT,
|
||||
transport_alive=True, drive_session='drive', car_fingerprint='vehicle')
|
||||
assert source.way_id == (frame.way_id if changed is without_limit else 0)
|
||||
assert observation.candidate is None
|
||||
assert observation.kind is not acc.ObservationKind.ABSENT
|
||||
decision = acc.step(acc.new_session('drive', prior), observation, authority, acc.Policy(), now_ns=MONO)
|
||||
assert decision.control_target_mps is None
|
||||
assert decision.control_target_mps == prior.candidate.speed_mps
|
||||
assert decision.state.accepted == prior
|
||||
assert decision.history_write is None
|
||||
|
||||
@@ -138,9 +144,13 @@ def test_actual_saved_maps_priority_starts_existing_readonly_provider_without_ne
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
cp = NS(notCar=False)
|
||||
assert read_params(params).selection.slots == (sel.Source.VISION, sel.Source.DASHBOARD)
|
||||
assert params.get_default_value('SLCPriority2') == 'Map Data'
|
||||
assert read_params(params).selection.slots == (sel.Source.VISION, sel.Source.MAP, None)
|
||||
assert shadow_lifecycle.enabled(True, params, cp)
|
||||
params.put('SLCPriority2', 'Dashboard', block=True)
|
||||
assert not shadow_lifecycle.enabled(True, params, cp)
|
||||
params.put('SLCPriority2', 'Map Data', block=True)
|
||||
params.put('SLCPriority2', 'Off', block=True)
|
||||
params.put('SLCPriority3', 'Map Data', block=True)
|
||||
assert shadow_lifecycle.enabled(True, params, cp)
|
||||
assert not Path(params.get_param_path('MapdShadowEnabled')).exists()
|
||||
params.put_bool('ShowSpeedLimits', False, block=True)
|
||||
@@ -152,7 +162,7 @@ def test_actual_saved_maps_priority_starts_existing_readonly_provider_without_ne
|
||||
assert not shadow_lifecycle.enabled(True, params, cp)
|
||||
|
||||
|
||||
def test_shared_settings_owner_maps_labels_both_slots_defaults_and_ready_change_guard():
|
||||
def test_shared_settings_owner_maps_labels_defaults_and_ready_change_guard():
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
ready = True
|
||||
@@ -162,14 +172,22 @@ def test_shared_settings_owner_maps_labels_both_slots_defaults_and_ready_change_
|
||||
state = owner.snapshot('slc', parked=True, system_long=False, lateral_context=False, metric=False)
|
||||
return next(r for r in state.rows if r.key == key)
|
||||
assert row('SLCPriority1').value == 'Vision'
|
||||
assert row('SLCPriority2').value == 'Dashboard'
|
||||
for key in ('SLCPriority1', 'SLCPriority2'):
|
||||
assert row('SLCPriority2').value == row('SLCPriority2').default_value == 'Maps'
|
||||
assert row('SLCPriority3').value == row('SLCPriority3').default_value == 'Off'
|
||||
params.put('SLCPriority2', 'Dashboard', block=True)
|
||||
for key in ('SLCPriority1', 'SLCPriority2', 'SLCPriority3'):
|
||||
assert 'Maps' in row(key).choices
|
||||
assert ('Off' in row(key).choices) == (key != 'SLCPriority1')
|
||||
while row(key).value != 'Maps':
|
||||
request = row_change(row(key))
|
||||
assert request is not None
|
||||
assert owner.apply(request)
|
||||
assert params.get(key) == 'Map Data'
|
||||
if key != 'SLCPriority1':
|
||||
request = row_change(row(key))
|
||||
assert request is not None and request.value == 'Off'
|
||||
assert owner.apply(request)
|
||||
assert params.get(key) == 'Off'
|
||||
request = row_change(row('SLCPriority1'))
|
||||
assert request is not None
|
||||
ready = False
|
||||
@@ -181,11 +199,10 @@ def test_shared_settings_owner_maps_labels_both_slots_defaults_and_ready_change_
|
||||
assert pages.index('curve') < pages.index('slc')
|
||||
|
||||
|
||||
def test_manager_default_demand_does_not_read_full_slc_settings():
|
||||
def test_manager_skips_full_slc_settings_read_when_saved_priorities_exclude_maps():
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
with patch('openpilot.starpilot.speed_limits.runtime_settings.read_params', side_effect=AssertionError('full scan')):
|
||||
assert not shadow_lifecycle.enabled(True, params, NS(notCar=False))
|
||||
params.put('SLCPriority1', 'Vision', block=True)
|
||||
params.put('SLCPriority2', 'Dashboard', block=True)
|
||||
assert not shadow_lifecycle.enabled(True, params, NS(notCar=False))
|
||||
@@ -283,7 +300,6 @@ def test_actual_runtime_maps_display_only_publishes_sign_without_ceiling_or_comm
|
||||
assert state.observationKind == 'valid' and state.source == 'map'
|
||||
assert abs(state.speedLimit-13.4112) < .001
|
||||
assert not state.hasCeiling and result.result.ceiling is None
|
||||
assert result.command is None
|
||||
assert not state.hasPending and not state.hasAccepted
|
||||
# Keep the real cereal envelopes live for the complete reader lifetime.
|
||||
assert len(envelopes) == 7 and dashboard.which() == 'slcDashboardObservation'
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Protect limit decisions against legacy Maps conversion noise on the Float32 wire."""
|
||||
from dataclasses import replace
|
||||
import math
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc, history
|
||||
from openpilot.starpilot.speed_limits.map_observation import repair_legacy_map_speed
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import parse
|
||||
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
|
||||
from openpilot.starpilot.speed_limits.tests.test_advance import Drive
|
||||
|
||||
|
||||
def numeric_drive(metric=True):
|
||||
drive = Drive(higher=0, lower=0)
|
||||
drive.sm['carState'].vCruise = drive.sm['carState'].vCruiseCluster = 140.
|
||||
drive.runtime.is_metric = metric
|
||||
drive.runtime.settings = replace(drive.runtime.settings,
|
||||
selection=parse({'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Maps'}).selection,
|
||||
offsets=OffsetSchedule((OffsetBand(0., None, 0.),)))
|
||||
return drive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('metric', [False, True])
|
||||
@pytest.mark.parametrize('posted', [30, 50, 80, 100, 130, 250])
|
||||
def test_legacy_map_conversion_does_not_reprompt_for_the_same_limit(metric, posted):
|
||||
drive = numeric_drive(metric)
|
||||
exact = posted / 3.6
|
||||
# Frozen maps.ParseMaxSpeed conversion; source correction must also account for existing tiles.
|
||||
mapped = posted * .277778
|
||||
first = drive.tick(current=mapped, dashboard=exact, target=0.)
|
||||
accepted = first.result.acceptance.state.accepted.candidate.speed_mps
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
switched = drive.tick(current=mapped, dashboard=None, target=0.)
|
||||
state = switched.message.slcState
|
||||
assert state.source == 'map' and not state.hasPending
|
||||
assert state.speedLimit == accepted
|
||||
assert switched.result.ceiling == first.result.ceiling
|
||||
|
||||
|
||||
@pytest.mark.parametrize('exact,mapped', [
|
||||
(50 / 3.6, 50 * (1 / 3.6)),
|
||||
(45 * .44704, 45 * 1.609344 / 3.6),
|
||||
(45 * .44704, math.nextafter(45 * .44704, math.inf)),
|
||||
])
|
||||
def test_equivalent_arithmetic_collapses_to_the_same_live_wire_value(exact, mapped):
|
||||
drive = numeric_drive()
|
||||
first = drive.tick(current=mapped, dashboard=exact, target=0.)
|
||||
accepted = first.result.acceptance.state.accepted.candidate.speed_mps
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
switched = drive.tick(current=mapped, dashboard=None, target=0.)
|
||||
assert switched.message.slcState.speedLimit == accepted
|
||||
assert not switched.message.slcState.hasPending
|
||||
|
||||
|
||||
@pytest.mark.parametrize('abbreviated', [False, True])
|
||||
def test_alternating_same_posted_sources_keeps_the_original_deadline(abbreviated):
|
||||
drive = numeric_drive()
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
exact = 50 / 3.6
|
||||
mapped = 50 * .277778 if abbreviated else exact
|
||||
decision_ids, starts, timeout_frames = set(), set(), []
|
||||
for index in range(801):
|
||||
output = drive.tick(current=mapped, dashboard=exact if (index // 10) % 2 == 0 else None,
|
||||
target=0., at=drive.now+50_000_000)
|
||||
state = output.message.slcState
|
||||
assert not output.result.errors
|
||||
if state.hasPending:
|
||||
decision_ids.add(state.decisionId)
|
||||
starts.add(state.pendingSinceMonoTime)
|
||||
if output.result.acceptance.basis == 'timeout_reject':
|
||||
timeout_frames.append(index)
|
||||
assert not state.hasPending and len(decision_ids) == len(starts) == 1
|
||||
assert timeout_frames == [600]
|
||||
|
||||
|
||||
def test_legacy_signature_sweep():
|
||||
def f32(value):
|
||||
return struct.unpack('f', struct.pack('f', value))[0]
|
||||
for posted in range(4, 251):
|
||||
assert repair_legacy_map_speed(f32(posted * .277778)) == f32(posted / 3.6)
|
||||
assert repair_legacy_map_speed(f32(posted / 3.6)) == f32(posted / 3.6)
|
||||
for posted in range(3, 251):
|
||||
assert repair_legacy_map_speed(f32(posted * .44704)) == f32(posted * .44704)
|
||||
for speed in (f32(50 / 3.6 + .00004), 1e300):
|
||||
assert repair_legacy_map_speed(speed) == speed
|
||||
|
||||
|
||||
def test_upcoming_legacy_limit_matches_at_crossing():
|
||||
drive = numeric_drive()
|
||||
drive.runtime.update_advance(5, 5)
|
||||
drive.tick(current=50 * .277778, target=0.)
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
preview = drive.tick(current=50 * .277778, target=55 * .277778).message.slcState
|
||||
assert preview.hasPending and preview.pendingSpeedLimit == preview.upcomingSpeedLimit
|
||||
crossed = drive.tick(current=55 / 3.6, target=0., way=2).message.slcState
|
||||
assert crossed.speedLimit == preview.upcomingSpeedLimit
|
||||
assert crossed.decisionId == preview.decisionId
|
||||
assert crossed.pendingSinceMonoTime == preview.pendingSinceMonoTime
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source', ['map', 'dashboard', 'vision'])
|
||||
def test_restore_repairs_only_saved_maps_limits(source):
|
||||
drive = numeric_drive()
|
||||
road = drive.tick(current=50 * .277778, target=0.)
|
||||
saved = road.result.acceptance.state.accepted
|
||||
# The saved record predates the repair and carries the original wire value.
|
||||
legacy = float(drive.sm['mapdOut'].speedLimit)
|
||||
saved = replace(saved, candidate=replace(saved.candidate, source=source, speed_mps=legacy))
|
||||
restored = history.decode(history.encode(saved, 1), session_id=drive.runtime.session_id)
|
||||
assert restored is not None
|
||||
if source != 'map':
|
||||
assert restored.candidate.speed_mps == legacy
|
||||
return
|
||||
drive.runtime.state = replace(drive.runtime.state, acceptance=acc.new_session(drive.runtime.session_id, restored))
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
state = drive.tick(current=50 / 3.6, dashboard=50 / 3.6, target=0.).message.slcState
|
||||
assert state.speedLimit == restored.candidate.speed_mps and not state.hasPending
|
||||
|
||||
|
||||
def test_same_numeric_producer_restart_keeps_decision_but_retires_the_old_press():
|
||||
drive = numeric_drive()
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
first = drive.tick(current=50 / 3.6, target=0.).message.slcState
|
||||
replacement = drive.tick(current=50 / 3.6, target=0., producer=42).message.slcState
|
||||
assert replacement.decisionId == first.decisionId
|
||||
assert replacement.pendingSinceMonoTime == first.pendingSinceMonoTime
|
||||
assert replacement.presentationId != first.presentationId
|
||||
acted = drive.tick(current=50 / 3.6, target=0., producer=42, action=drive.action(first, 'accept'))
|
||||
assert acted.message.slcState.hasPending
|
||||
assert acted.message.slcState.acceptedSpeedLimit == 20.
|
||||
|
||||
|
||||
def test_plain_source_gap_keeps_numeric_decision_and_original_expiry():
|
||||
drive = numeric_drive()
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
first = drive.tick(current=50 / 3.6, target=0.).message.slcState
|
||||
lost = drive.tick(current=50 / 3.6, target=0., alive=False).message.slcState
|
||||
assert lost.decisionId == first.decisionId and lost.presentationId == first.presentationId
|
||||
expired = drive.tick(current=50 / 3.6, target=0., at=first.pendingSinceMonoTime+acc.CONFIRMATION_NS)
|
||||
assert not expired.message.slcState.hasPending
|
||||
assert expired.result.acceptance.basis == 'timeout_reject'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('before,after,metric', [(40 / 3.6, 41 / 3.6, False), (45 * .44704, 46 * .44704, True)])
|
||||
def test_real_one_unit_changes_still_ask_even_when_a_display_can_hide_the_difference(before, after, metric):
|
||||
drive = numeric_drive(metric)
|
||||
first = drive.tick(current=before, dashboard=before, target=0.)
|
||||
drive.runtime.settings = replace(drive.runtime.settings, acceptance=acc.Policy())
|
||||
changed = drive.tick(current=after, dashboard=after, target=0.)
|
||||
assert changed.message.slcState.hasPending
|
||||
assert changed.message.slcState.pendingSpeedLimit != first.message.slcState.acceptedSpeedLimit
|
||||
if not metric:
|
||||
assert round(before / .44704) == round(after / .44704) == 25
|
||||
@@ -1,373 +0,0 @@
|
||||
"""Override intent requires the acceptance decision and causal ledger together."""
|
||||
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import action_arbitration as arb
|
||||
from openpilot.starpilot.speed_limits import overrides as ov
|
||||
|
||||
SESSION = "drive"
|
||||
HIGH = acc.Candidate("dashboard", acc.ObservationIdentity(acc.IdentityKind.SESSION_VALUE, session_id=SESSION), 20.0)
|
||||
LOW = acc.Candidate("map", acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value="road-2"), 15.0)
|
||||
SAME = acc.Candidate("map", acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value="road-3"), 20.0)
|
||||
ACTIVE = acc.Authority(acc.Mode.LONGITUDINAL_ONLY, acc.LongitudinalOwner.SYSTEM, False, True, False, False)
|
||||
POLICY = acc.Policy(confirm_lower=False, confirm_higher=False)
|
||||
PEDAL_OFF = ov.PedalEvidence(ov.EvidenceKind.VALID, False)
|
||||
|
||||
|
||||
def observation(candidate=HIGH):
|
||||
return acc.Observation(acc.ObservationKind.VALID, candidate)
|
||||
|
||||
|
||||
class OverrideTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
state = replace(acc.new_session(SESSION, acc.AcceptedLimit(HIGH, SESSION, 0)), received_speed_mps=HIGH.speed_mps)
|
||||
self.accepted = acc.step(state, observation(), ACTIVE, POLICY, now_ns=0)
|
||||
self.state = ov.new_session(SESSION)
|
||||
self.selected = ov.SelectedEvidence(ov.EvidenceKind.VALID, 20.0)
|
||||
self.first = self.reduce(0)
|
||||
|
||||
def decision(self, now, *, candidate=HIGH, authority=ACTIVE, policy=POLICY, source=None, adopt=None):
|
||||
obs = observation(candidate) if source is None else acc.Observation(source)
|
||||
result = acc.step(self.accepted.state, obs, authority, policy, now_ns=now, adopt=adopt)
|
||||
self.accepted = result
|
||||
return result
|
||||
|
||||
def reduce(self, now, *, acceptance=None, authority=ACTIVE, policy=POLICY, selected=None, pedal=PEDAL_OFF, change=None):
|
||||
result = ov.step(self.state, self.accepted if acceptance is None else acceptance, authority, policy,
|
||||
self.selected if selected is None else selected, pedal, now_ns=now, change=change)
|
||||
self.state = result.state
|
||||
return result
|
||||
|
||||
def classified(self, *, effect=1, now=2, previous=20.0, chosen=25.0, context=None, disposition=arb.Disposition.DRIVER_INTENT):
|
||||
ledger = arb.new_ledger(SESSION)
|
||||
context = self.first.context if context is None else context
|
||||
ledger = arb.step(ledger, arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE, context.context_id), now_ns=1).state
|
||||
ledger = arb.step(ledger, arb.ResolveAction(SESSION, 1, disposition), now_ns=1).state
|
||||
result = arb.step(ledger, arb.CruiseChange(SESSION, effect, 1, previous, chosen), now_ns=now)
|
||||
return result.change
|
||||
|
||||
def arm(self, now=2, chosen=25.0):
|
||||
change = self.classified(now=now, chosen=chosen)
|
||||
self.decision(now)
|
||||
return self.reduce(now, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, chosen), change=change)
|
||||
|
||||
def test_fresh_causal_intent_persists_and_scalar_does_not_rearm(self):
|
||||
armed = self.arm()
|
||||
self.assertEqual(armed.contribution_mps, 25.0)
|
||||
self.assertEqual(armed.event_receipt.status, "driver_intent")
|
||||
self.assertTrue(armed.event_receipt.consumed)
|
||||
self.decision(3)
|
||||
self.assertEqual(self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.0)
|
||||
self.decision(4, candidate=replace(LOW, source=HIGH.source))
|
||||
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 (
|
||||
(False, 30.0, 29.0, 29.0), (False, 30.0, 20.0, None), (False, 30.0, 19.0, 19.0),
|
||||
(True, 25.0, 24.0, 24.0), (True, 25.0, 20.0, None), (True, 25.0, 19.0, 19.0),
|
||||
(True, 30.0, 29.0, 29.0), (True, 30.0, 25.0, 25.0),
|
||||
):
|
||||
with self.subTest(existing=existing, chosen=chosen):
|
||||
self.setUp()
|
||||
if existing:
|
||||
self.arm()
|
||||
self.decision(2)
|
||||
self.reduce(2, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, previous))
|
||||
self.decision(3)
|
||||
result = self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, chosen),
|
||||
change=self.classified(effect=2, now=3, previous=previous, chosen=chosen))
|
||||
self.assertFalse(result.errors)
|
||||
self.assertTrue(result.event_receipt.consumed)
|
||||
self.assertEqual(result.state.persistent_selected_mps, expected)
|
||||
self.assertEqual(result.contribution_mps, expected)
|
||||
self.assertEqual(self.accepted.control_target_mps, HIGH.speed_mps)
|
||||
|
||||
def test_new_adoption_clears_override_and_equal_received_speed_does_not(self):
|
||||
self.arm(chosen=25.)
|
||||
self.decision(3, candidate=SAME)
|
||||
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.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()
|
||||
self.decision(3)
|
||||
self.assertEqual(self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0),
|
||||
pedal=ov.PedalEvidence(ov.EvidenceKind.VALID, True, 28.0)).contribution_mps, 25.0)
|
||||
self.decision(4)
|
||||
self.assertEqual(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0),
|
||||
pedal=ov.PedalEvidence(ov.EvidenceKind.VALID, True, 23.0)).contribution_mps, 25.0)
|
||||
self.decision(5)
|
||||
self.assertEqual(self.reduce(5, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.0)
|
||||
|
||||
def test_temporary_inactivity_invalidates_origin_and_grace_expires(self):
|
||||
self.arm()
|
||||
paused = replace(ACTIVE, longitudinal_active=False)
|
||||
self.decision(3, authority=paused)
|
||||
self.assertIsNone(self.reduce(3, authority=paused, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).context)
|
||||
self.decision(3 + ov.SUSPENSION_NS - 1, authority=paused)
|
||||
self.reduce(3 + ov.SUSPENSION_NS - 1, authority=paused, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.decision(3 + ov.SUSPENSION_NS - 1)
|
||||
resumed = self.reduce(3 + ov.SUSPENSION_NS - 1, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertEqual(resumed.contribution_mps, 25.0)
|
||||
self.assertNotEqual(resumed.context.context_id, self.first.context.context_id)
|
||||
self.decision(ov.SUSPENSION_NS + 10, authority=paused)
|
||||
self.reduce(ov.SUSPENSION_NS + 10, authority=paused, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.decision(2 * ov.SUSPENSION_NS + 10)
|
||||
self.assertEqual(self.reduce(2 * ov.SUSPENSION_NS + 10, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.)
|
||||
|
||||
def test_stale_missing_and_display_only_suppress_output_and_watermark(self):
|
||||
self.arm()
|
||||
change = self.classified(effect=2, now=3, chosen=26.0)
|
||||
self.decision(3, source=acc.ObservationKind.STALE)
|
||||
stale = self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0), change=change)
|
||||
self.assertEqual(stale.contribution_mps, 26.)
|
||||
self.assertEqual(stale.state.last_effect_id, 2)
|
||||
self.decision(4)
|
||||
self.assertEqual(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), change=change).event_receipt.status, "already_seen")
|
||||
display = acc.Policy(confirm_lower=False, confirm_higher=False, display_only=True)
|
||||
self.decision(5, policy=display)
|
||||
self.assertIsNone(self.reduce(5, policy=display, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps)
|
||||
|
||||
def test_source_gaps_preserve_the_manual_choice(self):
|
||||
for kind in (acc.ObservationKind.ABSENT, acc.ObservationKind.UNKNOWN, acc.ObservationKind.STALE):
|
||||
with self.subTest(kind=kind):
|
||||
self.setUp()
|
||||
self.arm()
|
||||
self.decision(3, source=kind)
|
||||
result = self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertEqual(result.contribution_mps, 25.0)
|
||||
self.assertEqual(result.context, self.first.context)
|
||||
|
||||
def test_delayed_acceptance_consumed_effect_cannot_arm_after_release(self):
|
||||
pending_policy = acc.Policy(confirm_lower=True, confirm_higher=True)
|
||||
state = replace(acc.new_session(SESSION, acc.AcceptedLimit(HIGH, SESSION, 0)), received_speed_mps=HIGH.speed_mps)
|
||||
pending = acc.step(state, observation(LOW), ACTIVE, pending_policy, now_ns=0)
|
||||
action = acc.DriverAction(SESSION, 1, pending.state.pending.decision_id, acc.ActionKind.ACCEPT)
|
||||
ledger = arb.step(arb.new_ledger(SESSION), arb.BeginAction(SESSION, 1, arb.Origin.DRIVER_CRUISE,
|
||||
self.first.context.context_id), now_ns=1).state
|
||||
accepted = acc.step(pending.state, observation(LOW), ACTIVE, pending_policy, now_ns=2, action=action)
|
||||
ledger = arb.resolve_acceptance(ledger, 1, accepted, now_ns=2).state
|
||||
delayed = arb.step(ledger, arb.CruiseChange(SESSION, 1, 1, 20.0, 25.0), now_ns=60_000_000_000).change
|
||||
self.assertEqual(delayed.disposition, arb.Disposition.SLC_CONSUMED)
|
||||
self.decision(60_000_000_000)
|
||||
result = self.reduce(60_000_000_000, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), change=delayed)
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertEqual(result.state.last_effect_id, 1)
|
||||
|
||||
def test_adoption_clears_once_and_current_pedal_still_contributes(self):
|
||||
self.arm()
|
||||
shown = self.decision(3, candidate=LOW)
|
||||
adopt = acc.AdoptRequest(SESSION, 2, shown.state.presentation.presentation_id, LOW)
|
||||
adopted = self.decision(4, candidate=LOW, adopt=adopt)
|
||||
result = self.reduce(4, acceptance=adopted, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0),
|
||||
pedal=ov.PedalEvidence(ov.EvidenceKind.VALID, True, 27.0))
|
||||
self.assertIsNone(result.state.persistent_selected_mps)
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertNotEqual(result.context.context_id, self.first.context.context_id)
|
||||
duplicate = self.reduce(4, acceptance=adopted, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertEqual(duplicate.context, result.context)
|
||||
|
||||
def test_mismatched_snapshot_and_inactive_origin_fail_closed(self):
|
||||
change = self.classified()
|
||||
self.decision(2)
|
||||
mismatch = self.reduce(2, policy=replace(POLICY, confirm_lower=True), change=change)
|
||||
self.assertTrue(mismatch.errors)
|
||||
self.assertEqual(mismatch.state.last_effect_id, 1)
|
||||
self.assertTrue(mismatch.state.reset_required)
|
||||
self.decision(3)
|
||||
self.assertIsNone(self.reduce(3).context)
|
||||
self.state = ov.new_session(SESSION)
|
||||
old = self.reduce(3).context
|
||||
paused = replace(ACTIVE, longitudinal_active=False)
|
||||
self.decision(4, authority=paused)
|
||||
self.reduce(4, authority=paused)
|
||||
self.decision(5)
|
||||
late = replace(change, effect_id=2, context_id=old.context_id, started_at_ns=4)
|
||||
result = self.reduce(5, change=late, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertEqual(result.event_receipt.status, "unqualified_effect")
|
||||
|
||||
def test_pending_lower_and_rejection_keep_accepted_target_and_intent(self):
|
||||
self.arm()
|
||||
confirmation = acc.Policy(confirm_lower=True, confirm_higher=True)
|
||||
lower = replace(LOW, source=HIGH.source)
|
||||
pending = self.decision(3, candidate=lower, policy=confirmation)
|
||||
self.assertEqual(pending.basis, "pending")
|
||||
self.assertEqual(self.reduce(3, policy=confirmation, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.0)
|
||||
action = acc.DriverAction(SESSION, 2, pending.state.pending.decision_id, acc.ActionKind.REJECT)
|
||||
rejected = acc.step(pending.state, observation(lower), ACTIVE, confirmation, now_ns=4, action=action)
|
||||
self.accepted = rejected
|
||||
self.assertEqual(self.reduce(4, policy=confirmation, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.0)
|
||||
self.assertIn(lower, rejected.state.rejected)
|
||||
|
||||
def test_provider_flapping_preserves_override_until_the_new_limit_qualifies(self):
|
||||
self.arm()
|
||||
for now, candidate in ((3, LOW), (4, HIGH), (5, LOW)):
|
||||
self.decision(now, candidate=candidate)
|
||||
retained = self.reduce(now, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.))
|
||||
self.assertEqual(retained.contribution_mps, 25.)
|
||||
now = 5 + acc.SOURCE_SWITCH_NS
|
||||
self.decision(now, candidate=LOW)
|
||||
adopted = self.reduce(now, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.))
|
||||
self.assertIsNone(adopted.contribution_mps)
|
||||
|
||||
def test_mode_owner_change_suppresses_output_without_forgetting_manual_choice(self):
|
||||
self.arm()
|
||||
stock = acc.Authority(acc.Mode.LONGITUDINAL_ONLY, acc.LongitudinalOwner.STOCK, False, False, True, False)
|
||||
self.decision(3, authority=stock)
|
||||
self.reduce(3, authority=stock, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertEqual(self.state.persistent_selected_mps, 25.)
|
||||
self.decision(4)
|
||||
self.assertEqual(self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0)).contribution_mps, 25.)
|
||||
|
||||
def test_previous_session_history_does_not_arm_overrides_without_a_live_limit(self):
|
||||
prior = acc.AcceptedLimit(acc.Candidate("map", acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value="road"), 20.0),
|
||||
"prior-drive", 0)
|
||||
policy = acc.Policy()
|
||||
decision = acc.step(acc.new_session(SESSION, prior), acc.Observation(acc.ObservationKind.ABSENT), ACTIVE, policy, now_ns=1)
|
||||
result = ov.step(ov.new_session(SESSION), decision, ACTIVE, policy, self.selected, PEDAL_OFF, now_ns=1)
|
||||
self.assertFalse(result.errors)
|
||||
self.assertIsNone(result.context)
|
||||
|
||||
def test_automatic_foreign_and_completed_effects_are_watermarked_without_intent(self):
|
||||
self.decision(2)
|
||||
automatic = replace(self.classified(now=2), disposition=arb.Disposition.AUTOMATIC)
|
||||
got = self.reduce(2, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), change=automatic)
|
||||
self.assertIsNone(got.contribution_mps)
|
||||
self.assertEqual(got.state.last_effect_id, 1)
|
||||
self.decision(3)
|
||||
foreign = replace(automatic, session_id="other", effect_id=2, disposition=arb.Disposition.DRIVER_INTENT)
|
||||
self.assertEqual(self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), change=foreign).event_receipt.status,
|
||||
"foreign_session")
|
||||
self.decision(4)
|
||||
completed = replace(automatic, effect_id=2, action_id=1, context_id=None, disposition=arb.Disposition.UNRESOLVED)
|
||||
result = self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), change=completed)
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertEqual(result.state.last_effect_id, 2)
|
||||
|
||||
def test_inconsistent_acceptance_target_and_reversed_clock_fail_closed(self):
|
||||
self.decision(2)
|
||||
forged = replace(self.accepted, control_target_mps=21.0)
|
||||
bad = self.reduce(2, acceptance=forged)
|
||||
self.assertTrue(bad.errors)
|
||||
self.assertIsNone(bad.contribution_mps)
|
||||
self.assertTrue(bad.state.reset_required)
|
||||
reversed_clock = self.reduce(1)
|
||||
self.assertTrue(reversed_clock.errors)
|
||||
|
||||
def test_zero_ego_speed_is_valid_but_negative_bool_and_nan_are_not(self):
|
||||
self.arm()
|
||||
for now, pressed in ((3, True), (4, False)):
|
||||
self.decision(now)
|
||||
zero = self.reduce(now, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0),
|
||||
pedal=ov.PedalEvidence(ov.EvidenceKind.VALID, pressed, 0.0))
|
||||
self.assertFalse(zero.errors)
|
||||
self.assertEqual(zero.contribution_mps, 25.0)
|
||||
for invalid in (-0.1, True, float("nan")):
|
||||
with self.subTest(invalid=invalid):
|
||||
self.decision(5)
|
||||
bad = ov.step(self.state, self.accepted, ACTIVE, POLICY, ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0),
|
||||
ov.PedalEvidence(ov.EvidenceKind.VALID, True, invalid), now_ns=5)
|
||||
self.assertTrue(bad.errors)
|
||||
self.assertTrue(bad.state.reset_required)
|
||||
|
||||
def test_malformed_effect_latches_session_and_cannot_replay_after_repair(self):
|
||||
self.arm()
|
||||
fresh = self.classified(effect=2, now=4, chosen=26.0)
|
||||
self.decision(3)
|
||||
malformed = replace(fresh, selected_mps=float("nan"))
|
||||
bad = self.reduce(3, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0), change=malformed)
|
||||
self.assertTrue(bad.state.reset_required)
|
||||
self.assertIsNone(bad.state.persistent_selected_mps)
|
||||
self.assertIsNone(bad.state.context)
|
||||
self.decision(4)
|
||||
retry = self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0), change=fresh)
|
||||
self.assertIsNone(retry.contribution_mps)
|
||||
self.assertEqual(retry.basis, "session_reset_required")
|
||||
next_candidate = acc.Candidate("map", acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value="new-road"), 20.0)
|
||||
next_acceptance = acc.step(acc.new_session("next-drive"), observation(next_candidate), ACTIVE, POLICY, now_ns=0)
|
||||
next_state = ov.step(ov.new_session("next-drive"), next_acceptance, ACTIVE, POLICY, self.selected, PEDAL_OFF, now_ns=0)
|
||||
next_acceptance = acc.step(next_acceptance.state, observation(next_candidate), ACTIVE, POLICY, now_ns=1)
|
||||
replay = ov.step(next_state.state, next_acceptance, ACTIVE, POLICY, ov.SelectedEvidence(ov.EvidenceKind.VALID, 26.0),
|
||||
PEDAL_OFF, now_ns=1, change=fresh)
|
||||
self.assertIsNone(replay.contribution_mps)
|
||||
self.assertEqual(replay.event_receipt.status, "foreign_session")
|
||||
|
||||
def test_malformed_state_is_inert(self):
|
||||
forged = replace(self.state, context=replace(self.first.context, context_id="caller-proof"))
|
||||
self.decision(2)
|
||||
result = ov.step(forged, self.accepted, ACTIVE, POLICY, self.selected, PEDAL_OFF, now_ns=2)
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertIs(result.state, forged)
|
||||
|
||||
def test_reversed_clock_clears_previously_armed_state_and_stays_latched(self):
|
||||
self.arm()
|
||||
bad = self.reduce(1)
|
||||
self.assertTrue(bad.state.reset_required)
|
||||
self.assertIsNone(bad.state.persistent_selected_mps)
|
||||
self.assertIsNone(bad.context)
|
||||
self.decision(3)
|
||||
recovered_input = self.reduce(3)
|
||||
self.assertEqual(recovered_input.basis, "session_reset_required")
|
||||
self.assertIsNone(recovered_input.contribution_mps)
|
||||
|
||||
def test_target_change_requires_new_action_from_the_new_eligible_context(self):
|
||||
self.arm()
|
||||
old_effect = self.classified(effect=2, now=3, chosen=26.0)
|
||||
self.decision(3, candidate=replace(LOW, source=HIGH.source))
|
||||
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=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)
|
||||
|
||||
def test_missing_selected_or_pedal_evidence_suppresses_output(self):
|
||||
self.arm()
|
||||
for now, selected, pedal in (
|
||||
(3, ov.SelectedEvidence(ov.EvidenceKind.STALE), PEDAL_OFF),
|
||||
(4, ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0), ov.PedalEvidence(ov.EvidenceKind.UNKNOWN)),
|
||||
(5, ov.SelectedEvidence(ov.EvidenceKind.ABSENT), PEDAL_OFF),
|
||||
):
|
||||
self.decision(now)
|
||||
result = self.reduce(now, selected=selected, pedal=pedal)
|
||||
self.assertIsNone(result.contribution_mps)
|
||||
self.assertIsNone(result.context)
|
||||
|
||||
def test_same_value_adoption_rotates_once_and_delayed_effect_is_consumed(self):
|
||||
self.arm()
|
||||
shown = self.decision(3)
|
||||
request = acc.AdoptRequest(SESSION, 2, shown.state.presentation.presentation_id, HIGH)
|
||||
ledger = arb.step(arb.new_ledger(SESSION), arb.BeginAction(SESSION, 2, arb.Origin.DRIVER_CRUISE,
|
||||
self.first.context.context_id), now_ns=3).state
|
||||
adopted = self.decision(4, adopt=request)
|
||||
ledger = arb.resolve_acceptance(ledger, 2, adopted, now_ns=4).state
|
||||
result = self.reduce(4, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertIsNone(result.state.persistent_selected_mps)
|
||||
self.assertNotEqual(result.context.context_id, self.first.context.context_id)
|
||||
again = self.reduce(4, acceptance=adopted, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 25.0))
|
||||
self.assertEqual(again.context, result.context)
|
||||
delayed = arb.step(ledger, arb.CruiseChange(SESSION, 3, 2, 25.0, 20.0), now_ns=10_000_000_000).change
|
||||
self.assertEqual(delayed.disposition, arb.Disposition.SLC_CONSUMED)
|
||||
self.decision(10_000_000_000)
|
||||
no_rearm = self.reduce(10_000_000_000, selected=ov.SelectedEvidence(ov.EvidenceKind.VALID, 20.0), change=delayed)
|
||||
self.assertIsNone(no_rearm.contribution_mps)
|
||||
self.assertEqual(no_rearm.event_receipt.status, "unqualified_effect")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Pending choices are source independent; selected-speed commands are not."""
|
||||
"""Card-side validation for confirmation, engagement, and active caps."""
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
from opendbc.car.dashboard_speed_limit import Observation, Status
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.selfdrive.car.cruise import SlcPendingConfirmation
|
||||
@@ -11,7 +12,7 @@ from openpilot.starpilot.speed_limits.selection import Source
|
||||
from openpilot.starpilot.speed_limits.tests.test_runtime_replay import ReplaySM
|
||||
|
||||
|
||||
NOW = 2_000_000_000
|
||||
NOW = 60_000_000_000
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -20,37 +21,36 @@ def sm():
|
||||
state.enabled, state.observationKind, state.source = True, 'valid', 'map'
|
||||
state.sessionId, state.frameMonoTime = 'drive', NOW
|
||||
state.hasPending, state.pendingSpeedLimit, state.speedLimit = True, 25.0, 25.0
|
||||
state.pendingSource, state.pendingSinceMonoTime = 'map', NOW
|
||||
state.decisionId, state.presentationId = 1, 2
|
||||
return ReplaySM({'slcState': state}, NOW)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source', tuple(Source))
|
||||
def test_confirm_any_source_without_dashboard_evidence(sm, source):
|
||||
sm['slcState'].source = source.value
|
||||
sm['slcState'].pendingSource = source.value
|
||||
assert physical_actions.pending_confirmation(sm, NOW, long_active=True) == SlcPendingConfirmation('drive', 1, 2)
|
||||
# A pending choice alone must never qualify a selected-speed command.
|
||||
current = physical_actions.state_current(sm, None, 'card', NOW)
|
||||
assert (current is not None) == (source is not Source.DASHBOARD)
|
||||
|
||||
|
||||
def test_retained_choice_still_consumes_a_press_as_its_published_deadline_passes(sm):
|
||||
state = sm['slcState']
|
||||
state.observationKind, state.source, state.pendingSource = 'unknown', 'none', 'map'
|
||||
state.presentationId, state.pendingSinceMonoTime = 0, NOW
|
||||
published = NOW + 9_990_000_000
|
||||
state.pendingSinceMonoTime = NOW
|
||||
published = NOW + physical_actions.CONFIRMATION_NS - 10_000_000
|
||||
state.frameMonoTime = sm.logMonoTime['slcState'] = published
|
||||
# The fresh displayed frame still has the choice; a late press must not adjust SET.
|
||||
assert physical_actions.pending_confirmation(sm, published + 20_000_000, long_active=True) is not None
|
||||
assert physical_actions.pending_confirmation(sm, published + physical_actions.STATE_MAX_AGE_NS + 1, long_active=True) is None
|
||||
assert physical_actions.state_current(sm, None, 'card', published) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('enabled', False), ('displayOnly', True), ('observationKind', 'unknown'), ('observationKind', 'absent'),
|
||||
('observationKind', 'stale'), ('source', 'none'), ('source', ''), ('source', 'unsupported'), ('sessionId', ''),
|
||||
('enabled', False), ('displayOnly', True), ('sessionId', ''), ('automaticAdoptionPending', True),
|
||||
('pendingSource', 'none'), ('pendingSource', ''), ('pendingSource', 'unsupported'),
|
||||
('pendingSinceMonoTime', 0), ('pendingSinceMonoTime', NOW + 1),
|
||||
('pendingSinceMonoTime', NOW - physical_actions.CONFIRMATION_NS - 1),
|
||||
('frameMonoTime', 0), ('frameMonoTime', NOW + 1), ('frameMonoTime', NOW - physical_actions.STATE_MAX_AGE_NS - 1),
|
||||
('hasPending', False), ('decisionId', 0), ('presentationId', 0), ('pendingSpeedLimit', 0.0),
|
||||
('pendingSpeedLimit', math.nan), ('pendingSpeedLimit', math.inf), ('pendingSpeedLimit', 26.0), ('speedLimit', math.nan),
|
||||
('pendingSpeedLimit', math.nan), ('pendingSpeedLimit', math.inf),
|
||||
])
|
||||
def test_reject_invalid_or_unavailable_pending_decision(sm, field, value):
|
||||
setattr(sm['slcState'], field, value)
|
||||
@@ -64,6 +64,28 @@ def test_reject_invalid_or_unavailable_pending_decision(sm, field, value):
|
||||
def test_reject_invalid_or_stale_transport(sm, valid, alive, stamp):
|
||||
sm.valid['slcState'], sm.alive['slcState'], sm.logMonoTime['slcState'] = valid, alive, stamp
|
||||
assert physical_actions.pending_confirmation(sm, NOW, long_active=True) is None
|
||||
sm['slcState'].hasCeiling, sm['slcState'].effectiveCap = True, 20.
|
||||
assert physical_actions.active_ceiling(sm, NOW) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kind', ['valid', 'unknown', 'stale', 'absent'])
|
||||
def test_active_ceiling_includes_a_qualified_retained_cap(sm, kind):
|
||||
state = sm['slcState']
|
||||
state.hasCeiling, state.effectiveCap, state.observationKind = True, 20., kind
|
||||
assert physical_actions.active_ceiling(sm, NOW) == 20.
|
||||
assert physical_actions.active_ceiling(sm, NOW, after_ns=NOW) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('enabled', False), ('displayOnly', True), ('sessionId', ''), ('hasCeiling', False),
|
||||
('frameMonoTime', 0), ('frameMonoTime', NOW + 1), ('frameMonoTime', NOW - physical_actions.STATE_MAX_AGE_NS - 1),
|
||||
('effectiveCap', 0.), ('effectiveCap', -1.), ('effectiveCap', math.nan), ('effectiveCap', math.inf), ('effectiveCap', 146 / 3.6),
|
||||
])
|
||||
def test_active_ceiling_rejects_unqualified_caps(sm, field, value):
|
||||
state = sm['slcState']
|
||||
state.hasCeiling, state.effectiveCap = True, 20.
|
||||
setattr(state, field, value)
|
||||
assert physical_actions.active_ceiling(sm, NOW) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('long_active,after_ns', [(False, 0), (True, NOW)])
|
||||
@@ -92,3 +114,33 @@ def test_engagement_rejects_unqualified_current_target(sm, field, value):
|
||||
def test_engagement_accepts_float32_cruise_boundaries(sm, target):
|
||||
sm['slcState'].hasPresentationAdjustedLimit, sm['slcState'].presentationAdjustedLimit = True, target
|
||||
assert physical_actions.engagement_state(sm, None, 'card', NOW) is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('state_changes, local_changes, after_ns, matches', [
|
||||
pytest.param({}, {}, 0, True, id='newer-same-episode'),
|
||||
pytest.param({}, {'observed_ns': NOW - 100_000_000, 'valid_until_ns': NOW + 50_000_000}, 0, True, id='same-packet'),
|
||||
pytest.param({}, {'observed_ns': NOW - 100_000_000}, 0, False, id='inconsistent-same-packet'),
|
||||
pytest.param({'sourceValidUntilMonoTime': NOW}, {}, 0, True, id='at-planner-expiry'),
|
||||
pytest.param({}, {'valid_until_ns': NOW}, 0, True, id='at-local-expiry'),
|
||||
pytest.param({'sourceValidUntilMonoTime': NOW - 1}, {}, 0, False, id='expired-planner-sample'),
|
||||
pytest.param({}, {'valid_until_ns': NOW - 1}, 0, False, id='expired-local-sample'),
|
||||
pytest.param({}, {'observed_ns': NOW - 100_000_001}, 0, False, id='older-local-sample'),
|
||||
pytest.param({}, {'observed_ns': NOW + 1}, 0, False, id='future-local-sample'),
|
||||
pytest.param({'sourceObservedMonoTime': NOW - 10_000_000}, {}, 0, False, id='sample-after-planner-frame'),
|
||||
pytest.param({}, {'episode': 2}, 0, False, id='new-episode'),
|
||||
pytest.param({}, {'speed_mps': 24.}, 0, False, id='different-speed'),
|
||||
pytest.param({'sourceProducerSessionId': 'old-card'}, {}, 0, False, id='different-producer'),
|
||||
pytest.param({}, {}, NOW - 100_000_000, False, id='planner-sample-before-settings-change'),
|
||||
])
|
||||
def test_dashboard_engagement_qualifies_both_samples(sm, state_changes, local_changes, after_ns, matches):
|
||||
state = sm['slcState']
|
||||
state.source, state.sourceProducerSessionId, state.sourceEpisode = 'dashboard', 'card', 1
|
||||
state.frameMonoTime = sm.logMonoTime['slcState'] = NOW - 20_000_000
|
||||
state.sourceObservedMonoTime, state.sourceValidUntilMonoTime = NOW - 100_000_000, NOW + 50_000_000
|
||||
state.hasPresentationAdjustedLimit, state.presentationAdjustedLimit = True, 25.
|
||||
for field, value in state_changes.items():
|
||||
setattr(state, field, value)
|
||||
local = Observation(**({'status': Status.VALID, 'speed_mps': 25., 'episode': 1,
|
||||
'observed_ns': NOW - 5_000_000, 'valid_until_ns': NOW + 145_000_000} | local_changes))
|
||||
|
||||
assert (physical_actions.engagement_state(sm, local, 'card', NOW, after_ns=after_ns) is not None) == matches
|
||||
|
||||
@@ -186,7 +186,7 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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.assertFalse(hasattr(process, 'slc_command_sock'))
|
||||
self.assertIn('slcDashboardObservation', process.pm.sock)
|
||||
shared_producer = process.slc_producer_session
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
@@ -196,18 +196,12 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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()
|
||||
@@ -256,6 +250,8 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
state.slcState.speedLimit = 25.0
|
||||
state.slcState.hasPending = True
|
||||
state.slcState.pendingSpeedLimit = 25.0
|
||||
state.slcState.pendingSource = 'dashboard'
|
||||
state.slcState.pendingSinceMonoTime = base_ns
|
||||
state.slcState.decisionId = 1
|
||||
state.slcState.presentationId = 1
|
||||
state.slcState.sourceProducerSessionId = 'ipc-card'
|
||||
@@ -305,6 +301,8 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
snapshot.speedLimit = 25.0
|
||||
snapshot.hasPending = True
|
||||
snapshot.pendingSpeedLimit = 25.0
|
||||
snapshot.pendingSource = 'dashboard'
|
||||
snapshot.pendingSinceMonoTime = base_ns
|
||||
snapshot.decisionId = 1
|
||||
snapshot.presentationId = 1
|
||||
snapshot.sourceProducerSessionId = 'ipc-card'
|
||||
@@ -349,157 +347,9 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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})
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': 1})
|
||||
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)
|
||||
@@ -524,255 +374,18 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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
|
||||
def test_identified_ui_adopt_updates_the_cap_without_changing_set(self):
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': 1, 'SLCPriority1': 'Dashboard'}))
|
||||
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'))
|
||||
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))
|
||||
self.assertEqual(adopted.result.ceiling.speed_mps, 25.)
|
||||
self.assertEqual(self.sm['carState'].vCruise, 100.)
|
||||
|
||||
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()
|
||||
# Advance logical source time explicitly below, independent of host scheduling.
|
||||
self.enterContext(mock.patch.object(card.time, 'monotonic_ns', return_value=now_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(
|
||||
CC=None, 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
|
||||
@@ -849,7 +462,7 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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})
|
||||
'SLCConfirmation': 1})
|
||||
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)
|
||||
@@ -910,111 +523,68 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
if has_pending:
|
||||
self.assertEqual(result.message.slcState.actionStatus, outcome)
|
||||
|
||||
def test_card_runtime_consumed_press_hold_command_release(self):
|
||||
|
||||
def test_physical_wheel_override_receipt_through_card_and_planner_queue(self):
|
||||
from openpilot.cereal import messaging
|
||||
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)
|
||||
from openpilot.selfdrive.controls.plannerd import current_cruise_event, queue_cruise_event, wheel_override_blocked
|
||||
from openpilot.starpilot.curve_speed.host import CurveHost
|
||||
|
||||
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, [])
|
||||
self.cp.pcmCruise = False
|
||||
self.sm['carState'].canValid = True
|
||||
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 90.
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCWheelOverride': True})
|
||||
for curve_claims, curve_backlog in ((False, False), (True, False), (False, True)):
|
||||
with self.subTest(curve_claims=curve_claims, curve_backlog=curve_backlog):
|
||||
start = 2_000_000_000
|
||||
self.sm.advance(start)
|
||||
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 90.
|
||||
runtime = Runtime(settings, is_metric=True)
|
||||
state = runtime.step(self.sm, self.cp, now_ns=start).message.slcState
|
||||
fake, current_cs = self._card_with_slc_state(state, None, start)
|
||||
fake.slc_configuration = fake.slc_requested_configuration = (True, settings)
|
||||
fake.v_cruise_helper.v_cruise_kph = fake.v_cruise_helper.v_cruise_cluster_kph = 90.
|
||||
fake.is_metric = True
|
||||
fake.curve_replay = 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.slc_cruise_event_id = 0
|
||||
fake.radar_publish = mock.Mock()
|
||||
fake.rk = SimpleNamespace(remaining=0.0)
|
||||
fake.aol_replay = False
|
||||
clock = [start + 10_000_000]
|
||||
|
||||
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
|
||||
def can_batch(*args, clock=clock, **kwargs):
|
||||
return [messaging.new_message('can', size=0, logMonoTime=clock[0]).to_bytes()]
|
||||
|
||||
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'])
|
||||
with (mock.patch.object(card, 'REPLAY', True),
|
||||
mock.patch.object(card.messaging, 'drain_sock_raw', side_effect=can_batch)):
|
||||
for pressed in (True, False):
|
||||
current_cs[0] = car.CarState(canValid=True, cruiseState={'available': True},
|
||||
buttonEvents=[car.CarState.ButtonEvent(type='accelCruise', pressed=pressed)])
|
||||
cs, radar = fake.state_update()
|
||||
fake.state_publish(cs, radar)
|
||||
clock[0] += 10_000_000
|
||||
fake.sm.logMonoTime['carControl'] = clock[0]
|
||||
slc_events, curve_events = deque(), deque()
|
||||
for call in fake.pm.send.call_args_list:
|
||||
name, message = call.args
|
||||
if name == 'slcCruiseEvent':
|
||||
queue_cruise_event(messaging.log_from_bytes(message.to_bytes()), slc_events, curve_events)
|
||||
self.assertEqual(len(curve_events), 1)
|
||||
self.assertEqual(len(slc_events), 1)
|
||||
self.assertEqual(cs.vCruise, 91.)
|
||||
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = cs.vCruise
|
||||
self.sm.advance(clock[0])
|
||||
event = current_cruise_event(slc_events, clock[0])
|
||||
self.assertEqual(event.wheel_override_since_ns, start)
|
||||
curve = CurveHost()
|
||||
curve.runtime.was_controlling = curve_claims
|
||||
output = runtime.step(self.sm, self.cp, now_ns=clock[0], cruise_event=event,
|
||||
wheel_override_blocked=wheel_override_blocked(None, curve, curve_backlog=curve_backlog))
|
||||
self.assertEqual(output.message.slcState.driverOverrideActive, not (curve_claims or curve_backlog))
|
||||
|
||||
def test_malformed_saved_priorities_disable_runtime_without_crashing(self):
|
||||
for key in ('SLCPriority1', 'SLCPriority2'):
|
||||
@@ -1079,14 +649,14 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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.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):
|
||||
def test_stored_history_is_schema_bound_and_restored_as_a_value_not_a_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)
|
||||
@@ -1095,17 +665,14 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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))
|
||||
remembered = history.decode(params.get('SLCQualifiedHistory'), session_id='next-drive')
|
||||
self.assertEqual(remembered.candidate.speed_mps, 20.)
|
||||
self.assertEqual(remembered.candidate.observation_identity.kind, acc.IdentityKind.SESSION_VALUE)
|
||||
self.assertEqual(remembered.candidate.observation_identity.session_id, 'next-drive')
|
||||
self.assertNotEqual(remembered.candidate.observation_identity, candidate.observation_identity)
|
||||
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))
|
||||
self.assertIsNone(history.decode(json.dumps(tampered), session_id='next-drive'))
|
||||
|
||||
def test_native_saved_settings_default_off_and_preserve_choices(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1113,9 +680,7 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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('SLCConfirmation', 1, block=True)
|
||||
params.put('SLCPriority1', 'Map Data', block=True)
|
||||
params.put('SLCPriority2', 'Dashboard', block=True)
|
||||
restored = read_params(params)
|
||||
@@ -1155,151 +720,45 @@ class RuntimeReplayTests(unittest.TestCase):
|
||||
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)
|
||||
runtime = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ceiling-drive')
|
||||
planner = LongitudinalPlanner(self.cp, init_v=20.)
|
||||
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)
|
||||
self.assertTrue(first.message.slcState.hasCeiling)
|
||||
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.assertEqual(planner.last_cruise_ceiling_status, 'applied')
|
||||
self.sm['carState'].vCruise = self.sm['carState'].vCruiseCluster = 105.
|
||||
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')
|
||||
|
||||
changed = 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(changed.message.slcState.driverOverrideActive)
|
||||
self.assertEqual(changed.result.ceiling.speed_mps, 25.)
|
||||
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, 'stale')
|
||||
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)
|
||||
self.assertEqual(stale.message.slcState.observationKind, 'stale')
|
||||
self.assertEqual(stale.result.ceiling.speed_mps, 25.)
|
||||
self.assertEqual(stale.result.acceptance.control_target_mps, 25.)
|
||||
self.assertEqual(self.sm['carState'].vCruise, 105.)
|
||||
|
||||
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 = self.accepted_runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard'}), session_id='ceiling-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))
|
||||
cruise_event=CruiseEvent(1, 2_010_000_000, 70*mph_to_mps, 69*mph_to_mps, '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)
|
||||
self.assertFalse(result.message.slcState.driverOverrideActive)
|
||||
self.assertAlmostEqual(result.result.ceiling.speed_mps, 45*mph_to_mps, places=5)
|
||||
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')
|
||||
self.assertEqual(planner.last_cruise_ceiling_status, 'applied')
|
||||
|
||||
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})
|
||||
settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': 1})
|
||||
runtime = Runtime(settings, session_id='receipt-drive')
|
||||
runtime.step(self.sm, self.cp, now_ns=2_000_000_000)
|
||||
source = self.sm['slcDashboardObservation']
|
||||
|
||||
@@ -4,27 +4,45 @@ from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.common.params import ParamKeyFlag, Params, UnknownKeyName
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc
|
||||
from openpilot.starpilot.speed_limits import offset_document as od
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import MPH_TO_MPS, parse, read_params
|
||||
from openpilot.starpilot.speed_limits.runtime_settings import PedalOverride, parse, read_params
|
||||
|
||||
|
||||
class RuntimeSettingsTests(unittest.TestCase):
|
||||
def test_confirmation_master_controls_both_directions_regardless_of_legacy_flags(self):
|
||||
for enabled in (False, True):
|
||||
for legacy in ({}, {'SLCConfirmationLower': False, 'SLCConfirmationHigher': False},
|
||||
{'SLCConfirmationLower': True, 'SLCConfirmationHigher': False},
|
||||
{'SLCConfirmationLower': False, 'SLCConfirmationHigher': True}):
|
||||
with self.subTest(enabled=enabled, legacy=legacy):
|
||||
settings = parse({'SpeedLimitController': True, 'SLCConfirmation': enabled, **legacy})
|
||||
self.assertFalse(settings.errors)
|
||||
self.assertEqual((settings.acceptance.confirm_lower, settings.acceptance.confirm_higher), (enabled, enabled))
|
||||
def test_old_splits_and_renames_do_not_seed_current_settings(self):
|
||||
from openpilot.starpilot.settings_retirement import retire_settings
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
old_values = {'SLCAdvanceSeconds': b'9', 'SLCConfirmationDirection': b'Lower only',
|
||||
'SetSpeedLimit': b'1', 'SLCPedalOverride': b'2'}
|
||||
for key, raw in old_values.items():
|
||||
self.assertNotIn(key.encode(), params.all_keys())
|
||||
Path(params.get_param_path(key)).write_bytes(raw)
|
||||
retire_settings(params)
|
||||
settings = read_params(params)
|
||||
self.assertTrue(settings.acceptance.confirm_lower and settings.acceptance.confirm_higher)
|
||||
self.assertEqual((settings.advance_higher_seconds, settings.advance_lower_seconds), (0, 0))
|
||||
self.assertIs(settings.pedal_override, PedalOverride.SPEED_REACHED)
|
||||
self.assertFalse(params.get_default_value('SLCOnEngage'))
|
||||
self.assertFalse(Path(params.get_param_path('SLCOnEngage')).exists())
|
||||
|
||||
def test_confirmation_modes_select_each_direction(self):
|
||||
for mode, lower, higher in ((0, False, False), (1, True, True), (3, False, True), (4, True, False)):
|
||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put('SLCConfirmation', mode, block=True)
|
||||
settings = read_params(params)
|
||||
self.assertFalse(settings.errors)
|
||||
self.assertEqual((settings.acceptance.confirm_lower, settings.acceptance.confirm_higher), (lower, higher))
|
||||
|
||||
def test_confirmation_on_never_autoaccepts_first_changed_or_disengaged_limits(self):
|
||||
active = acc.Authority(acc.Mode.COMBINED, acc.LongitudinalOwner.SYSTEM, True, True, False, False)
|
||||
stopped = acc.Authority(acc.Mode.COMBINED, acc.LongitudinalOwner.SYSTEM, False, False, False, True)
|
||||
policy = parse({'SpeedLimitController': True, 'SLCConfirmation': True}).acceptance
|
||||
policy = parse({'SpeedLimitController': True, 'SLCConfirmation': 1}).acceptance
|
||||
for source in ('dashboard', 'map', 'vision', 'online'):
|
||||
previous = acc.Candidate(source, acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value='accepted'), 20.)
|
||||
history = acc.AcceptedLimit(previous, 'drive', 0)
|
||||
@@ -47,21 +65,24 @@ class RuntimeSettingsTests(unittest.TestCase):
|
||||
settings = read_params(params)
|
||||
self.assertTrue(settings.display)
|
||||
self.assertFalse(settings.enabled)
|
||||
self.assertEqual(settings.selection.slots, (Source.VISION, Source.DASHBOARD))
|
||||
self.assertEqual(settings.selection.slots, (Source.VISION, Source.MAP, None))
|
||||
self.assertTrue(settings.acceptance.display_only)
|
||||
self.assertFalse(settings.acceptance.confirm_lower or settings.acceptance.confirm_higher)
|
||||
self.assertEqual((settings.advance_higher_seconds, settings.advance_lower_seconds), (0, 0))
|
||||
params.put_bool('ShowSpeedLimits', False, block=True)
|
||||
params.put('SLCPriority1', 'Dashboard', block=True)
|
||||
params.put('SLCPriority2', 'Map Data', block=True)
|
||||
params.put('SLCPriority2', 'Off', block=True)
|
||||
saved = read_params(params)
|
||||
self.assertFalse(saved.display)
|
||||
self.assertFalse(saved.enabled)
|
||||
self.assertEqual(saved.selection.slots, (Source.DASHBOARD, Source.MAP))
|
||||
self.assertEqual(saved.selection.slots, (Source.DASHBOARD, None, None))
|
||||
self.assertEqual(Path(params.get_param_path('ShowSpeedLimits')).read_bytes(), b'0')
|
||||
self.assertEqual(Path(params.get_param_path('SLCPriority1')).read_bytes(), b'Dashboard')
|
||||
self.assertEqual(Path(params.get_param_path('SLCPriority2')).read_bytes(), b'Map Data')
|
||||
self.assertEqual(Path(params.get_param_path('SLCPriority2')).read_bytes(), b'Off')
|
||||
|
||||
def test_corrupt_saved_values_disable_control_without_rewriting(self):
|
||||
cases = {'SLCConfirmation': b'invalid', 'IsMetric': b'2', 'SLCPriority1': b'\xff'}
|
||||
cases = {'SLCConfirmation': b'2',
|
||||
'IsMetric': b'2', 'SLCPriority1': b'\xff', 'SLCPriority3': b'M' * 33}
|
||||
for key, raw in cases.items():
|
||||
with self.subTest(key=key), tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
@@ -87,33 +108,40 @@ class RuntimeSettingsTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
params.put_bool('SLCConfirmation', True, block=True)
|
||||
params.put_bool('SLCConfirmationLower', True, block=True)
|
||||
params.put('SLCFallback', 0, block=True)
|
||||
document = od.OffsetDocument(od.IMPERIAL_BOUNDS, (0, 0, -5 * MPH_TO_MPS, 0, 0, 0, 0))
|
||||
params.put('SLCConfirmation', 1, block=True)
|
||||
document = od.OffsetDocument(od.IMPERIAL_BOUNDS, (0, 0, -5 * od.MPH_TO_MPS, 0, 0, 0, 0))
|
||||
params.put('SLCOffsetSchedule', od.to_value(document), block=True)
|
||||
settings = read_params(params)
|
||||
self.assertTrue(settings.enabled)
|
||||
self.assertFalse(settings.errors)
|
||||
self.assertTrue(settings.acceptance.confirm_lower)
|
||||
self.assertAlmostEqual(settings.offsets.bands[2].offset_mps, -5 * MPH_TO_MPS)
|
||||
self.assertAlmostEqual(settings.offsets.bands[2].offset_mps, -5 * od.MPH_TO_MPS)
|
||||
|
||||
def test_retired_fallback_bytes_are_ignored_without_creating_or_rewriting_them(self):
|
||||
def test_startup_clears_retired_slc_keys_and_preserves_current_mode(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
retired = ('SLCOverride', 'SLCFallback', 'SLCConfirmationHigher', 'SLCConfirmationLower')
|
||||
paths = [Path(params.get_param_path(key)) for key in retired]
|
||||
for path in paths:
|
||||
path.write_bytes(b'1')
|
||||
params.put('SLCOverrideMode', 2, block=True)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
|
||||
self.assertTrue(all(not path.exists() for path in paths))
|
||||
self.assertIs(read_params(params).pedal_override, PedalOverride.SET_SPEED)
|
||||
|
||||
def test_pedal_override_modes_and_bad_saved_value(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
path = Path(params.get_param_path('SLCFallback'))
|
||||
baseline = read_params(params)
|
||||
self.assertTrue(baseline.enabled)
|
||||
self.assertFalse(path.exists())
|
||||
self.assertIsNone(params.get_default_value('SLCFallback'))
|
||||
for raw in (b'0', b'1', b'2', b'', b'broken', b'999', b'\xff'):
|
||||
self.assertIs(read_params(params).pedal_override, PedalOverride.SPEED_REACHED)
|
||||
path = Path(params.get_param_path('SLCOverrideMode'))
|
||||
for raw, mode in ((b'0', PedalOverride.OFF), (b'1', PedalOverride.SPEED_REACHED),
|
||||
(b'2', PedalOverride.SET_SPEED), (b'broken', PedalOverride.OFF)):
|
||||
with self.subTest(raw=raw):
|
||||
path.write_bytes(raw)
|
||||
self.assertEqual(read_params(params), baseline)
|
||||
self.assertEqual(path.read_bytes(), raw)
|
||||
for old in (0, 1, 2, None, 'broken'):
|
||||
self.assertEqual(parse({'SpeedLimitController': True, 'SLCFallback': old}), baseline)
|
||||
settings = read_params(params)
|
||||
self.assertTrue(settings.enabled)
|
||||
self.assertIs(settings.pedal_override, mode)
|
||||
|
||||
def test_nonfinite_or_unrepresentable_current_offset_disables_control(self):
|
||||
for value in (float('nan'), float('inf'), 10 ** 1000):
|
||||
@@ -123,6 +151,19 @@ class RuntimeSettingsTests(unittest.TestCase):
|
||||
self.assertFalse(settings.enabled)
|
||||
self.assertTrue(settings.errors)
|
||||
|
||||
def test_wheel_override_is_opt_in_and_bad_bytes_keep_slc_enabled(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
params = Params(directory)
|
||||
params.put_bool('SpeedLimitController', True, block=True)
|
||||
self.assertFalse(params.get_default_value('SLCWheelOverride'))
|
||||
self.assertFalse(read_params(params).wheel_override)
|
||||
params.put_bool('SLCWheelOverride', True, block=True)
|
||||
self.assertTrue(read_params(params).wheel_override)
|
||||
Path(params.get_param_path('SLCWheelOverride')).write_bytes(b'bad')
|
||||
settings = read_params(params)
|
||||
self.assertTrue(settings.enabled)
|
||||
self.assertFalse(settings.wheel_override)
|
||||
|
||||
def test_retired_offsets_are_unregistered_and_ignored_with_zero_defaults(self):
|
||||
for metric in (False, True):
|
||||
with self.subTest(metric=metric), tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
@@ -45,10 +45,17 @@ class TestSourceSelection(unittest.TestCase):
|
||||
selected = select_limit(observations(dashboard=30, map=20, vision=vision), policy(mode, slots))
|
||||
self.assertEqual(selected.selected_source, Source.DASHBOARD if mode == SelectionMode.HIGHEST else Source.MAP)
|
||||
self.assertNotIn(Source.VISION, selected.considered)
|
||||
for slots in ((Source.VISION, None), (None, Source.VISION)):
|
||||
for slots in ((Source.VISION, None), (None, Source.VISION), (None, None, Source.VISION)):
|
||||
selected = select_limit(observations(dashboard=30, map=20, vision=vision), policy(mode, slots))
|
||||
self.assertEqual(selected.selected_source, Source.VISION)
|
||||
|
||||
def test_three_ranked_sources_fall_through_in_order(self):
|
||||
slots = (Source.MAP, Source.DASHBOARD, Source.VISION)
|
||||
supplied = observations(map=35, dashboard=45, vision=25)
|
||||
for source in slots:
|
||||
self.assertIs(select_limit(supplied, policy(slots=slots)).selected_source, source)
|
||||
supplied[source] = Observation(ObservationKind.STALE)
|
||||
|
||||
def test_extrema_ties_keep_stable_dashboard_map_vision_order(self):
|
||||
original = observations(dashboard=20, map=20, vision=20)
|
||||
for mode in (SelectionMode.HIGHEST, SelectionMode.LOWEST):
|
||||
@@ -164,7 +171,7 @@ class TestSourceSelection(unittest.TestCase):
|
||||
action = DriverAction(state.session_id, 1, state.pending.decision_id, ActionKind.REJECT) if reject else None
|
||||
decision = step(state, chosen.observation, authority, acceptance_policy, now_ns=index * 50_000_000, action=action)
|
||||
self.assertFalse(decision.errors)
|
||||
self.assertEqual(decision.control_target_mps, 29.0576 if speed is not None else None)
|
||||
self.assertEqual(decision.control_target_mps, 29.0576)
|
||||
self.assertEqual(decision.state.accepted.candidate.speed_mps, 29.0576)
|
||||
self.assertEqual(decision.state.pending is not None, index == 1)
|
||||
if index > 0:
|
||||
@@ -191,7 +198,7 @@ class TestSourceSelection(unittest.TestCase):
|
||||
selected = select_limit(supplied, source_policy)
|
||||
decision = step(accepted.state, selected.observation, system, acceptance_policy, now_ns=1)
|
||||
self.assertEqual(decision.state.accepted, accepted.state.accepted)
|
||||
self.assertIsNone(decision.control_target_mps)
|
||||
self.assertEqual(decision.control_target_mps, accepted.state.accepted.candidate.speed_mps)
|
||||
self.assertEqual(decision.state.observation.kind, kind)
|
||||
for authority in (Authority(Mode.LATERAL_ONLY, LongitudinalOwner.NONE, True, False, False, False),
|
||||
Authority(Mode.LATERAL_ONLY, LongitudinalOwner.STOCK, True, False, True, False),
|
||||
@@ -239,7 +246,7 @@ class TestSourceSelection(unittest.TestCase):
|
||||
action = DriverAction("drive", 0, state.pending.decision_id, ActionKind.REJECT) if index == 2 else None
|
||||
decision = step(state, selected.observation, authority, acceptance_policy, now_ns=index, action=action)
|
||||
self.assertEqual(decision.errors, ())
|
||||
self.assertEqual(decision.control_target_mps, 29.0576 if speed is not None else None)
|
||||
self.assertEqual(decision.control_target_mps, 29.0576)
|
||||
state = decision.state
|
||||
self.assertEqual(decision.basis, "rejected")
|
||||
shown = state.presentation
|
||||
@@ -247,8 +254,8 @@ class TestSourceSelection(unittest.TestCase):
|
||||
adopted = step(state, selected.observation, authority, acceptance_policy, now_ns=5, adopt=request)
|
||||
self.assertTrue(adopted.adoption_receipt.consumed)
|
||||
self.assertEqual(adopted.control_target_mps, 20.1168)
|
||||
self.assertEqual(adopted.state.rejected, frozenset())
|
||||
self.assertEqual(adopted.adoption.action_sequence_id, request.sequence_id)
|
||||
self.assertIsNone(adopted.state.rejected)
|
||||
self.assertEqual(adopted.adoption_receipt.sequence_id, request.sequence_id)
|
||||
self.assertEqual(adopted.history_write.candidate, shown.candidate)
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user