This commit is contained in:
firestarsdog
2026-09-28 23:21:10 -04:00
parent faf5b53321
commit 452dc42868
14 changed files with 1127 additions and 1504 deletions
+9 -6
View File
@@ -118,14 +118,17 @@ class TestVCruiseHelper:
)
assert pressed == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_accel_stops_at_slc_target_before_crossing_it(self):
@pytest.mark.parametrize(
("starting_kph", "waypoint_kph", "expected_speeds"),
[(30, 33, (33, 35, 40)), (65, 68, (68, 70, 75))],
)
def test_accel_stops_at_slc_target_before_crossing_it(self, starting_kph, waypoint_kph, expected_speeds):
self.starpilot_toggles.cruise_increase = 5
self.v_cruise_helper.v_cruise_kph = 30
self.v_cruise_helper.v_cruise_cluster_kph = 30
slc_target_with_offset = 33 * CV.KPH_TO_MS
self.v_cruise_helper.v_cruise_kph = starting_kph
self.v_cruise_helper.v_cruise_cluster_kph = starting_kph
slc_target_with_offset = waypoint_kph * CV.KPH_TO_MS
# A 30 km/h limit with a +3 km/h SLC offset should be an intermediate stop.
for expected_kph in (33, 35, 40):
for expected_kph in expected_speeds:
for pressed in (True, False):
CS = car.CarState(cruiseState={"available": True})
CS.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=pressed)]
File diff suppressed because it is too large Load Diff
@@ -64,6 +64,7 @@ def make_sm(*, set_speed_kph=100.0, lead_one=None, lead_two=None, standstill=Fal
"carState": SimpleNamespace(vCruise=set_speed_kph, standstill=standstill, vEgoCluster=v_ego_cluster),
"carControl": SimpleNamespace(orientationNED=[0.0, pitch, 0.0]),
"controlsState": SimpleNamespace(forceDecel=force_decel),
"selfdriveState": SimpleNamespace(personality=1),
"radarState": SimpleNamespace(
leadOne=lead_one or make_lead(),
leadTwo=lead_two or make_lead(),
@@ -2,6 +2,7 @@ import datetime
import pytest
from cereal import custom
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.starpilot.common.starpilot_variables import PLANNER_TIME
@@ -36,15 +37,26 @@ class FakeParams:
def get_float(self, *args, **kwargs):
return 0.0
def get_bool(self, key):
return bool(self.values.get(key, False))
def remove(self, key):
self.values.pop(key, None)
def put_nonblocking(self, key, value):
self.values[key] = value
self.writes.append((key, value))
def put_float(self, key, value):
self.values[key] = value
def make_vcruise(*, red_light=False, raw_model_stopped=False, forcing_stop=False, nav_state=None, road_curvature=0.0):
planner = SimpleNamespace(
params=FakeParams(),
params_memory=FakeParams({"NavInstructionState": nav_state or {}}),
gps_position={},
gps_valid=False,
lead_one=SimpleNamespace(status=False, dRel=float("inf"), vLead=0.0),
starpilot_cem=SimpleNamespace(stop_light_detected=red_light),
starpilot_following=SimpleNamespace(following_lead=False),
@@ -77,9 +89,14 @@ def make_sm(*, standstill=True, min_steer_speed=0.0, car_fingerprint=""):
leftBlinker=False,
rightBlinker=False,
steeringAngleDeg=0.0,
vCruise=72.0,
),
"liveParameters": SimpleNamespace(angleOffsetDeg=0.0),
"mapdOut": SimpleNamespace(nextSpeedLimitDistance=0.0, nextSpeedLimit=0.0, speedLimit=0.0,
waySelectionType=custom.WaySelectionType.fail, roadName=""),
"selfdriveState": SimpleNamespace(enabled=True),
"carParams": SimpleNamespace(minSteerSpeed=min_steer_speed, carFingerprint=car_fingerprint),
"starpilotCarState": SimpleNamespace(accelPressed=False, dashboardStopSign=0, dashboardSpeedLimit=0),
"starpilotCarState": SimpleNamespace(accelPressed=False, decelPressed=False, dashboardStopSign=0, dashboardSpeedLimit=0),
"onroadEvents": [],
}
@@ -105,6 +122,27 @@ def make_toggles():
nav_longitudinal_allowed=False,
speed_limit_controller=False,
show_speed_limits=False,
is_metric=False,
map_speed_lookahead_higher=0.0,
map_speed_lookahead_lower=0.0,
slc_fallback_previous_speed_limit=False,
slc_fallback_set_speed=False,
slc_fallback_experimental_mode=False,
slc_mapbox_filler=False,
speed_limit_confirmation_higher=False,
speed_limit_confirmation_lower=False,
speed_limit_priority1="Dashboard",
speed_limit_priority2="Map Data",
speed_limit_priority_highest=False,
speed_limit_priority_lowest=False,
vision_speed_limit_detection=False,
speed_limit_offset1=0.0,
speed_limit_offset2=0.0,
speed_limit_offset3=0.0,
speed_limit_offset4=0.0,
speed_limit_offset5=0.0,
speed_limit_offset6=0.0,
speed_limit_offset7=0.0,
force_stop_distance_offset=0,
)
@@ -112,7 +150,6 @@ def make_toggles():
def test_active_slc_control_target_does_not_require_set_speed_limit():
target = get_active_slc_control_target(
speed_limit_controller=True,
set_speed_limit=False,
slc_target=45.0 * CV.MPH_TO_MS,
slc_offset=3.0 * CV.MPH_TO_MS,
overridden_speed=0.0,
@@ -145,8 +182,7 @@ def test_active_slc_target_constrains_vcruise_below_csc_minimum(slc_target_mph,
vcruise.slc.target = slc_target_mph * CV.MPH_TO_MS
vcruise.slc.source = "Dashboard"
vcruise.slc.update_limits = lambda *_args, **_kwargs: None
vcruise.slc.update_override = lambda *_args, **_kwargs: None
vcruise.slc.update = lambda *_args, **_kwargs: None
result = update_vcruise(
vcruise,
@@ -417,6 +453,8 @@ def test_csc_res_press_defers_to_slc_confirmation():
sm = make_sm(standstill=False)
toggles = make_toggles()
toggles.curve_speed_controller = True
toggles.speed_limit_controller = True
toggles.speed_limit_confirmation_higher = True
def set_curve_target(_v_ego, _v_cruise):
vcruise.csc.target = 14.0
@@ -426,10 +464,12 @@ def test_csc_res_press_defers_to_slc_confirmation():
assert vcruise.csc_controlling_speed
vcruise.slc.speed_limit_changed_timer = 1.0
vcruise.slc.unconfirmed_speed_limit = 25.0
# The candidate and accel press arrive together. SLC consumes the press in
# this frame, even though accepting immediately clears the pending state.
sm["starpilotCarState"].dashboardSpeedLimit = 45.0 * CV.MPH_TO_MS
sm["starpilotCarState"].accelPressed = True
update_vcruise(vcruise, sm, toggles, now=80.05, v_ego=20.0)
assert vcruise.slc.confirmation_button_consumed
assert not vcruise.csc_override
sm["starpilotCarState"].accelPressed = False
@@ -622,7 +662,6 @@ def test_curve_speed_controller_hysteresis_keeps_glow_off_for_marginal_targets()
def test_active_slc_control_target_applies_offset_and_cluster_diff():
target = get_active_slc_control_target(
speed_limit_controller=True,
set_speed_limit=True,
slc_target=45.0 * CV.MPH_TO_MS,
slc_offset=3.0 * CV.MPH_TO_MS,
overridden_speed=0.0,
@@ -635,7 +674,6 @@ def test_active_slc_control_target_applies_offset_and_cluster_diff():
def test_active_slc_control_target_allows_lower_redneck_override():
target = get_active_slc_control_target(
speed_limit_controller=True,
set_speed_limit=False,
slc_target=65.0 * CV.MPH_TO_MS,
slc_offset=0.0,
overridden_speed=35.0 * CV.MPH_TO_MS,
+2
View File
@@ -1461,6 +1461,8 @@ class StarPilotVariables:
speed_limit_confirmation = self.get_value("SLCConfirmation", condition=toggle.speed_limit_controller)
toggle.speed_limit_confirmation_higher = self.get_value("SLCConfirmationHigher", condition=speed_limit_confirmation)
toggle.speed_limit_confirmation_lower = self.get_value("SLCConfirmationLower", condition=speed_limit_confirmation)
# Legacy setting is hidden in the current UI. SLC's pedal and +/- overrides
# remain available regardless of its saved value; keep loading it for compatibility.
slc_override_method = self.get_value("SLCOverride", cast=float, condition=toggle.speed_limit_controller)
toggle.speed_limit_controller_override_manual = slc_override_method == 1
toggle.speed_limit_controller_override_set_speed = slc_override_method == 2
@@ -0,0 +1,130 @@
import calendar
import json
from concurrent.futures import ThreadPoolExecutor
import requests
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.starpilot.common.starpilot_utilities import calculate_bearing_offset, is_url_pingable
FREE_MAPBOX_REQUESTS = 100_000
class MapboxSpeedLimit:
def __init__(self, params):
self.params = params
try:
self.requests = json.loads(params.get("MapBoxRequests", encoding="utf-8") or "{}")
except (TypeError, ValueError):
self.requests = {}
self.requests.setdefault("total_requests", 0)
self.requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - 28 * 100)
self.host = "https://api.mapbox.com"
self.token = params.get("MapboxSecretKey", encoding="utf-8")
self.limit = 0.0
self.segment_distance = 0.0
self.future = None
self.executor = ThreadPoolExecutor(max_workers=1)
self.session = requests.Session()
self.session.headers.update({"Accept-Language": "en"})
self.session.headers.update({"User-Agent": "starpilot-mapbox-speed-limit-retriever/1.0 (https://github.com/FrogAi/StarPilot)"})
def reset(self):
# A discarded future may still finish, but only update() can publish its result.
if self.future is not None:
self.future.cancel()
self.future = None
self.limit = 0.0
self.segment_distance = 0.0
def shutdown(self):
self.reset()
self.executor.shutdown(wait=False, cancel_futures=True)
self.session.close()
def _request(self, position, v_ego):
if not is_url_pingable(self.host):
return 0.0, v_ego
self.requests["total_requests"] += 1
self.params.put_nonblocking("MapBoxRequests", json.dumps(self.requests))
bearing = position.get("bearing")
latitude = position.get("latitude")
longitude = position.get("longitude")
future_latitude, future_longitude = calculate_bearing_offset(latitude, longitude, bearing, v_ego)
url = f"{self.host}/matching/v5/mapbox/driving/{longitude},{latitude};{future_longitude},{future_latitude}.json"
params = {
"access_token": self.token,
"annotations": "maxspeed,distance",
"geometries": "polyline6",
"overview": "full",
"steps": "false",
"radiuses": "10;10",
"tidy": "true",
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
matchings = response.json().get("matchings") or []
if not matchings:
return 0.0, v_ego
legs = (matchings[0] or {}).get("legs") or []
if not legs:
return 0.0, v_ego
annotation = legs[0].get("annotation") or {}
distances = annotation.get("distance") or [v_ego]
speeds = annotation.get("maxspeed") or []
if not speeds:
return 0.0, v_ego
first = speeds[0]
try:
speed = float(first.get("speed")) if first.get("speed") != "none" else 0.0
except (TypeError, ValueError):
speed = 0.0
if speed <= 0:
return 0.0, v_ego
conversion = CV.MPH_TO_MS if first.get("unit", "km/h") == "mph" else CV.KPH_TO_MS
return speed * conversion, distances[0]
def update(self, now, time_validated, v_ego, gps_valid, position, steering_angle, angle_offset):
if not gps_valid or not self.token or abs(steering_angle - angle_offset) >= 45:
self.reset()
return
if time_validated and now.month != self.requests.get("month"):
self.requests.update({
"month": now.month,
"total_requests": 0,
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, now.month)[1] * 100,
})
if self.requests["total_requests"] >= self.requests["max_requests"]:
self.reset()
return
if self.future is not None:
if not self.future.done():
return
future = self.future
self.future = None
try:
self.limit, self.segment_distance = future.result()
except Exception as exception:
print(f"Unexpected error in Mapbox request: {exception}")
self.limit, self.segment_distance = 0.0, v_ego
return
if v_ego < 1:
return
if self.segment_distance > 0:
self.segment_distance -= v_ego * DT_MDL
return
try:
self.future = self.executor.submit(self._request, dict(position), v_ego)
except RuntimeError:
self.segment_distance = v_ego
return
+331 -496
View File
@@ -1,19 +1,19 @@
#!/usr/bin/env python3
# PFEIFER - SLC - Modified by FrogAi
import calendar
import json
import requests
from concurrent.futures import ThreadPoolExecutor
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from cereal import custom
from openpilot.starpilot.common.starpilot_utilities import calculate_bearing_offset, calculate_distance_to_point, is_url_pingable
from openpilot.starpilot.controls.lib.mapbox_speed_limit import MapboxSpeedLimit
FREE_MAPBOX_REQUESTS = 100_000
SOURCE_NONE = "None"
SOURCE_DASHBOARD = "Dashboard"
SOURCE_MAP = "Map Data"
SOURCE_VISION = "Vision"
SOURCE_MAPBOX = "Mapbox"
REAL_SOURCES = (SOURCE_DASHBOARD, SOURCE_MAP, SOURCE_VISION, SOURCE_MAPBOX)
OFFSET_MAP_IMPERIAL = [
(0, 11.2, "speed_limit_offset1"), # 0–24 mph
@@ -36,83 +36,76 @@ OFFSET_MAP_METRIC = [
]
SLC_OVERRIDE_DISABLE_CLEAR_TIME = 0.75
# Minimum set-speed increase (m/s) counted as a deliberate +/- press. Below the smallest
# real step (1 km/h ≈ 0.28 m/s), above cluster/float jitter.
SET_SPEED_RAISE_EPS = 0.1
SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND = 0.1
SAME_LIMIT_TOLERANCE = 1.0
VISION_LARGE_REFERENCE_SPEED_DELTA = 30 * CV.MPH_TO_MS
VISION_LARGE_SET_SPEED_MIN_SUPPORT = 3
VISION_SUPPORT_SPEED_TOLERANCE = 0.5 * CV.MPH_TO_MS
class SpeedLimitController:
def __init__(self, StarPilotVCruise):
self.starpilot_planner = StarPilotVCruise.starpilot_planner
self.starpilot_toggles = None
self.mapbox = MapboxSpeedLimit(self.starpilot_planner.params)
self.calling_mapbox = False
self.override_slc = False
self.override_disable_timer = 0.0
self._prev_v_cruise = None
self._persistent_override_speed = 0.0
self._set_speed_override_input_consumed = False
self.source = SOURCE_NONE
self.target = 0.0
self.map_speed_limit = 0.0
self.next_speed_limit = 0.0
self.vision_limit = 0.0
self.overridden_speed = 0.0
self.denied_target = 0
self.map_speed_limit = 0
self.mapbox_limit = 0
self.next_speed_limit = 0
self.overridden_speed = 0
self.segment_distance = 0
self.speed_limit_changed_timer = 0
self.target = 0
self.unconfirmed_speed_limit = 0
self.vision_limit = 0
self.previous_source = "None"
self.source = "None"
self.last_valid_limit = max(self.starpilot_planner.params.get_float("PreviousSpeedLimit"), 0.0)
self.last_valid_source = SOURCE_NONE # The persisted number has no known live source.
self.pending_limit = 0.0
self.pending_source = SOURCE_NONE
self.confirmation_time = 0.0
self.denied_limit = 0.0
self.previous_road_name = ""
self._slc_adopt_counter = 0
mapbox_requests_raw = self.starpilot_planner.params.get("MapBoxRequests", encoding="utf-8")
try:
self.mapbox_requests = json.loads(mapbox_requests_raw or "{}")
except (TypeError, ValueError):
self.mapbox_requests = {}
self.mapbox_requests.setdefault("total_requests", 0)
self.mapbox_requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - (28 * 100))
self.mapbox_host = "https://api.mapbox.com"
self.mapbox_token = self.starpilot_planner.params.get("MapboxSecretKey", encoding="utf-8")
self.previous_target = self.starpilot_planner.params.get_float("PreviousSpeedLimit")
self.last_valid_limit = self.previous_target if self.previous_target > 0 else 0
self.executor = ThreadPoolExecutor(max_workers=1)
self.mapbox_future = None
self.session = requests.Session()
self.session.headers.update({"Accept-Language": "en"})
self.session.headers.update({"User-Agent": "starpilot-mapbox-speed-limit-retriever/1.0 (https://github.com/FrogAi/StarPilot)"})
self.set_speed_override = 0.0
self.pedal_override = 0.0
self.previous_set_speed = None
self.consume_set_speed_change = False
self.override_disable_time = 0.0
self.limit_change_started = False
self.confirmation_button_consumed = False
self._active_control = False
self._using_experimental_fallback = False
self._mode = "off"
def shutdown(self):
self.executor.shutdown(wait=False, cancel_futures=True)
self.session.close()
self.mapbox.shutdown()
@property
def mapbox_limit(self):
return self.mapbox.limit
@property
def confirmation_pending(self):
return self.pending_limit >= 1
@property
def unconfirmed_speed_limit(self):
return self.pending_limit
@property
def experimental_mode(self):
return self.target == 0 and bool(getattr(self.starpilot_toggles, "slc_fallback_experimental_mode", False))
return self._active_control and self._using_experimental_fallback
@property
def target_to_use(self):
if self.source == "None" and self.target > 0 and self.last_valid_limit > 0:
if self.target >= self.last_valid_limit:
return self.last_valid_limit
# Keep Set Speed fallback from arming an override against a higher fake limit.
if self.source == SOURCE_NONE and self.target > 0 and self.last_valid_limit > 0:
return min(self.target, self.last_valid_limit)
return self.target
def get_offset(self, target_speed):
def get_offset(self, limit):
if self.starpilot_toggles is None:
return 0
return 0.0
offset_map = OFFSET_MAP_METRIC if self.starpilot_toggles.is_metric else OFFSET_MAP_IMPERIAL
return next((getattr(self.starpilot_toggles, offset) for low, high, offset in offset_map if low <= target_speed < high), 0)
return next((getattr(self.starpilot_toggles, name) for low, high, name in offset_map if low <= limit < high), 0.0)
@property
def offset(self):
@@ -124,458 +117,300 @@ class SpeedLimitController:
0 < limit <= max(getattr(self.starpilot_toggles, "vision_speed_limit_low_limit_threshold", 0), 0)
)
def _confirmation_required(self, desired_source, desired_target):
return desired_source != "None" and (
(desired_target < self.target and self.starpilot_toggles.speed_limit_confirmation_lower) or
(desired_target > self.target and self.starpilot_toggles.speed_limit_confirmation_higher)
)
def reset_control_state(self):
self._clear_pending()
self.clear_override()
self.previous_set_speed = None
self.consume_set_speed_change = False
self.override_disable_time = 0.0
self.limit_change_started = False
self.confirmation_button_consumed = False
self._active_control = False
self._using_experimental_fallback = False
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
self.starpilot_planner.params_memory.remove("SLCAdoptSpeedLimit")
def clear_override(self):
self.override_slc = False
self.overridden_speed = 0
self._persistent_override_speed = 0.0
def _get_vision_limit(self, v_ego, sm, display_only):
enabled = getattr(self.starpilot_toggles, "vision_speed_limit_detection", False)
self.vision_limit = self.starpilot_planner.params_memory.get_float("VisionSpeedLimit") if enabled else 0.0
limit = self.vision_limit
if not display_only and self.low_vision_limit_filtered(limit):
return 0.0
def clear_persistent_override(self):
self._persistent_override_speed = 0.0
def clear_persistent_override_for_limit_change(self, previous_limit, new_limit):
if self._persistent_override_speed <= 0:
return
if previous_limit <= 0 or new_limit <= 0 or abs(new_limit - previous_limit) < 0.1:
return
new_target_with_offset = new_limit + self.get_offset(new_limit)
if new_limit < previous_limit or self._persistent_override_speed <= new_target_with_offset:
self.clear_persistent_override()
def get_mapbox_speed_limit(self, now, time_validated, v_ego, sm):
if not self.starpilot_planner.gps_valid or not self.mapbox_token or abs(sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg) >= 45:
self.mapbox_limit = 0
self.segment_distance = 0
return
if v_ego < 1:
return
if self.segment_distance > 0:
self.segment_distance -= v_ego * DT_MDL
return
if self.calling_mapbox:
self.segment_distance = v_ego
return
def make_request():
successful = False
response_data = None
try:
if not is_url_pingable(self.mapbox_host):
self.segment_distance = 1000
successful = True
return None
if time_validated:
current_month = now.month
if current_month != self.mapbox_requests.get("month"):
self.mapbox_requests.update({
"month": current_month,
"total_requests": 0,
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
})
self.mapbox_requests["total_requests"] += 1
self.starpilot_planner.params.put_nonblocking("MapBoxRequests", json.dumps(self.mapbox_requests))
current_bearing = self.starpilot_planner.gps_position.get("bearing")
current_latitude = self.starpilot_planner.gps_position.get("latitude")
current_longitude = self.starpilot_planner.gps_position.get("longitude")
future_latitude, future_longitude = calculate_bearing_offset(current_latitude, current_longitude, current_bearing, v_ego)
url = (
f"{self.mapbox_host}/matching/v5/mapbox/driving/"
f"{current_longitude},{current_latitude};"
f"{future_longitude},{future_latitude}.json"
)
mapbox_params = {
"access_token": self.mapbox_token,
"annotations": "maxspeed,distance",
"geometries": "polyline6",
"overview": "full",
"steps": "false",
"radiuses": "10;10",
"tidy": "true",
}
response = self.session.get(url, params=mapbox_params, timeout=10)
response.raise_for_status()
successful = True
response_data = response.json()
except Exception as exception:
print(f"Unexpected error in Mapbox request: {exception}")
finally:
self.calling_mapbox = False
if not successful:
self.mapbox_limit = 0
self.segment_distance = v_ego
return response_data
def complete_request(future):
try:
data = future.result()
if data:
matchings = data.get("matchings") or []
if not matchings:
self.mapbox_limit = 0
self.segment_distance = v_ego
return
legs = (matchings[0] or {}).get("legs") or []
if not legs:
self.mapbox_limit = 0
self.segment_distance = v_ego
return
annotation = legs[0].get("annotation") or {}
distances = annotation.get("distance") or [v_ego]
segment_distance = distances[0]
speed_data = annotation.get("maxspeed", [])
if speed_data:
first_segment_speed = speed_data[0]
try:
raw_speed = float(first_segment_speed.get("speed") if first_segment_speed.get("speed") != "none" else 0.0)
except (ValueError, TypeError):
raw_speed = 0.0
unit = first_segment_speed.get("unit", "km/h")
if raw_speed > 0:
if unit == "mph":
self.mapbox_limit = raw_speed * CV.MPH_TO_MS
else:
self.mapbox_limit = raw_speed * CV.KPH_TO_MS
self.segment_distance = segment_distance
return
self.mapbox_limit = 0
self.segment_distance = v_ego
except Exception as exception:
print(f"Mapbox Callback Error: {exception}")
self.mapbox_limit = 0
self.segment_distance = v_ego
finally:
self.mapbox_future = None
self.calling_mapbox = True
try:
future = self.executor.submit(make_request)
except RuntimeError:
self.calling_mapbox = False
self.segment_distance = v_ego
return
self.mapbox_future = future
future.add_done_callback(complete_request)
def handle_limit_change(self, desired_source, desired_target, current_road_name, v_ego, sm):
self.speed_limit_changed_timer += DT_MDL
previous_limit = self.last_valid_limit if self.last_valid_limit > 0 else self.target
long_active = sm["carControl"].longActive
accepted_by_accel_button = sm["starpilotCarState"].accelPressed and long_active
confirmation_required = self._confirmation_required(desired_source, desired_target)
higher_confirmation = confirmation_required and desired_target > self.target
speed_limit_accepted = confirmation_required and accepted_by_accel_button
if confirmation_required and not speed_limit_accepted and self._slc_adopt_counter % 4 == 0:
speed_limit_accepted = self.starpilot_planner.params_memory.get_bool("SpeedLimitAccepted")
if not confirmation_required:
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
self.unconfirmed_speed_limit = 0
speed_limit_denied = confirmation_required and (
sm["starpilotCarState"].decelPressed or (self.speed_limit_changed_timer >= 30 and long_active)
)
if not long_active and not sm["selfdriveState"].enabled:
speed_limit_accepted = True
if speed_limit_accepted:
self.source = desired_source
self.target = desired_target
self.clear_persistent_override_for_limit_change(previous_limit, desired_target)
set_speed_kph = float(sm["carState"].vCruise)
target_with_offset = self.target + self.offset
if (
higher_confirmation
and long_active
and 0 < set_speed_kph < V_CRUISE_UNSET
and set_speed_kph * CV.KPH_TO_MS < target_with_offset
):
self.starpilot_planner.params_memory.put_float("SLCForceCruiseSpeed", target_with_offset)
if accepted_by_accel_button and confirmation_required:
self._set_speed_override_input_consumed = True
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
elif speed_limit_denied:
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
self.denied_target = desired_target
self.previous_source = desired_source
self.previous_target = desired_target
self.previous_road_name = current_road_name
elif desired_target != self.target and not confirmation_required:
self.source = desired_source
self.target = desired_target
self.clear_persistent_override_for_limit_change(previous_limit, desired_target)
elif desired_target == self.target:
self.source = desired_source
self.target = desired_target
else:
self.source = "None"
self.unconfirmed_speed_limit = desired_target
if (self.target != self.previous_target or self.previous_road_name != current_road_name) and self.target > 0 and not speed_limit_denied:
self.denied_target = 0
self.previous_source = self.source
self.previous_target = self.target
self.previous_road_name = current_road_name
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(self.target))
def update_limits(self, dashboard_speed_limit, now, time_validated, v_cruise, v_ego, sm, display_only=False):
self.update_map_speed_limit(v_ego, sm)
vision_enabled = getattr(self.starpilot_toggles, "vision_speed_limit_detection", False)
self.vision_limit = self.starpilot_planner.params_memory.get_float("VisionSpeedLimit") if vision_enabled else 0
usable_vision_limit = self.vision_limit
if not display_only and self.low_vision_limit_filtered(usable_vision_limit):
usable_vision_limit = 0
# The planner clamps V_CRUISE_UNSET to V_CRUISE_MAX, so plausibility must use the raw selected speed.
raw_set_speed_kph = float(sm["carState"].vCruise)
selected_set_speed = raw_set_speed_kph * CV.KPH_TO_MS if 0 < raw_set_speed_kph < V_CRUISE_UNSET else 0
reference_speed = selected_set_speed if selected_set_speed > 0 else max(float(v_ego), 0)
# vEgo jitters around zero at standstill; do not let that switch the active source.
if (
usable_vision_limit > 0 and reference_speed > 0 and not sm["carState"].standstill and
abs(usable_vision_limit - reference_speed) >= VISION_LARGE_REFERENCE_SPEED_DELTA
):
support_count = self.starpilot_planner.params_memory.get_int("VisionSpeedLimitSupportCount")
support_speed = self.starpilot_planner.params_memory.get_float("VisionSpeedLimitSupportSpeed")
if support_count < VISION_LARGE_SET_SPEED_MIN_SUPPORT or abs(support_speed - usable_vision_limit) > VISION_SUPPORT_SPEED_TOLERANCE:
usable_vision_limit = 0
selected_speed = raw_set_speed_kph * CV.KPH_TO_MS if 0 < raw_set_speed_kph < V_CRUISE_UNSET else 0.0
reference_speed = selected_speed if selected_speed > 0 else max(float(v_ego), 0.0)
if (limit > 0 and reference_speed > 0 and not sm["carState"].standstill and
abs(limit - reference_speed) >= VISION_LARGE_REFERENCE_SPEED_DELTA):
memory = self.starpilot_planner.params_memory
count = memory.get_int("VisionSpeedLimitSupportCount")
support_speed = memory.get_float("VisionSpeedLimitSupportSpeed")
if count < VISION_LARGE_SET_SPEED_MIN_SUPPORT or abs(support_speed - limit) > VISION_SUPPORT_SPEED_TOLERANCE:
return 0.0
return limit
configured_priorities = {
self.starpilot_toggles.speed_limit_priority1,
self.starpilot_toggles.speed_limit_priority2,
}
limits = {
"Dashboard": dashboard_speed_limit,
"Map Data": self.map_speed_limit,
}
if "Vision" in configured_priorities:
limits["Vision"] = usable_vision_limit
filtered_limits = {source: limit for source, limit in limits.items() if limit >= 1}
if self.starpilot_toggles.speed_limit_priority_highest:
desired_source = max(filtered_limits, key=filtered_limits.get, default="None")
desired_target = filtered_limits.get(desired_source, 0)
elif self.starpilot_toggles.speed_limit_priority_lowest:
desired_source = min(filtered_limits, key=filtered_limits.get, default="None")
desired_target = filtered_limits.get(desired_source, 0)
elif filtered_limits:
for priority in [
self.starpilot_toggles.speed_limit_priority1,
self.starpilot_toggles.speed_limit_priority2
]:
if priority in filtered_limits:
desired_source = priority
desired_target = filtered_limits[desired_source]
break
else:
desired_source = "None"
desired_target = 0
else:
desired_source = "None"
desired_target = 0
if desired_target == 0:
if self.mapbox_requests["total_requests"] < self.mapbox_requests["max_requests"] and self.starpilot_toggles.slc_mapbox_filler:
self.get_mapbox_speed_limit(now, time_validated, v_ego, sm)
if self.mapbox_limit >= 1:
desired_source = "Mapbox"
desired_target = self.mapbox_limit
if not display_only and desired_target == 0:
previous_vision_limit_filtered = self.previous_source == "Vision" and self.low_vision_limit_filtered(self.previous_target)
if self.previous_target > 0 and self.starpilot_toggles.slc_fallback_previous_speed_limit and not previous_vision_limit_filtered:
desired_source = self.previous_source
desired_target = self.previous_target
self.target = desired_target
elif sm["selfdriveState"].enabled and self.starpilot_toggles.slc_fallback_set_speed:
desired_source = "None"
desired_target = v_cruise
else:
self.mapbox_limit = 0
self.segment_distance = 0
if display_only:
self.speed_limit_changed_timer = 0
self.unconfirmed_speed_limit = 0
self.clear_override()
if desired_target >= 1:
self.source = desired_source
self.target = desired_target
else:
self.source = "None"
self.target = 0
return
current_road_name = sm["mapdOut"].roadName if desired_source == "Map Data" else ""
current_speed = self.target if (self.source != "None" and self.target > 0) else self.last_valid_limit
# Do not trigger alerts when shifting to fallback or when re-obtaining the same speed limit
is_fallback = desired_source == "None" or desired_target == 0
same_speed = desired_target > 0 and current_speed > 0 and abs(desired_target - current_speed) < 1
confirmation_required = self._confirmation_required(desired_source, desired_target)
denied_same_limit = (
confirmation_required and self.denied_target > 0 and
abs(desired_target - self.denied_target) < 1
)
if not denied_same_limit:
self.denied_target = 0
if denied_same_limit:
self.speed_limit_changed_timer = 0
self.unconfirmed_speed_limit = 0
elif not is_fallback and not same_speed and (abs(desired_target - self.previous_target) >= 1 or current_speed == 0):
self.handle_limit_change(desired_source, desired_target, current_road_name, v_ego, sm)
else:
self.speed_limit_changed_timer = 0
self.unconfirmed_speed_limit = 0
if desired_source != self.source or desired_target != self.target:
if not is_fallback:
self.clear_persistent_override_for_limit_change(current_speed, desired_target)
self.source = desired_source
self.target = desired_target
if desired_source != "None" and desired_target > 0:
self.previous_source = desired_source
self.previous_target = desired_target
if current_road_name != self.previous_road_name and current_road_name != "":
self.previous_road_name = current_road_name
self.denied_target = 0
if self.source != "None" and self.target > 0:
self.last_valid_limit = self.target
self._slc_adopt_counter += 1
if self._slc_adopt_counter % 4 == 0 and self.starpilot_planner.params_memory.get_bool("SLCAdoptSpeedLimit"):
self.starpilot_planner.params_memory.remove("SLCAdoptSpeedLimit")
if desired_target > 0:
self.clear_override()
self.denied_target = 0
self.source = desired_source
self.target = desired_target
self.previous_source = desired_source
self.previous_target = desired_target
self.speed_limit_changed_timer = 0
self.unconfirmed_speed_limit = 0
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(self.target))
self.starpilot_planner.params_memory.put_float("SLCForceCruiseSpeed", self.target + self.offset)
def update_map_speed_limit(self, v_ego, sm):
next_speed_limit_distance = sm["mapdOut"].nextSpeedLimitDistance
way_sel = sm["mapdOut"].waySelectionType
if way_sel in (custom.WaySelectionType.current,
custom.WaySelectionType.extended):
self.map_speed_limit = sm["mapdOut"].speedLimit
self.next_speed_limit = sm["mapdOut"].nextSpeedLimit
elif way_sel in (custom.WaySelectionType.predicted,
custom.WaySelectionType.possible):
speed = sm["mapdOut"].speedLimit
def _update_map_speed_limit(self, v_ego, sm):
map_data = sm["mapdOut"]
way_sel = map_data.waySelectionType
if way_sel in (custom.WaySelectionType.current, custom.WaySelectionType.extended):
self.map_speed_limit = map_data.speedLimit
self.next_speed_limit = map_data.nextSpeedLimit
elif way_sel in (custom.WaySelectionType.predicted, custom.WaySelectionType.possible):
speed = map_data.speedLimit
if speed > 0 and (self.map_speed_limit == 0 or speed < self.map_speed_limit):
self.map_speed_limit = speed
self.next_speed_limit = 0
self.next_speed_limit = 0.0
else:
self.next_speed_limit = 0
# Explicit selection failure means the old current limit is no longer live.
self.map_speed_limit = 0.0
self.next_speed_limit = 0.0
if self.next_speed_limit > 0:
if self.map_speed_limit < self.next_speed_limit:
max_lookahead = self.starpilot_toggles.map_speed_lookahead_higher * v_ego
lookahead = self.starpilot_toggles.map_speed_lookahead_higher * v_ego
elif self.map_speed_limit > self.next_speed_limit:
max_lookahead = self.starpilot_toggles.map_speed_lookahead_lower * v_ego
lookahead = self.starpilot_toggles.map_speed_lookahead_lower * v_ego
else:
max_lookahead = 0
if next_speed_limit_distance < max_lookahead:
lookahead = 0.0
if map_data.nextSpeedLimitDistance < lookahead:
self.map_speed_limit = self.next_speed_limit
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
# Detect +/- changes on the raw set speed (button-driven, no cluster jitter). A fresh edge
# keeps a cleared override from re-arming while the selected speed stays high.
prev_v_cruise = self._prev_v_cruise
self._prev_v_cruise = v_cruise
set_speed_changed = prev_v_cruise is not None and abs(v_cruise - prev_v_cruise) > SET_SPEED_RAISE_EPS
set_speed_raised = prev_v_cruise is not None and v_cruise > prev_v_cruise + SET_SPEED_RAISE_EPS
set_speed_input_consumed = self._set_speed_override_input_consumed
# The button and its vCruise update can arrive in adjacent frames. Clear a consumed
# confirmation only after this frame has seen the speed change or button release.
if set_speed_input_consumed and (set_speed_changed or not sm["starpilotCarState"].accelPressed):
self._set_speed_override_input_consumed = False
def _select_limit(self, dashboard, map_limit, vision_limit):
priorities = (self.starpilot_toggles.speed_limit_priority1, self.starpilot_toggles.speed_limit_priority2)
limits = {SOURCE_DASHBOARD: dashboard, SOURCE_MAP: map_limit}
if SOURCE_VISION in priorities:
limits[SOURCE_VISION] = vision_limit
valid = {source: limit for source, limit in limits.items() if limit >= 1}
if not valid:
return SOURCE_NONE, 0.0
if self.starpilot_toggles.speed_limit_priority_highest:
source = max(valid, key=valid.get)
elif self.starpilot_toggles.speed_limit_priority_lowest:
source = min(valid, key=valid.get)
else:
source = next((name for name in priorities if name in valid), SOURCE_NONE)
return source, valid.get(source, 0.0)
if not sm["selfdriveState"].enabled:
self.override_disable_timer += DT_MDL
if self.override_disable_timer >= SLC_OVERRIDE_DISABLE_CLEAR_TIME:
self.clear_override()
def _apply_mapbox_filler(self, source, limit, now, time_validated, v_ego, sm):
if source != SOURCE_NONE or not self.starpilot_toggles.slc_mapbox_filler:
self.mapbox.reset()
return source, limit
self.mapbox.update(
now, time_validated, v_ego, self.starpilot_planner.gps_valid, self.starpilot_planner.gps_position,
sm["carState"].steeringAngleDeg, sm["liveParameters"].angleOffsetDeg,
)
if self.mapbox.limit >= 1:
return SOURCE_MAPBOX, self.mapbox.limit
return source, limit
def _apply_fallback(self, v_cruise, enabled):
self._using_experimental_fallback = False
previous_vision_filtered = self.last_valid_source == SOURCE_VISION and self.low_vision_limit_filtered(self.last_valid_limit)
if self.starpilot_toggles.slc_fallback_previous_speed_limit and self.last_valid_limit > 0 and not previous_vision_filtered:
self.source = self.last_valid_source
self.target = self.last_valid_limit
elif enabled and self.starpilot_toggles.slc_fallback_set_speed:
self.source = SOURCE_NONE
self.target = v_cruise
else:
self.source = SOURCE_NONE
self.target = 0.0
self._using_experimental_fallback = bool(self.starpilot_toggles.slc_fallback_experimental_mode)
def _confirmation_required(self, limit):
current = self.last_valid_limit
return ((limit < current and self.starpilot_toggles.speed_limit_confirmation_lower) or
(limit > current and self.starpilot_toggles.speed_limit_confirmation_higher))
def _clear_pending(self):
self.pending_limit = 0.0
self.pending_source = SOURCE_NONE
self.confirmation_time = 0.0
def _reconcile_set_speed_override(self, old_limit, new_limit):
if self.set_speed_override <= 0 or old_limit <= 0 or new_limit <= 0 or abs(new_limit - old_limit) < 0.1:
return
if (new_limit < old_limit or
self.set_speed_override <= new_limit + self.get_offset(new_limit) + SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND):
self.set_speed_override = 0.0
self.overridden_speed = self.pedal_override
def _accept_limit(self, source, limit, *, persist=True):
assert source in REAL_SOURCES and limit >= 1
old_limit = self.last_valid_limit
self._reconcile_set_speed_override(old_limit, limit)
self.source = source
self.target = limit
self.last_valid_limit = limit
self.last_valid_source = source
self.denied_limit = 0.0
self._clear_pending()
if persist and abs(limit - old_limit) >= 0.1:
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(limit))
def _reject_limit(self, limit):
self.denied_limit = limit
self.source = SOURCE_NONE
self._clear_pending()
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
def _update_limit(self, source, limit, sm):
road_name = sm["mapdOut"].roadName if source == SOURCE_MAP else ""
if road_name and road_name != self.previous_road_name:
self.denied_limit = 0.0
self.previous_road_name = road_name
if source == SOURCE_NONE:
self._clear_pending()
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
return
self.override_disable_timer = 0.0
current = self.last_valid_limit
if current > 0 and abs(limit - current) < SAME_LIMIT_TOLERANCE:
self._accept_limit(source, limit, persist=False)
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
return
target_to_use = self.target_to_use
target_with_offset = target_to_use + self.get_offset(target_to_use)
confirmation_required = self._confirmation_required(limit)
if self.denied_limit > 0 and abs(limit - self.denied_limit) < SAME_LIMIT_TOLERANCE:
if confirmation_required:
self._clear_pending()
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
self.source = SOURCE_NONE
return
# Turning confirmation off applies the already-announced candidate.
self._accept_limit(source, limit)
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
return
self.denied_limit = 0.0
set_speed = v_cruise + v_cruise_diff
bidirectional_set_speed = getattr(self.starpilot_toggles, "redneck_cruise", False)
if self._persistent_override_speed > 0:
if bidirectional_set_speed:
if set_speed <= 0:
self.clear_persistent_override()
else:
self._persistent_override_speed = set_speed
elif set_speed <= 0 or (target_with_offset > 0 and set_speed <= target_with_offset and (self.source != "None" or set_speed_changed)):
self.clear_persistent_override()
else:
self._persistent_override_speed = set_speed
elif (
target_with_offset > 0
and set_speed > 0
and not set_speed_input_consumed
and ((bidirectional_set_speed and set_speed_changed) or (not bidirectional_set_speed and set_speed_raised and set_speed > target_with_offset))
):
self._persistent_override_speed = set_speed
if sm["carState"].gasPressed and v_ego > target_with_offset > 0:
self.override_slc = True
self.overridden_speed = v_ego + v_ego_diff
elif self._persistent_override_speed > 0:
self.override_slc = True
self.overridden_speed = self._persistent_override_speed
if self.pending_limit == 0 or abs(limit - self.pending_limit) >= SAME_LIMIT_TOLERANCE:
self.pending_limit = limit
self.pending_source = source
self.confirmation_time = 0.0
self.limit_change_started = True
new_pending = True
else:
self.clear_override()
self.pending_source = source
new_pending = False
if not confirmation_required:
self._accept_limit(source, limit)
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
return
self.source = SOURCE_NONE
self.confirmation_time += DT_MDL
long_active = sm["carControl"].longActive
accel_accept = bool(sm["starpilotCarState"].accelPressed and long_active)
if new_pending and accel_accept:
# This press cannot confirm a candidate that was replaced on this frame.
self.confirmation_button_consumed = True
self.consume_set_speed_change = True
memory = self.starpilot_planner.params_memory
ui_accept = memory.get_bool("SpeedLimitAccepted")
if ui_accept:
memory.remove("SpeedLimitAccepted")
fully_disengaged = not long_active and not sm["selfdriveState"].enabled
if ((accel_accept or ui_accept) and not new_pending) or fully_disengaged:
pending_limit, pending_source = self.pending_limit, self.pending_source
higher = pending_limit > current
self._accept_limit(pending_source, pending_limit)
if accel_accept:
self.consume_set_speed_change = True
self.confirmation_button_consumed = True
set_speed_kph = float(sm["carState"].vCruise)
target_with_offset = self.target + self.offset
if (higher and long_active and 0 < set_speed_kph < V_CRUISE_UNSET and
set_speed_kph * CV.KPH_TO_MS < target_with_offset):
memory.put_float("SLCForceCruiseSpeed", target_with_offset)
elif sm["starpilotCarState"].decelPressed or (self.confirmation_time >= 30 and long_active):
self._reject_limit(self.pending_limit)
def _process_adopt_request(self, source, limit):
memory = self.starpilot_planner.params_memory
if not memory.get_bool("SLCAdoptSpeedLimit"):
return
memory.remove("SLCAdoptSpeedLimit")
if source not in REAL_SOURCES or limit < 1:
return
self.clear_override()
self.consume_set_speed_change = True
self._accept_limit(source, limit)
memory.put_float("SLCForceCruiseSpeed", self.target + self.offset)
def clear_override(self):
self.set_speed_override = 0.0
self.pedal_override = 0.0
self.overridden_speed = 0.0
def _update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
previous = self.previous_set_speed
self.previous_set_speed = v_cruise
changed = previous is not None and abs(v_cruise - previous) > SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND
raised = previous is not None and v_cruise > previous + SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND
consumed = self.consume_set_speed_change
if consumed and (changed or not sm["starpilotCarState"].accelPressed):
self.consume_set_speed_change = False
if not sm["selfdriveState"].enabled:
self.override_disable_time += DT_MDL
if self.override_disable_time >= SLC_OVERRIDE_DISABLE_CLEAR_TIME:
self.clear_override()
return
self.override_disable_time = 0.0
target = self.target_to_use
target_with_offset = target + self.get_offset(target)
set_speed = v_cruise + v_cruise_diff
bidirectional = getattr(self.starpilot_toggles, "redneck_cruise", False)
if self.set_speed_override > 0:
if bidirectional:
self.set_speed_override = max(set_speed, 0.0)
elif set_speed <= 0 or (target_with_offset > 0 and set_speed <= target_with_offset and
(self.source != SOURCE_NONE or changed)):
self.set_speed_override = 0.0
else:
self.set_speed_override = set_speed
elif (target_with_offset > 0 and set_speed > 0 and not consumed and
((bidirectional and changed) or (not bidirectional and raised and set_speed > target_with_offset))):
self.set_speed_override = set_speed
self.pedal_override = v_ego + v_ego_diff if sm["carState"].gasPressed and v_ego > target_with_offset > 0 else 0.0
self.overridden_speed = self.pedal_override or self.set_speed_override
def update(self, dashboard_speed_limit, now, time_validated, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm,
*, active=True, display_only=False):
self.limit_change_started = False
self.confirmation_button_consumed = False
self._using_experimental_fallback = False
mode = "display" if display_only else "active" if active else "off"
if mode != self._mode:
self.mapbox.reset()
self._mode = mode
if not active and not display_only:
self.reset_control_state()
self.mapbox.reset()
self.source, self.target = SOURCE_NONE, 0.0
self.map_speed_limit = self.next_speed_limit = self.vision_limit = 0.0
return
self._update_map_speed_limit(v_ego, sm)
vision_limit = self._get_vision_limit(v_ego, sm, display_only)
source, limit = self._select_limit(dashboard_speed_limit, self.map_speed_limit, vision_limit)
source, limit = self._apply_mapbox_filler(source, limit, now, time_validated, v_ego, sm)
if display_only:
self.reset_control_state()
self.source, self.target = (source, limit) if limit >= 1 else (SOURCE_NONE, 0.0)
return
self._active_control = True
if source == SOURCE_NONE:
self._update_limit(source, limit, sm)
self._apply_fallback(v_cruise, sm["selfdriveState"].enabled)
else:
self._update_limit(source, limit, sm)
self._process_adopt_request(source, limit)
self._update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
@@ -216,7 +216,6 @@ class StarPilotAcceleration:
effective_slc_target = get_active_slc_control_target(
getattr(starpilot_toggles, "speed_limit_controller", False),
getattr(starpilot_toggles, "set_speed_limit", False),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
@@ -271,7 +270,6 @@ class StarPilotAcceleration:
v_ego_diff = v_ego_cluster - v_ego
effective_slc_target = get_active_slc_control_target(
getattr(starpilot_toggles, "speed_limit_controller", False),
getattr(starpilot_toggles, "set_speed_limit", False),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
@@ -312,7 +310,6 @@ class StarPilotAcceleration:
v_ego_cluster = v_ego
effective_slc_target = get_active_slc_control_target(
getattr(starpilot_toggles, "speed_limit_controller", False),
getattr(starpilot_toggles, "set_speed_limit", False),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
+1 -1
View File
@@ -189,7 +189,7 @@ class StarPilotEvents:
else:
self.events.add(StarPilotEventName.openpilotCrashed)
if self.starpilot_planner.starpilot_vcruise.slc.speed_limit_changed_timer == DT_MDL and starpilot_toggles.speed_limit_changed_alert:
if self.starpilot_planner.starpilot_vcruise.slc.limit_change_started and starpilot_toggles.speed_limit_changed_alert:
self.events.add(StarPilotEventName.speedLimitChanged)
self.startup_seen |= sm["starpilotSelfdriveState"].alertText1 == starpilot_toggles.startup_alert_top and sm["starpilotSelfdriveState"].alertText2 == starpilot_toggles.startup_alert_bottom
+17 -24
View File
@@ -105,10 +105,9 @@ def get_lead_veto_distance(car_params):
return LEAD_VETO_M_OVERRIDES.get(fingerprint, LEAD_VETO_M)
def get_active_slc_control_target(speed_limit_controller, set_speed_limit, slc_target, slc_offset, overridden_speed,
def get_active_slc_control_target(speed_limit_controller, slc_target, slc_offset, overridden_speed,
v_ego_diff, allow_lower_override=False):
# `SetSpeedLimit` only controls engage-time set-speed initialization. Ongoing
# SLC speed matching must remain active whenever Speed Limit Controller is on.
# SetSpeedLimit controls engage-time initialization; SLC limits ongoing cruise.
if not speed_limit_controller:
return 0.0
@@ -568,6 +567,18 @@ class StarPilotVCruise:
v_ego_cluster = max(sm["carState"].vEgoCluster, v_ego)
v_ego_diff = v_ego_cluster - v_ego
# Resolve this frame's SLC confirmation before CSC can consume accel/+.
self.slc.starpilot_toggles = starpilot_toggles
slc_active = starpilot_toggles.speed_limit_controller
slc_display_only = not slc_active and starpilot_toggles.show_speed_limits
self.slc.update(
sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated,
v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm,
active=slc_active, display_only=slc_display_only,
)
self.slc_offset = self.slc.offset if slc_active else 0
self.slc_target = self.slc.target if (slc_active or slc_display_only) else 0
# Curve Speed Controller
following_lead = bool(getattr(self.starpilot_planner.starpilot_following, "following_lead", False))
manual_speed_control = is_manual_speed_control(sm)
@@ -586,8 +597,9 @@ class StarPilotVCruise:
not self.starpilot_planner.driving_in_curve)
csc_was_controlling = self.csc_controlling_speed
slc_confirmation_pending = self.slc.speed_limit_changed_timer > DT_MDL and self.slc.unconfirmed_speed_limit >= 1
csc_accel_button = bool(sm["starpilotCarState"].accelPressed) and not slc_confirmation_pending
csc_accel_button = (bool(sm["starpilotCarState"].accelPressed) and
not self.slc.confirmation_pending and
not self.slc.confirmation_button_consumed)
@@ -637,24 +649,6 @@ class StarPilotVCruise:
self.csc.handle_override(v_ego, csc_was_controlling, sm, accel_button=csc_accel_button)
self.csc.log_data(v_ego, sm)
# Pfeiferj's Speed Limit Controller
self.slc.starpilot_toggles = starpilot_toggles
if starpilot_toggles.speed_limit_controller:
self.slc.update_limits(sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
self.slc.update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
self.slc_offset = self.slc.offset
self.slc_target = self.slc.target
elif starpilot_toggles.show_speed_limits:
self.slc.update_limits(sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated, v_cruise, v_ego, sm, display_only=True)
self.slc_offset = 0
self.slc_target = self.slc.target
else:
self.slc_offset = 0
self.slc_target = 0
self.nav_turn_target = self._get_nav_turn_control_target(v_cruise, sm, starpilot_toggles)
# Single tuning knob (signed feet -> meters). Defense clamp on top of UI bounds.
@@ -750,7 +744,6 @@ class StarPilotVCruise:
targets.append(self.csc_target)
slc_control_target = get_active_slc_control_target(
starpilot_toggles.speed_limit_controller,
getattr(starpilot_toggles, "set_speed_limit", False),
self.slc_target,
self.slc_offset,
self.slc.overridden_speed,
+25 -3
View File
@@ -4,7 +4,7 @@ from opendbc.car.chrysler.values import pacifica_hybrid_aol_requires_set_press
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, HyundaiFlags
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from openpilot.common.params import Params
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, ButtonType
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, ButtonType, is_speed_limit_confirmation_pending
from openpilot.selfdrive.selfdrived.events import ET
from openpilot.starpilot.common.experimental_state import (
@@ -57,6 +57,8 @@ class StarPilotCard:
self.params_memory = Params(memory=True)
self.accel_pressed = False
self.confirmation_button_suppressed = set()
self.pressed_accel_buttons = set()
self.always_on_lateral_allowed = False
self.controller_aol_override = None
self.pacifica_aol_set_seen = False
@@ -271,6 +273,23 @@ class StarPilotCard:
]
button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents]
accel_button_types = (int(ButtonType.accelCruise), int(ButtonType.resumeCruise))
confirmation_pending = is_speed_limit_confirmation_pending(sm["starpilotPlan"])
if confirmation_pending:
self.confirmation_button_suppressed.update(self.pressed_accel_buttons)
suppressed_releases = set()
for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False):
if be_type not in accel_button_types:
continue
if be.pressed:
self.pressed_accel_buttons.add(be_type)
if confirmation_pending:
self.confirmation_button_suppressed.add(be_type)
else:
self.pressed_accel_buttons.discard(be_type)
if be_type in self.confirmation_button_suppressed:
self.confirmation_button_suppressed.remove(be_type)
suppressed_releases.add(be_type)
button_aol_supported = self.always_on_lateral_supported and (
self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol
)
@@ -392,8 +411,11 @@ class StarPilotCard:
if not self.always_on_lateral_supported:
self.always_on_lateral_allowed = False
if sm.updated["starpilotPlan"] or any(be_type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be_type in button_event_types):
self.accel_pressed = any(be_type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be_type in button_event_types)
if sm.updated["starpilotPlan"] or any(be_type in accel_button_types for be_type in button_event_types):
self.accel_pressed = any(
be_type in accel_button_types and (be.pressed or be_type not in suppressed_releases)
for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False)
)
if sm.updated["starpilotPlan"] or any(be_type == ButtonType.decelCruise for be_type in button_event_types):
self.decel_pressed = any(be_type == ButtonType.decelCruise for be_type in button_event_types)
+1 -1
View File
@@ -382,7 +382,7 @@ class StarPilotPlanner:
starpilotPlan.slcSpeedLimit = self.starpilot_vcruise.slc_target
starpilotPlan.slcSpeedLimitOffset = self.starpilot_vcruise.slc_offset
starpilotPlan.slcSpeedLimitSource = self.starpilot_vcruise.slc.source
starpilotPlan.speedLimitChanged = self.starpilot_vcruise.slc.speed_limit_changed_timer > DT_MDL
starpilotPlan.speedLimitChanged = self.starpilot_vcruise.slc.confirmation_pending
starpilotPlan.unconfirmedSlcSpeedLimit = self.starpilot_vcruise.slc.unconfirmed_speed_limit
starpilotPlan.themeUpdated = theme_updated
@@ -39,8 +39,8 @@ sys.modules["openpilot.selfdrive.controls.lib.longitudinal_planner"] = _module(
)
sys.modules["openpilot.starpilot.controls.lib.starpilot_vcruise"] = _module(
"openpilot.starpilot.controls.lib.starpilot_vcruise",
get_active_slc_control_target=lambda enabled, set_speed_limit, target, offset, overridden_speed, *_args, **_kwargs: (
float(overridden_speed or target) + float(offset) if enabled and set_speed_limit else 0.0
get_active_slc_control_target=lambda enabled, target, offset, overridden_speed, *_args, **_kwargs: (
float(overridden_speed or target) + float(offset) if enabled else 0.0
),
)
@@ -59,7 +59,7 @@ def make_sm():
"carControl": SimpleNamespace(longActive=False),
"selfdriveState": SimpleNamespace(active=False, alertType=[], experimentalMode=False),
"starpilotSelfdriveState": SimpleNamespace(alertType=[]),
"starpilotPlan": SimpleNamespace(lateralCheck=True),
"starpilotPlan": SimpleNamespace(lateralCheck=True, speedLimitChanged=False, unconfirmedSlcSpeedLimit=0.0),
"liveCalibration": SimpleNamespace(calPerc=100),
}, updated={"starpilotPlan": False})
@@ -295,6 +295,37 @@ def make_wrapped_button_event(button_type, pressed):
return SimpleNamespace(type=SimpleNamespace(raw=int(button_type)), pressed=pressed)
@pytest.mark.parametrize("pending_before_press", [False, True])
def test_slc_confirmation_release_does_not_republish_accel(monkeypatch, tmp_path, pending_before_press):
monkeypatch.setattr(spc, "Params", FakeParams)
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0))
sm = make_sm()
toggles = make_toggles(speed_limit_controller=True)
starpilot_car_state = SimpleNamespace(distancePressed=False)
button_type = spc.ButtonType.accelCruise
if pending_before_press:
sm["starpilotPlan"].speedLimitChanged = True
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 20.0
pressed = make_car_state(button_events=[make_wrapped_button_event(button_type, True)])
assert card.update(pressed, starpilot_car_state, sm, toggles).accelPressed
if not pending_before_press:
sm["starpilotPlan"].speedLimitChanged = True
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 20.0
card.update(make_car_state(), starpilot_car_state, sm, toggles)
sm["starpilotPlan"].speedLimitChanged = False
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 0.0
released = make_car_state(button_events=[make_wrapped_button_event(button_type, False)])
assert not card.update(released, starpilot_car_state, sm, toggles).accelPressed
assert not card.confirmation_button_suppressed
assert card.update(pressed, starpilot_car_state, sm, toggles).accelPressed
assert card.update(released, starpilot_car_state, sm, toggles).accelPressed
@pytest.mark.parametrize(
("car_fingerprint", "expect_normalized_release"),
(