Compare commits

..

20 Commits

Author SHA1 Message Date
Prabhaav Pillai 3c49d47d01 hem rolled through sign temp exp test 2026-08-27 16:58:54 -04:00
Prabhaav Pillai 367b9559b1 hem rolled through sign 2026-08-27 16:47:21 -04:00
Prabhaav Pillai 1489b38dac hem rolled through sign 2026-08-27 16:39:15 -04:00
Prabhaav Pillai d3c8050a36 hem rolled through sign 2026-08-27 16:29:37 -04:00
Prabhaav Pillai 30215f7935 hem rolled through sign 2026-08-27 16:18:12 -04:00
Prabhaav Pillai ba9ceac8c7 hem rolled through sign 2026-08-27 16:08:45 -04:00
Prabhaav Pillai 5f95574f02 Add HybridExperimental mode 2026-08-27 14:36:33 -04:00
firestar5683 227b5cf82a gpu 2026-08-27 12:25:23 -05:00
firestar5683 adb5058cb8 build 2026-08-27 11:59:02 -05:00
firestar5683 3c655e79a9 the morrow 2026-08-27 11:57:40 -05:00
Prabhaav Pillai 032f085500 ui: add PiP side-camera orientation toggle 2026-08-27 11:36:58 -05:00
whoisdomi 5882bd23ba Force Stop: Fast Approach
Force Stop is now better at faster approach speeds. It has more authority on braking in general.

Force Stop Distance Offset: still the lever that moves the ending point sooner or later.

Radar bug fixed
2026-08-27 08:22:54 -05:00
firestarsdog 78aba03b4f Revert "tell me wtf is wrong"
This reverts commit 2c002755c7.
2026-08-27 03:19:39 -04:00
firestarsdog 2c002755c7 tell me wtf is wrong 2026-08-26 23:20:52 -04:00
firestar5683 81d20d304f gpu 2026-08-26 20:50:16 -05:00
firestar5683 d683da24ea lines 2026-08-26 16:28:11 -05:00
firestar5683 4509316877 model 2026-08-26 16:17:04 -05:00
whoisdomi 6a17743513 Force Stop Tweak
Ratched only down below 40m to prevent model jitter from overshooting.

Original PR author: whoisdomi <dcantu83@gmail.com>.
2026-08-26 10:24:29 -05:00
whoisdomi e20ea5d0d1 Remove Takeoff Twitch
Limit the curvature guard to a short pull-away window and reject invalid model plans.

