From 334f32f5d8f70035e0962e729b842fa8448c37f7 Mon Sep 17 00:00:00 2001
From: firestar5683 <168790843+firestar5683@users.noreply.github.com>
Date: Thu, 10 Sep 2026 20:49:06 -0500
Subject: [PATCH] Cabo
---
.../opendbc/car/hyundai/carcontroller.py | 6 +-
opendbc_repo/opendbc/car/hyundai/interface.py | 2 +-
.../opendbc/car/hyundai/tests/test_hyundai.py | 58 +++++++++++---
opendbc_repo/opendbc/car/hyundai/values.py | 7 +-
.../opendbc/car/subaru/carcontroller.py | 18 ++++-
.../opendbc/car/subaru/tests/test_subaru.py | 75 ++++++++++++++++++-
selfdrive/controls/controlsd.py | 7 --
selfdrive/controls/lib/drive_helpers.py | 5 +-
selfdrive/controls/lib/latcontrol_angle.py | 46 +++++++++++-
selfdrive/controls/lib/latcontrol_torque.py | 4 +
.../controls/lib/latcontrol_vehicle_tunes.py | 37 +++++++++
.../controls/tests/test_drive_helpers.py | 10 ++-
selfdrive/controls/tests/test_latcontrol.py | 53 ++++++++++++-
starpilot/car/ford/lateral.py | 32 ++++----
starpilot/car/ford/tests/test_lateral.py | 36 +++++++--
.../mobile/js/components/GalaxyModal.js | 54 ++++++++++++-
.../js/components/LateralTuningPanel.js | 23 +++++-
.../the_galaxy/tests/test_ui_vue_frontend.py | 6 ++
18 files changed, 418 insertions(+), 61 deletions(-)
diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py
index 533e52cdc3..e0cb101ae9 100644
--- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py
+++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py
@@ -860,13 +860,13 @@ class CarController(CarControllerBase):
can_sends = []
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
- longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
- lfa_status_cars = (
+ persistent_lfa_status_cars = (
CAR.HYUNDAI_IONIQ_6,
+ CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
CAR.KIA_EV6,
)
lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \
- if self.CP.carFingerprint in lfa_status_cars else longitudinal_active
+ if self.CP.carFingerprint in persistent_lfa_status_cars else self.long_active_ecu
lka_steering_long = lka_steering and lfa_longitudinal_active
ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not lka_steering
use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \
diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py
index 470bb339e8..a17b841f6b 100644
--- a/opendbc_repo/opendbc/car/hyundai/interface.py
+++ b/opendbc_repo/opendbc/car/hyundai/interface.py
@@ -29,7 +29,7 @@ Ecu = structs.CarParams.Ecu
def get_communication_control_request(car_fingerprint):
- if car_fingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR and car_fingerprint != CAR.HYUNDAI_IONIQ_5:
+ if car_fingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR:
return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX,
uds.MESSAGE_TYPE.NORMAL])
diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py
index aa075c6546..85fcd065a7 100644
--- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py
+++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py
@@ -139,9 +139,13 @@ class TestHyundaiFingerprint:
assert get_communication_control_request(CAR.KIA_EV6) == stock_request
assert CAR.HYUNDAI_IONIQ_5 in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
- assert CAR.HYUNDAI_IONIQ_5 in CANFD_RADAR_ECU_KEEPALIVE_CAR
+ assert CAR.HYUNDAI_IONIQ_5 not in CANFD_RADAR_ECU_KEEPALIVE_CAR
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_5) == stock_request
+ assert CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
+ assert CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN not in CANFD_RADAR_ECU_KEEPALIVE_CAR
+ assert get_communication_control_request(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) == stock_request
+
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_6) == radar_keepalive_request
def test_carnival_hev_low_speed_torque_rate_limits(self):
@@ -2594,7 +2598,7 @@ class TestHyundaiFingerprint:
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2)
steering_names = [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in inactive_msgs
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
- assert steering_names == [("LKAS", can_bus.ACAN)]
+ assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
controller.frame = 1
cc.longActive = True
@@ -2604,11 +2608,18 @@ class TestHyundaiFingerprint:
if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")]
assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)]
- @pytest.mark.parametrize("car", [CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6])
- def test_egmp_keeps_lfa_status_when_longitudinal_is_inactive(self, car):
+ @pytest.mark.parametrize(("car", "powertrain_flag"), [
+ (CAR.HYUNDAI_IONIQ_5, HyundaiFlags.EV),
+ (CAR.HYUNDAI_IONIQ_6, HyundaiFlags.EV),
+ (CAR.KIA_EV6, HyundaiFlags.EV),
+ (CAR.KIA_CARNIVAL_2025, 0),
+ (CAR.KIA_CARNIVAL_HEV_4TH_GEN, HyundaiFlags.HYBRID),
+ (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, HyundaiFlags.EV),
+ ])
+ def test_hda2_keeps_lfa_status_when_longitudinal_is_inactive(self, car, powertrain_flag):
CP = CarParams.new_message()
CP.carFingerprint = car
- CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
+ CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | powertrain_flag)
CP.openpilotLongitudinalControl = True
controller = CarController(DBC[CP.carFingerprint], CP)
@@ -2624,10 +2635,39 @@ class TestHyundaiFingerprint:
)
controller.frame = 1
- for controller.long_active_ecu in (False, True):
- msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False,
- cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
- assert any(addr == 0x12A for addr, _, _ in msgs)
+ controller.long_active_ecu = True
+ msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False,
+ cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
+ assert any(addr == 0x12A for addr, _, _ in msgs)
+
+ @pytest.mark.parametrize("car", [
+ CAR.HYUNDAI_IONIQ_6,
+ CAR.KIA_EV6,
+ CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
+ ])
+ def test_egmp_persistent_lfa_status_survives_ecu_fallback_state(self, car):
+ CP = CarParams.new_message()
+ CP.carFingerprint = car
+ CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING)
+ CP.openpilotLongitudinalControl = True
+
+ controller = CarController(DBC[CP.carFingerprint], CP)
+ controller.frame = 1
+ controller.long_active_ecu = False
+ cc = SimpleNamespace(
+ enabled=False, latActive=False, longActive=False,
+ actuators=SimpleNamespace(longControlState=LongCtrlState.off),
+ leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace(),
+ )
+ cs = SimpleNamespace(
+ stock_lfa_msg=None, stock_lkas_msg=None,
+ left_blindspot_from_radar=False, right_blindspot_from_radar=False,
+ out=SimpleNamespace(gearShifter=structs.CarState.GearShifter.park),
+ )
+
+ msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False,
+ cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
+ assert any(addr == 0x12A for addr, _, _ in msgs)
def test_gv70_electrified_longitudinal_uses_hda2_scc_contract(self):
CP = CarParams.new_message()
diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py
index b85531477f..d7cfa094c7 100644
--- a/opendbc_repo/opendbc/car/hyundai/values.py
+++ b/opendbc_repo/opendbc/car/hyundai/values.py
@@ -1220,7 +1220,12 @@ CANFD_RADAR_LIVE_LONGITUDINAL_CAR = {
CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.KIA_EV9, CAR.GENESIS_GV60_EV_1ST_GEN,
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
}
-CANFD_RADAR_ECU_KEEPALIVE_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR - {CAR.KIA_EV6}
+CANFD_RADAR_ECU_KEEPALIVE_CAR = {
+ CAR.HYUNDAI_IONIQ_5_PE,
+ CAR.HYUNDAI_IONIQ_6,
+ CAR.KIA_EV9,
+ CAR.GENESIS_GV60_EV_1ST_GEN,
+}
RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | {
CAR.HYUNDAI_IONIQ,
CAR.HYUNDAI_KONA_EV_2022,
diff --git a/opendbc_repo/opendbc/car/subaru/carcontroller.py b/opendbc_repo/opendbc/car/subaru/carcontroller.py
index abee950955..f721b0e9a6 100644
--- a/opendbc_repo/opendbc/car/subaru/carcontroller.py
+++ b/opendbc_repo/opendbc/car/subaru/carcontroller.py
@@ -20,6 +20,7 @@ _ANGLE_OVERRIDE_CONFIRM_FRAMES = 2
_ANGLE_REENGAGE_MAX_STEER_RATE = 2.0
_ANGLE_MADS_MIN_SPEED = 0.44704
_ANGLE_MADS_MAX_STEER_ANGLE = 120.0
+_ASCENT_AOL_ARM_FRAMES = 30
_STOP_START_STARTUP_DELAY_FRAMES = 100
# StarPilot's first populated toggle message can arrive several seconds after
# the car controller starts while fingerprinting and settings settle.
@@ -44,6 +45,7 @@ class CarController(CarControllerBase):
self.angle_override_confirm_frames = 0
self.angle_lkas_active = False
self.angle_handoff_active = False
+ self.ascent_aol_arm_frames = 0
self.cruise_button_prev = 0
self.steer_rate_counter = 0
@@ -170,6 +172,14 @@ class CarController(CarControllerBase):
return self.driver_override
+ def _ascent_aol_ready(self, ready):
+ if not ready:
+ self.ascent_aol_arm_frames = 0
+ return False
+
+ self.ascent_aol_arm_frames = min(self.ascent_aol_arm_frames + 1, _ASCENT_AOL_ARM_FRAMES)
+ return self.ascent_aol_arm_frames >= _ASCENT_AOL_ARM_FRAMES
+
def lateral_angle(self, CC, CS):
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
mads_only = CC.latActive and not CC.enabled
@@ -199,6 +209,12 @@ class CarController(CarControllerBase):
abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
+ if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
+ if mads_only:
+ cruise_available = getattr(getattr(CS.out, "cruiseState", None), "available", True)
+ lkas_available = self._ascent_aol_ready(lkas_available and cruise_available)
+ else:
+ self.ascent_aol_arm_frames = _ASCENT_AOL_ARM_FRAMES if lkas_available else 0
manual_handoff = self._angle_manual_handoff(
CS, lkas_available, use_steering_pressed=self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023,
@@ -284,7 +300,7 @@ class CarController(CarControllerBase):
return subarucan.create_steering_control(self.packer, apply_torque, apply_steer_req)
def _lkas_status_active(self, CC):
- if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
+ if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
return self.angle_lkas_active
return CC.latActive
diff --git a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py
index 1865c6b446..b9ccce9c95 100644
--- a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py
+++ b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py
@@ -8,7 +8,7 @@ from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, fw_versions, gen_empty_fingerprint, structs
from opendbc.car.fw_query_definitions import StdQueries
from opendbc.car.subaru import subarucan
-from opendbc.car.subaru.carcontroller import CarController
+from opendbc.car.subaru.carcontroller import CarController, _ASCENT_AOL_ARM_FRAMES
from opendbc.car.subaru.carstate import CarState
from opendbc.car.subaru.fingerprints import FW_VERSIONS
from opendbc.car.fw_versions import match_fw_to_car
@@ -692,6 +692,7 @@ def test_ascent_angle_controller_waits_for_parking_lot_safety_envelope():
steeringRateDeg=96.0,
steeringTorque=7.0,
steeringPressed=False,
+ cruiseState=SimpleNamespace(available=True),
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
))
@@ -704,18 +705,74 @@ def test_ascent_angle_controller_waits_for_parking_lot_safety_envelope():
CS.out.steeringAngleDeg = -100.0
CS.out.steeringRateDeg = 0.0
+ for frame in range(2, _ASCENT_AOL_ARM_FRAMES + 1):
+ msg = controller.lateral_angle(CC, CS)
+ parser.update([(frame, [msg])])
+ assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
+
msg = controller.lateral_angle(CC, CS)
- parser.update([(2, [msg])])
+ parser.update([(_ASCENT_AOL_ARM_FRAMES + 1, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
CS.out.gearShifter = structs.CarState.GearShifter.reverse
msg = controller.lateral_angle(CC, CS)
- parser.update([(12, [msg])])
+ parser.update([(_ASCENT_AOL_ARM_FRAMES + 2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
-def test_lkas_hud_state_uses_outback_angle_request_state():
+def test_ascent_aol_does_not_arm_before_cruise_main_is_available():
+ CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
+ controller = CarController({}, CP)
+ CC = SimpleNamespace(enabled=False, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=5.0))
+ CS = SimpleNamespace(out=SimpleNamespace(
+ vEgoRaw=10.0,
+ steeringAngleDeg=0.0,
+ steeringRateDeg=0.0,
+ steeringTorque=0.0,
+ steeringPressed=False,
+ cruiseState=SimpleNamespace(available=False),
+ gearShifter=structs.CarState.GearShifter.drive,
+ standstill=False,
+ ))
+ parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
+
+ for frame in range(_ASCENT_AOL_ARM_FRAMES):
+ msg = controller.lateral_angle(CC, CS)
+ parser.update([(frame + 1, [msg])])
+
+ assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
+ assert controller.ascent_aol_arm_frames == 0
+
+ CS.out.cruiseState.available = True
+ msg = controller.lateral_angle(CC, CS)
+ parser.update([(_ASCENT_AOL_ARM_FRAMES + 1, [msg])])
+ assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
+ assert controller.ascent_aol_arm_frames == 1
+
+
+def test_ascent_angle_controller_does_not_delay_normal_engagement():
+ CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
+ controller = CarController({}, CP)
+ CC = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=5.0))
+ CS = SimpleNamespace(out=SimpleNamespace(
+ vEgoRaw=10.0,
+ steeringAngleDeg=0.0,
+ steeringRateDeg=0.0,
+ steeringTorque=0.0,
+ steeringPressed=False,
+ 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"] == 1
+
+
+def test_lkas_hud_state_uses_angle_request_state():
update_source = inspect.getsource(CarController.update)
assert "create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, self._lkas_status_active(CC)" in update_source
@@ -765,6 +822,16 @@ def test_outback_manual_steering_releases_angle_request_before_lkas_fault():
assert not controller._lkas_status_active(CC)
+def test_ascent_hud_waits_for_angle_request():
+ CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
+ controller = CarController({}, CP)
+ CC = SimpleNamespace(latActive=True)
+
+ assert not controller._lkas_status_active(CC)
+ controller.angle_lkas_active = True
+ assert controller._lkas_status_active(CC)
+
+
def test_other_angle_cars_keep_lateral_status_behavior():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
controller = CarController({}, CP)
diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py
index 3174fac494..5bb3d2b4a5 100644
--- a/selfdrive/controls/controlsd.py
+++ b/selfdrive/controls/controlsd.py
@@ -404,7 +404,6 @@ class Controls:
self.turn_blinker_swept = 0.0
self.twitch_guard_remaining = 0.0
self.kona_non_scc_lateral_active = False
- self.kona_non_scc_lateral_faulted = False
self.elantra_hev_2024_lateral_faulted = False
self.elantra_hev_2024_previous_cruise_enabled = False
@@ -504,11 +503,6 @@ class Controls:
standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, 0.3) or CS.standstill
if self.CP.carFingerprint == HYUNDAI_CAR.HYUNDAI_KONA_NON_SCC:
always_on_lateral_enabled = self.sm['starpilotCarState'].alwaysOnLateralEnabled
- lateral_requested = (CC.enabled and self.sm['selfdriveState'].active) or always_on_lateral_enabled
- if not lateral_requested:
- self.kona_non_scc_lateral_faulted = False
- elif CS.steerFaultTemporary:
- self.kona_non_scc_lateral_faulted = True
CC.latActive = get_kona_non_scc_lateral_active(
CC.enabled, self.sm['selfdriveState'].active,
always_on_lateral_enabled,
@@ -516,7 +510,6 @@ class Controls:
standstill, self.CP.steerAtStandstill,
self.sm['starpilotPlan'].lateralCheck,
CS.steeringPressed, self.kona_non_scc_lateral_active,
- self.kona_non_scc_lateral_faulted,
)
self.kona_non_scc_lateral_active = CC.latActive
elif self.CP.carFingerprint == HYUNDAI_CAR.HYUNDAI_ELANTRA_HEV_2024:
diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py
index 7a6d5b8550..8a2a1881f8 100644
--- a/selfdrive/controls/lib/drive_helpers.py
+++ b/selfdrive/controls/lib/drive_helpers.py
@@ -89,13 +89,12 @@ def update_lateral_fault_latch(previous_latched: bool, lateral_requested: bool,
def get_kona_non_scc_lateral_active(enabled: bool, active: bool, always_on_lateral_enabled: bool,
steer_fault_temporary: bool, steer_fault_permanent: bool,
standstill: bool, steer_at_standstill: bool, lateral_check: bool,
- steering_pressed: bool, previous_lateral_active: bool,
- steer_fault_latched: bool = False) -> bool:
+ steering_pressed: bool, previous_lateral_active: bool) -> bool:
"""Avoid the Kona EPS torque fault when AOL is enabled over driver steering input."""
lateral_active = get_lateral_active(enabled, active, always_on_lateral_enabled,
steer_fault_temporary, steer_fault_permanent,
standstill, steer_at_standstill, lateral_check)
- if not lateral_active or steer_fault_latched:
+ if not lateral_active:
return False
aol_rising_edge = always_on_lateral_enabled and not enabled and not previous_lateral_active
diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py
index 6062b4780d..ffd7d3d122 100644
--- a/selfdrive/controls/lib/latcontrol_angle.py
+++ b/selfdrive/controls/lib/latcontrol_angle.py
@@ -9,26 +9,54 @@ STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees
_ASCENT_ANGLE_TRACKING_GAIN = 0.25
_ASCENT_ANGLE_TRACKING_MAX_CORRECTION = 8.0
-_ASCENT_ANGLE_TRACKING_MIN_SPEED = 5.0
+_ASCENT_ANGLE_TRACKING_MIN_SPEED = 9.0
+_ASCENT_ANGLE_TRACKING_FULL_SPEED = 15.0
+_ASCENT_ANGLE_TRACKING_TURN_START = 15.0
+_ASCENT_ANGLE_TRACKING_TURN_FULL = 35.0
+_ASCENT_LOW_SPEED_FILTER_MAX_SPEED = 10.0
+_ASCENT_LOW_SPEED_FILTER_CENTER_ANGLE = 35.0
+_ASCENT_LOW_SPEED_FILTER_TIME_CONSTANT = 0.18
+
+
+def _clipped_weight(value: float, start: float, end: float) -> float:
+ return max(0.0, min(1.0, (value - start) / (end - start)))
def _ascent_angle_tracking_target(target_angle: float, steering_angle: float,
v_ego: float, steering_pressed: bool) -> float:
- if steering_pressed or v_ego < _ASCENT_ANGLE_TRACKING_MIN_SPEED:
+ if steering_pressed:
return target_angle
- correction = (target_angle - steering_angle) * _ASCENT_ANGLE_TRACKING_GAIN
+ speed_weight = _clipped_weight(v_ego, _ASCENT_ANGLE_TRACKING_MIN_SPEED, _ASCENT_ANGLE_TRACKING_FULL_SPEED)
+ turn_weight = _clipped_weight(abs(target_angle), _ASCENT_ANGLE_TRACKING_TURN_START, _ASCENT_ANGLE_TRACKING_TURN_FULL)
+ correction = (target_angle - steering_angle) * _ASCENT_ANGLE_TRACKING_GAIN * max(speed_weight, turn_weight)
correction = max(-_ASCENT_ANGLE_TRACKING_MAX_CORRECTION,
min(_ASCENT_ANGLE_TRACKING_MAX_CORRECTION, correction))
return target_angle + correction
+def _ascent_low_speed_angle_target(target_angle: float, previous_target: float,
+ v_ego: float, steering_pressed: bool, dt: float) -> float:
+ if steering_pressed:
+ return target_angle
+
+ speed_weight = 1.0 - _clipped_weight(v_ego, 0.0, _ASCENT_LOW_SPEED_FILTER_MAX_SPEED)
+ center_weight = 1.0 - _clipped_weight(abs(target_angle), 0.0, _ASCENT_LOW_SPEED_FILTER_CENTER_ANGLE)
+ time_constant = _ASCENT_LOW_SPEED_FILTER_TIME_CONSTANT * speed_weight * center_weight
+ if time_constant <= 0.0:
+ return target_angle
+
+ alpha = dt / (time_constant + dt)
+ return previous_target + alpha * (target_angle - previous_target)
+
+
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.ascent_angle_target = None
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()
@@ -36,14 +64,24 @@ class LatControlAngle(LatControl):
if not active:
angle_log.active = False
angle_steers_des = float(CS.steeringAngleDeg)
+ self.ascent_angle_target = angle_steers_des
else:
angle_log.active = True
angle_steers_des = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll))
angle_steers_des += params.angleOffsetDeg
if self.is_ascent:
- angle_steers_des = _ascent_angle_tracking_target(
+ if self.ascent_angle_target is None:
+ self.ascent_angle_target = float(CS.steeringAngleDeg)
+ self.ascent_angle_target = _ascent_low_speed_angle_target(
angle_steers_des,
+ self.ascent_angle_target,
+ CS.vEgo,
+ bool(getattr(CS, "steeringPressed", False)),
+ self.dt,
+ )
+ angle_steers_des = _ascent_angle_tracking_target(
+ self.ascent_angle_target,
CS.steeringAngleDeg,
CS.vEgo,
bool(getattr(CS, "steeringPressed", False)),
diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py
index 8a98b7b7d9..e8f58002b7 100644
--- a/selfdrive/controls/lib/latcontrol_torque.py
+++ b/selfdrive/controls/lib/latcontrol_torque.py
@@ -667,6 +667,10 @@ class LatControlTorque(LatControl):
output_torque *= get_genesis_gv70_reversal_output_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
+ if not CS.steeringPressed:
+ output_torque = get_genesis_gv70_stabilized_output(
+ output_torque, self.prev_output_torque, setpoint, desired_lateral_jerk, CS.vEgo, self.dt,
+ )
elif sonata_hybrid_active:
output_torque *= sonata_hybrid_center_taper
output_torque *= sonata_hybrid_center_output_taper
diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py
index 8029dfe2af..5c53209854 100644
--- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py
+++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py
@@ -260,6 +260,17 @@ GENESIS_GV70_LOW_SPEED_CENTER_OVERSHOOT_CENTER_LAT_WIDTH = 0.08
GENESIS_GV70_LOW_SPEED_CENTER_OVERSHOOT_MIN = 0.06
GENESIS_GV70_LOW_SPEED_CENTER_OVERSHOOT_LAT = 0.12
GENESIS_GV70_LOW_SPEED_CENTER_OVERSHOOT_LAT_WIDTH = 0.10
+GENESIS_GV70_OUTPUT_SMOOTHING_SPEED = 38.0 * CV.MPH_TO_MS
+GENESIS_GV70_OUTPUT_SMOOTHING_SPEED_WIDTH = 6.0 * CV.MPH_TO_MS
+GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_LAT = 0.48
+GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_LAT_WIDTH = 0.16
+GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_RC = 0.42
+GENESIS_GV70_OUTPUT_SMOOTHING_CURVE_RC = 0.14
+GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_RC = 0.12
+GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_PHASE = 0.04
+GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_PHASE_WIDTH = 0.08
+GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_LAT = 0.55
+GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC = 0.065
GENESIS_G70_FRICTION_THRESHOLD_GAIN = 0.10
GENESIS_G70_FRICTION_SPEED_ONSET = 10.0
@@ -3230,6 +3241,32 @@ def get_genesis_gv70_low_speed_center_overshoot_scale(setpoint: float, measured_
speed_weight * speed_cutoff)
+def get_genesis_gv70_stabilized_output(output_torque: float, prev_output_torque: float,
+ desired_lateral_accel: float, desired_lateral_jerk: float,
+ v_ego: float, dt: float) -> float:
+ speed_weight = _sigmoid((max(v_ego, 0.0) - GENESIS_GV70_OUTPUT_SMOOTHING_SPEED) /
+ GENESIS_GV70_OUTPUT_SMOOTHING_SPEED_WIDTH)
+ center_weight = _sigmoid((GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_LAT - abs(desired_lateral_accel)) /
+ GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_LAT_WIDTH)
+ curve_weight = 1.0 - center_weight
+ response_time = (GENESIS_GV70_OUTPUT_SMOOTHING_CURVE_RC * curve_weight +
+ GENESIS_GV70_OUTPUT_SMOOTHING_CENTER_RC * center_weight)
+
+ unwind_phase = -desired_lateral_accel * desired_lateral_jerk
+ unwind_weight = _sigmoid((unwind_phase - GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_PHASE) /
+ GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_PHASE_WIDTH)
+ response_time += GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_RC * curve_weight * unwind_weight
+
+ changing_direction = (abs(desired_lateral_accel) >= GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_LAT and
+ prev_output_torque * desired_lateral_accel <= 0.0)
+ if changing_direction:
+ response_time = min(response_time, GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC)
+
+ output_alpha = dt / (max(response_time, 0.0) + dt)
+ smoothed_output = prev_output_torque + output_alpha * (output_torque - prev_output_torque)
+ return float(output_torque + speed_weight * (smoothed_output - output_torque))
+
+
def get_genesis_g70_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
desired_lateral_jerk: float = 0.0) -> float:
base_threshold = get_standard_friction_threshold(v_ego)
diff --git a/selfdrive/controls/tests/test_drive_helpers.py b/selfdrive/controls/tests/test_drive_helpers.py
index 02eaf96239..8ec54a9bdb 100644
--- a/selfdrive/controls/tests/test_drive_helpers.py
+++ b/selfdrive/controls/tests/test_drive_helpers.py
@@ -35,9 +35,15 @@ def test_kona_non_scc_aol_gate_does_not_change_fault_or_normal_lateral_gates():
)
-def test_kona_non_scc_does_not_retry_after_a_latched_temporary_fault():
+def test_kona_non_scc_recovers_after_temporary_fault_clears():
assert not get_kona_non_scc_lateral_active(
- False, False, True, False, False, False, False, True, False, False, True,
+ False, False, True, True, False, False, False, True, False, True,
+ )
+ assert not get_kona_non_scc_lateral_active(
+ False, False, True, False, False, False, False, True, True, False,
+ )
+ assert get_kona_non_scc_lateral_active(
+ False, False, True, False, False, False, False, True, False, False,
)
diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py
index 9372470fc6..7edb0efc93 100644
--- a/selfdrive/controls/tests/test_latcontrol.py
+++ b/selfdrive/controls/tests/test_latcontrol.py
@@ -20,6 +20,7 @@ from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.controls.lib.latcontrol_angle import (
LatControlAngle,
_ascent_angle_tracking_target,
+ _ascent_low_speed_angle_target,
)
from openpilot.selfdrive.controls.lib.latcontrol_pid import (
LatControlPID,
@@ -54,6 +55,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
get_rav4_tss2_pid_output,
get_subaru_impreza_pid_output_scale,
get_genesis_gv70_low_speed_center_overshoot_scale,
+ get_genesis_gv70_stabilized_output,
get_genesis_g70_high_speed_transition_scale,
get_genesis_g70_stabilized_output,
normalize_flm_overrides,
@@ -203,9 +205,19 @@ class TestLatControl:
def test_ascent_angle_tracking_correction_is_bounded_and_handoff_safe(self):
assert _ascent_angle_tracking_target(10.0, 0.0, 20.0, False) == pytest.approx(12.5)
assert _ascent_angle_tracking_target(40.0, 0.0, 20.0, False) == pytest.approx(48.0)
- assert _ascent_angle_tracking_target(10.0, 0.0, 4.0, False) == pytest.approx(10.0)
+ assert _ascent_angle_tracking_target(10.0, 0.0, 9.0, False) == pytest.approx(10.0)
+ assert 10.0 < _ascent_angle_tracking_target(10.0, 0.0, 12.0, False) < 12.5
+ assert _ascent_angle_tracking_target(40.0, 0.0, 4.0, False) == pytest.approx(48.0)
assert _ascent_angle_tracking_target(10.0, 0.0, 20.0, True) == pytest.approx(10.0)
+ def test_ascent_low_speed_filter_is_center_gated_and_handoff_safe(self):
+ filtered = _ascent_low_speed_angle_target(10.0, 0.0, 4.0, False, DT_CTRL)
+
+ assert 0.0 < filtered < 10.0
+ assert _ascent_low_speed_angle_target(10.0, 0.0, 10.0, False, DT_CTRL) == pytest.approx(10.0)
+ assert _ascent_low_speed_angle_target(40.0, 0.0, 4.0, False, DT_CTRL) == pytest.approx(40.0)
+ assert _ascent_low_speed_angle_target(10.0, 0.0, 4.0, True, DT_CTRL) == pytest.approx(10.0)
+
def test_torque_log_exposes_friction_controller_state(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(GM.CHEVROLET_BOLT_ACC_2022_2023)
@@ -1724,6 +1736,45 @@ class TestLatControl:
assert base_output != 0.0
assert tapered_output == pytest.approx(base_output * 0.5)
+ def test_genesis_gv70_output_stabilizer_is_speed_and_phase_aware(self):
+ low_speed = get_genesis_gv70_stabilized_output(-0.2, 0.2, 0.1, -0.4, 5.0, DT_CTRL)
+ high_speed_center = get_genesis_gv70_stabilized_output(-0.2, 0.2, 0.1, -0.4, 30.0, DT_CTRL)
+ high_speed_wind = get_genesis_gv70_stabilized_output(0.1, 0.3, 0.8, 0.5, 30.0, DT_CTRL)
+ high_speed_unwind = get_genesis_gv70_stabilized_output(0.1, 0.3, 0.8, -0.5, 30.0, DT_CTRL)
+ high_speed_direction_change = get_genesis_gv70_stabilized_output(-0.3, 0.3, -0.8, -0.5, 30.0, DT_CTRL)
+
+ assert low_speed == pytest.approx(-0.2, abs=0.005)
+ assert abs(high_speed_center - 0.2) < abs(low_speed - 0.2)
+ assert high_speed_unwind > high_speed_wind > 0.1
+ assert abs(high_speed_direction_change - 0.3) > abs(high_speed_center - 0.2)
+
+ def test_genesis_gv70_output_stabilizer_update_path(self, monkeypatch):
+ calls = []
+
+ def stabilized_output(output_torque, prev_output_torque, desired_lateral_accel,
+ desired_lateral_jerk, v_ego, dt):
+ calls.append((output_torque, prev_output_torque, desired_lateral_accel,
+ desired_lateral_jerk, v_ego, dt))
+ return 0.123
+
+ monkeypatch.setattr(latcontrol_torque, "get_genesis_gv70_stabilized_output", stabilized_output)
+ controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
+ CS.vEgo = 25.0
+ output, _, lac_log = controller.update(
+ True, CS, VM, params, False, 0.0002, False, 0.2, None, None, starpilot_toggles,
+ )
+
+ assert calls
+ assert lac_log.active
+ assert output == pytest.approx(-0.123)
+
+ call_count = len(calls)
+ CS.steeringPressed = True
+ controller.update(
+ True, CS, VM, params, False, 0.0002, False, 0.2, None, None, starpilot_toggles,
+ )
+ assert len(calls) == call_count
+
def test_genesis_g70_low_speed_output_guard_update_path(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.GENESIS_G70_2020)
CS.vEgo = 2.0
diff --git a/starpilot/car/ford/lateral.py b/starpilot/car/ford/lateral.py
index 5416d31ace..ac7f0f505f 100644
--- a/starpilot/car/ford/lateral.py
+++ b/starpilot/car/ford/lateral.py
@@ -34,9 +34,9 @@ MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL - ACCELERATION_DUE_TO_GRAVITY * 0.06
STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL
CURVATURE_LOOKAHEAD_MIN = 0.20
CURVATURE_LOOKAHEAD_MAX = 0.40
-MACH_E_TURN_IN_LOOKAHEAD_EXTRA = 0.40
-MACH_E_TURN_IN_MIN_CURVATURE = 0.006
-MACH_E_TURN_IN_FULL_CURVATURE = 0.009
+MACH_E_TURN_IN_LOOKAHEAD_EXTRA = 0.80
+MACH_E_TURN_IN_MIN_CURVATURE = 0.002
+MACH_E_TURN_IN_FULL_CURVATURE = 0.008
MACH_E_TURN_IN_LAG_CURVATURE = 0.006
FORD_CURVATURE_LOOKAHEAD = {
CAR.FORD_EXPLORER_MK6: 0.20,
@@ -184,21 +184,23 @@ class FordLateralController:
precision = 0
return requested, precision
- def _turn_in_preview_weight(self, desired: float, predicted: float, current: float) -> float:
+ def _turn_in_preview_weight(self, desired: float, preview: float, current: float) -> float:
if self.CP.carFingerprint not in FORD_CONSERVATIVE_PREVIEW_CARS:
return 0.0
- if desired * predicted <= 0.0 or desired * self.desired_curvature_last < 0.0:
+ if desired * preview <= 0.0 or desired * self.desired_curvature_last < 0.0:
return 0.0
- if abs(desired) <= abs(self.desired_curvature_last) or abs(current) >= abs(desired):
+ if abs(desired) <= abs(self.desired_curvature_last):
return 0.0
+ target = max(abs(desired), abs(preview))
curvature_weight = float(np.interp(
- abs(desired),
+ target,
[MACH_E_TURN_IN_MIN_CURVATURE, MACH_E_TURN_IN_FULL_CURVATURE],
[0.0, 1.0],
))
+ direction = float(np.sign(desired))
lag_weight = float(np.clip(
- (abs(desired) - abs(current)) / MACH_E_TURN_IN_LAG_CURVATURE,
+ (target - direction * current) / MACH_E_TURN_IN_LAG_CURVATURE,
0.0, 1.0,
))
return curvature_weight * lag_weight
@@ -263,13 +265,15 @@ class FordLateralController:
manual_turn and self.CP.carFingerprint in FORD_MANUAL_TURN_LATCH_CARS))
v_ego = float(CS.out.vEgoRaw)
- predicted = self._predicted_curvature(v_ego, self._curvature_lookahead())
+ lookahead = self._curvature_lookahead()
+ predicted = self._predicted_curvature(v_ego, lookahead)
desired = float(actuators.curvature)
- turn_in_weight = self._turn_in_preview_weight(desired, predicted, current)
- if turn_in_weight > 0.0:
- turn_in_predicted = self._predicted_curvature(
- v_ego, self._curvature_lookahead() + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
- predicted = float(np.interp(turn_in_weight, [0.0, 1.0], [predicted, turn_in_predicted]))
+ if self.CP.carFingerprint in FORD_CONSERVATIVE_PREVIEW_CARS:
+ turn_in_predicted = self._predicted_curvature(v_ego, lookahead + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
+ turn_in_weight = self._turn_in_preview_weight(desired, turn_in_predicted, current)
+ if turn_in_weight > 0.0:
+ turn_in_target = float(np.copysign(max(abs(desired), abs(turn_in_predicted)), desired))
+ predicted = float(np.interp(turn_in_weight, [0.0, 1.0], [predicted, turn_in_target]))
requested, precision = self._blend_and_scale(desired, predicted, v_ego, current)
self.desired_curvature_last = desired
diff --git a/starpilot/car/ford/tests/test_lateral.py b/starpilot/car/ford/tests/test_lateral.py
index 2132cfac56..a583b0ba5e 100644
--- a/starpilot/car/ford/tests/test_lateral.py
+++ b/starpilot/car/ford/tests/test_lateral.py
@@ -144,12 +144,22 @@ def test_mach_e_preview_remains_available_on_curve_entry(controller):
def test_mach_e_turn_in_preview_leads_when_path_lags(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
- controller.desired_curvature_last = 0.007
+ controller.desired_curvature_last = 0.004
weight = controller._turn_in_preview_weight(
- desired=0.009, predicted=0.007, current=0.006)
+ desired=0.005, preview=0.005, current=0.002)
- assert weight == pytest.approx(0.5)
+ assert weight == pytest.approx(0.25)
+
+
+def test_mach_e_turn_in_preview_leads_opposite_measured_curvature(controller):
+ controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
+ controller.desired_curvature_last = 0.001
+
+ weight = controller._turn_in_preview_weight(
+ desired=0.003, preview=0.009, current=-0.003)
+
+ assert weight == pytest.approx(1.0)
def test_mach_e_turn_in_preview_is_not_carried_into_unwind(controller):
@@ -157,7 +167,7 @@ def test_mach_e_turn_in_preview_is_not_carried_into_unwind(controller):
controller.desired_curvature_last = 0.010
assert controller._turn_in_preview_weight(
- desired=0.008, predicted=0.009, current=0.004) == 0.0
+ desired=0.008, preview=0.009, current=0.004) == 0.0
def test_mach_e_turn_in_preview_uses_extra_model_horizon(controller, monkeypatch):
@@ -165,6 +175,20 @@ def test_mach_e_turn_in_preview_uses_extra_model_horizon(controller, monkeypatch
controller.sm["liveDelay"].lateralDelay = 0.4
controller.desired_curvature_last = 0.007
lookaheads = []
+ monkeypatch.setattr(controller, "_predicted_curvature",
+ lambda _v_ego, lookahead: lookaheads.append(lookahead) or 0.012)
+
+ controller.update(
+ SimpleNamespace(latActive=True), car_state(speed=8.0, curvature=0.002),
+ SimpleNamespace(curvature=0.010),
+ )
+
+ assert lookaheads == [pytest.approx(0.4), pytest.approx(1.2)]
+
+
+def test_non_mach_e_does_not_request_extra_model_horizon(controller, monkeypatch):
+ controller.sm["liveDelay"].lateralDelay = 0.4
+ lookaheads = []
monkeypatch.setattr(controller, "_predicted_curvature",
lambda _v_ego, lookahead: lookaheads.append(lookahead) or 0.007)
@@ -173,14 +197,14 @@ def test_mach_e_turn_in_preview_uses_extra_model_horizon(controller, monkeypatch
SimpleNamespace(curvature=0.010),
)
- assert lookaheads == [pytest.approx(0.4), pytest.approx(0.8)]
+ assert lookaheads == [pytest.approx(0.4)]
def test_non_mach_e_turn_in_preview_is_unchanged(controller):
controller.desired_curvature_last = 0.007
assert controller._turn_in_preview_weight(
- desired=0.010, predicted=0.007, current=0.004) == 0.0
+ desired=0.010, preview=0.007, current=0.004) == 0.0
def test_non_mach_e_preview_blend_is_unchanged(controller):
diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyModal.js b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyModal.js
index c0d7f3488c..81687dac96 100644
--- a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyModal.js
+++ b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyModal.js
@@ -8,12 +8,25 @@ export const GalaxyModal = {
cancelLabel: { type: String, default: "Cancel" },
danger: { type: Boolean, default: false },
sheet: { type: Boolean, default: true },
+ input: { type: Boolean, default: false },
+ inputValue: { type: String, default: "" },
+ inputPlaceholder: { type: String, default: "" },
+ inputRequired: { type: Boolean, default: false },
},
emits: ["update:modelValue", "confirm", "cancel"],
+ data() { return { value: this.inputValue } },
+ mounted() { this.focusInput() },
methods: {
+ focusInput() {
+ if (this.input) this.$nextTick(() => this.$refs.input?.focus())
+ },
close() { this.$emit("update:modelValue", false) },
cancel() { this.close(); this.$emit("cancel") },
- confirm() { this.$emit("confirm"); this.close() },
+ confirm() {
+ if (this.inputRequired && !String(this.value || "").trim()) return
+ this.$emit("confirm", this.input ? this.value : undefined)
+ this.close()
+ },
},
template: `
{{ message }}{{ title }}