Force Stop 2.0

Force Stop 2.0 — Smoother deceleration curve. On cars with the ADAS stop-sign signal, stops are even more confident; cars without dashboard stop sign signal will also benefit from a new deceleration curve and MPC handoff.

Toggle off = stock behavior.
This commit is contained in:
whoisdomi
2026-04-27 09:16:34 -05:00
committed by firestar5683
parent e24520254b
commit 40372c63ee
11 changed files with 131 additions and 8 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(python3 -c \"import json; json.load\\(open\\(r'c:\\\\Users\\\\user\\\\Documents\\\\GitHub\\\\openpilot\\\\starpilot\\\\system\\\\the_pond\\\\assets\\\\components\\\\tools\\\\device_settings_layout.json'\\)\\)\")",
"Bash(python3 -m py_compile \"c:\\\\Users\\\\user\\\\Documents\\\\GitHub\\\\openpilot\\\\starpilot\\\\controls\\\\lib\\\\starpilot_vcruise.py\")",
"Bash(python3 -m py_compile \"c:\\\\Users\\\\user\\\\Documents\\\\GitHub\\\\openpilot\\\\selfdrive\\\\ui\\\\layouts\\\\settings\\\\starpilot\\\\longitudinal.py\")"
]
}
}

View File

@@ -85,6 +85,7 @@ struct StarPilotCarState @0xf35cc4560bbf6ec2 {
modePressed @16 :Bool;
customPressed @17 :Bool;
alwaysOnLateralAllowed @18 :Bool;
dashboardStopSign @19 :UInt8; # 0 = no signal / platform doesn't publish
}
struct StarPilotDeviceState @0xda96579883444c35 {

View File

@@ -276,6 +276,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ForceOffroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceOnroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceStops", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceStopDistanceOffset", {PERSISTENT, INT, "0", "0", 2}},
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},

View File

@@ -386,6 +386,10 @@ class CarState(CarStateBase):
fp_ret.modePressed = bool(self.mode_button)
fp_ret.customPressed = bool(self.custom_button)
# ADAS camera dashboard stop-sign signal (CANFD HKG only). Registered as optional
# (freq=0); cars without it never publish, so the read returns 0 and the field stays 0.
fp_ret.dashboardStopSign = 1 if bool(cp_cam.vl["ADAS_0x380"]["STOP_SIGN"]) else 0
return ret, fp_ret
def get_can_parsers_canfd(self, CP):
@@ -408,6 +412,7 @@ class CarState(CarStateBase):
if CP.flags & HyundaiFlags.EV:
msgs.append(("DRIVE_MODE_EV", 0)) # optional: not all CAN-FD EV variants publish drive mode
msgs.append(("STEERING_WHEEL_MEDIA_BUTTONS", 0)) # optional: absent or slower on some CAN-FD variants
cam_msgs.append(("ADAS_0x380", 0)) # optional: dashboard stop-sign signal, only on ADAS-equipped HKG CANFD
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, CanBus(CP).ECAN),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_msgs, CanBus(CP).CAM),

View File

@@ -724,6 +724,14 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel):
"color": "#597497",
"visible": lambda: self._params.get_bool("QOLLongitudinal"),
},
{
"title": tr_noop("Force Stop Distance Offset"),
"type": "value",
"get_value": lambda: f"{self._params.get_int('ForceStopDistanceOffset'):+d} ft",
"on_click": lambda: self._show_int_selector("ForceStopDistanceOffset", -20, 20, " ft"),
"color": "#597497",
"visible": lambda: self._params.get_bool("QOLLongitudinal") and self._params.get_bool("ForceStops"),
},
{
"title": tr_noop("Force Standstill State"),
"type": "toggle",

View File

@@ -997,6 +997,7 @@ class StarPilotVariables:
toggle.cruise_increase = self.get_value("CustomCruise", cast=float, condition=(quality_of_life_longitudinal and not pcm_cruise))
toggle.cruise_increase_long = self.get_value("CustomCruiseLong", cast=float, condition=(quality_of_life_longitudinal and not pcm_cruise))
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal)
toggle.force_stop_distance_offset = self.get_value("ForceStopDistanceOffset", cast=int, condition=(quality_of_life_longitudinal and toggle.force_stops))
toggle.force_standstill = self.get_value("ForceStandstill", condition=quality_of_life_longitudinal)
toggle.increase_stopped_distance = self.get_value("IncreasedStoppedDistance", cast=float, condition=quality_of_life_longitudinal, conversion=distance_conversion)
map_gears = self.get_value("MapGears", condition=quality_of_life_longitudinal)