Original PR author: whoisdomi <dcantu83@gmail.com>.
2026-08-26 10:24:25 -05:00
firestar5683 f20b256473 UI 2026-08-26 08:28:28 -05:00
176 changed files with 7697 additions and 3179 deletions
Binary file not shown.
Binary file not shown.
+6
View File
@@ -242,6 +242,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CommunityFavorites", {PERSISTENT, STRING, "", "", 1}},
{"ConditionalChill", {PERSISTENT, BOOL, "0", "0", 1}},
{"ConditionalExperimental", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"HybridExpBias", {PERSISTENT, FLOAT, "0", "0", 1}},
{"HybridExperimental", {PERSISTENT, BOOL, "0", "0", 1}},
{"HybridVisionBrakeSensitivity", {PERSISTENT, FLOAT, "1", "1", 1}},
{"HEMExpDominant", {CLEAR_ON_MANAGER_START, BOOL, "0", "0", 2}},
{"CurvatureData", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"CurveSpeedController", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"CurveSpeedControllerNoLead", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
@@ -522,6 +526,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"PIPPreviewMask", {PERSISTENT, JSON, "{\"width\":1928,\"height\":1208,\"center_left\":[315,548],\"center_right\":[1571,539],\"crop_size\":580}", "{\"width\":1928,\"height\":1208,\"center_left\":[315,548],\"center_right\":[1571,539],\"crop_size\":580}", 2}},
{"PIPPreviewShowOnBlinker", {PERSISTENT, BOOL, "0", "0", 1}},
{"PIPPreviewShowOnBSM", {PERSISTENT, BOOL, "0", "0", 1}},
{"PIPPreviewInvert", {PERSISTENT, BOOL, "0", "0", 1}},
{"GalaxyPaired", {PERSISTENT, BOOL, "0", "0", 0}},
{"GalaxyUploadPending", {PERSISTENT, BOOL, "0", "0", 0}},
{"PreferredSchedule", {PERSISTENT, INT, "2", "0", 0}},
@@ -664,6 +669,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SwitchbackModeEnabled", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
{"SubaruSNGManualParkingBrake", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruStopStartOff", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
Binary file not shown.
@@ -180,6 +180,13 @@ def should_use_ev6_gt_line_stop_direct_tracking(ev6_gt_line: bool, stopping: boo
return bool(ev6_gt_line and stopping and v_ego > EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED and accel_cmd < actual_accel)
def apply_carnival_steering_override(car_fingerprint, steering_pressed: bool,
apply_steer_req: bool, apply_torque: int) -> tuple[bool, int]:
if car_fingerprint == CAR.KIA_CARNIVAL_2025 and steering_pressed:
return False, 0
return apply_steer_req, apply_torque
def update_ev9_longitudinal_tuning(state: EV9LongitudinalTuningState, enabled: bool,
stopping: bool, v_ego: float) -> EV9LongitudinalTuningState:
if not enabled:
@@ -613,6 +620,10 @@ class CarController(CarControllerBase):
if not CC.latActive:
apply_torque = 0
apply_steer_req, apply_torque = apply_carnival_steering_override(
self.CP.carFingerprint, CS.out.steeringPressed, apply_steer_req, apply_torque,
)
# Hold torque with induced temporary fault when cutting the actuation bit
# FIXME: we don't use this with CAN FD?
torque_fault = CC.latActive and not apply_steer_req
@@ -20,6 +20,7 @@ from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalT
should_track_stop_accel_directly_for_car, \
preserve_stock_canfd_lfa_status, \
preserve_stock_canfd_lkas_status, \
apply_carnival_steering_override, \
suppress_redundant_gv70_brake_cancel
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
get_canfd_cruise_available
@@ -208,6 +209,12 @@ class TestHyundaiFingerprint:
assert parser.vl["LKAS_ALT"]["TORQUE_REQUEST"] == 123
assert parser.vl["LKAS_ALT"]["STEER_REQ"] == 1
def test_carnival_steering_override_is_scoped_to_2025_platform(self):
assert apply_carnival_steering_override(CAR.KIA_CARNIVAL_2025, True, True, 123) == (False, 0)
assert apply_carnival_steering_override(CAR.KIA_CARNIVAL_2025, False, True, 123) == (True, 123)
assert apply_carnival_steering_override(CAR.KIA_CARNIVAL_HEV_4TH_GEN, True, True, 123) == (True, 123)
assert apply_carnival_steering_override(CAR.HYUNDAI_IONIQ_6, True, True, 123) == (True, 123)
def test_canfd_torque_bsm_parser_registers_rear_blindspots(self):
CP = CarParams.new_message()
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
@@ -30,6 +30,10 @@ _ANGLE_RECLAIM_FRAMES = 36
_ANGLE_RECLAIM_EXPONENT = 2.5
_ANGLE_MADS_MIN_SPEED = 0.44704
_ANGLE_MADS_MAX_STEER_ANGLE = 120.0
_STOP_START_STARTUP_DELAY_FRAMES = 100
_STOP_START_STARTUP_DEADLINE_FRAMES = 300
_STOP_START_PULSE_FRAMES = 30
_STOP_START_PULSE_PERIOD_FRAMES = 5
def get_safety_CP():
@@ -73,6 +77,66 @@ class CarController(CarControllerBase):
self.prev_close_distance = 0
self.epb_resume_frames_remaining = -1
self.last_standstill_frame = 0
self.stop_start_attempted = False
self.stop_start_request_started = False
self.stop_start_request_frame = 0
self.stop_start_initial_state = None
self.stop_start_counter = 0
self.stop_start_acknowledged = False
def _stop_start_off_request(self, CC, CS, starpilot_toggles):
"""Send one bounded Outback Stop/Start OFF request after ignition.
This is intentionally opt-in and limited to a stationary vehicle in
Park/Neutral. A single ignition session gets at most one attempt.
"""
if self.CP.carFingerprint != CAR.SUBARU_OUTBACK_2023 or \
not getattr(starpilot_toggles, "subaru_stop_start_off", False) or self.stop_start_attempted:
return None
if self.frame > _STOP_START_STARTUP_DEADLINE_FRAMES or getattr(CC, "enabled", False):
self.stop_start_attempted = True
return None
if self.frame < _STOP_START_STARTUP_DELAY_FRAMES or not getattr(getattr(CS, "out", None), "canValid", True):
return None
out = CS.out
if not getattr(out, "standstill", False) or out.gearShifter not in (
structs.CarState.GearShifter.park,
structs.CarState.GearShifter.neutral,
):
return None
dashlights_msg = getattr(CS, "dashlights_msg", None)
if not dashlights_msg:
return None
if not self.stop_start_request_started:
self.stop_start_request_started = True
self.stop_start_request_frame = self.frame
self.stop_start_initial_state = getattr(CS, "stop_start_state", None)
self.stop_start_counter = (int(dashlights_msg.get("COUNTER", 0)) + 1) % 0x10
current_state = getattr(CS, "stop_start_state", None)
if self.stop_start_initial_state is not None and self.stop_start_initial_state != 3 and current_state == 3:
self.stop_start_attempted = True
self.stop_start_acknowledged = True
return None
elapsed = self.frame - self.stop_start_request_frame
if elapsed >= _STOP_START_PULSE_FRAMES:
self.stop_start_attempted = True
return None
if elapsed % _STOP_START_PULSE_PERIOD_FRAMES != 0:
return None
msg = subarucan.create_stop_start_control(
self.packer, dashlights_msg, counter=self.stop_start_counter, bus=self.main_bus,
)
self.stop_start_counter = (self.stop_start_counter + 1) % 0x10
return msg
def _reset_legacy_2025_handoff(self):
self.legacy_2025_handoff_active = False
@@ -87,7 +151,7 @@ class CarController(CarControllerBase):
self._reset_legacy_2025_handoff()
return False
if CS.out.steeringPressed:
if getattr(CS.out, "steeringPressed", False):
self.legacy_2025_handoff_active = True
self.legacy_2025_override_hold_frames = _LEGACY_2025_OVERRIDE_HOLD_FRAMES
self.legacy_2025_reengage_settle_frames = 0
@@ -96,7 +160,7 @@ class CarController(CarControllerBase):
return True
if not self.legacy_2025_handoff_active and not self.legacy_2025_lkas_active and \
abs(CS.out.steeringRateDeg) > _LEGACY_2025_REENGAGE_MAX_STEER_RATE:
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _LEGACY_2025_REENGAGE_MAX_STEER_RATE:
self.legacy_2025_handoff_active = True
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
@@ -109,7 +173,7 @@ class CarController(CarControllerBase):
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
return True
wheel_stable = abs(CS.out.steeringRateDeg) <= _LEGACY_2025_REENGAGE_MAX_STEER_RATE and \
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _LEGACY_2025_REENGAGE_MAX_STEER_RATE and \
abs(CS.out.steeringAngleDeg - self.legacy_2025_reengage_reference_angle) <= _LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA
if wheel_stable:
self.legacy_2025_reengage_settle_frames += 1
@@ -150,7 +214,7 @@ class CarController(CarControllerBase):
self._reset_angle_handoff()
return False
if CS.out.steeringPressed:
if getattr(CS.out, "steeringPressed", False):
self.angle_handoff_active = True
self.angle_override_hold_frames = _ANGLE_OVERRIDE_HOLD_FRAMES
self.angle_reengage_settle_frames = 0
@@ -159,7 +223,7 @@ class CarController(CarControllerBase):
return True
if not self.angle_handoff_active and not self.angle_lkas_active and \
abs(CS.out.steeringRateDeg) > _ANGLE_REENGAGE_MAX_STEER_RATE:
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _ANGLE_REENGAGE_MAX_STEER_RATE:
self.angle_handoff_active = True
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
@@ -172,7 +236,7 @@ class CarController(CarControllerBase):
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
return True
wheel_stable = abs(CS.out.steeringRateDeg) <= _ANGLE_REENGAGE_MAX_STEER_RATE and \
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _ANGLE_REENGAGE_MAX_STEER_RATE and \
abs(CS.out.steeringAngleDeg - self.angle_reengage_reference_angle) <= _ANGLE_REENGAGE_MAX_ANGLE_DELTA
if wheel_stable:
self.angle_reengage_settle_frames += 1
@@ -267,9 +331,19 @@ class CarController(CarControllerBase):
elif abs_torque < self.p.STEER_OVERRIDE_TORQUE_LOW:
self.driver_override = False
lat_active = CC.latActive and not self.driver_override
mads_only = CC.latActive and not getattr(CC, "enabled", False)
mads_only_ok = CS.out.vEgoRaw > _ANGLE_MADS_MIN_SPEED and \
abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
getattr(CS.out, "gearShifter", structs.CarState.GearShifter.drive) == structs.CarState.GearShifter.drive and \
not getattr(CS.out, "standstill", False)
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
lat_active = lkas_available and not self.driver_override and not manual_handoff
if lat_active and not self.angle_lkas_active:
self.apply_steer_last = CS.out.steeringAngleDeg
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lat_active else CC.actuators.steeringAngleDeg
apply_steer = apply_steer_angle_limits_vm(
CC.actuators.steeringAngleDeg,
steer_target,
self.apply_steer_last,
CS.out.vEgoRaw,
CS.out.steeringAngleDeg,
@@ -282,6 +356,7 @@ class CarController(CarControllerBase):
apply_steer = CS.out.steeringAngleDeg
self.apply_steer_last = apply_steer
self.angle_lkas_active = lat_active
return subarucan.create_steering_control_angle(self.packer, apply_steer, lat_active, self.angle_bus)
def lateral_torque(self, CC, CS):
@@ -316,6 +391,10 @@ class CarController(CarControllerBase):
can_sends = []
stop_start_msg = self._stop_start_off_request(CC, CS, starpilot_toggles)
if stop_start_msg is not None:
can_sends.append(stop_start_msg)
# *** steering ***
if (self.frame % self.p.STEER_STEP) == 0:
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
+7 -1
View File
@@ -4,7 +4,7 @@ from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.subaru.values import DBC, CanBus, SubaruFlags
from opendbc.car.subaru.values import CAR, DBC, CanBus, SubaruFlags
from opendbc.car import CanSignalRateCalculator
@@ -15,6 +15,8 @@ class CarState(CarStateBase):
self.shifter_values = can_define.dv["Transmission"]["Gear"]
self.angle_rate_calulator = CanSignalRateCalculator(50)
self.dashlights_msg = {}
self.stop_start_state = 0
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -24,6 +26,10 @@ class CarState(CarStateBase):
cp_angle = cp_main if self.CP.flags & SubaruFlags.D_PLATFORM else cp
ret = structs.CarState()
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
self.dashlights_msg = copy.copy(cp.vl["Dashlights"])
self.stop_start_state = cp.vl["Engine_Stop_Start"]["STOP_START_STATE"]
throttle_msg = cp.vl["Throttle"] if not (self.CP.flags & SubaruFlags.HYBRID) else cp_alt.vl["Throttle_Hybrid"]
ret.gasPressed = throttle_msg["Throttle_Pedal"] > 1e-5
if self.CP.flags & SubaruFlags.PREGLOBAL:
@@ -40,6 +40,8 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM.value
if ret.flags & SubaruFlags.D_PLATFORM_CAMERA:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
if candidate == CAR.SUBARU_OUTBACK_2023:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
@@ -182,6 +182,20 @@ def create_es_dashstatus(packer, frame, dashstatus_msg, enabled, long_enabled, l
return packer.make_can_msg("ES_DashStatus", bus, values)
def create_stop_start_control(packer, dashlights_msg, counter=None, bus=CanBus.alt):
"""Create the Outback 2023-24 momentary Stop/Start button request.
Dashlights is a stock periodic message, so preserve the live frame and only
change the event bit. CANPacker calculates the Subaru checksum for us.
"""
values = dict(dashlights_msg)
if counter is None:
counter = (int(values.get("COUNTER", 0)) + 1) % 0x10
values["COUNTER"] = counter % 0x10
values["STOP_START"] = 1
return packer.make_can_msg("Dashlights", bus, values)
def create_es_brake(packer, frame, es_brake_msg, long_enabled, long_active, brake_value, bus=CanBus.main):
values = {s: es_brake_msg[s] for s in [
"CHECKSUM",
@@ -193,6 +193,7 @@ def test_outback_2023_uses_d_platform_bus_layout():
assert CP.flags & SubaruFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.LEGACY_2025_ANGLE_LIMITS)
assert CanBus.main_for_cp(CP) == CanBus.alt
assert CanBus.angle_for_cp(CP) == CanBus.main
@@ -205,6 +206,52 @@ def test_outback_2023_uses_d_platform_bus_layout():
assert CP.lateralSmoothSeconds == pytest.approx(0.4)
def test_stop_start_request_is_bounded_and_uses_live_dashlights():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
controller = CarController({}, CP)
controller.frame = 101
class TestActuators:
steeringAngleDeg = 0.0
def as_builder(self):
return SimpleNamespace(steeringAngleDeg=self.steeringAngleDeg)
CC = SimpleNamespace(
enabled=False,
latActive=False,
longActive=False,
actuators=TestActuators(),
hudControl=SimpleNamespace(leadVisible=False),
cruiseControl=SimpleNamespace(cancel=False),
)
CS = SimpleNamespace(
canValid=True,
dashlights_msg={"COUNTER": 6, "STOP_START": 0},
stop_start_state=0,
out=SimpleNamespace(
standstill=True,
gearShifter=structs.CarState.GearShifter.park,
),
)
toggles = SimpleNamespace(subaru_stop_start_off=True, subaru_sng=False)
_, can_sends = controller.update(CC, CS, 0, toggles)
stop_start_msgs = [msg for msg in can_sends if msg[0] == 0x390]
assert len(stop_start_msgs) == 1
assert stop_start_msgs[0][2] == CanBus.alt
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("Dashlights", 0)], CanBus.alt)
parser.update([(1, [stop_start_msgs[0]])])
assert parser.vl["Dashlights"]["STOP_START"] == 1
assert parser.vl["Dashlights"]["COUNTER"] == 7
controller.frame = 103
CS.stop_start_state = 3
_, can_sends = controller.update(CC, CS, 0, toggles)
assert not any(msg[0] == 0x390 for msg in can_sends)
assert controller.stop_start_acknowledged
def test_legacy_2025_uses_gen2_angle_bus_layout():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
parsers = CarState.get_can_parsers(CP)
@@ -448,6 +495,44 @@ def test_angle_controller_tracks_driver_override():
assert not controller.driver_override
def test_angle_controller_blocks_low_speed_mads_engagement():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
controller = CarController({}, CP)
CC = SimpleNamespace(
enabled=False,
latActive=True,
actuators=SimpleNamespace(steeringAngleDeg=15.0),
)
CS = SimpleNamespace(out=SimpleNamespace(
vEgoRaw=0.3,
steeringAngleDeg=80.0,
steeringTorque=0.0,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
))
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
msg = controller.lateral_angle(CC, CS)
parser.update([(1, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.vEgoRaw = 1.0
CS.out.steeringAngleDeg = 130.0
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.steeringAngleDeg = 0.0
msg = controller.lateral_angle(CC, CS)
parser.update([(3, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
@@ -88,6 +88,7 @@ class SubaruSafetyFlags(IntFlag):
D_PLATFORM = 32
D_PLATFORM_CAMERA = 64
FIXED_ANGLE_LIMITS = 128
STOP_START_BUTTON = 256
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
@@ -42,6 +42,7 @@ TOYOTA_NO_LEAD_CRUISE_SIGN_FLIP_MIN_SET_SPEED_ERROR = 0.35 # m/s
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
MAX_STEER_RATE = 100 # deg/s
MAX_STEER_RATE_FRAMES = 18 # tx control frames needed before torque can be cut
TOYOTA_HIGHLANDER_TSS2_MAX_STEER_RATE_FRAMES = 8
# EPS allows user torque above threshold for 50 frames before permanently faulting
MAX_USER_TORQUE = 500
@@ -72,6 +73,11 @@ def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
) or highlander_sdsu)
def get_steer_rate_limit_frames(car_fingerprint) -> int:
return (TOYOTA_HIGHLANDER_TSS2_MAX_STEER_RATE_FRAMES
if car_fingerprint == CAR.TOYOTA_HIGHLANDER_TSS2 else MAX_STEER_RATE_FRAMES)
def get_long_tune(CP, params):
kiBP = [2., 5.]
kiV = [0.5, 0.25]
@@ -223,6 +229,7 @@ class CarController(CarControllerBase):
self.standstill_req = False
self.permit_braking = True
self.steer_rate_counter = 0
self.steer_rate_limit_frames = get_steer_rate_limit_frames(self.CP.carFingerprint)
self.distance_button = 0
# *** start long control state ***
@@ -345,8 +352,10 @@ class CarController(CarControllerBase):
apply_torque = apply_meas_steer_torque_limits(new_torque, self.last_torque, CS.out.steeringTorqueEps, self.params)
# >100 degree/sec steering fault prevention
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
self.steer_rate_counter, MAX_STEER_RATE_FRAMES)
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(
abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
self.steer_rate_counter, self.steer_rate_limit_frames,
)
if not lat_active:
apply_torque = 0
@@ -10,6 +10,7 @@ from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car
from opendbc.car.toyota import toyotacan
from opendbc.car.toyota.carcontroller import CarController, get_camry_hybrid_feedforward, get_long_tune, get_prius_feedforward, \
get_prius_positive_feedforward_scale, \
get_steer_rate_limit_frames, \
limit_interceptor_pcm_accel, \
limit_interceptor_stopping_accel, limit_no_lead_cruise_sign_flip, \
limit_prius_stopping_accel, should_bypass_toyota_long_pid, update_permit_braking
@@ -706,6 +707,10 @@ class TestToyotaFingerprint:
class TestToyotaCarController:
def test_highlander_tss2_uses_early_steer_rate_fault_guard(self):
assert get_steer_rate_limit_frames(CAR.TOYOTA_HIGHLANDER_TSS2) == 8
assert get_steer_rate_limit_frames(CAR.TOYOTA_RAV4_TSS2) == 18
@staticmethod
def _make_controller(*, standstill_req=False, last_standstill=False):
controller = CarController.__new__(CarController)
+24 -2
View File
@@ -41,6 +41,7 @@
#define MSG_SUBARU_ES_HighBeamAssist 0x121U
#define MSG_SUBARU_ES_STATIC_1 0x22aU
#define MSG_SUBARU_ES_STATIC_2 0x325U
#define MSG_SUBARU_Dashlights 0x390U
#define SUBARU_MAIN_BUS 0U
#define SUBARU_ALT_BUS 1U
@@ -61,6 +62,9 @@
{MSG_SUBARU_ES_LKAS_State, bus, 8, .check_relay = true}, \
{MSG_SUBARU_ES_Infotainment, bus, 8, .check_relay = true}, \
#define SUBARU_STOP_START_TX_MSGS(bus) \
{MSG_SUBARU_Dashlights, bus, 8, .check_relay = false}, \
#define SUBARU_COMMON_LONG_TX_MSGS(alt_bus) \
{MSG_SUBARU_ES_Distance, alt_bus, 8, .check_relay = true}, \
{MSG_SUBARU_ES_Brake, alt_bus, 8, .check_relay = true}, \
@@ -108,6 +112,7 @@ static bool subaru_stop_and_go = false;
static bool subaru_lkas_angle = false;
static bool subaru_d_platform = false;
static bool subaru_fixed_angle_limits = false;
static bool subaru_stop_start_button = false;
static uint32_t subaru_get_checksum(const CANPacket_t *msg) {
return (uint8_t)msg->data[0];
@@ -285,6 +290,13 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
violation |= !(is_tester_present || is_button_rdbi);
}
if (msg->addr == MSG_SUBARU_Dashlights) {
violation |= !subaru_stop_start_button;
violation |= msg->bus != SUBARU_ALT_BUS;
violation |= !GET_BIT(msg, 54U);
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (violation){
tx = false;
}
@@ -334,6 +346,12 @@ static safety_config subaru_init(uint16_t param) {
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_MAIN_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_CAM_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
@@ -378,6 +396,9 @@ static safety_config subaru_init(uint16_t param) {
const uint16_t SUBARU_PARAM_FIXED_ANGLE_LIMITS = 128;
subaru_fixed_angle_limits = GET_FLAG(param, SUBARU_PARAM_FIXED_ANGLE_LIMITS);
const uint16_t SUBARU_PARAM_STOP_START_BUTTON = 256;
subaru_stop_start_button = GET_FLAG(param, SUBARU_PARAM_STOP_START_BUTTON);
#ifdef ALLOW_DEBUG
const uint16_t SUBARU_PARAM_LONGITUDINAL = 2;
subaru_longitudinal = GET_FLAG(param, SUBARU_PARAM_LONGITUDINAL);
@@ -385,8 +406,9 @@ static safety_config subaru_init(uint16_t param) {
safety_config ret;
if (subaru_lkas_angle) {
ret = subaru_d_platform ? (subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS)) : \
ret = subaru_d_platform ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS) : \
(subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS))) : \
subaru_gen2 ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_lkas_angle_rx_checks, SUBARU_LKAS_ANGLE_TX_MSGS);
} else if (subaru_gen2) {
+5 -4
View File
@@ -1075,13 +1075,14 @@ class SafetyTest(SafetyTestBase):
continue
if attr.startswith('TestSubaruGen') and current_test.startswith('TestSubaruGen'):
continue
if 'TestSubaruDPlatformAngleSafety' in {attr, current_test} and \
'Angle' in attr and 'Angle' in current_test:
if attr.startswith('TestSubaruDPlatform') and current_test.startswith('TestSubaruDPlatform'):
continue
if 'TestSubaruDPlatformAngleSafety' in {attr, current_test}:
if attr.startswith('TestSubaruDPlatform'):
# D-platform uses the same main-bus HUD messages as the other
# Subaru modes, so those modes cannot be distinguished by ID.
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x321, 0x322, 0x323]), tx))
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x124, 0x321, 0x322, 0x323]), tx))
if current_test.startswith('TestSubaruDPlatform') and attr.startswith('TestSubaruGen'):
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x124, 0x321, 0x322, 0x323]), tx))
if attr.startswith('TestSubaruPreglobal') and current_test.startswith('TestSubaruPreglobal'):
continue
if {attr, current_test}.issubset({'TestVolkswagenPqSafety', 'TestVolkswagenPqStockSafety', 'TestVolkswagenPqLongSafety'}):
@@ -36,6 +36,7 @@ class SubaruMsg(enum.IntEnum):
ES_HighBeamAssist = 0x121
ES_STATIC_1 = 0x22a
ES_STATIC_2 = 0x325
Dashlights = 0x390
SUBARU_MAIN_BUS = 0
@@ -401,6 +402,21 @@ class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, Test
return self.packer.make_can_msg_safety("Steering_2", SUBARU_MAIN_BUS, {"Steering_Angle": angle})
class TestSubaruDPlatformStopStartSafety(TestSubaruDPlatformAngleSafety):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | \
SubaruSafetyFlags.STOP_START_BUTTON
TX_MSGS = TestSubaruDPlatformAngleSafety.TX_MSGS + [[SubaruMsg.Dashlights, SUBARU_ALT_BUS]]
def _stop_start_msg(self, pressed):
return self.packer.make_can_msg_safety(
"Dashlights", SUBARU_ALT_BUS, {"COUNTER": 0, "STOP_START": pressed},
)
def test_stop_start_tx_requires_pressed_bit(self):
self.assertTrue(self._tx(self._stop_start_msg(True)))
self.assertFalse(self._tx(self._stop_start_msg(False)))
class TestSubaruDPlatformCameraAngleSafety(TestSubaruDPlatformAngleSafety):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | SubaruSafetyFlags.D_PLATFORM_CAMERA
TX_MSGS = [[SubaruMsg.ES_LKAS_ANGLE, SUBARU_CAM_BUS],
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-f8d47139-DEBUG";
const uint8_t gitversion[19] = "DEV-3c655e79-DEBUG";
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.
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.
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.
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.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-f8d47139-DEBUG
DEV-3c655e79-DEBUG
+39 -20
View File
@@ -16,6 +16,8 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from openpilot.starpilot.common.model_versions import UNIFIED_ARTIFACT_FORMAT
DEFAULT_INPUT_ROOT = Path("/data/openpilot/uncompiledmodels")
DEFAULT_OUTPUT_ROOT = Path("/data/openpilot/compiledmodels")
COMPILE_SCRIPT = REPO_ROOT / "tinygrad_repo/examples/openpilot/compile3.py"
@@ -389,15 +391,33 @@ def multipart_output_paths(artifact: Path, output_dir: Path | None = None) -> li
]
def install_local_artifact(artifact: Path, model_key: str, version: str) -> None:
def _update_local_artifact_metadata(model_key: str, external_gpu: bool) -> None:
metadata_path = MODELS_PATH / ".model_artifacts.json"
try:
payload = json.loads(metadata_path.read_text()) if metadata_path.is_file() else {}
if not isinstance(payload, dict):
payload = {}
metadata = payload.get(model_key)
if not isinstance(metadata, dict):
metadata = {}
metadata["artifact_format"] = UNIFIED_ARTIFACT_FORMAT
metadata["uses_external_gpu"] = bool(external_gpu)
payload[model_key] = metadata
metadata_path.write_text(json.dumps(payload))
except Exception as error:
print(f" WARN: could not update {metadata_path.name}: {error}")
def install_local_artifact(artifact: Path, model_key: str, version: str, external_gpu: bool = False) -> None:
"""Copy a freshly compiled local- model into the runtime dir modeld loads from,
and ensure its <id>.json sidecar carries the correct version.
and ensure its <id>.json sidecar and artifact cache carry the correct metadata.
The sidecar version is NOT cosmetic: without it _discover_local_models() records
an empty version, which downstream parses on the wrong contract (a v15 model then
drives like v11). Since we know the build version here, we write it so the local
install is correct by default. Local models must be a single is_file() in
/data/models to show in the picker. No-ops off-device (no /data/models).
install is correct by default. The GPU flag is equally important: it selects
the out-of-band loader and AMD queue at runtime. Local models must be a single
is_file() in /data/models to show in the picker. No-ops off-device.
"""
if not MODELS_PATH.is_dir():
print(f" skipped auto-install: {MODELS_PATH} not present (not on device?)")
@@ -415,20 +435,19 @@ def install_local_artifact(artifact: Path, model_key: str, version: str) -> None
info = loaded
except Exception as error:
print(f" WARN: existing sidecar {sidecar.name} is malformed, rewriting: {error}")
if not version:
if not str(info.get("version") or "").strip():
print(f" WARN: could not determine version -- set it by hand in {sidecar.name} "
"or the model may drive on the wrong version contract")
return
# keep any user-set name/series; only guarantee a correct, non-empty version
if str(info.get("version") or "").strip() == version and sidecar.is_file():
print(f" sidecar ok: {sidecar.name} (version {version})")
return
info.setdefault("name", model_key[len("local-"):].replace("_", " ").replace("-", " ").strip())
info.setdefault("series", "Local")
info["version"] = version
sidecar.write_text(json.dumps(info, indent=2) + "\n")
print(f" wrote sidecar {sidecar.name} (version {version})")
if version:
info.setdefault("name", model_key[len("local-"):].replace("_", " ").replace("-", " ").strip())
info.setdefault("series", "Local")
info["version"] = version
elif not str(info.get("version") or "").strip():
print(f" WARN: could not determine version -- set it by hand in {sidecar.name} "
"or the model may drive on the wrong version contract")
info["uses_external_gpu"] = bool(external_gpu)
if version or sidecar.is_file() or external_gpu:
sidecar.write_text(json.dumps(info, indent=2) + "\n")
print(f" wrote sidecar {sidecar.name} (gpu={bool(external_gpu)})")
_update_local_artifact_metadata(model_key, external_gpu)
def split_oversized_artifact(
@@ -534,7 +553,7 @@ def compile_driving(
for qcom_only_flag in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"):
compile_env.pop(qcom_only_flag, None)
compile_env.update({
"DEBUG": "2",
"DEBUG": "1",
"DEV": "USB+AMD:LLVM",
"WARP_DEV": "QCOM",
"FLOAT16": "1",
@@ -665,7 +684,7 @@ def main() -> int:
print(f" --no-split: kept one {size_mb:.1f} MB file; over 100 MB, so split it "
"before committing to a repo (re-run without --no-split, or --split-artifact)")
if is_local and not args.no_install:
install_local_artifact(output, model_key, version)
install_local_artifact(output, model_key, version, args.external_gpu)
else:
multipart_outputs = split_oversized_artifact(output)
if multipart_outputs:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+8 -3
View File
@@ -85,7 +85,8 @@ class Car:
def __init__(self, CI=None, RI=None) -> None:
self.can_sock = messaging.sub_sock('can', timeout=20)
self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents', 'radarState', 'longitudinalPlan'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks', 'gpsLocationExternal'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'])
self.gps_pm = None
self.can_rcv_cum_timeout_counter = 0
self._last_car_gps_timestamp_nanos = 0
@@ -138,7 +139,10 @@ class Car:
self.CI, self.CP, self.FPCP = CI, CI.CP, CI.FPCP
self.RI = RI
self.params.put_bool("CarGpsAvailable", bool(getattr(self.CI.CS, 'car_gps_supported', False)))
car_gps_supported = bool(getattr(self.CI.CS, 'car_gps_supported', False))
self.params.put_bool("CarGpsAvailable", car_gps_supported)
if car_gps_supported:
self.gps_pm = messaging.PubMaster(['gpsLocationExternal'])
interface_alternative_experience = self.CP.alternativeExperience
self.CP.alternativeExperience = interface_alternative_experience
@@ -364,7 +368,8 @@ class Car:
gps.speedAccuracy = car_gps['speedAccuracy']
gps.hasFix = car_gps['hasFix']
gps.satelliteCount = car_gps['satelliteCount']
self.pm.send('gpsLocationExternal', gps_send)
assert self.gps_pm is not None
self.gps_pm.send('gpsLocationExternal', gps_send)
self._last_car_gps_publish_monotonic = now
# carParams - logged every 50 seconds (> 1 per segment)
+58 -31
View File
@@ -169,26 +169,27 @@ CURVATURE_HOLD_OPPOSITE_RELEASE = 0.01 # 1/m
CURVATURE_HOLD_CONFIRM_MIN = 0.003 # 1/m (~7 deg) of wound curvature before capture
CURVATURE_HOLD_CONFIRM_SWEPT = 0.6 # rad of heading swept this blinker cycle; past this the push is exit-shaping, not initiation
# Pull-away twitch guard. modeld divides the action head's lateral-ACCELERATION output by
# max(1, v)^2, so its residual at pull-away (~0.02 m/s^2, the head's noise floor) reads as
# curvature 0.015 — 38 deg of wheel — where the same value at highway speed is 0.2 deg.
# Route 78511c37 twitched on 10 of 10 straight takeoffs. The model's own planned path is the
# tell: it read straight there while the action demanded 6-108x more.
TWITCH_GUARD_MAX_SPEED = 4.0 # m/s; above this the 1/v^2 amplification is gone
TWITCH_GUARD_FADE_SPEED = 3.0 # m/s; full strength below, faded out by MAX_SPEED
TWITCH_GUARD_PLAN_RATIO = 4.0 # allowed |action| / |plan curvature|
TWITCH_GUARD_FLOOR = 0.002 # 1/m (~5 deg); a near-zero probe must not clamp to nothing
TWITCH_GUARD_STRAIGHT_LO = 0.005 # 1/m; a plain ratio is too permissive near straight (3x of
TWITCH_GUARD_STRAIGHT_HI = 0.014 # 0.003 still licenses 22 deg), so fade the allowance out too
TWITCH_GUARD_MIN_REACH = 12.0 # m; shorter plans read straight while the action legitimately
# unwinds a turn (ce2b186c51 seg 28 t=14.6). Twitches: p5 24 m
# Suppress low-speed action spikes while the model's spatial path remains straight.
TWITCH_GUARD_MAX_SPEED = 4.0
TWITCH_GUARD_FADE_SPEED = 3.0
TWITCH_GUARD_DURATION = 1.5
TWITCH_GUARD_PLAN_RATIO = 4.0
TWITCH_GUARD_FLOOR = 0.002
TWITCH_GUARD_STRAIGHT_LO = 0.005
TWITCH_GUARD_STRAIGHT_HI = 0.014
TWITCH_GUARD_MIN_REACH = 12.0
def _plan_circle_curvature(xs, ys, lookahead: float) -> float:
# curvature of the circle through the origin, tangent to the car's heading, passing
# through the plan point ~lookahead meters ahead: kappa = 2y / (x^2 + y^2)
# Fit curvature through the plan point at the requested lookahead.
px, py = 0.0, 0.0
for x, y in zip(xs, ys):
for x, y in zip(xs, ys, strict=False):
try:
x, y = float(x), float(y)
except (TypeError, ValueError, OverflowError):
return 0.0
if not (math.isfinite(x) and math.isfinite(y)):
return 0.0
px, py = x, y
if math.hypot(x, y) >= lookahead:
break
@@ -199,11 +200,7 @@ def _plan_circle_curvature(xs, ys, lookahead: float) -> float:
def _plan_dual_probe(model_v2, d_near: float, d_far: float) -> float:
# Min-magnitude of a near and a far circle fit. The far probe alone assumes the turn
# starts immediately, which over-winds wide turns whose arc begins several meters out
# (wide multi-lane lefts): the near probe reads ~straight there and only grows as the
# car approaches the arc, so the readout self-scales to the turn geometry. Sign
# disagreement means no coherent turn ahead: contribute nothing.
# Use the smaller magnitude of near and far probes to avoid early turn bias.
xs, ys = model_v2.position.x, model_v2.position.y
near = _plan_circle_curvature(xs, ys, d_near)
far = _plan_circle_curvature(xs, ys, d_far)
@@ -236,20 +233,37 @@ def get_plan_turn_onset_dist(model_v2) -> float:
def get_plan_reach(model_v2) -> float:
xs = model_v2.position.x
return xs[-1] if len(xs) else 0.0
try:
xs = model_v2.position.x
return float(xs[-1]) if len(xs) else 0.0
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
return 0.0
def _plan_positions_are_finite(model_v2) -> bool:
try:
xs, ys = model_v2.position.x, model_v2.position.y
return len(xs) == len(ys) and all(
math.isfinite(float(x)) and math.isfinite(float(y)) for x, y in zip(xs, ys, strict=True)
)
except (AttributeError, TypeError, ValueError, OverflowError):
return False
def limit_curvature_to_plan(model_v2, curvature: float, v_ego: float) -> float:
# See TWITCH_GUARD_*. Magnitude only: the command is bounded, never reversed. FAR fit alone —
# the near probe swings with the car's heading error, so once a twitch has yawed the car it
# bends to correct it and licenses the very command that caused it (seg 10 t=53.1).
if not (math.isfinite(curvature) and math.isfinite(v_ego)):
return curvature
if v_ego >= TWITCH_GUARD_MAX_SPEED or curvature == 0.0:
return curvature
if get_plan_reach(model_v2) < TWITCH_GUARD_MIN_REACH:
if not _plan_positions_are_finite(model_v2):
return curvature
reach = get_plan_reach(model_v2)
if not math.isfinite(reach) or reach < TWITCH_GUARD_MIN_REACH:
return curvature
plan = abs(_plan_circle_curvature(model_v2.position.x, model_v2.position.y,
CURVATURE_HOLD_PLAN_LOOKAHEAD_FAR))
if not math.isfinite(plan):
return curvature
straightness = (plan - TWITCH_GUARD_STRAIGHT_LO) / (TWITCH_GUARD_STRAIGHT_HI - TWITCH_GUARD_STRAIGHT_LO)
limit = max(TWITCH_GUARD_PLAN_RATIO * plan * min(max(straightness, 0.0), 1.0), TWITCH_GUARD_FLOOR)
if abs(curvature) <= limit:
@@ -259,6 +273,14 @@ def limit_curvature_to_plan(model_v2, curvature: float, v_ego: float) -> float:
return curvature + (math.copysign(limit, curvature) - curvature) * fade
def update_twitch_guard(remaining: float, v_ego: float, standstill: bool) -> float:
if not (math.isfinite(remaining) and math.isfinite(v_ego)):
return 0.0
if standstill or abs(v_ego) <= 0.3:
return TWITCH_GUARD_DURATION
return max(remaining - DT_CTRL, 0.0)
def get_control_lateral_smooth_seconds(brand: str, v_ego: float, vehicle_smooth_seconds: float) -> float:
if brand == "rivian" or (brand == "subaru" and vehicle_smooth_seconds > 0.0):
return get_car_lateral_smooth_seconds(brand, v_ego, vehicle_smooth_seconds)
@@ -379,6 +401,7 @@ class Controls:
self.turn_hold_handoff_t = 0.0
self.turn_hold_done = False
self.turn_blinker_swept = 0.0
self.twitch_guard_remaining = 0.0
self.kona_non_scc_lateral_active = False
self.pose_calibrator = PoseCalibrator()
@@ -434,6 +457,7 @@ class Controls:
def state_control(self):
CS = self.sm['carState']
self.twitch_guard_remaining = update_twitch_guard(self.twitch_guard_remaining, CS.vEgo, CS.standstill)
# Update VehicleModel
lp = self.sm['liveParameters']
@@ -493,7 +517,11 @@ class Controls:
# EcuDisableFailed is set when car started in READY mode (ECU disable was rejected)
# Disable longitudinal so stock ACC works instead
self.update_ecu_disable_failed()
CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and not self.sm['starpilotCarState'].pauseLongitudinal and self.CP.openpilotLongitudinalControl and not self.ecu_disable_failed
CC.longActive = (
CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and
not self.sm['starpilotCarState'].pauseLongitudinal and self.CP.openpilotLongitudinalControl and
not self.ecu_disable_failed
)
actuators = CC.actuators
actuators.longControlState = self.LoC.long_control_state
@@ -532,9 +560,8 @@ class Controls:
# here is positive for RIGHT turns (pauseturn log: left turn at +148 deg steering
# angle logs desiredCurvature -0.07), so the blinker maps right=+1, left=-1.
blinker_dir = float(CS.rightBlinker) - float(CS.leftBlinker)
# Pull-away twitch guard (see TWITCH_GUARD_*). Requires no turn intent in play, so the
# pre-wind ratchet, turn lead and exit opposite-release never see a reduced command.
if CC.latActive and blinker_dir == 0.0 and self.turn_hold_curvature == 0.0:
if (CC.latActive and self.twitch_guard_remaining > 0.0 and
blinker_dir == 0.0 and self.turn_hold_curvature == 0.0):
new_desired_curvature = limit_curvature_to_plan(model_v2, new_desired_curvature, CS.vEgo)
# heading swept in the blinker's direction over the whole blinker cycle (any speed):
# discriminates a turn not yet made from one being exited (see the re-arm below)
+49 -4
View File
@@ -14,6 +14,7 @@ _MAX_OFFSET = 0.3
_MIN_CENTER_TO_LINE = 1.1
_MAX_RAW_CORRECTION = 0.004
_MAX_GAIN = 0.30
_VISUAL_CORRECTION_EPSILON = 1e-6
_SMOOTH_TAU = 0.4
_SIGNAL_RELEASE_TAU = 0.20
_CONFIDENCE_RELEASE_TAU = 0.20
@@ -85,7 +86,8 @@ class LaneCenteringController:
def _covers(x, distance: float) -> bool:
return bool(x[0] <= distance <= x[-1])
def _raw_correction(self, model_v2, v_ego: float, offset: float, e2e_authority: float) -> tuple[bool, float]:
@staticmethod
def _raw_correction(model_v2, v_ego: float, offset: float, e2e_authority: float) -> tuple[bool, float]:
try:
lane_lines = model_v2.laneLines
probs = np.asarray(model_v2.laneLineProbs, dtype=float)
@@ -105,11 +107,13 @@ class LaneCenteringController:
right_y = np.asarray(lane_lines[2].y, dtype=float)
pos_x = np.asarray(model_v2.position.x, dtype=float)
pos_y = np.asarray(model_v2.position.y, dtype=float)
if not (self._valid_path(left_x, left_y) and self._valid_path(right_x, right_y) and self._valid_path(pos_x, pos_y)):
if not (LaneCenteringController._valid_path(left_x, left_y) and
LaneCenteringController._valid_path(right_x, right_y) and
LaneCenteringController._valid_path(pos_x, pos_y)):
return False, 0.0
lookahead = float(np.clip(v_ego, 8.0, 35.0))
if not all(self._covers(x, lookahead) for x in (left_x, right_x, pos_x)):
if not all(LaneCenteringController._covers(x, lookahead) for x in (left_x, right_x, pos_x)):
return False, 0.0
left = float(np.interp(lookahead, left_x, left_y))
@@ -130,7 +134,7 @@ class LaneCenteringController:
try:
pos_y_std = np.asarray(model_v2.position.yStd, dtype=float)
if self._valid_path(pos_x, pos_y_std):
if LaneCenteringController._valid_path(pos_x, pos_y_std):
path_std = float(np.interp(lookahead, pos_x, pos_y_std))
if 0.0 <= path_std <= _E2E_MAX_PATH_STD:
break_in = np.clip(
@@ -145,3 +149,44 @@ class LaneCenteringController:
return True, float(2.0 * error / lookahead ** 2)
except (AttributeError, IndexError, TypeError, ValueError):
return False, 0.0
def get_raw_lane_centering_correction(model_v2, v_ego: float, offset: float,
e2e_authority: float) -> tuple[bool, float]:
"""Return the instantaneous lane-centering correction without controller filtering."""
return LaneCenteringController._raw_correction(model_v2, v_ego, offset, e2e_authority)
def get_lane_centering_visual_direction(model_v2, v_ego: float, offset: float, e2e_authority: float,
enabled: bool, lat_active: bool, pause_on_signal: bool = False,
turn_signal_active: bool = False,
applied_correction: float | None = None) -> int:
"""Return 1 for a right correction, -1 for left, and 0 when no correction is active."""
if not enabled or not lat_active or (pause_on_signal and turn_signal_active):
return 0
try:
v_ego = float(v_ego)
offset = float(offset)
e2e_authority = float(e2e_authority)
if not np.isfinite([v_ego, offset, e2e_authority]).all() or v_ego < _MIN_V_EGO:
return 0
if model_v2.meta.laneChangeState != log.LaneChangeState.off:
return 0
except (AttributeError, TypeError, ValueError):
return 0
valid, correction = get_raw_lane_centering_correction(
model_v2,
v_ego,
float(np.clip(offset, -_MAX_OFFSET, _MAX_OFFSET)),
float(np.clip(e2e_authority, 0.0, 1.0)),
)
if not valid or not np.isfinite(correction):
return 0
if applied_correction is not None and np.isfinite(applied_correction) and \
abs(applied_correction) > _VISUAL_CORRECTION_EPSILON:
correction = float(applied_correction)
if abs(correction) <= _VISUAL_CORRECTION_EPSILON:
return 0
return 1 if correction > 0.0 else -1
+31 -1
View File
@@ -6,6 +6,8 @@ from openpilot.selfdrive.controls.lib.latcontrol import LatControl
# TODO This is speed dependent
STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
FORD_ANGLE_CATCH_UP_HORIZON = 1.25 # Seconds
FORD_ANGLE_RATE_FILTER_TIME_CONSTANT = 0.15 # Seconds
_ASCENT_ANGLE_TRACKING_GAIN = 0.25
_ASCENT_ANGLE_TRACKING_MAX_CORRECTION = 8.0
@@ -23,12 +25,32 @@ def _ascent_angle_tracking_target(target_angle: float, steering_angle: float,
return target_angle + correction
def _ford_angle_tracking_saturated(angle_error: float, steering_rate: float) -> bool:
"""Only call a Ford angle request saturated when the EPS is not on track to catch it."""
catching_up = angle_error * steering_rate > 0.0
catching_up &= abs(angle_error) <= abs(steering_rate) * FORD_ANGLE_CATCH_UP_HORIZON
return abs(angle_error) > STEER_ANGLE_SATURATION_THRESHOLD and not catching_up
class LatControlAngle(LatControl):
def __init__(self, CP, CI, dt):
super().__init__(CP, CI, dt)
self.sat_check_min_speed = 5.
self.use_steer_limited_by_safety = CP.brand in ("tesla", "hyundai")
self.is_ascent = CP.carFingerprint == SUBARU_CAR.SUBARU_ASCENT_2023
self.is_ford = CP.brand == "ford"
self.measured_angle_last = None
self.measured_angle_rate = 0.0
def _update_measured_angle_rate(self, steering_angle: float, reset: bool) -> float:
if reset or self.measured_angle_last is None:
self.measured_angle_rate = 0.0
else:
raw_rate = (steering_angle - self.measured_angle_last) / self.dt
alpha = self.dt / (FORD_ANGLE_RATE_FILTER_TIME_CONSTANT + self.dt)
self.measured_angle_rate += alpha * (raw_rate - self.measured_angle_rate)
self.measured_angle_last = steering_angle
return self.measured_angle_rate
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay, calibrated_pose, model_data, starpilot_toggles):
angle_log = log.ControlsState.LateralAngleState.new_message()
@@ -49,13 +71,21 @@ class LatControlAngle(LatControl):
bool(getattr(CS, "steeringPressed", False)),
)
ford_angle_mode = self.is_ford and getattr(starpilot_toggles, "ford_lateral_mode", -1) == 2
measured_angle_rate = self._update_measured_angle_rate(
float(CS.steeringAngleDeg), not active or bool(CS.steeringPressed) or not ford_angle_mode)
if self.use_steer_limited_by_safety:
# these cars' carcontrollers calculate max lateral accel and jerk, so we can rely on carOutput for saturation
angle_control_saturated = steer_limited_by_safety
else:
# for cars which use a method of limiting torque such as a torque signal (Nissan and Toyota)
# or relying on EPS (Ford Q3), carOutput does not capture maxing out torque # TODO: this can be improved
angle_control_saturated = abs(angle_steers_des - CS.steeringAngleDeg) > STEER_ANGLE_SATURATION_THRESHOLD
angle_error = angle_steers_des - CS.steeringAngleDeg
if ford_angle_mode:
angle_control_saturated = _ford_angle_tracking_saturated(angle_error, measured_angle_rate)
else:
angle_control_saturated = abs(angle_error) > STEER_ANGLE_SATURATION_THRESHOLD
angle_log.saturated = bool(self._check_saturation(angle_control_saturated, CS, False, curvature_limited))
angle_log.steeringAngleDeg = float(CS.steeringAngleDeg)
angle_log.steeringAngleDesiredDeg = angle_steers_des
+4 -1
View File
@@ -108,6 +108,7 @@ class LatControlTorque(LatControl):
self.is_genesis_gv70 = CP.carFingerprint in GENESIS_GV70_CARS
self.is_palisade = CP.carFingerprint in PALISADE_CARS
self.is_prius = CP.carFingerprint in PRIUS_CARS
self.is_standard_prius = CP.carFingerprint == TOYOTA_CAR.TOYOTA_PRIUS
self.is_camry = CP.carFingerprint in CAMRY_CARS
self.is_rav4_tss2 = CP.carFingerprint in RAV4_TSS2_CARS
self.is_rav4_prime = CP.carFingerprint in RAV4_PRIME_CARS
@@ -610,7 +611,9 @@ class LatControlTorque(LatControl):
output_torque *= get_toyota_corolla_tss2_center_output_scale(setpoint, CS.vEgo)
elif prius_active:
output_torque *= prius_center_taper
output_torque *= get_prius_high_speed_output_taper_scale(setpoint, CS.vEgo)
prius_taper_max = (PRIUS_STANDARD_HIGH_SPEED_OUTPUT_TAPER_MAX if self.is_standard_prius
else PRIUS_HIGH_SPEED_OUTPUT_TAPER_MAX)
output_torque *= get_prius_high_speed_output_taper_scale(setpoint, CS.vEgo, prius_taper_max)
elif volt_standard_test_active:
output_torque *= volt_standard_center_taper
elif volt_plexy_test_active:
@@ -247,7 +247,7 @@ GENESIS_G70_FRICTION_JERK_DEADZONE_LAT = 0.30
GENESIS_G70_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.08
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED = 12.0
GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 3.5
GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.14
GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.16
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT = 0.30
GENESIS_G70_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.10
GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED = 18.0
@@ -277,7 +277,7 @@ GENESIS_G70_CURVE_UNWIND_LAT = 0.25
GENESIS_G70_CURVE_UNWIND_LAT_WIDTH = 0.12
GENESIS_G70_CURVE_UNWIND_JERK = 0.08
GENESIS_G70_CURVE_UNWIND_JERK_WIDTH = 0.08
GENESIS_G70_UNWIND_FF_REDUCTION_MAX = 0.28
GENESIS_G70_UNWIND_FF_REDUCTION_MAX = 0.32
GENESIS_G70_UNWIND_FF_OVERSHOOT = 0.12
GENESIS_G70_UNWIND_FF_OVERSHOOT_WIDTH = 0.12
GENESIS_G70_UNWIND_FF_JERK = 0.10
@@ -1035,6 +1035,7 @@ PRIUS_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.07
PRIUS_FRICTION_JERK_DEADZONE_SPEED = 18.0
PRIUS_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 2.2
PRIUS_HIGH_SPEED_OUTPUT_TAPER_MAX = 0.06
PRIUS_STANDARD_HIGH_SPEED_OUTPUT_TAPER_MAX = 0.10
PRIUS_HIGH_SPEED_OUTPUT_TAPER_LAT = 0.30
PRIUS_HIGH_SPEED_OUTPUT_TAPER_LAT_WIDTH = 0.35
PRIUS_HIGH_SPEED_OUTPUT_TAPER_SPEED = 22.0
@@ -1493,12 +1494,13 @@ def get_prius_friction_jerk_deadzone(v_ego: float, desired_lateral_accel: float)
return PRIUS_FRICTION_JERK_DEADZONE_MAX * speed_weight * center_weight
def get_prius_high_speed_output_taper_scale(desired_lateral_accel: float, v_ego: float) -> float:
def get_prius_high_speed_output_taper_scale(desired_lateral_accel: float, v_ego: float,
taper_max: float = PRIUS_HIGH_SPEED_OUTPUT_TAPER_MAX) -> float:
speed_weight = _prius_sigmoid((v_ego - PRIUS_HIGH_SPEED_OUTPUT_TAPER_SPEED) /
PRIUS_HIGH_SPEED_OUTPUT_TAPER_SPEED_WIDTH)
curve_weight = _prius_sigmoid((abs(desired_lateral_accel) - PRIUS_HIGH_SPEED_OUTPUT_TAPER_LAT) /
PRIUS_HIGH_SPEED_OUTPUT_TAPER_LAT_WIDTH)
return 1.0 - PRIUS_HIGH_SPEED_OUTPUT_TAPER_MAX * speed_weight * curve_weight
return 1.0 - taper_max * speed_weight * curve_weight
def get_camry_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
+5 -3
View File
@@ -113,7 +113,9 @@ def is_radarless_matched_follow_window(v_ego: float, lead_distance: float, v_lea
def get_tracked_lead_catchup_bias(v_ego: float, lead_distance: float, desired_gap: float, closing_speed: float,
v_cruise: float | None = None, y_rel: float | None = None) -> float:
v_cruise: float | None = None, y_rel: float | None = None,
min_headway_margin: float = TRACKED_LEAD_CATCHUP_BIAS_MIN_HEADWAY_MARGIN,
full_headway_margin: float = TRACKED_LEAD_CATCHUP_BIAS_FULL_HEADWAY_MARGIN) -> float:
gap_error = lead_distance - desired_gap
actual_hw = lead_distance / max(v_ego, 1e-3)
desired_hw = desired_gap / max(v_ego, 1e-3)
@@ -137,8 +139,8 @@ def get_tracked_lead_catchup_bias(v_ego: float, lead_distance: float, desired_ga
fade_end_margin = max(TRACKED_LEAD_CATCHUP_BIAS_MIN_FADE_END_MARGIN,
TRACKED_LEAD_CATCHUP_BIAS_ABSOLUTE_FADE_END - desired_hw)
entry_factor = _smoothstep(headway_margin,
TRACKED_LEAD_CATCHUP_BIAS_MIN_HEADWAY_MARGIN,
TRACKED_LEAD_CATCHUP_BIAS_FULL_HEADWAY_MARGIN)
min_headway_margin,
full_headway_margin)
exit_factor = 1.0 - _smoothstep(headway_margin, fade_start_margin, fade_end_margin)
closing_fade_end = max(2.5, 0.12 * v_ego)
@@ -924,7 +924,7 @@ class LongitudinalMpc:
personality=log.LongitudinalPersonality.standard, tracking_lead=True,
optional_far_lead_comfort=True, smooth_duplicate_vision=False,
stop_x=None, silverado_early_follow=False, modelV2=None,
lead_obstacle_bias=(0.0, 0.0)):
lead_obstacle_bias=(0.0, 0.0), tracked_lead_catchup_headway_margins=None):
v_ego = self.x0[1]
lead_one = radarstate.leadOne
lead_two = radarstate.leadTwo
@@ -972,6 +972,12 @@ class LongitudinalMpc:
if optional_far_lead_comfort and tracking_lead and lead_one.status:
desired_gap = desired_follow_distance(v_ego, lead_one.vLead, t_follow)
closing_speed = max(0.0, v_ego - lead_one.vLead)
catchup_kwargs = {}
if tracked_lead_catchup_headway_margins is not None:
catchup_kwargs = {
"min_headway_margin": tracked_lead_catchup_headway_margins[0],
"full_headway_margin": tracked_lead_catchup_headway_margins[1],
}
cruise_obstacle += get_tracked_lead_catchup_bias(
v_ego,
lead_one.dRel,
@@ -979,6 +985,7 @@ class LongitudinalMpc:
closing_speed,
v_cruise=v_cruise,
y_rel=float(getattr(lead_one, "yRel", 0.0)),
**catchup_kwargs,
)
if optional_far_lead_comfort:
cruise_obstacle += self.get_identical_radar_duplicate_cruise_bias(lead_one, lead_two, v_ego, t_follow)
+151 -16
View File
@@ -7,8 +7,10 @@ from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
from openpilot.starpilot.controls.lib.starpilot_vcruise import FT_TO_M, OFFSET_FT_MAX, OFFSET_FT_MIN
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
@@ -37,8 +39,11 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
get_honda_crv_5g_stopped_lead_obstacle_bias,
get_honda_crv_5g_low_speed_stopped_lead_cap,
allow_honda_crv_5g_vision_gap_settle,
get_honda_crv_5g_early_radar_follow_cap,
get_standstill_gap_settle_max_extra_gap,
get_standstill_stopped_lead_guard_distance_margin,
get_standstill_stopped_lead_guard_max_lead_speed,
get_tracked_lead_catchup_headway_margins,
)
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
@@ -139,6 +144,26 @@ VISION_LEAD_APPROACH_BRAKING_DEFICIT_MIN = 0.75
VISION_LEAD_APPROACH_BRAKING_MIN_LEAD_BRAKE = 0.45
VISION_LEAD_APPROACH_BRAKING_FULL_LEAD_BRAKE = 1.20
PLANNER_SAFETY_WARNING_INTERVAL = 5.0
HEM_STATUS_LOG_INTERVAL = 10.0
HEM_AUTH_PUB_INTERVAL = 0.5
def _hem_log_timestamp() -> str:
"""Wall-clock timestamp with millisecond precision for the [HEM] live log."""
from datetime import datetime
now = datetime.now()
return now.strftime("%H:%M:%S") + f".{now.microsecond // 1000:03d}"
def _hem_format_diag_value(value):
"""Compact, deterministic formatting for the per-frame HEM diagnostic dump."""
if isinstance(value, (bool, np.bool_)):
return "1" if value else "0"
if isinstance(value, float):
return f"{value:.4f}"
if isinstance(value, (int, np.integer)):
return str(int(value))
return str(value)
VISION_LEAD_APPROACH_BRAKING_FLOOR_MIN_DECEL = 1.30
VISION_LEAD_APPROACH_BRAKING_FLOOR_MAX_DECEL = 1.75
VISION_LEAD_APPROACH_CONFIRM_TIME = 0.25
@@ -560,6 +585,7 @@ class LongitudinalPlanner:
self.output_should_stop = False
self.far_follow_brake_slew_rate, self.far_follow_release_slew_rate = get_far_follow_output_slew_rates(CP)
self.untracked_slow_lead_decel_scale = get_untracked_slow_lead_decel_scale(CP)
self.tracked_lead_catchup_headway_margins = get_tracked_lead_catchup_headway_margins(CP)
self.far_follow_output_slew_active = False
self.model_launch_armed = False
self.model_launch_stop_seen = False
@@ -607,6 +633,11 @@ class LongitudinalPlanner:
self.duplicate_vision_comfort_lead_source = None
self.prev_experimental_mode = None
self.experimental_release_accel_until = 0.0
self.hybrid_controller = HybridExperimentalMode()
self._hem_status_log_t = 0.0
self._hem_logged_active = False
self._hem_auth_pub_t = 0.0
self._hem_params_memory = None
if self.is_preap:
try:
@@ -1638,13 +1669,16 @@ class LongitudinalPlanner:
lead_speed = max(float(getattr(lead, "vLead", 0.0)), 0.0)
lead_delta = lead_speed - float(v_ego)
max_lead_speed = get_standstill_stopped_lead_guard_max_lead_speed(
self.CP, STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_SPEED,
)
max_distance = max(
STANDSTILL_STOPPED_LEAD_GUARD_MIN_DISTANCE,
float(stop_distance) + get_standstill_stopped_lead_guard_distance_margin(self.CP),
)
if (
float(getattr(lead, "dRel", float("inf"))) > max_distance or
lead_speed > STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_SPEED or
lead_speed > max_lead_speed or
lead_delta > STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_DELTA
):
return None
@@ -1652,7 +1686,7 @@ class LongitudinalPlanner:
distance_factor = float(np.clip((max_distance - float(lead.dRel)) /
max(max_distance - STANDSTILL_STOPPED_LEAD_GUARD_MIN_DISTANCE, 0.1),
0.0, 1.0))
speed_factor = float(np.clip(lead_speed / max(STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_SPEED, 0.1), 0.0, 1.0))
speed_factor = float(np.clip(lead_speed / max(max_lead_speed, 0.1), 0.0, 1.0))
delta_factor = float(np.clip((STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_DELTA - lead_delta) /
max(STANDSTILL_STOPPED_LEAD_GUARD_MAX_LEAD_DELTA, 0.1),
0.0, 1.0))
@@ -1903,6 +1937,36 @@ class LongitudinalPlanner:
floor = min(LC_MERGE_ACCEL_BIAS, cruise_cap)
return floor
def _log_hem_status(self, now_t, active, v_ego, a_chill, a_exp, a_fused):
hc = self.hybrid_controller
hc.record_diag = True
ts = _hem_log_timestamp()
if active != self._hem_logged_active:
self._hem_logged_active = active
self._hem_status_log_t = 0.0
print(f"[HEM] {ts} mode {'ON' if active else 'OFF'}")
if not active:
return
# Rich per-frame dump of every HEM decision variable so a missed stop can be
# diagnosed to the exact frame and signal (see hybrid_experimental_mode diag).
d = hc.diag
if d:
print(f"[HEM] {ts} " + " ".join(f"{k}={_hem_format_diag_value(v)}" for k, v in d.items()))
def _publish_hem_status(self, now_t):
if self._hem_params_memory is None:
try:
self._hem_params_memory = Params(memory=True)
except Exception:
return
if now_t - self._hem_auth_pub_t < HEM_AUTH_PUB_INTERVAL:
return
self._hem_auth_pub_t = now_t
try:
self._hem_params_memory.put_bool("HEMExpDominant", bool(self.hybrid_controller.last_exp_dominant))
except Exception:
pass
def update(self, sm, starpilot_toggles):
if self.is_preap:
self._preap_param_frame += 1
@@ -1955,6 +2019,7 @@ class LongitudinalPlanner:
self.a_desired = np.clip(sm['carState'].aEgo, accel_limits[0], accel_limits[1])
self.model_allow_throttle = True
self.model_allow_throttle_transition_t = 0.0
self.hybrid_controller.reset(float(self.a_desired))
# Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
@@ -2261,7 +2326,8 @@ class LongitudinalPlanner:
stop_x=force_stop_x,
silverado_early_follow=early_truck_follow,
modelV2=sm['modelV2'],
lead_obstacle_bias=stopped_lead_obstacle_bias)
lead_obstacle_bias=stopped_lead_obstacle_bias,
tracked_lead_catchup_headway_margins=self.tracked_lead_catchup_headway_margins)
self.a_desired_trajectory_full = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
@@ -2315,26 +2381,31 @@ class LongitudinalPlanner:
model_launch_accel = self.get_model_launch_accel(model_launch_v, model_launch_a, action_t, scene_v_ego)
if classic_model:
output_a_target, output_should_stop = get_accel_from_plan_classic(
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan_classic(
self.CP, self.v_desired_trajectory, self.a_desired_trajectory, starpilot_toggles.vEgoStopping)
elif tinygrad_model:
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(
self.v_desired_trajectory, self.a_desired_trajectory,
action_t=action_t, vEgoStopping=starpilot_toggles.vEgoStopping)
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc' or self.generation == 'v9':
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
else:
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
output_should_stop = output_should_stop_e2e or output_should_stop_mpc
else:
output_a_target, output_should_stop = get_accel_from_plan(
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(
self.v_desired_trajectory, self.a_desired_trajectory,
action_t=action_t, vEgoStopping=starpilot_toggles.vEgoStopping)
if bool(getattr(starpilot_toggles, "hybrid_experimental_mode", False)):
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
elif tinygrad_model and self.mode != 'acc' and self.generation != 'v9':
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
output_should_stop = output_should_stop_e2e or output_should_stop_mpc
self._log_hem_status(now_t, False, scene_v_ego, output_a_target_mpc, output_a_target_e2e, output_a_target)
else:
output_a_target = output_a_target_mpc
output_should_stop = output_should_stop_mpc
self._log_hem_status(now_t, False, scene_v_ego, output_a_target_mpc, float('nan'), output_a_target)
comfort_output_accel_min = get_vehicle_min_accel(self.CP, v_ego) if experimental_mlsim else accel_limits_turns[0]
vision_cap_accel_min = min(comfort_output_accel_min, get_vehicle_min_accel(self.CP, v_ego))
output_accel_min = comfort_output_accel_min
@@ -2351,6 +2422,22 @@ class LongitudinalPlanner:
if approach_lift_caps:
raw_approach_lift_cap = min(approach_lift_caps)
if not experimental_mode:
early_radar_caps = [
cap for cap in (
get_honda_crv_5g_early_radar_follow_cap(
self.CP, self.lead_one, v_ego, vision_cap_accel_min,
),
get_honda_crv_5g_early_radar_follow_cap(
self.CP, self.lead_two, v_ego, vision_cap_accel_min,
),
) if cap is not None
]
if early_radar_caps:
early_radar_cap = min(early_radar_caps)
self.a_desired = min(self.a_desired, early_radar_cap)
output_a_target = min(output_a_target, early_radar_cap)
pretracking_vision_caps = []
for lead in (self.lead_one, self.lead_two):
if lead.status and not bool(getattr(lead, "radar", False)):
@@ -2951,8 +3038,56 @@ class LongitudinalPlanner:
if force_slow_decel and scene_v_ego > 0.1:
output_a_target = min(output_a_target, FORCE_DECEL_MIN_ACCEL)
self.output_a_target = output_a_target
self.output_should_stop = bool(output_should_stop or vision_low_speed_stop_active)
a_chill_final = output_a_target
should_stop_chill = bool(output_should_stop or vision_low_speed_stop_active)
if bool(getattr(starpilot_toggles, "hybrid_experimental_mode", False)):
self.hybrid_controller.set_tuning(
getattr(starpilot_toggles, "hybrid_exp_bias", 0.0),
getattr(starpilot_toggles, "hybrid_vision_brake_sensitivity", 1.0),
)
a_exp_raw = float(sm['modelV2'].action.desiredAcceleration)
should_stop_exp = bool(sm['modelV2'].action.shouldStop)
# Pass the active lead that MPC is tracking (leadTwo when source == "lead1")
# so HEM is never blind to the radar lead actually being followed.
active_lead = self.lead_two if self.mpc.source == "lead1" else self.lead_one
a_fused, should_stop_fused = self.hybrid_controller.update(
v_ego=scene_v_ego,
v_cruise=v_cruise,
lead_one=active_lead,
model_v2=sm['modelV2'],
a_chill=a_chill_final,
a_exp=a_exp_raw,
should_stop_exp=should_stop_exp,
should_stop_chill=should_stop_chill,
gas_pressed=bool(getattr(sm['carState'], 'gasPressed', False)),
)
# HEM output can never exceed the physical
# vehicle acceleration envelope (same bounds as the non-hybrid path) or a
# per-frame jerk slew from the previously commanded target, regardless of
# tuning bias.
a_fused = float(np.clip(a_fused, output_accel_min, output_accel_max))
if not np.isfinite(a_fused):
a_fused = float(a_chill_final)
max_jerk_accel = float(getattr(sm['starpilotPlan'], 'accelerationJerk', 1.0)) * 3.0
max_jerk_brake = 4.0
max_delta_up = max_jerk_accel * self.dt
max_delta_down = max_jerk_brake * self.dt
prev_target = float(prev_output_a_target)
a_fused = float(np.clip(a_fused, prev_target - max_delta_down, prev_target + max_delta_up))
self.output_a_target = a_fused
self.output_should_stop = should_stop_fused
self._log_hem_status(now_t, True, scene_v_ego, a_chill_final, a_exp_raw, a_fused)
self._publish_hem_status(now_t)
else:
self.output_a_target = a_chill_final
self.output_should_stop = should_stop_chill
def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan')

Some files were not shown because too many files have changed in this diff Show More