View File

@@ -170,6 +170,15 @@ class ConditionalExperimentalMode:
self.slow_lead_detected = False
def stop_sign_and_light(self, v_ego, sm, model_time):
# While the dashboard has confirmed a stop sign on this approach, pin CEM in EXP.
# Approaches routinely exceed the mode_hold_until/mode_false_since hysteresis (0.5s/0.25s),
# so without this the model briefly losing the sign drops CEM to CHILL and stalls the
# force-stop activation path. Latch is owned by starpilot_vcruise.
if getattr(self.starpilot_planner.starpilot_vcruise, 'stop_sign_confirmed', False):
self.stop_light_filter.x = 1.0
self.stop_light_detected = True
return
if not sm["starpilotCarState"].trafficModeEnabled:
speed_mph = v_ego * CV.MS_TO_MPH # Convert m/s to mph

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env python3
import math
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
@@ -9,6 +11,21 @@ from openpilot.starpilot.controls.lib.speed_limit_controller import SpeedLimitCo
CSC_MIN_SPEED = CITY_SPEED_LIMIT * CV.MPH_TO_MS
OVERRIDE_FORCE_STOP_TIMER = 10
# Force-stop kinematic profile. The user tunes one signed knob (ForceStopDistanceOffset,
# in feet); positive = stop later/longer, negative = stop sooner/shorter. All other
# shape parameters are fixed constants converged from FP-Testing Sessions A-O.
COMFORT_DECEL = 1.0 # m/s^2 — kinematic decel ceiling
ACTIVATION_M = 75.0 # m — CEM/model path activates when model_length < this
MPC_HANDOFF_M = 6.0 # m — below this, command 0 and let MPC finish the stop
ADAS_MAX_MS = 17.88 # 40 mph — cross-street ADAS guard
DASH_SEED_M = 27.0 # ~88 ft — typical ADAS detection distance, used to snap
# tracked length closer when dashboard confirms a sign
FT_TO_M = 0.3048
# Knob bounds (mirror of UI slider; defense in depth)
OFFSET_FT_MIN = -20
OFFSET_FT_MAX = 20
class StarPilotVCruise:
def __init__(self, StarPilotPlanner):
@@ -23,19 +40,54 @@ class StarPilotVCruise:
self.override_force_stop_timer = 0
self.force_stop_timer = 0.0
# Kinematic distance estimator. Same attribute also published as
# starpilotPlan.forcingStopLength, so the existing reader keeps working.
self.tracked_model_length = 0.0
self.stop_sign_confirmed = False
# ===== Main update =====
def update(self, controls_enabled, now, time_validated, v_cruise, v_ego, sm, starpilot_toggles):
long_control_active = sm["carControl"].longActive
force_stop = self.starpilot_planner.starpilot_cem.stop_light_detected and controls_enabled and starpilot_toggles.force_stops
force_stop &= self.starpilot_planner.model_stopped
force_stop &= self.override_force_stop_timer <= 0
# ----- Activation paths -----
# CEM/model path: model predicted stop within ACTIVATION_M
cem_path = (self.starpilot_planner.starpilot_cem.stop_light_detected
and controls_enabled and starpilot_toggles.force_stops
and self.starpilot_planner.model_length < ACTIVATION_M
and self.override_force_stop_timer <= 0
and not self.starpilot_planner.driving_in_curve)
self.force_stop_timer = self.force_stop_timer + DT_MDL if force_stop else 0
# Dashboard path: ADAS camera confirms a stop sign on our road. Field is 0 on
# platforms that don't publish ADAS_0x380, so dash_path is naturally inert there.
dash_value = sm["starpilotCarState"].dashboardStopSign
dash_active = dash_value > 0
dash_path = (dash_active and controls_enabled and starpilot_toggles.force_stops
and v_ego < ADAS_MAX_MS
and self.override_force_stop_timer <= 0
and not self.starpilot_planner.driving_in_curve
and not self.starpilot_planner.tracking_lead)
force_stop_enabled = self.force_stop_timer >= 1
force_stop_active = cem_path or dash_path
# Latch on first dash frame so the CEM pin can fire and we don't release on
# transient dashboard dropouts. Cleared in the no-force-stop branch below.
if dash_path:
self.stop_sign_confirmed = True
# Timer ramp. Faster commitment when the dashboard confirms.
if force_stop_active and not sm["carState"].standstill:
rate = DT_MDL * 2 if dash_active else DT_MDL
self.force_stop_timer = min(self.force_stop_timer + rate, 2.0)
else:
self.force_stop_timer = max(self.force_stop_timer - DT_MDL * 0.25, 0.0)
force_stop_enabled = self.force_stop_timer >= 0.5
# Stay committed across model dropouts until standstill
force_stop_enabled |= self.forcing_stop and not sm["carState"].standstill
# Override: gas/accel pedal during an active force stop
self.override_force_stop |= sm["carState"].gasPressed
self.override_force_stop |= sm["starpilotCarState"].accelPressed
self.override_force_stop &= force_stop_enabled
@@ -45,6 +97,7 @@ class StarPilotVCruise:
elif self.override_force_stop_timer > 0:
self.override_force_stop_timer -= DT_MDL
# ----- Force standstill (independent sibling toggle) -----
force_standstill_enabled = controls_enabled and starpilot_toggles.force_standstill and sm["carState"].standstill
if force_standstill_enabled:
self.override_force_standstill |= sm["carState"].gasPressed
@@ -91,6 +144,11 @@ class StarPilotVCruise:
self.slc_offset = 0
self.slc_target = 0
# Single tuning knob (signed feet -> meters). Defense clamp on top of UI bounds.
offset_ft_raw = int(getattr(starpilot_toggles, 'force_stop_distance_offset', 0) or 0)
offset_ft = max(OFFSET_FT_MIN, min(OFFSET_FT_MAX, offset_ft_raw))
offset_m = offset_ft * FT_TO_M
if force_standstill_enabled and not self.override_force_standstill:
self.forcing_stop = True
self.tracked_model_length = 0.0
@@ -99,11 +157,29 @@ class StarPilotVCruise:
elif force_stop_enabled and not self.override_force_stop:
self.forcing_stop |= not sm["carState"].standstill
self.tracked_model_length = max(self.tracked_model_length - (v_ego * DT_MDL), 0)
v_cruise = min((self.tracked_model_length // PLANNER_TIME), v_cruise)
# Kinematic distance estimator (also published as forcingStopLength).
# Decay one-to-one with motion, clamp by current model_length so we adopt
# the model's view when it regains sight, and snap closer to DASH_SEED_M
# whenever the dashboard signal is active.
self.tracked_model_length = max(self.tracked_model_length - (v_ego * DT_MDL), 0.0)
self.tracked_model_length = min(self.tracked_model_length, self.starpilot_planner.model_length)
if dash_active:
self.tracked_model_length = min(self.tracked_model_length, DASH_SEED_M)
# Kinematic profile with user offset. Positive offset shifts the perceived
# line further down the road -> car rolls further before commanding 0.
effective_d = self.tracked_model_length + offset_m
if effective_d <= MPC_HANDOFF_M:
v_target = 0.0
else:
v_target = math.sqrt(2.0 * COMFORT_DECEL * (effective_d - MPC_HANDOFF_M))
v_cruise = min(v_target, v_cruise)
else:
self.forcing_stop = False
# Latch is only meaningful during an active force-stop cycle
self.stop_sign_confirmed = False
self.tracked_model_length = self.starpilot_planner.model_length

View File

@@ -1048,6 +1048,16 @@
"ui_type": "toggle",
"parent_key": "QOLLongitudinal"
},
{
"key": "ForceStopDistanceOffset",
"label": "Force Stop Distance Offset (ft)",
"description": "Tune where Force Stops bring the car to rest. Positive values let the car roll further before stopping (longer stop, closer to the line). Negative values stop the car sooner (more buffer before the line).",
"data_type": "int",
"ui_type": "numeric",
"min": -20.0,
"max": 20.0,
"parent_key": "QOLLongitudinal"
},
{
"key": "ForceStandstill",
"label": "Force Standstill State",

View File

@@ -157,6 +157,7 @@ StarPilotLongitudinalPanel::StarPilotLongitudinalPanel(StarPilotSettingsWindow *
{"CustomCruise", tr("Cruise Interval"), tr("<b>How much the set speed increases or decreases</b> for each + or cruise control button press."), ""},
{"CustomCruiseLong", tr("Cruise Interval (Hold)"), tr("<b>How much the set speed increases or decreases while holding the + or cruise control buttons.</b>"), ""},
{"ForceStops", tr("Force Stop at \"Detected\" Stop Lights/Signs"), tr("<b>Force openpilot to stop whenever the driving model \"detects\" a red light or stop sign.</b><br><br><i><b>Disclaimer</b>: openpilot does not explicitly detect traffic lights or stop signs. In \"Experimental Mode\", openpilot makes end-to-end driving decisions from camera input, which means it may stop even when there's no clear reason!</i>"), ""},
{"ForceStopDistanceOffset", tr("Force Stop Distance Offset"), tr("<b>Tune where Force Stops bring the car to rest.</b> Positive values let the car roll further before stopping (longer stop, closer to the line). Negative values stop the car sooner (more buffer before the line)."), ""},
{"ForceStandstill", tr("Force Standstill State"), tr("<b>Keep openpilot in the standstill state until you press the gas pedal or the Resume/+ cruise button.</b><br><br>This applies to any engaged stop, not just red lights or stop signs."), ""},
{"IncreasedStoppedDistance", tr("Increase Stopped Distance by:"), tr("<b>Add extra space when stopped behind vehicles.</b> Increase for more room; decrease for shorter gaps."), ""},
{"MapGears", tr("Map Accel/Decel to Gears"), tr("<b>Map the Acceleration or Deceleration profiles to the vehicle's \"Eco\" and \"Sport\" gear modes.</b>"), ""},
@@ -393,6 +394,8 @@ StarPilotLongitudinalPanel::StarPilotLongitudinalPanel(StarPilotSettingsWindow *
longitudinalToggle = new StarPilotParamValueControl(param, title, desc, icon, 1, 99, tr(" mph"));
} else if (param == "IncreasedStoppedDistance") {
longitudinalToggle = new StarPilotParamValueControl(param, title, desc, icon, 0, 10, tr(" feet"));
} else if (param == "ForceStopDistanceOffset") {
longitudinalToggle = new StarPilotParamValueControl(param, title, desc, icon, -20, 20, tr(" feet"));
} else if (param == "MapGears") {
std::vector<QString> mapGearsToggles{"MapAcceleration", "MapDeceleration"};
std::vector<QString> mapGearsToggleNames{tr("Acceleration"), tr("Deceleration")};

View File

@@ -34,7 +34,7 @@ private:
QSet<QString> curveSpeedKeys = {"CalibratedLateralAcceleration", "CalibrationProgress", "ResetCurveData", "ShowCSCStatus"};
QSet<QString> customDrivingPersonalityKeys = {"AggressivePersonalityProfile", "RelaxedPersonalityProfile", "StandardPersonalityProfile", "TrafficPersonalityProfile"};
QSet<QString> longitudinalTuneKeys = {"AccelerationProfile", "DecelerationProfile", "HumanAcceleration", "HumanFollowing", "CoastUpToLeads", "HumanLaneChanges", "LeadDetectionThreshold", "TacoTune"};
QSet<QString> qolKeys = {"CustomCruise", "CustomCruiseLong", "ForceStops", "ForceStandstill", "IncreasedStoppedDistance", "MapGears", "ReverseCruise", "SetSpeedOffset", "WeatherPresets"};
QSet<QString> qolKeys = {"CustomCruise", "CustomCruiseLong", "ForceStops", "ForceStopDistanceOffset", "ForceStandstill", "IncreasedStoppedDistance", "MapGears", "ReverseCruise", "SetSpeedOffset", "WeatherPresets"};
QSet<QString> relaxedPersonalityKeys = {"RelaxedFollow", "RelaxedFollowHigh", "RelaxedJerkAcceleration", "RelaxedJerkDeceleration", "RelaxedJerkDanger", "RelaxedJerkSpeed", "RelaxedJerkSpeedDecrease", "ResetRelaxedPersonality"};
QSet<QString> speedLimitControllerKeys = {"SLCOffsets", "SLCFallback", "SLCOverride", "SLCPriority", "SLCQOL", "SLCVisuals"};
QSet<QString> speedLimitControllerOffsetsKeys = {"Offset1", "Offset2", "Offset3", "Offset4", "Offset5", "Offset6", "Offset7"};