mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-10 10:13:45 +08:00
iPhone FoldGate
This commit is contained in:
@@ -194,7 +194,7 @@ def get_checksum_state(dbc_name: str) -> ChecksumState | None:
|
||||
return ChecksumState(4, 2, 3, 5, False, SignalType.HONDA_CHECKSUM, honda_checksum)
|
||||
elif dbc_name.startswith(("toyota_", "lexus_")):
|
||||
return ChecksumState(8, -1, 7, -1, False, SignalType.TOYOTA_CHECKSUM, toyota_checksum)
|
||||
elif dbc_name.startswith("hyundai_canfd_generated"):
|
||||
elif dbc_name.startswith(("hyundai_canfd_generated", "hyundai_radar_210_21f_generated")):
|
||||
return ChecksumState(16, -1, 0, -1, True, SignalType.HKG_CAN_FD_CHECKSUM, hkg_can_fd_checksum)
|
||||
elif dbc_name.startswith(("vw_mqb", "vw_mqbevo", "vw_meb")):
|
||||
return ChecksumState(8, 4, 0, 0, True, SignalType.VOLKSWAGEN_MQB_MEB_CHECKSUM, volkswagen_mqb_meb_checksum)
|
||||
|
||||
@@ -6,7 +6,7 @@ from opendbc.car.lateral import apply_driver_steer_torque_limits
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.gm.values import (
|
||||
ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, SDGM_CAR, AccState, CanBus, CarControllerParams,
|
||||
ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, GM_AUTO_HOLD_CARS, SDGM_CAR, AccState, CanBus, CarControllerParams,
|
||||
CruiseButtons, GMFlags, GMSafetyFlags,
|
||||
)
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
@@ -309,7 +309,7 @@ def supports_volt_auto_hold(CP, auto_hold_enabled: bool):
|
||||
auto_hold_enabled and
|
||||
getattr(CP, "openpilotLongitudinalControl", False) and
|
||||
stock_hold_safety_ready and
|
||||
CP.carFingerprint in AUTO_HOLD_VOLT_CARS
|
||||
CP.carFingerprint in GM_AUTO_HOLD_CARS
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ STANDSTILL_THRESHOLD = 10 * 0.0311
|
||||
VOLT_EBCM_BRAKE_PRESSED_THRESHOLD = 6 / 0xd0
|
||||
AUTO_HOLD_MIN_DRIVE_TIME_S = 3.0
|
||||
AUTO_HOLD_REGEN_RELEASE_COOLDOWN_S = 1.0
|
||||
ACC_STARTUP_FAULT_GRACE_PERIOD_S = 5.0
|
||||
|
||||
BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
|
||||
CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel}
|
||||
@@ -67,6 +68,24 @@ def update_auto_hold_drive_timers(in_drive_for_hold: bool, moving_for_hold: bool
|
||||
return auto_hold_drive_time, one_pedal_drive_time
|
||||
|
||||
|
||||
def update_startup_acc_fault_suppression(car_fingerprint: str, system_power_mode: int,
|
||||
previous_system_power_mode: int, timer: float,
|
||||
acc_state: int, friction_brake_unavailable: bool) -> tuple[float, bool]:
|
||||
if car_fingerprint != CAR.BUICK_LACROSSE:
|
||||
return 0.0, False
|
||||
|
||||
if system_power_mode == 2 and previous_system_power_mode != 2:
|
||||
timer = ACC_STARTUP_FAULT_GRACE_PERIOD_S
|
||||
elif system_power_mode != 2:
|
||||
timer = 0.0
|
||||
|
||||
if timer <= 0.0 or acc_state != AccState.FAULTED:
|
||||
return 0.0, False
|
||||
|
||||
timer = max(timer - DT_CTRL, 0.0)
|
||||
return timer, timer > 0.0 and not friction_brake_unavailable
|
||||
|
||||
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
super().__init__(CP, FPCP)
|
||||
@@ -105,6 +124,8 @@ class CarState(CarStateBase):
|
||||
self.lkas_previously_enabled = 0
|
||||
self.lkas_enabled = 0
|
||||
self.pcm_acc_status = AccState.OFF
|
||||
self.system_power_mode = 0
|
||||
self.startup_acc_fault_suppression_timer = 0.0
|
||||
self.stock_fcw_alert = 0
|
||||
self.car_gps_config = get_car_gps_config(CP)
|
||||
self.car_gps_supported = self.car_gps_config is not None
|
||||
@@ -350,8 +371,18 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0
|
||||
ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1
|
||||
ret.accFaulted = (pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED or
|
||||
pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
|
||||
acc_state = pt_cp.vl["AcceleratorPedal2"]["CruiseState"]
|
||||
friction_brake_unavailable = pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1
|
||||
self.startup_acc_fault_suppression_timer, suppress_startup_acc_fault = update_startup_acc_fault_suppression(
|
||||
self.CP.carFingerprint,
|
||||
int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"]),
|
||||
self.system_power_mode,
|
||||
self.startup_acc_fault_suppression_timer,
|
||||
acc_state,
|
||||
friction_brake_unavailable,
|
||||
)
|
||||
self.system_power_mode = int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"])
|
||||
ret.accFaulted = (acc_state == AccState.FAULTED and not suppress_startup_acc_fault) or friction_brake_unavailable
|
||||
|
||||
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
|
||||
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
|
||||
|
||||
@@ -31,6 +31,8 @@ BOLT_CC_DIRECTION_MEMORY_S = 1.5
|
||||
VOLT_CC_CARS = {
|
||||
CAR.CHEVROLET_VOLT_CC,
|
||||
}
|
||||
VOLT_CC_TARGET_DEADBAND_MPH = 5.0
|
||||
VOLT_CC_ACCEL_DEADBAND_MS2 = 0.15
|
||||
|
||||
|
||||
def malibu_phase_map_for_button(button):
|
||||
@@ -344,7 +346,15 @@ def _create_volt_cc_spam_command(CS, actuators, ms_convert):
|
||||
speed_setpoint = int(round(CS.out.cruiseState.speed * ms_convert))
|
||||
ego_speed = CS.out.vEgo * ms_convert
|
||||
|
||||
if accel == 0.0:
|
||||
v_cruise_kph = float(getattr(CS.out, "vCruise", 0.0))
|
||||
if 0.0 < v_cruise_kph < 255.0:
|
||||
is_metric = ms_convert == CV.MS_TO_KPH
|
||||
target_setpoint = v_cruise_kph if is_metric else v_cruise_kph * CV.KPH_TO_MPH
|
||||
target_deadband = VOLT_CC_TARGET_DEADBAND_MPH * (CV.MPH_TO_KPH if is_metric else 1.0)
|
||||
if abs(target_setpoint - speed_setpoint) <= target_deadband:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
|
||||
if abs(accel) <= VOLT_CC_ACCEL_DEADBAND_MS2:
|
||||
return CruiseButtons.INIT, float("inf")
|
||||
|
||||
if accel < 0.0:
|
||||
|
||||
@@ -15,6 +15,7 @@ from opendbc.car.gm.values import (
|
||||
CC_ONLY_CAR,
|
||||
CC_REGEN_PADDLE_CAR,
|
||||
EV_CAR,
|
||||
GM_AUTO_HOLD_CARS,
|
||||
SDGM_CAR,
|
||||
CarControllerParams,
|
||||
CanBus,
|
||||
@@ -710,18 +711,19 @@ class CarInterface(CarInterfaceBase):
|
||||
if remote_start_boots_comma:
|
||||
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_REMOTE_START_BOOTS_COMMA.value
|
||||
|
||||
volt_stock_friction_brake_safety = (
|
||||
gm_stock_friction_brake_safety = (
|
||||
ret.openpilotLongitudinalControl and
|
||||
(gm_auto_hold or volt_one_pedal_mode) and
|
||||
candidate in {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
}
|
||||
(
|
||||
(gm_auto_hold and candidate in GM_AUTO_HOLD_CARS) or
|
||||
(volt_one_pedal_mode and candidate in {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
})
|
||||
)
|
||||
)
|
||||
if volt_stock_friction_brake_safety:
|
||||
# Reuse the paddle-scheduler safety bit as a Volt stock friction-brake
|
||||
if gm_stock_friction_brake_safety:
|
||||
# marker on non-pedal paths. Auto hold and one-pedal can run while OP
|
||||
# longitudinal is configured but not currently active, so the bit must
|
||||
# be present regardless of the current long-control mode. Do not expose
|
||||
|
||||
@@ -431,6 +431,15 @@ def test_volt_auto_hold_requires_toggle_supported_non_cc_only_volt_and_stock_saf
|
||||
),
|
||||
True,
|
||||
)
|
||||
assert supports_volt_auto_hold(
|
||||
SimpleNamespace(
|
||||
carFingerprint=CAR.BUICK_LACROSSE,
|
||||
openpilotLongitudinalControl=True,
|
||||
networkLocation=CarParams.NetworkLocation.gateway,
|
||||
safetyConfigs=stock_safety,
|
||||
),
|
||||
True,
|
||||
)
|
||||
assert not supports_volt_auto_hold(
|
||||
SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT,
|
||||
|
||||
@@ -8,7 +8,12 @@ from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car import Bus, DT_CTRL, structs
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.carstate import CarState as GMCarState, get_hard_cruise_buttons, update_auto_hold_drive_timers
|
||||
from opendbc.car.gm.carstate import (
|
||||
CarState as GMCarState,
|
||||
get_hard_cruise_buttons,
|
||||
update_auto_hold_drive_timers,
|
||||
update_startup_acc_fault_suppression,
|
||||
)
|
||||
from opendbc.car.gm.carcontroller import (
|
||||
VisualAlert,
|
||||
get_acc_dashboard_always_one,
|
||||
@@ -204,6 +209,50 @@ class TestBoltGps:
|
||||
assert all(message in parsers[Bus.pt].vl for message in CHEVROLET_BOLT_GPS_MESSAGES)
|
||||
|
||||
|
||||
class TestGMCarState:
|
||||
def test_lacrosse_startup_acc_fault_is_suppressed(self):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
assert suppressed
|
||||
assert timer == pytest.approx(5.0 - DT_CTRL)
|
||||
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 2, timer, 0, False,
|
||||
)
|
||||
|
||||
assert timer == 0.0
|
||||
assert not suppressed
|
||||
|
||||
def test_lacrosse_persistent_acc_fault_is_reported_after_startup(self):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
for _ in range(int(5.0 / DT_CTRL)):
|
||||
timer, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 2, timer, 3, False,
|
||||
)
|
||||
|
||||
assert timer == 0.0
|
||||
assert not suppressed
|
||||
|
||||
def test_lacrosse_brake_unavailable_fault_is_never_suppressed(self):
|
||||
_, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, True,
|
||||
)
|
||||
|
||||
assert not suppressed
|
||||
|
||||
def test_startup_acc_fault_suppression_is_scoped_to_lacrosse(self):
|
||||
_, suppressed = update_startup_acc_fault_suppression(
|
||||
CAR.BUICK_REGAL, 2, 0, 0.0, 3, False,
|
||||
)
|
||||
|
||||
assert not suppressed
|
||||
|
||||
|
||||
class TestGMInterface:
|
||||
def test_lacrosse_obd_and_ascm_integrations_remain_separate(self):
|
||||
obd_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
@@ -521,6 +570,43 @@ class TestGMInterface:
|
||||
assert car_params.openpilotLongitudinalControl
|
||||
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_buick_lacrosse_auto_hold_sets_stock_hold_safety_bit_with_op_long_enabled(self):
|
||||
params = Params()
|
||||
try:
|
||||
params.put_bool("GMAutoHold", True)
|
||||
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
CAR.BUICK_LACROSSE,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
finally:
|
||||
params.remove("GMAutoHold")
|
||||
|
||||
assert car_params.openpilotLongitudinalControl
|
||||
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_buick_lacrosse_auto_hold_is_off_when_toggle_is_disabled(self):
|
||||
params = Params()
|
||||
try:
|
||||
params.put_bool("GMAutoHold", False)
|
||||
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
|
||||
CAR.BUICK_LACROSSE,
|
||||
_empty_fingerprint(),
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=_test_starpilot_toggles(),
|
||||
)
|
||||
finally:
|
||||
params.remove("GMAutoHold")
|
||||
|
||||
assert not car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
|
||||
|
||||
def test_volt_auto_hold_does_not_set_stock_hold_safety_bit_with_op_long_disabled(self):
|
||||
CarInterface = interfaces[CAR.CHEVROLET_VOLT_ASCM]
|
||||
fingerprint = _empty_fingerprint()
|
||||
@@ -847,6 +933,63 @@ class TestGMCarController:
|
||||
|
||||
assert len(msgs) == 1
|
||||
|
||||
def test_volt_cc_redneck_holds_when_stock_setpoint_is_within_target_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(2.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=100.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=99.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
assert controller.apply_speed == 99
|
||||
|
||||
def test_volt_cc_redneck_catches_up_when_target_exceeds_deadband(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(0.5 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
cs = SimpleNamespace(
|
||||
CP=SimpleNamespace(
|
||||
carFingerprint=CAR.CHEVROLET_VOLT_CC,
|
||||
flags=GMFlags.NO_CAMERA.value,
|
||||
networkLocation=structs.CarParams.NetworkLocation.gateway,
|
||||
minEnableSpeed=0.0,
|
||||
),
|
||||
buttons_counter=2,
|
||||
out=SimpleNamespace(
|
||||
vEgo=90.0 * CV.KPH_TO_MS,
|
||||
cruiseState=SimpleNamespace(speed=90.0 * CV.KPH_TO_MS),
|
||||
vCruise=100.0,
|
||||
),
|
||||
)
|
||||
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert msgs == []
|
||||
|
||||
controller.frame = int(0.7 / DT_CTRL)
|
||||
msgs = gmcan.create_gm_cc_spam_command(
|
||||
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
|
||||
)
|
||||
|
||||
assert len(msgs) == 1
|
||||
assert controller.apply_speed == 91
|
||||
|
||||
def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self):
|
||||
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
|
||||
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
|
||||
|
||||
@@ -533,6 +533,14 @@ EV_CAR = {
|
||||
CAR.CHEVROLET_MALIBU_HYBRID_CC,
|
||||
}
|
||||
|
||||
GM_AUTO_HOLD_CARS = {
|
||||
CAR.CHEVROLET_VOLT,
|
||||
CAR.CHEVROLET_VOLT_2019,
|
||||
CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA,
|
||||
CAR.BUICK_LACROSSE,
|
||||
}
|
||||
|
||||
# We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness)
|
||||
CAMERA_ACC_CAR = {
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023,
|
||||
|
||||
@@ -11,7 +11,7 @@ from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.hyundai import hyundaicanfd, hyundaican
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.values import HyundaiFlags, HyundaiSafetyFlags, HyundaiStarPilotFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_LIVE_LONGITUDINAL_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \
|
||||
KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
@@ -860,7 +860,11 @@ class CarController(CarControllerBase):
|
||||
|
||||
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
|
||||
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
|
||||
lfa_status_cars = (CAR.HYUNDAI_IONIQ_6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
|
||||
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
|
||||
lka_steering_long = lka_steering and lfa_longitudinal_active
|
||||
@@ -892,8 +896,7 @@ class CarController(CarControllerBase):
|
||||
if angle_lkas_alt:
|
||||
steering_msg_active = bool(steering_msg_active and drive_gear)
|
||||
angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive)
|
||||
forward_stock_lkas = (self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR or
|
||||
self.CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026) and angle_lkas_alt and (
|
||||
forward_stock_lkas = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and angle_lkas_alt and (
|
||||
angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled))
|
||||
)
|
||||
preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint)
|
||||
@@ -995,7 +998,7 @@ class CarController(CarControllerBase):
|
||||
# The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat
|
||||
# and stops publishing object tracks when it disappears.
|
||||
radar_heartbeat_step = 1 if ccnc_angle_long else 4
|
||||
if self.CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and self.frame % radar_heartbeat_step == 0:
|
||||
if self.CP.carFingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR and self.frame % radar_heartbeat_step == 0:
|
||||
can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // radar_heartbeat_step,
|
||||
CS.out.brakePressed, CS.out.gasPressed,
|
||||
self.CP.carFingerprint))
|
||||
|
||||
@@ -8,6 +8,7 @@ from opendbc.car.hyundai.values import HyundaiFlags, CAR, CarControllerParams, \
|
||||
CANFD_SECURITYACCESS_CAR, \
|
||||
CANFD_ANGLE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_LIVE_LONGITUDINAL_CAR, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, \
|
||||
RADAR_LIVE_LONGITUDINAL_CAR, \
|
||||
UNSUPPORTED_LONGITUDINAL_CAR, HyundaiSafetyFlags, \
|
||||
LEGACY_LONGITUDINAL_CAR, \
|
||||
@@ -27,6 +28,15 @@ from openpilot.starpilot.common.testing_grounds import testing_ground
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
Ecu = structs.CarParams.Ecu
|
||||
|
||||
|
||||
def get_communication_control_request(car_fingerprint):
|
||||
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])
|
||||
|
||||
return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX,
|
||||
uds.MESSAGE_TYPE.NORMAL])
|
||||
|
||||
# Cancel button can sometimes be ACC pause/resume button, main button can also enable on some cars
|
||||
ENABLE_BUTTONS = (ButtonType.accelCruise, ButtonType.decelCruise, ButtonType.cancel, ButtonType.mainCruise)
|
||||
|
||||
@@ -353,14 +363,7 @@ class CarInterface(CarInterfaceBase):
|
||||
params = Params()
|
||||
|
||||
if communication_control is None:
|
||||
if CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR:
|
||||
# Don't use 0x80 suppress bit so we can read the ECU response.
|
||||
# Use ENABLE_RX_DISABLE_TX (0x01) so the ECU can still receive from rear radars for BSM
|
||||
# while blocking SCC TX.
|
||||
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
|
||||
else:
|
||||
# 0x80 silences response for other cars (original behavior)
|
||||
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
|
||||
communication_control = get_communication_control_request(CP.carFingerprint)
|
||||
|
||||
ecu_log(f"=== init() called: opLong={CP.openpilotLongitudinalControl}, flags=0x{CP.flags:x}, safetyParam={CP.safetyConfigs[-1].safetyParam} ===")
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ MRR30_RADAR_START_ADDR = 0x210
|
||||
MRR30_RADAR_MSG_COUNT = 16
|
||||
MRR35_RADAR_START_ADDR = 0x3A5
|
||||
MRR35_RADAR_MSG_COUNT = 32
|
||||
GV70_RADAR_START_ADDR = 0x210
|
||||
GV70_RADAR_MSG_COUNT = 16
|
||||
GV70_RADAR_DBC = "hyundai_radar_210_21f_generated"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -30,6 +33,7 @@ class RadarTrackConfig:
|
||||
frequency: int = 50
|
||||
parser_msg_count: int | None = None
|
||||
expected_length: int | None = None
|
||||
dbc_name: str | None = None
|
||||
|
||||
@property
|
||||
def can_parser_msg_count(self) -> int:
|
||||
@@ -47,6 +51,10 @@ RADAR_TRACK_CONFIGS = {
|
||||
|
||||
|
||||
def get_radar_track_config(car_fingerprint, flags: int = 0) -> RadarTrackConfig | None:
|
||||
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
||||
return RadarTrackConfig(GV70_RADAR_START_ADDR, GV70_RADAR_MSG_COUNT, "gv70_210", bus=0,
|
||||
frequency=20, expected_length=32, dbc_name=GV70_RADAR_DBC)
|
||||
|
||||
radar_dbc = DBC[car_fingerprint].get(Bus.radar)
|
||||
if car_fingerprint == CAR.GENESIS_G90 and radar_dbc == HYUNDAI_MANDO_FRONT_RADAR_DBC:
|
||||
return RadarTrackConfig(RADAR_START_ADDR, G90_RADAR_MSG_COUNT, "mando", parser_msg_count=RADAR_MSG_COUNT)
|
||||
@@ -65,6 +73,10 @@ def radar_tracks_available(radar_config: RadarTrackConfig | None, fingerprint) -
|
||||
if radar_config is None:
|
||||
return False
|
||||
|
||||
if radar_config.radar_type == "gv70_210":
|
||||
return all(fingerprint[radar_config.bus].get(addr) == radar_config.expected_length
|
||||
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.msg_count))
|
||||
|
||||
msg_len = fingerprint[radar_config.bus].get(radar_config.start_addr)
|
||||
if msg_len is None:
|
||||
return False
|
||||
@@ -78,7 +90,8 @@ def get_radar_can_parser(CP, radar_config):
|
||||
|
||||
messages = [(f"RADAR_TRACK_{addr:x}", radar_config.frequency)
|
||||
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.can_parser_msg_count)]
|
||||
return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, radar_config.bus)
|
||||
dbc_name = radar_config.dbc_name or DBC[CP.carFingerprint][Bus.radar]
|
||||
return CANParser(dbc_name, messages, radar_config.bus)
|
||||
|
||||
|
||||
class RadarInterface(RadarInterfaceBase):
|
||||
@@ -223,6 +236,27 @@ class RadarInterface(RadarInterfaceBase):
|
||||
del self.pts[track_key]
|
||||
continue
|
||||
|
||||
if radar_type == "gv70_210":
|
||||
for i in ("1", "2"):
|
||||
track_key = addr * 2 + int(i) - 1
|
||||
valid = msg[f"{i}_STATE"] in (3, 4)
|
||||
if valid:
|
||||
pt = self.pts.get(track_key)
|
||||
if pt is None:
|
||||
pt = structs.RadarData.RadarPoint()
|
||||
pt.trackId = self.track_id
|
||||
self.track_id += 1
|
||||
self.pts[track_key] = pt
|
||||
pt.measured = True
|
||||
pt.dRel = msg[f"{i}_LONG_DIST"]
|
||||
pt.yRel = msg[f"{i}_LAT_DIST"]
|
||||
pt.vRel = msg[f"{i}_REL_SPEED"]
|
||||
pt.aRel = msg[f"{i}_REL_ACCEL"]
|
||||
pt.yvRel = msg[f"{i}_REL_LAT_SPEED"]
|
||||
elif track_key in self.pts:
|
||||
del self.pts[track_key]
|
||||
continue
|
||||
|
||||
if radar_type == "mrrevo14f":
|
||||
for i in ("1", "2"):
|
||||
track_key = addr * 2 + int(i) - 1
|
||||
|
||||
@@ -24,16 +24,17 @@ from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY
|
||||
clear_ioniq_6_torque_when_request_inactive
|
||||
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
|
||||
get_canfd_cruise_available
|
||||
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX
|
||||
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX, get_communication_control_request
|
||||
from opendbc.car.hyundai import hyundaican, hyundaicanfd
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.radar_interface import MRREVO14F_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, MRR35_RADAR_START_ADDR, \
|
||||
RADAR_START_ADDR, get_radar_track_config
|
||||
RADAR_START_ADDR, RadarInterface, get_radar_track_config, radar_tracks_available
|
||||
from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, DATELESS_FUZZY_CARS, \
|
||||
HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \
|
||||
UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \
|
||||
LEGACY_LONGITUDINAL_CAR, DBC, HyundaiFlags, get_platform_codes, HyundaiSafetyFlags, \
|
||||
HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, kia_ev6_gt_line_longitudinal_tuning
|
||||
HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, \
|
||||
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning
|
||||
|
||||
LongCtrlState = CarControl.Actuators.LongControlState
|
||||
from opendbc.car.hyundai.fingerprints import FW_VERSIONS
|
||||
@@ -129,6 +130,15 @@ def get_test_toggles() -> SimpleNamespace:
|
||||
|
||||
|
||||
class TestHyundaiFingerprint:
|
||||
def test_ev6_uses_stock_hda2_communication_control_path(self):
|
||||
stock_request = bytes([0x28, 0x83, 0x01])
|
||||
radar_keepalive_request = bytes([0x28, 0x01, 0x01])
|
||||
|
||||
assert CAR.KIA_EV6 in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
|
||||
assert CAR.KIA_EV6 not in CANFD_RADAR_ECU_KEEPALIVE_CAR
|
||||
assert get_communication_control_request(CAR.KIA_EV6) == stock_request
|
||||
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_6) == radar_keepalive_request
|
||||
|
||||
def test_carnival_hev_low_speed_torque_rate_limits(self):
|
||||
CP = CarInterface.get_params(CAR.KIA_CARNIVAL_HEV_4TH_GEN, gen_empty_fingerprint(), [],
|
||||
False, False, False, None)
|
||||
@@ -426,6 +436,42 @@ class TestHyundaiFingerprint:
|
||||
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_LKA_STEERING
|
||||
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.EV_GAS
|
||||
|
||||
gv70_radar_config = get_radar_track_config(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
|
||||
assert gv70_radar_config.radar_type == "gv70_210"
|
||||
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
|
||||
gv70_fingerprint[gv70_radar_config.bus][addr] = gv70_radar_config.expected_length
|
||||
assert radar_tracks_available(gv70_radar_config, gv70_fingerprint)
|
||||
|
||||
CP = CarInterface.get_params(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, gv70_fingerprint, gv70_car_fw,
|
||||
True, False, False, None)
|
||||
assert not CP.radarUnavailable
|
||||
|
||||
radar = RadarInterface(CP)
|
||||
packer = CANPacker(gv70_radar_config.dbc_name)
|
||||
messages = []
|
||||
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
|
||||
message = packer.make_can_msg(f"RADAR_TRACK_{addr:x}", 0, {
|
||||
"1_STATE": 3,
|
||||
"1_LONG_DIST": 25.0,
|
||||
"1_LAT_DIST": 0.5,
|
||||
"1_REL_SPEED": -2.0,
|
||||
"1_REL_LAT_SPEED": 0.1,
|
||||
"1_REL_ACCEL": -0.2,
|
||||
})
|
||||
data = bytearray(message[1])
|
||||
checksum = hkg_can_fd_checksum(addr, None, data)
|
||||
data[0] = checksum & 0xff
|
||||
data[1] = (checksum >> 8) & 0xff
|
||||
messages.append((message[0], bytes(data), message[2]))
|
||||
radar_data = radar.update([(1, messages)])
|
||||
assert radar_data is not None
|
||||
assert len(radar_data.points) == 16
|
||||
assert radar_data.points[0].dRel == pytest.approx(25.0)
|
||||
|
||||
other_config = get_radar_track_config(CAR.HYUNDAI_IONIQ_5)
|
||||
assert other_config.radar_type == "mrr30"
|
||||
assert other_config.dbc_name is None
|
||||
|
||||
for candidate in HYUNDAI_NON_SCC_CARS:
|
||||
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None)
|
||||
assert bool(CP.flags & HyundaiFlags.NON_SCC)
|
||||
@@ -2552,15 +2598,14 @@ 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)]
|
||||
|
||||
def test_ioniq_6_keeps_lfa_status_when_longitudinal_is_inactive(self):
|
||||
@pytest.mark.parametrize("car", [CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6])
|
||||
def test_egmp_keeps_lfa_status_when_longitudinal_is_inactive(self, car):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.HYUNDAI_IONIQ_6
|
||||
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),
|
||||
@@ -2568,12 +2613,15 @@ class TestHyundaiFingerprint:
|
||||
)
|
||||
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)
|
||||
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)
|
||||
|
||||
def test_gv70_electrified_longitudinal_uses_hda2_scc_contract(self):
|
||||
CP = CarParams.new_message()
|
||||
@@ -2707,7 +2755,7 @@ class TestHyundaiFingerprint:
|
||||
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
|
||||
|
||||
@pytest.mark.parametrize("standstill", [False, True])
|
||||
def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(self, standstill):
|
||||
def test_sportage_angle_lkas_alt_publishes_inactive_status(self, standstill):
|
||||
CP = CarParams.new_message()
|
||||
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
|
||||
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
|
||||
@@ -2715,6 +2763,7 @@ class TestHyundaiFingerprint:
|
||||
CP.openpilotLongitudinalControl = False
|
||||
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
can_bus = CanBus(CP)
|
||||
cc = SimpleNamespace(enabled=False, latActive=False,
|
||||
actuators=SimpleNamespace(longControlState=LongCtrlState.off),
|
||||
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
|
||||
@@ -2724,7 +2773,16 @@ class TestHyundaiFingerprint:
|
||||
|
||||
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 not [msg for msg in msgs if msg[0] in (0x110, 0x12A)]
|
||||
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110]
|
||||
assert len(lkas_msgs) == 1
|
||||
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
|
||||
parser.update([(1, lkas_msgs)])
|
||||
assert parser.can_valid
|
||||
assert parser.vl["LKAS_ALT"]["LKA_ICON"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1
|
||||
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0
|
||||
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1
|
||||
|
||||
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
|
||||
CP = CarParams.new_message()
|
||||
|
||||
@@ -1218,7 +1218,9 @@ CANFD_ALT_BUTTONS_RESUME_CAR = {CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_
|
||||
CANFD_CORNER_RADAR_BSM_CAR = {CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9}
|
||||
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}
|
||||
RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | {
|
||||
CAR.HYUNDAI_IONIQ,
|
||||
CAR.HYUNDAI_KONA_EV_2022,
|
||||
|
||||
@@ -302,7 +302,7 @@ class CarController(CarControllerBase):
|
||||
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
|
||||
|
||||
manual_handoff = self._legacy_2025_manual_handoff(CS, lkas_available)
|
||||
manual_handoff = self._legacy_2025_manual_handoff(CS, CC.latActive)
|
||||
lkas_active = lkas_available and not manual_handoff
|
||||
|
||||
steer_target = self._legacy_2025_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
|
||||
@@ -325,7 +325,7 @@ class CarController(CarControllerBase):
|
||||
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
|
||||
|
||||
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
|
||||
manual_handoff = self._angle_manual_handoff(CS, CC.latActive)
|
||||
lkas_active = lkas_available and not manual_handoff
|
||||
|
||||
if lkas_active and not self.angle_lkas_active:
|
||||
@@ -361,7 +361,7 @@ class CarController(CarControllerBase):
|
||||
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)
|
||||
manual_handoff = self._angle_manual_handoff(CS, CC.latActive)
|
||||
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
|
||||
|
||||
@@ -85,14 +85,14 @@ class CarState(CarStateBase):
|
||||
|
||||
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
|
||||
ret.steeringAngleDeg = cp_angle.vl["Steering_2"]["Steering_Angle"]
|
||||
steering_updated = len(cp_angle.vl_all["Steering_2"]["Steering_Angle"]) > 0
|
||||
steering_counter = cp_angle.vl["Steering_2"]["COUNTER"]
|
||||
else:
|
||||
ret.steeringAngleDeg = cp.vl["Steering_Torque"]["Steering_Angle"]
|
||||
steering_updated = len(cp.vl_all["Steering_Torque"]["Steering_Angle"]) > 0
|
||||
steering_counter = cp.vl["Steering_Torque"].get("COUNTER", 0)
|
||||
|
||||
if not (self.CP.flags & SubaruFlags.PREGLOBAL):
|
||||
# ideally we get this from the car, but unclear if it exists. diagnostic software doesn't even have it
|
||||
ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_updated)
|
||||
ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_counter)
|
||||
|
||||
ret.steeringTorque = cp_angle.vl["Steering_Torque"]["Steer_Torque_Sensor"]
|
||||
ret.steeringTorqueEps = cp_angle.vl["Steering_Torque"]["Steer_Torque_Output"]
|
||||
|
||||
@@ -546,6 +546,25 @@ def test_ascent_2023_uses_gen2_angle_bus_layout():
|
||||
assert controller.status_bus == CanBus.main
|
||||
|
||||
|
||||
def test_ascent_steering_rate_retains_last_can_sample():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
car_state = CarState(CP, None)
|
||||
parsers = car_state.get_can_parsers(CP)
|
||||
toggles = SimpleNamespace(subaru_sng=False)
|
||||
|
||||
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 1.0
|
||||
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 1
|
||||
car_state.update(parsers, toggles)
|
||||
|
||||
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 2.0
|
||||
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 2
|
||||
state, _ = car_state.update(parsers, toggles)
|
||||
assert state.steeringRateDeg == pytest.approx(50.0)
|
||||
|
||||
state, _ = car_state.update(parsers, toggles)
|
||||
assert state.steeringRateDeg == pytest.approx(50.0)
|
||||
|
||||
|
||||
def test_other_angle_platforms_keep_existing_bus_layout():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
|
||||
parsers = CarState.get_can_parsers(CP)
|
||||
@@ -707,11 +726,20 @@ def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
|
||||
CS.out.steeringRateDeg = 0.0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(2, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
|
||||
for i in range(8):
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(3 + i, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
parser.update([(11, [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([(3, [msg])])
|
||||
parser.update([(12, [msg])])
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ non_tested_cars = [
|
||||
TOYOTA.TOYOTA_RAV4H,
|
||||
|
||||
# No recorded routes yet
|
||||
VOLVO.VOLVO_V40,
|
||||
VOLVO.VOLVO_XC40_RECHARGE,
|
||||
VOLVO.VOLVO_S60_RECHARGE,
|
||||
VOLVO.POLESTAR_2,
|
||||
|
||||
@@ -145,6 +145,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2]
|
||||
"VOLVO_XC40_RECHARGE" = [1.5, 1.5, 0.1]
|
||||
"VOLVO_S60_RECHARGE" = [1.5, 1.5, 0.1]
|
||||
"VOLVO_V40" = [1.5, 1.5, 0.1]
|
||||
|
||||
# Dashcam or fallback configured as ideal car
|
||||
"MOCK" = [10.0, 10, 0.0]
|
||||
|
||||
@@ -40,6 +40,8 @@ TOYOTA_NO_LEAD_CRUISE_SIGN_FLIP_MIN_SET_SPEED_ERROR = 0.35 # m/s
|
||||
TOYOTA_RAV4_LAUNCH_PEDAL_BLEND_SPEED = 5.0 # m/s
|
||||
TOYOTA_RAV4_LAUNCH_PEDAL_SCALE = 0.11
|
||||
TOYOTA_RAV4_LOW_SPEED_PEDAL_SCALE = 0.23
|
||||
TOYOTA_AUTO_HOLD_ACCEL = -1.0
|
||||
TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES = 100
|
||||
|
||||
# LKA limits
|
||||
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
|
||||
@@ -309,23 +311,24 @@ class CarController(CarControllerBase):
|
||||
|
||||
self.last_standstill = CS.out.standstill
|
||||
|
||||
def create_auto_brake_hold_messages(self, CS: structs.CarState, brake_hold_allowed_timer: int = 100):
|
||||
can_sends = []
|
||||
brake_hold_allowed = (CS.out.standstill and CS.out.cruiseState.available and
|
||||
def update_auto_hold_state(self, CS: structs.CarState, cancel_requested: bool = False,
|
||||
activation_frames: int = TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES):
|
||||
brake_hold_allowed = (not cancel_requested and CS.out.standstill and CS.out.cruiseState.available and
|
||||
not CS.out.gasPressed and not CS.out.cruiseState.enabled and
|
||||
CS.out.gearShifter not in (PARK, REVERSE))
|
||||
|
||||
if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed:
|
||||
self._brake_hold_counter += 1
|
||||
self.brake_hold_active = self._brake_hold_counter > brake_hold_allowed_timer
|
||||
self.brake_hold_active = self._brake_hold_counter > activation_frames
|
||||
elif not brake_hold_allowed:
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
can_sends.append(toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active))
|
||||
return self.brake_hold_active
|
||||
|
||||
return can_sends
|
||||
def reset_auto_hold_state(self):
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
actuators = CC.actuators
|
||||
@@ -423,10 +426,9 @@ class CarController(CarControllerBase):
|
||||
|
||||
self._update_standstill_request(CC, CS, actuators, starpilot_toggles)
|
||||
if supports_toyota_auto_hold(self.CP, getattr(starpilot_toggles, "toyota_auto_hold", False)):
|
||||
can_sends.extend(self.create_auto_brake_hold_messages(CS))
|
||||
elif self.brake_hold_active:
|
||||
self._brake_hold_counter = 0
|
||||
self.brake_hold_active = False
|
||||
self.update_auto_hold_state(CS, pcm_cancel_cmd)
|
||||
else:
|
||||
self.reset_auto_hold_state()
|
||||
|
||||
interceptor_gas_cmd = self._compute_interceptor_gas_cmd(CC, CS)
|
||||
|
||||
@@ -534,6 +536,11 @@ class CarController(CarControllerBase):
|
||||
|
||||
pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
|
||||
|
||||
if self.brake_hold_active:
|
||||
pcm_accel_cmd = TOYOTA_AUTO_HOLD_ACCEL
|
||||
self.permit_braking = True
|
||||
self.standstill_req = True
|
||||
|
||||
main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd
|
||||
can_sends.append(toyotacan.create_accel_command(self.packer, main_accel_cmd, pcm_cancel_cmd, self.permit_braking, self.standstill_req, lead,
|
||||
CS.acc_type, fcw_alert, self.distance_button,
|
||||
|
||||
@@ -90,8 +90,6 @@ class CarState(CarStateBase):
|
||||
self.has_can_filter = self.FPCP.flags & ToyotaStarPilotFlags.RADAR_CAN_FILTER.value
|
||||
self.has_SDSU = self.FPCP.flags & ToyotaStarPilotFlags.SMART_DSU.value
|
||||
self.has_ZSS = self.FPCP.flags & ToyotaStarPilotFlags.ZSS.value
|
||||
self.auto_brake_hold = bool(self.CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value)
|
||||
self.pre_collision_2 = {}
|
||||
|
||||
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
@@ -227,9 +225,6 @@ class CarState(CarStateBase):
|
||||
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
|
||||
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"])
|
||||
|
||||
if self.auto_brake_hold:
|
||||
self.pre_collision_2 = copy.copy(cp_cam.vl["PRE_COLLISION_2"])
|
||||
|
||||
if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
|
||||
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
|
||||
|
||||
@@ -314,9 +309,6 @@ class CarState(CarStateBase):
|
||||
if CP.carFingerprint in DISTANCE_BUTTON_CAR:
|
||||
pt_messages.append(("PCM_CRUISE_4", 1))
|
||||
|
||||
if CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value:
|
||||
cam_messages.append(("PRE_COLLISION_2", 50))
|
||||
|
||||
return {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2),
|
||||
|
||||
@@ -164,8 +164,8 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.GAS_INTERCEPTOR.value
|
||||
|
||||
toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold")
|
||||
if toyota_auto_hold and candidate in TOYOTA_AUTO_HOLD_CARS:
|
||||
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
if toyota_auto_hold and ret.openpilotLongitudinalControl and candidate in TOYOTA_AUTO_HOLD_CARS:
|
||||
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
ret.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
|
||||
if not ret.openpilotLongitudinalControl:
|
||||
|
||||
@@ -196,7 +196,8 @@ class TestToyotaInterfaces:
|
||||
params.put_bool("ToyotaAutoHold", True)
|
||||
car_params = CarInterface.get_params(
|
||||
candidate,
|
||||
{bus: {} for bus in range(8)},
|
||||
{bus: ({0x2FF: 8} if candidate in (CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H) and bus == 0 else {})
|
||||
for bus in range(8)},
|
||||
[],
|
||||
alpha_long=False,
|
||||
is_release=False,
|
||||
@@ -207,12 +208,13 @@ class TestToyotaInterfaces:
|
||||
params.remove("ToyotaAutoHold")
|
||||
|
||||
assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
|
||||
can_parsers = CarState.get_can_parsers(car_params)
|
||||
car_state = CarState(car_params, SimpleNamespace(flags=0))
|
||||
car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
|
||||
assert "PRE_COLLISION_2" in can_parsers[Bus.cam].vl
|
||||
assert "PRE_COLLISION_2" not in can_parsers[Bus.cam].vl
|
||||
|
||||
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H])
|
||||
def test_auto_hold_is_disabled_by_default(self, candidate):
|
||||
@@ -229,7 +231,7 @@ class TestToyotaInterfaces:
|
||||
)
|
||||
|
||||
assert not car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD
|
||||
|
||||
def test_prius_openpilot_long_uses_hybrid_long_defaults(self):
|
||||
car_params = CarInterface.get_params(
|
||||
@@ -744,6 +746,8 @@ class TestToyotaCarController:
|
||||
controller.standstill_req = standstill_req
|
||||
controller.last_standstill = last_standstill
|
||||
controller.accel = 0.0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
return controller
|
||||
|
||||
@staticmethod
|
||||
@@ -806,9 +810,6 @@ class TestToyotaCarController:
|
||||
def test_toyota_auto_hold_latches_after_brake_press_until_gas(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
@@ -818,28 +819,22 @@ class TestToyotaCarController:
|
||||
brakePressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert controller.brake_hold_active
|
||||
|
||||
cs.out.brakePressed = False
|
||||
controller.frame = 2
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert controller.brake_hold_active
|
||||
|
||||
cs.out.gasPressed = True
|
||||
controller.frame = 4
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert not controller.brake_hold_active
|
||||
|
||||
def test_toyota_auto_hold_does_not_trigger_without_brake_press(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
@@ -848,10 +843,9 @@ class TestToyotaCarController:
|
||||
brakePressed=False,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
assert not controller.brake_hold_active
|
||||
|
||||
def test_prius_resume_request_releases_standstill_latch(self):
|
||||
@@ -991,12 +985,9 @@ class TestToyotaCarController:
|
||||
assert parser.can_valid
|
||||
assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 1
|
||||
|
||||
def test_auto_brake_hold_sends_modified_pre_collision_after_timer(self):
|
||||
def test_auto_hold_uses_acc_control_brake_path(self):
|
||||
controller = self._make_controller()
|
||||
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
|
||||
controller.frame = 0
|
||||
controller.brake_hold_active = False
|
||||
controller._brake_hold_counter = 0
|
||||
cs = SimpleNamespace(
|
||||
out=SimpleNamespace(
|
||||
standstill=True,
|
||||
@@ -1005,16 +996,19 @@ class TestToyotaCarController:
|
||||
brakePressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
),
|
||||
pre_collision_2={},
|
||||
)
|
||||
|
||||
can_sends = controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
|
||||
controller.update_auto_hold_state(cs, activation_frames=0)
|
||||
can_sends = [toyotacan.create_accel_command(
|
||||
controller.packer, -1.0, False, True, True, False, 1, False, 0, False,
|
||||
)]
|
||||
|
||||
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("PRE_COLLISION_2", 0)], 0)
|
||||
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("ACC_CONTROL", 0)], 0)
|
||||
parser.update([(1, can_sends)])
|
||||
assert controller.brake_hold_active
|
||||
assert parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
|
||||
assert parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 1
|
||||
assert parser.vl["ACC_CONTROL"]["ACCEL_CMD"] == -1.0
|
||||
assert parser.vl["ACC_CONTROL"]["PERMIT_BRAKING"] == 1
|
||||
assert parser.vl["ACC_CONTROL"]["RELEASE_STANDSTILL"] == 0
|
||||
|
||||
def test_interceptor_stop_and_go_holds_small_launch_at_standstill(self):
|
||||
controller = self._make_controller()
|
||||
|
||||
@@ -89,38 +89,6 @@ def create_pcs_commands(packer, accel, active, mass):
|
||||
return [msg1, msg2]
|
||||
|
||||
|
||||
def create_brake_hold_command(packer, frame, pre_collision_2, brake_hold_active):
|
||||
values = {s: pre_collision_2[s] for s in [
|
||||
"DSS1GDRV",
|
||||
"DS1STAT2",
|
||||
"DS1STBK2",
|
||||
"PCSWAR",
|
||||
"PCSALM",
|
||||
"PCSOPR",
|
||||
"PCSABK",
|
||||
"PBATRGR",
|
||||
"PPTRGR",
|
||||
"IBTRGR",
|
||||
"CLEXTRGR",
|
||||
"IRLT_REQ",
|
||||
"BRKHLD",
|
||||
"AVSTRGR",
|
||||
"VGRSTRGR",
|
||||
"PREFILL",
|
||||
"PBRTRGR",
|
||||
"PCSDIS",
|
||||
"PBPREPMP",
|
||||
] if s in pre_collision_2}
|
||||
|
||||
if brake_hold_active:
|
||||
values = {
|
||||
"DSS1GDRV": 0x3FF,
|
||||
"PBRTRGR": frame % 730 < 727,
|
||||
}
|
||||
|
||||
return packer.make_can_msg("PRE_COLLISION_2", 0, values)
|
||||
|
||||
|
||||
def create_acc_cancel_command(packer):
|
||||
values = {
|
||||
"GAS_RELEASED": 0,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.can.packer import CANPacker
|
||||
@@ -5,16 +7,20 @@ from opendbc.car import Bus
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.lateral import apply_std_steer_angle_limits
|
||||
from opendbc.car.volvo.helpers import LCA3CounterSync
|
||||
from opendbc.car.volvo.volvocan import (create_lca_message, create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message,
|
||||
create_lca_5_message, create_lca_6_message, create_lca_7_message, create_pscm_related_message)
|
||||
from opendbc.car.volvo.values import CarControllerParams
|
||||
from opendbc.car.volvo.volvocan import (create_c1_cancel, create_c1_pscm_message, create_c1_steering_control, create_lca_message,
|
||||
create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message, create_lca_5_message,
|
||||
create_lca_6_message, create_lca_7_message, create_pscm_related_message)
|
||||
from opendbc.car.volvo.values import CAR, CarControllerParams, VolvoC1PlatformConfig
|
||||
|
||||
|
||||
class CarController(CarControllerBase):
|
||||
def __init__(self, dbc_names, CP):
|
||||
super().__init__(dbc_names, CP)
|
||||
self.packer = CANPacker(dbc_names[Bus.party])
|
||||
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig)
|
||||
self.packer = CANPacker(dbc_names[Bus.pt] if self.is_c1 else dbc_names[Bus.party])
|
||||
self.apply_angle_last = 0.0 # Track last applied steering angle
|
||||
self.c1_torque_samples = deque(maxlen=CarControllerParams.C1_N_ZERO_TORQUE)
|
||||
self.c1_recovery_until = -1
|
||||
|
||||
self.gear_acc = 60
|
||||
self.lca_4_acc = 0 # Bresenham accumulator for 29 Hz
|
||||
@@ -62,6 +68,9 @@ class CarController(CarControllerBase):
|
||||
self.lca_auth_drv_mag_filt = 0.0
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
if self.is_c1:
|
||||
return self._update_c1(CC, CS)
|
||||
|
||||
can_sends = []
|
||||
actuators = CC.actuators
|
||||
|
||||
@@ -285,3 +294,50 @@ class CarController(CarControllerBase):
|
||||
self.frame += 1
|
||||
self.last_lat_active = CC.latActive
|
||||
return new_actuators, can_sends
|
||||
|
||||
def _update_c1(self, CC, CS):
|
||||
can_sends = []
|
||||
actuators = CC.actuators
|
||||
|
||||
if self.frame % 2 == 0: # stock FSM1 and PSCM1 messages are 50 Hz
|
||||
requested_active = CC.latActive and CS.out.vEgo > self.CP.minSteerSpeed
|
||||
recovering = requested_active and self.frame < self.c1_recovery_until
|
||||
|
||||
if not requested_active:
|
||||
self.c1_torque_samples.clear()
|
||||
self.c1_recovery_until = -1
|
||||
elif recovering:
|
||||
self.c1_torque_samples.clear()
|
||||
else:
|
||||
if self.c1_recovery_until >= 0:
|
||||
self.c1_recovery_until = -1
|
||||
self.c1_torque_samples.clear()
|
||||
self.c1_torque_samples.append(CS.c1_lka_torque)
|
||||
if (len(self.c1_torque_samples) == CarControllerParams.C1_N_ZERO_TORQUE and
|
||||
all(torque == 0 for torque in self.c1_torque_samples)):
|
||||
self.c1_recovery_until = self.frame + 100
|
||||
self.c1_torque_samples.clear()
|
||||
recovering = True
|
||||
|
||||
lat_active = requested_active and not recovering
|
||||
desired_angle = float(np.clip(
|
||||
actuators.steeringAngleDeg,
|
||||
CS.out.steeringAngleDeg - CarControllerParams.C1_ANGLE_ERROR,
|
||||
CS.out.steeringAngleDeg + CarControllerParams.C1_ANGLE_ERROR,
|
||||
))
|
||||
apply_angle = apply_std_steer_angle_limits(
|
||||
desired_angle, self.apply_angle_last, CS.out.vEgoRaw,
|
||||
CS.out.steeringAngleDeg, lat_active, CarControllerParams.C1_ANGLE_LIMITS,
|
||||
)
|
||||
|
||||
can_sends.append(create_c1_pscm_message(self.packer, CS.c1_msg_pscm))
|
||||
can_sends.append(create_c1_steering_control(self.packer, apply_angle, lat_active))
|
||||
self.apply_angle_last = apply_angle
|
||||
|
||||
if CC.cruiseControl.cancel and self.frame % 10 == 0:
|
||||
can_sends.append(create_c1_cancel(self.packer))
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
new_actuators.steeringAngleDeg = self.apply_angle_last
|
||||
self.frame += 1
|
||||
return new_actuators, can_sends
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from cereal import custom
|
||||
from opendbc.car import structs, Bus
|
||||
from opendbc.car import Bus, ButtonType, create_button_events, structs
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.volvo.values import DBC, VolvoSPAPlatformConfig, CAR
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.volvo.values import CAR, DBC, VolvoC1PlatformConfig, VolvoSPAPlatformConfig
|
||||
|
||||
GearShifter = structs.CarState.GearShifter
|
||||
TransmissionType = structs.CarParams.TransmissionType
|
||||
@@ -16,6 +17,7 @@ STEERING_PRESSED_THRESHOLD = 2
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
super().__init__(CP, FPCP)
|
||||
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig)
|
||||
self.is_spa = isinstance(CAR(CP.carFingerprint).config, VolvoSPAPlatformConfig)
|
||||
self.gas_pressed_prev = False
|
||||
self.dispatch_lca_2_msg = False
|
||||
@@ -34,8 +36,22 @@ class CarState(CarStateBase):
|
||||
self.msg_lca_4 = {}
|
||||
self.msg_lca_6 = {}
|
||||
self.msg_lca_7 = {}
|
||||
self.c1_msg_pscm = {}
|
||||
self.c1_lka_torque = 0
|
||||
self.c1_button_states = {
|
||||
"ACCOnOffBtn": False,
|
||||
"ACCStopBtn": False,
|
||||
"ACCSetBtn": False,
|
||||
"ACCResumeBtn": False,
|
||||
"ACCMinusBtn": False,
|
||||
"TimeGapIncreaseBtn": False,
|
||||
"TimeGapDecreaseBtn": False,
|
||||
}
|
||||
|
||||
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
|
||||
if self.is_c1:
|
||||
return self._update_c1(can_parsers)
|
||||
|
||||
cp_main = can_parsers[Bus.main]
|
||||
cp_pt = can_parsers[Bus.pt]
|
||||
cp_party = can_parsers[Bus.party]
|
||||
@@ -137,8 +153,83 @@ class CarState(CarStateBase):
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
return ret, fp_ret
|
||||
|
||||
def _update_c1(self, can_parsers):
|
||||
cp = can_parsers[Bus.pt]
|
||||
cp_cam = can_parsers[Bus.cam]
|
||||
ret = structs.CarState()
|
||||
|
||||
ret.vEgoRaw = cp.vl["VehicleSpeed1"]["VehicleSpeed"] * CV.KPH_TO_MS
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
ret.standstill = ret.vEgoRaw < 0.1
|
||||
|
||||
ret.steeringAngleDeg = cp.vl["PSCM1"]["SteeringAngleServo"]
|
||||
ret.steeringTorque = cp.vl["PSCM1"]["LKATorque"]
|
||||
ret.steeringPressed = False
|
||||
|
||||
ret.gasPressed = cp.vl["PedalandBrake"]["AccPedal"] > 5.0
|
||||
ret.brakePressed = bool(cp.vl["PedalandBrake"]["BrakePedalActive2"] or
|
||||
cp.vl["PedalandBrake"]["BrakePedalActive"])
|
||||
|
||||
ret.gearShifter = {
|
||||
0: GearShifter.park,
|
||||
1: GearShifter.reverse,
|
||||
2: GearShifter.neutral,
|
||||
3: GearShifter.drive,
|
||||
}.get(int(cp.vl["TCM0"]["GearShifter"]), GearShifter.unknown)
|
||||
|
||||
ret.cruiseState.available = bool(cp_cam.vl["FSM0"]["ACCStatusOnOff"])
|
||||
ret.cruiseState.enabled = bool(cp_cam.vl["FSM0"]["ACCStatusActive"])
|
||||
ret.cruiseState.speed = cp.vl["ACC"]["SpeedTargetACC"] * CV.KPH_TO_MS
|
||||
ret.cruiseState.nonAdaptive = False
|
||||
ret.cruiseState.standstill = ret.standstill
|
||||
|
||||
turn_signal = int(cp.vl["MiscCarInfo"]["TurnSignal"])
|
||||
ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(
|
||||
50, turn_signal == 1, turn_signal == 3)
|
||||
ret.doorOpen = False
|
||||
ret.seatbeltUnlatched = False
|
||||
|
||||
button_types = {
|
||||
"ACCOnOffBtn": ButtonType.mainCruise,
|
||||
"ACCStopBtn": ButtonType.cancel,
|
||||
"ACCSetBtn": ButtonType.setCruise,
|
||||
"ACCResumeBtn": ButtonType.resumeCruise,
|
||||
"ACCMinusBtn": ButtonType.decelCruise,
|
||||
"TimeGapIncreaseBtn": ButtonType.gapAdjustCruise,
|
||||
"TimeGapDecreaseBtn": ButtonType.gapAdjustCruise,
|
||||
}
|
||||
button_events = []
|
||||
for signal, button_type in button_types.items():
|
||||
pressed = bool(cp.vl["CCButtons"][signal])
|
||||
button_events.extend(create_button_events(pressed, self.c1_button_states[signal], {True: button_type}))
|
||||
self.c1_button_states[signal] = pressed
|
||||
ret.buttonEvents = button_events
|
||||
|
||||
self.c1_msg_pscm = cp.vl["PSCM1"]
|
||||
self.c1_lka_torque = int(cp.vl["PSCM1"]["LKATorque"])
|
||||
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
return ret, fp_ret
|
||||
|
||||
@staticmethod
|
||||
def get_can_parsers(CP):
|
||||
if isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig):
|
||||
return {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [
|
||||
("VehicleSpeed1", 50),
|
||||
("CCButtons", 100),
|
||||
("PSCM1", 50),
|
||||
("PedalandBrake", 100),
|
||||
("TCM0", 10),
|
||||
("ACC", 17),
|
||||
("MiscCarInfo", 25),
|
||||
], 0),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.cam], [
|
||||
("FSM0", 100),
|
||||
("FSM1", 50),
|
||||
], 2),
|
||||
}
|
||||
|
||||
return {
|
||||
Bus.main: CANParser(DBC[CP.carFingerprint][Bus.main], [], 0),
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1),
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
from opendbc.car.volvo.values import CAR
|
||||
|
||||
FINGERPRINTS = {
|
||||
CAR.VOLVO_V40: [
|
||||
# V40 2017
|
||||
{8: 8, 16: 8, 48: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 208: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 352: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 624: 8, 640: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 848: 8, 853: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
|
||||
# V40 2015
|
||||
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
|
||||
# V40 2014
|
||||
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1072: 8, 1409: 8},
|
||||
],
|
||||
CAR.VOLVO_XC40_RECHARGE: [{
|
||||
21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 304: 8, 336: 8, 339: 8, 341: 8, 394: 8, 395: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1302: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8
|
||||
}],
|
||||
|
||||
@@ -2,11 +2,10 @@ from opendbc.car import structs, get_safety_config
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.car.volvo.carcontroller import CarController
|
||||
from opendbc.car.volvo.carstate import CarState
|
||||
from opendbc.car.volvo.values import VolvoSPAPlatformConfig, CAR
|
||||
from opendbc.car.volvo.values import CAR, VolvoC1PlatformConfig, VolvoSafetyFlags, VolvoSPAPlatformConfig
|
||||
|
||||
TransmissionType = structs.CarParams.TransmissionType
|
||||
|
||||
VOLVO_FLAG_SPA = 1
|
||||
SAFETY_VOLVO = structs.CarParams.SafetyModel.volvo
|
||||
|
||||
|
||||
@@ -18,17 +17,20 @@ class CarInterface(CarInterfaceBase):
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
ret.brand = 'volvo'
|
||||
|
||||
platform = CAR(candidate).config
|
||||
safety_param = 0
|
||||
if isinstance(CAR(candidate).config, VolvoSPAPlatformConfig):
|
||||
safety_param = VOLVO_FLAG_SPA
|
||||
if isinstance(platform, VolvoSPAPlatformConfig):
|
||||
safety_param = VolvoSafetyFlags.SPA.value
|
||||
elif isinstance(platform, VolvoC1PlatformConfig):
|
||||
safety_param = VolvoSafetyFlags.C1.value
|
||||
ret.safetyConfigs = [get_safety_config(SAFETY_VOLVO, safety_param)]
|
||||
#ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
|
||||
|
||||
ret.dashcamOnly = False
|
||||
|
||||
ret.steerActuatorDelay = 0.3
|
||||
ret.steerActuatorDelay = 0.2 if isinstance(platform, VolvoC1PlatformConfig) else 0.3
|
||||
ret.steerLimitTimer = 0.1
|
||||
ret.steerAtStandstill = True
|
||||
ret.steerAtStandstill = not isinstance(platform, VolvoC1PlatformConfig)
|
||||
|
||||
# Use angle-based steering control for Volvo CMA platform
|
||||
ret.steerControlType = structs.CarParams.SteerControlType.angle
|
||||
@@ -39,4 +41,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
ret.pcmCruise = True
|
||||
|
||||
if isinstance(platform, VolvoC1PlatformConfig):
|
||||
ret.transmissionType = TransmissionType.automatic
|
||||
|
||||
return ret
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
from opendbc.can.packer import CANPacker
|
||||
from opendbc.car import Bus, ButtonType, CanData, structs
|
||||
from opendbc.car.volvo.carstate import CarState
|
||||
from opendbc.car.volvo.interface import CarInterface
|
||||
from opendbc.car.volvo.values import CAR, DBC
|
||||
|
||||
|
||||
def _can_data(msg):
|
||||
address, data, bus = msg
|
||||
return CanData(address, data, bus)
|
||||
|
||||
|
||||
def test_c1_carstate_decodes_vehicle_and_cruise_signals():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
cs = CarState(cp, custom.StarPilotCarParams.new_message())
|
||||
parsers = CarState.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
|
||||
messages = [
|
||||
packer.make_can_msg("VehicleSpeed1", 0, {"VehicleSpeed": 72}),
|
||||
packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1, "ACCSetBtn": 1}),
|
||||
packer.make_can_msg("PSCM1", 0, {"SteeringAngleServo": -12.5, "LKATorque": 7}),
|
||||
packer.make_can_msg("PedalandBrake", 0, {"AccPedal": 6, "BrakePedalActive2": 1}),
|
||||
packer.make_can_msg("TCM0", 0, {"GearShifter": 3}),
|
||||
packer.make_can_msg("ACC", 0, {"SpeedTargetACC": 100}),
|
||||
packer.make_can_msg("MiscCarInfo", 0, {"TurnSignal": 1}),
|
||||
packer.make_can_msg("FSM0", 2, {"ACCStatusOnOff": 1, "ACCStatusActive": 1}),
|
||||
packer.make_can_msg("FSM1", 2, {}),
|
||||
]
|
||||
packets = [(1_000_000, [_can_data(msg) for msg in messages])]
|
||||
for parser in parsers.values():
|
||||
parser.update(packets)
|
||||
|
||||
ret, _ = cs.update(parsers, None)
|
||||
assert ret.vEgoRaw == pytest.approx(20.0)
|
||||
assert ret.steeringAngleDeg == pytest.approx(-12.5, abs=0.05)
|
||||
assert ret.steeringTorque == 7
|
||||
assert ret.gasPressed and ret.brakePressed
|
||||
assert ret.gearShifter == structs.CarState.GearShifter.drive
|
||||
assert ret.cruiseState.available and ret.cruiseState.enabled
|
||||
assert ret.cruiseState.speed == pytest.approx(100 / 3.6)
|
||||
assert ret.leftBlinker and not ret.rightBlinker
|
||||
assert len(ret.buttonEvents) == 2
|
||||
assert any(event.type == ButtonType.cancel and event.pressed for event in ret.buttonEvents)
|
||||
assert any(event.type == ButtonType.setCruise and event.pressed for event in ret.buttonEvents)
|
||||
|
||||
release = packer.make_can_msg("CCButtons", 0, {})
|
||||
packets = [(2_000_000, [_can_data(release)])]
|
||||
for parser in parsers.values():
|
||||
parser.update(packets)
|
||||
ret, _ = cs.update(parsers, None)
|
||||
assert len(ret.buttonEvents) == 2
|
||||
assert any(event.type == ButtonType.cancel and not event.pressed for event in ret.buttonEvents)
|
||||
assert any(event.type == ButtonType.setCruise and not event.pressed for event in ret.buttonEvents)
|
||||
@@ -4,7 +4,8 @@ from types import SimpleNamespace
|
||||
from opendbc.car.volvo.carcontroller import CarController
|
||||
from opendbc.car.volvo.helpers import checksum_lca_5_message
|
||||
from opendbc.car.volvo.interface import CarInterface
|
||||
from opendbc.car.volvo.values import DBC
|
||||
from opendbc.car.volvo.values import CAR, DBC
|
||||
from opendbc.car.volvo.volvocan import create_c1_checksum
|
||||
|
||||
|
||||
def _zero_message():
|
||||
@@ -67,3 +68,70 @@ def test_controller_relays_stock_lca5_angle_when_inactive():
|
||||
if raw & (1 << 14):
|
||||
raw -= 1 << 15
|
||||
assert abs(raw * 0.05596 - 12.0) < 0.1
|
||||
|
||||
|
||||
def _c1_state():
|
||||
return SimpleNamespace(
|
||||
out=SimpleNamespace(steeringAngleDeg=10.0, vEgo=12.0, vEgoRaw=12.0),
|
||||
c1_lka_torque=5,
|
||||
c1_msg_pscm=_zero_message(),
|
||||
)
|
||||
|
||||
|
||||
def test_c1_controller_emits_checked_steering_and_pscm_relay():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cc = SimpleNamespace(
|
||||
latActive=True,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=False),
|
||||
)
|
||||
|
||||
actuators, can_sends = controller.update(cc, _c1_state(), 0, None)
|
||||
assert [(msg[0], msg[2]) for msg in can_sends] == [(0x125, 2), (0xD0, 0)]
|
||||
|
||||
fsm = can_sends[1][1]
|
||||
assert fsm[7] & 0x3 == 3
|
||||
assert fsm[6] == create_c1_checksum(fsm)
|
||||
assert 0.0 < actuators.steeringAngleDeg <= 2.0
|
||||
|
||||
|
||||
def test_c1_controller_sends_only_cancel_button():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cc = SimpleNamespace(
|
||||
latActive=False,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=True),
|
||||
)
|
||||
|
||||
_, can_sends = controller.update(cc, _c1_state(), 0, None)
|
||||
buttons = next(msg for msg in can_sends if msg[0] == 0x10)
|
||||
assert buttons[2] == 0
|
||||
assert buttons[1][7] == 0x10
|
||||
assert buttons[1][6] == 0
|
||||
|
||||
|
||||
def test_c1_controller_temporarily_drops_steering_on_zero_torque_fault():
|
||||
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cs = _c1_state()
|
||||
cs.c1_lka_torque = 0
|
||||
cc = SimpleNamespace(
|
||||
latActive=True,
|
||||
actuators=_Actuators(),
|
||||
cruiseControl=SimpleNamespace(cancel=False),
|
||||
)
|
||||
|
||||
directions = []
|
||||
for _ in range(23):
|
||||
_, can_sends = controller.update(cc, cs, 0, None)
|
||||
directions.extend(msg[1][7] & 0x3 for msg in can_sends if msg[0] == 0xD0)
|
||||
|
||||
assert directions[:-1] == [3] * 11
|
||||
assert directions[-1] == 0
|
||||
|
||||
while controller.frame <= 122:
|
||||
_, can_sends = controller.update(cc, cs, 0, None)
|
||||
fsm = next(msg for msg in can_sends if msg[0] == 0xD0)
|
||||
assert fsm[1][7] & 0x3 == 3
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntFlag
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
|
||||
from opendbc.car.lateral import AngleSteeringLimits
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
|
||||
from opendbc.car.fw_query_definitions import FwQueryConfig
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
# C1 support is adapted from the original dragonpilot V40 port:
|
||||
# https://github.com/dragonpilot/dragonpilot/commit/773dce507082d931236b64dca8024dce9625446f
|
||||
|
||||
|
||||
class VolvoSafetyFlags(IntFlag):
|
||||
SPA = 1
|
||||
C1 = 2
|
||||
|
||||
|
||||
class CarControllerParams:
|
||||
STEER_STEP = 1 # 100 Hz LCA command frequency (controlsd runs at 100 Hz)
|
||||
@@ -81,6 +91,19 @@ class CarControllerParams:
|
||||
([0., 5., 25.], [5., 2., .3]), # rate down limits at different speeds
|
||||
)
|
||||
|
||||
C1_STEER_NO = 0
|
||||
C1_STEER = 3
|
||||
C1_N_ZERO_TORQUE = 12
|
||||
C1_ANGLE_ERROR = 20.0
|
||||
C1_ANGLE_DELTA_BP = [0., 8.33, 13.89, 19.44, 25., 30.55, 36.1]
|
||||
C1_ANGLE_DELTA_UP = [2., 1.2, .25, .20, .15, .10, .10]
|
||||
C1_ANGLE_DELTA_DOWN = [2., 1.2, .25, .20, .15, .10, .10]
|
||||
C1_ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits(
|
||||
359.9,
|
||||
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_UP),
|
||||
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_DOWN),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolvoCarDocs(CarDocs):
|
||||
@@ -105,7 +128,26 @@ class VolvoSPAPlatformConfig(PlatformConfig):
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolvoC1PlatformConfig(PlatformConfig):
|
||||
dbc_dict: DbcDict = field(default_factory=lambda: {
|
||||
Bus.pt: 'volvo_v40_2017_pt',
|
||||
Bus.cam: 'volvo_v40_2017_pt',
|
||||
})
|
||||
|
||||
|
||||
class CAR(Platforms):
|
||||
VOLVO_V40 = VolvoC1PlatformConfig(
|
||||
[VolvoCarDocs("Volvo V40 2013-19")],
|
||||
CarSpecs(
|
||||
mass=1610,
|
||||
wheelbase=2.647,
|
||||
steerRatio=14.7,
|
||||
centerToFrontRatio=0.44,
|
||||
minSteerSpeed=1.0 * CV.KPH_TO_MS,
|
||||
),
|
||||
)
|
||||
|
||||
VOLVO_XC40_RECHARGE = VolvoCMAPlatformConfig(
|
||||
[VolvoCarDocs("Volvo XC40 Recharge 2021-23")],
|
||||
CarSpecs(
|
||||
|
||||
@@ -2,6 +2,47 @@ from opendbc.car.volvo.helpers import (checksum_lca_2_message, checksum_2_0x69_m
|
||||
checksum_2_pscm_related_message, checksum_lca_5_message)
|
||||
from opendbc.car.carlog import carlog
|
||||
|
||||
|
||||
def create_c1_pscm_message(packer, msg_pscm: dict):
|
||||
values = {
|
||||
"LKATorque": 0,
|
||||
"SteeringAngleServo": msg_pscm["SteeringAngleServo"],
|
||||
"byte0": msg_pscm["byte0"],
|
||||
"byte3": msg_pscm["byte3"],
|
||||
"byte4": msg_pscm["byte4"],
|
||||
"byte7": msg_pscm["byte7"],
|
||||
"LKAActive": int(msg_pscm["LKAActive"]) & 0xD,
|
||||
}
|
||||
return packer.make_can_msg("PSCM1", 2, values)
|
||||
|
||||
|
||||
def create_c1_checksum(data: bytes) -> int:
|
||||
angle_raw = ((data[4] & 0x3F) << 8) | data[5]
|
||||
direction = data[7] & 0x3
|
||||
checksum_sum = (data[3] + direction + angle_raw + (angle_raw >> 8)) & 0xFF
|
||||
return checksum_sum ^ 0xFF
|
||||
|
||||
|
||||
def create_c1_steering_control(packer, apply_angle: float, lat_active: bool):
|
||||
values = {
|
||||
"SET_X_E3": 0xE3,
|
||||
"SET_X_B4": 0xB4,
|
||||
"SET_X_08": 0x08,
|
||||
"TrqLim": 0,
|
||||
"LKAAngleReq": apply_angle,
|
||||
"LKASteerDirection": 3 if lat_active else 0,
|
||||
"SET_X_25": 0x25,
|
||||
"SET_X_02": 0x02,
|
||||
}
|
||||
data = packer.make_can_msg("FSM1", 0, values)[1]
|
||||
values["Checksum"] = create_c1_checksum(data)
|
||||
return packer.make_can_msg("FSM1", 0, values)
|
||||
|
||||
|
||||
def create_c1_cancel(packer):
|
||||
return packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1})
|
||||
|
||||
|
||||
def create_lca_message(packer, lat_active: bool, apply_angle: float, msg_lca: dict,
|
||||
authority_pos: int = 614, authority_neg: int = -614,
|
||||
overrides: dict | None = None):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,4 @@ class ALTERNATIVE_EXPERIENCE:
|
||||
|
||||
ALWAYS_ON_LATERAL = 32
|
||||
GM_REMAP_CANCEL_TO_DISTANCE = 64
|
||||
TOYOTA_AUTO_HOLD = 128
|
||||
|
||||
@@ -339,6 +339,7 @@ extern bool gm_remote_start_boots_comma;
|
||||
|
||||
#define ALT_EXP_ALWAYS_ON_LATERAL 32
|
||||
#define ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE 64
|
||||
#define ALT_EXP_TOYOTA_AUTO_HOLD 128
|
||||
|
||||
extern int alternative_experience;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "opendbc/safety/declarations.h"
|
||||
|
||||
#define TOYOTA_AUTO_HOLD_ACCEL -1000 // -1.0 m/s^2 in ACC_CONTROL units
|
||||
|
||||
// Stock longitudinal
|
||||
#define TOYOTA_BASE_TX_MSGS \
|
||||
{0x191, 0, 8, .check_relay = true}, {0x412, 0, 8, .check_relay = true}, {0x1D2, 0, 8, .check_relay = false}, /* LKAS + LTA + PCM cancel cmd */ \
|
||||
@@ -277,7 +279,15 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
// SecOC cars move accel to 0x183. Only allow inactive accel on 0x343 to match stock behavior
|
||||
violation = desired_accel != TOYOTA_LONG_LIMITS.inactive_accel;
|
||||
}
|
||||
violation |= longitudinal_accel_checks(desired_accel, TOYOTA_LONG_LIMITS);
|
||||
|
||||
bool toyota_auto_hold =
|
||||
!toyota_stock_longitudinal &&
|
||||
((alternative_experience & ALT_EXP_TOYOTA_AUTO_HOLD) != 0) &&
|
||||
!vehicle_moving && !gas_pressed && acc_main_on &&
|
||||
(desired_accel == TOYOTA_AUTO_HOLD_ACCEL) &&
|
||||
GET_BIT(msg, 30U) && !GET_BIT(msg, 31U) && !GET_BIT(msg, 24U);
|
||||
|
||||
violation |= !toyota_auto_hold && longitudinal_accel_checks(desired_accel, TOYOTA_LONG_LIMITS);
|
||||
|
||||
// only ACC messages that cancel are allowed when openpilot is not controlling longitudinal
|
||||
if (toyota_stock_longitudinal) {
|
||||
@@ -394,12 +404,7 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
tx = false;
|
||||
}
|
||||
|
||||
// Auto brake hold replaces the camera AEB message only while stopped.
|
||||
if ((msg->addr == 0x344U) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0)) {
|
||||
if (vehicle_moving || gas_pressed || !acc_main_on) {
|
||||
tx = false;
|
||||
}
|
||||
} else if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
|
||||
if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
@@ -566,21 +571,11 @@ static safety_config toyota_init(uint16_t param) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool toyota_fwd_hook(int bus_num, int addr) {
|
||||
bool block_msg = false;
|
||||
if (bus_num == 2) {
|
||||
block_msg = (addr == 0x344) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0) &&
|
||||
!vehicle_moving && !gas_pressed && acc_main_on;
|
||||
}
|
||||
return block_msg;
|
||||
}
|
||||
|
||||
const safety_hooks toyota_hooks = {
|
||||
.init = toyota_init,
|
||||
.rx = toyota_rx_hook,
|
||||
.rx_all = toyota_rx_all_hook,
|
||||
.tx = toyota_tx_hook,
|
||||
.fwd = toyota_fwd_hook,
|
||||
.get_checksum = toyota_get_checksum,
|
||||
.compute_checksum = toyota_compute_checksum,
|
||||
.get_quality_flag_valid = toyota_get_quality_flag_valid,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
#include "opendbc/safety/declarations.h"
|
||||
|
||||
// safetyParam: 0 = CMA (XC40 Recharge), 1 = SPA (S60 Recharge, Polestar 2)
|
||||
// safetyParam: 0 = CMA (XC40 Recharge), 1 = SPA (S60 Recharge, Polestar 2),
|
||||
// 2 = C1 (V40)
|
||||
// Polestar 2 is technically CMA, but appears to use SPA DBC for CAN 1 bus
|
||||
#define VOLVO_FLAG_SPA 1U
|
||||
#define VOLVO_FLAG_C1 2U
|
||||
|
||||
// Volvo CAN message addresses shared between CMA and SPA
|
||||
#define VOLVO_LCA_STEER 0x58U // TX from VCU1 to PSCM, LCA steering command (0x58)
|
||||
@@ -24,6 +26,15 @@
|
||||
#define VOLVO_LCA_6 0x97U // TX LCA_6 message
|
||||
#define VOLVO_LCA_7 0x92U // TX LCA_7 message
|
||||
|
||||
// C1-specific addresses (V40). The V40 powertrain bus is bus 0 and its
|
||||
// forward-camera bus is bus 2; bus 1 is unused by this port.
|
||||
#define VOLVO_C1_BUTTONS 0x10U
|
||||
#define VOLVO_C1_FSM_0 0x30U
|
||||
#define VOLVO_C1_FSM_1 0xD0U
|
||||
#define VOLVO_C1_PSCM_1 0x125U
|
||||
#define VOLVO_C1_PEDAL_AND_BRAKE 0x55U
|
||||
#define VOLVO_C1_SPEED 0x150U
|
||||
|
||||
// CMA-specific PT bus addresses
|
||||
#define VOLVO_CMA_BUS1_SPEED 0x70U // RX vehicle speed
|
||||
#define VOLVO_CMA_ECM_1 0x250U // RX accelerator pedal position
|
||||
@@ -44,6 +55,10 @@
|
||||
#define VOLVO_MAX_ANGLE_CAN 9650
|
||||
#define VOLVO_RELAY_ANGLE_TOLERANCE 54 // approximately 3 degrees
|
||||
|
||||
#define VOLVO_C1_ANGLE_DEG_TO_CAN 22.753128f
|
||||
#define VOLVO_C1_MAX_ANGLE_CAN 8189
|
||||
#define VOLVO_C1_RELAY_ANGLE_TOLERANCE 2
|
||||
|
||||
|
||||
// CAN bus definitions for Volvo
|
||||
// Using same naming as carstate.py for consistency: main, pt, party
|
||||
@@ -54,6 +69,7 @@
|
||||
// Runtime addresses set by volvo_init based on safetyParam
|
||||
static uint16_t volvo_ecm_1_addr;
|
||||
static uint16_t volvo_bus1_cruise_control_addr;
|
||||
static bool volvo_c1;
|
||||
|
||||
static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) {
|
||||
return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]);
|
||||
@@ -67,6 +83,21 @@ static int volvo_lca_5_angle(const CANPacket_t *msg) {
|
||||
return to_signed(volvo_be_15(msg, 6U), 15);
|
||||
}
|
||||
|
||||
static int volvo_c1_pscm_angle(const CANPacket_t *msg) {
|
||||
return (int)(((uint16_t)msg->data[5] << 8U) | msg->data[6]) - 32768;
|
||||
}
|
||||
|
||||
static int volvo_c1_fsm_angle(const CANPacket_t *msg) {
|
||||
return (int)(((uint16_t)(msg->data[4] & 0x3FU) << 8U) | msg->data[5]) - 8192;
|
||||
}
|
||||
|
||||
static uint8_t volvo_c1_fsm_checksum(const CANPacket_t *msg) {
|
||||
const uint16_t angle_raw = ((uint16_t)(msg->data[4] & 0x3FU) << 8U) | msg->data[5];
|
||||
const uint8_t direction = msg->data[7] & 0x3U;
|
||||
const uint8_t checksum_sum = (msg->data[3] + direction + angle_raw + (angle_raw >> 8U)) & 0xFFU;
|
||||
return checksum_sum ^ 0xFFU;
|
||||
}
|
||||
|
||||
static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
|
||||
.max_angle = VOLVO_MAX_ANGLE_CAN,
|
||||
.angle_deg_to_can = VOLVO_ANGLE_DEG_TO_CAN,
|
||||
@@ -81,8 +112,51 @@ static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
|
||||
.frequency = 50U,
|
||||
};
|
||||
|
||||
static const AngleSteeringLimits VOLVO_C1_ANGLE_STEERING_LIMITS = {
|
||||
.max_angle = VOLVO_C1_MAX_ANGLE_CAN,
|
||||
.angle_deg_to_can = VOLVO_C1_ANGLE_DEG_TO_CAN,
|
||||
.angle_rate_up_lookup = {
|
||||
{7.0f, 17.0f, 36.0f},
|
||||
{2.0f, 0.25f, 0.1f},
|
||||
},
|
||||
.angle_rate_down_lookup = {
|
||||
{7.0f, 17.0f, 36.0f},
|
||||
{2.0f, 0.25f, 0.1f},
|
||||
},
|
||||
.max_angle_error = 455, // 20 degrees
|
||||
.angle_error_min_speed = 0.0f,
|
||||
.frequency = 50U,
|
||||
.enforce_angle_error = true,
|
||||
};
|
||||
|
||||
static void volvo_rx_hook(const CANPacket_t *msg) {
|
||||
|
||||
if (volvo_c1) {
|
||||
if (msg->bus == VOLVO_MAIN_BUS) {
|
||||
if (msg->addr == VOLVO_C1_PSCM_1) {
|
||||
update_sample(&angle_meas, volvo_c1_pscm_angle(msg));
|
||||
}
|
||||
|
||||
if (msg->addr == VOLVO_C1_SPEED) {
|
||||
const uint16_t speed_raw = ((uint16_t)msg->data[6] << 8U) | msg->data[7];
|
||||
const float speed = ((float)speed_raw * 0.01f) / 3.6f;
|
||||
vehicle_moving = speed > 0.1f;
|
||||
UPDATE_VEHICLE_SPEED(speed);
|
||||
}
|
||||
|
||||
if (msg->addr == VOLVO_C1_PEDAL_AND_BRAKE) {
|
||||
const uint16_t gas_raw = ((uint16_t)(msg->data[1] & 0x3U) << 8U) | msg->data[2];
|
||||
gas_pressed = gas_raw > 50U; // DBC factor 0.1: greater than 5 percent
|
||||
brake_pressed = GET_BIT(msg, 24U) || GET_BIT(msg, 38U);
|
||||
}
|
||||
}
|
||||
|
||||
if ((msg->bus == VOLVO_PARTY_BUS) && (msg->addr == VOLVO_C1_FSM_0)) {
|
||||
pcm_cruise_check(GET_BIT(msg, 58U));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Main bus (bus 0) messages
|
||||
if (msg->bus == VOLVO_MAIN_BUS) {
|
||||
// Update brake pedal and cruise state from BCM2
|
||||
@@ -158,6 +232,33 @@ static void volvo_rx_hook(const CANPacket_t *msg) {
|
||||
static bool volvo_tx_hook(const CANPacket_t *msg) {
|
||||
bool tx = true;
|
||||
|
||||
if (volvo_c1) {
|
||||
if (msg->addr == VOLVO_C1_FSM_1) {
|
||||
const int desired_angle = volvo_c1_fsm_angle(msg);
|
||||
const uint8_t direction = msg->data[7] & 0x3U;
|
||||
const bool steer_control_enabled = direction != 0U;
|
||||
tx &= SAFETY_ABS(desired_angle) <= VOLVO_C1_MAX_ANGLE_CAN;
|
||||
tx &= !steer_angle_cmd_checks(desired_angle, steer_control_enabled, VOLVO_C1_ANGLE_STEERING_LIMITS);
|
||||
tx &= (direction == 0U) || (direction == 3U);
|
||||
tx &= (msg->data[0] == 0xE3U) && (msg->data[1] == 0xB4U) && (msg->data[2] == 0x08U);
|
||||
tx &= (msg->data[3] == 0x80U) && ((msg->data[4] & 0xC0U) == 0x80U) && ((msg->data[7] & 0xFCU) == 0x94U);
|
||||
tx &= msg->data[6] == volvo_c1_fsm_checksum(msg);
|
||||
}
|
||||
|
||||
if (msg->addr == VOLVO_C1_PSCM_1) {
|
||||
const int relayed_angle = volvo_c1_pscm_angle(msg);
|
||||
const int measured_max = angle_meas.max + VOLVO_C1_RELAY_ANGLE_TOLERANCE;
|
||||
const int measured_min = angle_meas.min - VOLVO_C1_RELAY_ANGLE_TOLERANCE;
|
||||
tx &= !safety_max_limit_check(relayed_angle, measured_max, measured_min);
|
||||
}
|
||||
|
||||
// Only ACC cancel (byte 7 bit 4) may be synthesized.
|
||||
if (msg->addr == VOLVO_C1_BUTTONS) {
|
||||
tx &= ((msg->data[7] & 0xEFU) == 0U) && (msg->data[6] == 0U);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
// LCA_5 carries the actual angle command used by the controller. The stock
|
||||
// LCA frame also contains an angle-shaped field, but the imported controller
|
||||
// deliberately leaves that field at the observed vehicle value.
|
||||
@@ -255,6 +356,22 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
static safety_config volvo_init(uint16_t param) {
|
||||
bool spa = GET_FLAG(param, VOLVO_FLAG_SPA);
|
||||
volvo_c1 = GET_FLAG(param, VOLVO_FLAG_C1);
|
||||
|
||||
if (volvo_c1) {
|
||||
static const CanMsg VOLVO_C1_TX_MSGS[] = {
|
||||
{VOLVO_C1_FSM_1, VOLVO_MAIN_BUS, 8, .check_relay = true},
|
||||
{VOLVO_C1_PSCM_1, VOLVO_PARTY_BUS, 8, .check_relay = true},
|
||||
{VOLVO_C1_BUTTONS, VOLVO_MAIN_BUS, 8, .check_relay = false},
|
||||
};
|
||||
static RxCheck volvo_c1_rx_checks[] = {
|
||||
{.msg = {{VOLVO_C1_PSCM_1, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
|
||||
{.msg = {{VOLVO_C1_FSM_0, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
|
||||
{.msg = {{VOLVO_C1_PEDAL_AND_BRAKE, VOLVO_MAIN_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
|
||||
{.msg = {{VOLVO_C1_SPEED, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
|
||||
};
|
||||
return BUILD_SAFETY_CFG(volvo_c1_rx_checks, VOLVO_C1_TX_MSGS);
|
||||
}
|
||||
|
||||
// Set PT bus addresses based on platform
|
||||
volvo_ecm_1_addr = spa ? VOLVO_SPA_ECM_1 : VOLVO_CMA_ECM_1;
|
||||
|
||||
@@ -97,29 +97,55 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
|
||||
msg = libsafety_py.make_CANPacket(0x283, 0, bytes(dat))
|
||||
self.assertEqual(not bad and not stock_longitudinal, self._tx(msg))
|
||||
|
||||
def test_auto_brake_hold_aeb_replacement_only_at_standstill(self):
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALLOW_AEB)
|
||||
hold_msg = libsafety_py.make_CANPacket(0x344, 0, b"\xfd\x80\x00\x00\x00\x00\x00\xcc")
|
||||
def test_auto_hold_acc_control_is_narrowly_allowed_only_at_standstill(self):
|
||||
if (not self.LONGITUDINAL or
|
||||
self.safety.get_current_safety_param() & (ToyotaSafetyFlags.STOCK_LONGITUDINAL.value | ToyotaSafetyFlags.SECOC.value)):
|
||||
raise unittest.SkipTest("Toyota Auto Hold requires non-SecOC openpilot longitudinal control")
|
||||
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD)
|
||||
hold_msg = self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
|
||||
"ACCEL_CMD": -1.0,
|
||||
"PERMIT_BRAKING": 1,
|
||||
"RELEASE_STANDSTILL": 0,
|
||||
"CANCEL_REQ": 0,
|
||||
})
|
||||
|
||||
self._rx(self._speed_msg(0))
|
||||
self._rx(self._toggle_aol(True))
|
||||
self._rx(self._user_gas_msg(False))
|
||||
self.safety.set_controls_allowed(False)
|
||||
self.assertTrue(self._tx(hold_msg))
|
||||
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x344))
|
||||
|
||||
self.assertFalse(self._tx(self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
|
||||
"ACCEL_CMD": -1.1,
|
||||
"PERMIT_BRAKING": 1,
|
||||
"RELEASE_STANDSTILL": 0,
|
||||
})))
|
||||
|
||||
self._rx(self._speed_msg(1.0))
|
||||
self.assertFalse(self._tx(hold_msg))
|
||||
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
|
||||
|
||||
self._rx(self._speed_msg(0))
|
||||
self._rx(self._user_gas_msg(True))
|
||||
self.assertFalse(self._tx(hold_msg))
|
||||
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
|
||||
|
||||
self._rx(self._user_gas_msg(False))
|
||||
self._rx(self._toggle_aol(False))
|
||||
self.assertFalse(self._tx(hold_msg))
|
||||
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
|
||||
|
||||
def test_auto_hold_acc_control_is_blocked_without_toyota_hold_toggle(self):
|
||||
hold_msg = self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
|
||||
"ACCEL_CMD": -1.0,
|
||||
"PERMIT_BRAKING": 1,
|
||||
"RELEASE_STANDSTILL": 0,
|
||||
"CANCEL_REQ": 0,
|
||||
})
|
||||
self._rx(self._speed_msg(0))
|
||||
self._rx(self._toggle_aol(True))
|
||||
self._rx(self._user_gas_msg(False))
|
||||
self.safety.set_controls_allowed(False)
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.assertFalse(self._tx(hold_msg))
|
||||
|
||||
# Only allow LTA msgs with no actuation
|
||||
def test_lta_steer_cmd(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Safety tests for Volvo CMA/SPA.
|
||||
Safety tests for Volvo C1/CMA/SPA.
|
||||
|
||||
The safety mode lives at ``opendbc/safety/modes/volvo.h`` and is parameterized
|
||||
by ``safetyParam``:
|
||||
@@ -8,14 +8,13 @@ by ``safetyParam``:
|
||||
- ``safetyParam == 0`` → CMA platform (Volvo XC40 Recharge)
|
||||
- ``safetyParam == VOLVO_FLAG_SPA`` → SPA platform (Volvo S60 Recharge,
|
||||
Polestar 2)
|
||||
- ``safetyParam == VOLVO_FLAG_C1`` → C1 platform (Volvo V40)
|
||||
|
||||
The two platforms share LCA/PSCM/etc. addresses on the main and party buses
|
||||
but use *different* PT-bus addresses and signal scales for ECM_1 and
|
||||
BUS1_CRUISE_CONTROL. Vehicle speed is read from main-bus SPEED on both, so it
|
||||
is not platform-dependent. This test file exercises both platforms through the
|
||||
same generic ``CarSafetyTest`` harness so that any future divergence between
|
||||
``carstate.py`` and ``volvo.h`` — e.g. a threshold drifting out of sync — is
|
||||
caught on a laptop instead of in the car.
|
||||
CMA and SPA share LCA/PSCM/etc. addresses on the main and party buses but use
|
||||
different PT-bus addresses and signal scales. C1 uses the V40's legacy CAN
|
||||
layout and its own safety allowlist. The tests exercise all three through the
|
||||
generic ``CarSafetyTest`` harness so divergence between ``carstate.py`` and
|
||||
``volvo.h`` is caught before running in a car.
|
||||
|
||||
Companion to: ``opendbc/car/volvo/carstate.py`` (must agree on thresholds).
|
||||
"""
|
||||
@@ -25,13 +24,16 @@ import re
|
||||
import unittest
|
||||
|
||||
from opendbc.car.volvo.interface import SAFETY_VOLVO
|
||||
from opendbc.car.volvo.values import VolvoSafetyFlags
|
||||
from opendbc.car.volvo.volvocan import create_c1_checksum, create_c1_steering_control
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
import opendbc.safety.tests.common as common
|
||||
from opendbc.safety.tests.common import CANPackerSafety
|
||||
|
||||
|
||||
# Must match VOLVO_FLAG_SPA in opendbc/safety/modes/volvo.h
|
||||
VOLVO_FLAG_SPA = 1
|
||||
# Must match the flags in opendbc/safety/modes/volvo.h
|
||||
VOLVO_FLAG_SPA = VolvoSafetyFlags.SPA.value
|
||||
VOLVO_FLAG_C1 = VolvoSafetyFlags.C1.value
|
||||
|
||||
# Must match VOLVO_SPEED_TO_MS in volvo.h and SPEED_TO_MS in carstate.py
|
||||
VOLVO_SPEED_TO_MS = 0.003977
|
||||
@@ -332,5 +334,124 @@ class TestVolvoSPA(TestVolvoSafetyBase):
|
||||
"BUS1_CRUISE_CONTROL", VOLVO_PT_BUS, values)
|
||||
|
||||
|
||||
class TestVolvoC1(common.CarSafetyTest, common.AngleSteeringSafetyTest):
|
||||
TX_MSGS = [[0xD0, VOLVO_MAIN_BUS], [0x125, VOLVO_PARTY_BUS], [0x10, VOLVO_MAIN_BUS]]
|
||||
RELAY_MALFUNCTION_ADDRS = {
|
||||
VOLVO_MAIN_BUS: (0xD0,),
|
||||
VOLVO_PARTY_BUS: (0x125,),
|
||||
}
|
||||
FWD_BLACKLISTED_ADDRS = {
|
||||
VOLVO_MAIN_BUS: [0x125],
|
||||
VOLVO_PARTY_BUS: [0xD0],
|
||||
}
|
||||
STANDSTILL_THRESHOLD = 0.1
|
||||
GAS_PRESSED_THRESHOLD = 5.0
|
||||
|
||||
STEER_ANGLE_MAX = 359.9
|
||||
STEER_ANGLE_TEST_MAX = 350.0
|
||||
DEG_TO_CAN = 1 / 0.04395
|
||||
ANGLE_RATE_BP = [7.0, 17.0, 36.0]
|
||||
ANGLE_RATE_UP = [2.0, 0.25, 0.1]
|
||||
ANGLE_RATE_DOWN = [2.0, 0.25, 0.1]
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("volvo_v40_2017_pt")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(SAFETY_VOLVO, VOLVO_FLAG_C1)
|
||||
self.safety.init_tests()
|
||||
|
||||
def _angle_cmd_msg(self, angle: float, enabled: bool, increment_timer: bool = True):
|
||||
values = {
|
||||
"SET_X_E3": 0xE3,
|
||||
"SET_X_B4": 0xB4,
|
||||
"SET_X_08": 0x08,
|
||||
"LKAAngleReq": angle,
|
||||
"LKASteerDirection": 3 if enabled else 0,
|
||||
"TrqLim": 0,
|
||||
"SET_X_25": 0x25,
|
||||
"SET_X_02": 0x02,
|
||||
}
|
||||
|
||||
def fix_checksum(msg):
|
||||
address, data, bus = msg
|
||||
data = bytearray(data)
|
||||
data[6] = create_c1_checksum(data)
|
||||
return address, data, bus
|
||||
|
||||
return self.packer.make_can_msg_safety("FSM1", VOLVO_MAIN_BUS, values, fix_checksum)
|
||||
|
||||
def _angle_meas_msg(self, angle: float):
|
||||
return self.packer.make_can_msg_safety(
|
||||
"PSCM1", VOLVO_MAIN_BUS, {"SteeringAngleServo": angle})
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
return self.packer.make_can_msg_safety(
|
||||
"VehicleSpeed1", VOLVO_MAIN_BUS, {"VehicleSpeed": speed * 3.6})
|
||||
|
||||
def _speed_msg_2(self, speed):
|
||||
return None
|
||||
|
||||
def _user_brake_msg(self, brake):
|
||||
return self.packer.make_can_msg_safety(
|
||||
"PedalandBrake", VOLVO_MAIN_BUS, {"BrakePedalActive2": bool(brake)})
|
||||
|
||||
def _user_gas_msg(self, gas):
|
||||
return self.packer.make_can_msg_safety(
|
||||
"PedalandBrake", VOLVO_MAIN_BUS, {"AccPedal": gas})
|
||||
|
||||
def _pcm_status_msg(self, enable):
|
||||
return self.packer.make_can_msg_safety(
|
||||
"FSM0", VOLVO_PARTY_BUS, {"ACCStatusActive": bool(enable)})
|
||||
|
||||
def test_cancel_button_only(self):
|
||||
allowed = self.packer.make_can_msg_safety(
|
||||
"CCButtons", VOLVO_MAIN_BUS, {"ACCStopBtn": 1})
|
||||
self.assertTrue(self._tx(allowed))
|
||||
|
||||
for signal in ("ACCOnOffBtn", "ACCSetBtn", "ACCResumeBtn", "ACCMinusBtn",
|
||||
"TimeGapIncreaseBtn", "TimeGapDecreaseBtn"):
|
||||
msg = self.packer.make_can_msg_safety("CCButtons", VOLVO_MAIN_BUS, {signal: 1})
|
||||
self.assertFalse(self._tx(msg), signal)
|
||||
|
||||
def test_pscm_relay_cannot_invent_angle(self):
|
||||
for _ in range(common.MAX_SAMPLE_VALS):
|
||||
self._rx(self._angle_meas_msg(10))
|
||||
valid = self.packer.make_can_msg_safety(
|
||||
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": 10})
|
||||
invalid = self.packer.make_can_msg_safety(
|
||||
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": 20})
|
||||
self.assertTrue(self._tx(valid))
|
||||
self.assertFalse(self._tx(invalid))
|
||||
|
||||
def test_pscm_relay_preserves_full_lock_angle(self):
|
||||
for angle in (-720, 500):
|
||||
for _ in range(common.MAX_SAMPLE_VALS):
|
||||
self._rx(self._angle_meas_msg(angle))
|
||||
relayed = self.packer.make_can_msg_safety(
|
||||
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": angle})
|
||||
self.assertTrue(self._tx(relayed), angle)
|
||||
|
||||
def test_steering_static_fields_and_checksum(self):
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_angle_measurement(0)
|
||||
self._reset_speed_measurement(10)
|
||||
self._set_prev_desired_angle(0)
|
||||
valid = self._angle_cmd_msg(0, True)
|
||||
self.assertTrue(self._tx(valid))
|
||||
|
||||
for byte_index in (0, 1, 2, 3, 4, 6, 7):
|
||||
invalid = self._angle_cmd_msg(0, True)
|
||||
invalid[0].data[byte_index] ^= 0x4 if byte_index in (4, 7) else 0x1
|
||||
self.assertFalse(self._tx(invalid), byte_index)
|
||||
|
||||
def test_controller_steering_message_is_allowed(self):
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_angle_measurement(0)
|
||||
self._reset_speed_measurement(10)
|
||||
self._set_prev_desired_angle(0)
|
||||
address, data, bus = create_c1_steering_control(self.packer, 0, True)
|
||||
self.assertTrue(self._tx(libsafety_py.make_CANPacket(address, bus, data)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -652,9 +652,6 @@ class Controls:
|
||||
elif CC.latActive and CS.steeringPressed and CS.steeringTorque * blinker_dir < 0.0 and \
|
||||
self.curvature * blinker_dir > CURVATURE_HOLD_CONFIRM_MIN and \
|
||||
self.turn_blinker_swept < CURVATURE_HOLD_CONFIRM_SWEPT:
|
||||
# an active driver push into the signaled turn BEFORE the turn is made is fresh
|
||||
# turn intent: re-arm the cycle even after a prior handoff. A long blinker-on
|
||||
# approach can latch done on a trivial micro-handoff and lock out
|
||||
# nudge-to-commit ten seconds later at the real turn (0000087f seg 1: +418 haul
|
||||
# unassisted). The swept gate keeps a light same-direction touch during the
|
||||
# EXIT unwind from re-latching a large hold against the model's recentering
|
||||
|
||||
@@ -632,6 +632,9 @@ class LatControlTorque(LatControl):
|
||||
output_torque *= get_kia_ev6_center_output_scale(setpoint, CS.vEgo)
|
||||
elif kia_carnival_active:
|
||||
output_torque *= kia_carnival_center_taper
|
||||
output_torque *= get_kia_carnival_unwind_output_scale(
|
||||
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
|
||||
)
|
||||
output_torque *= get_kia_carnival_highway_transition_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
|
||||
elif palisade_active:
|
||||
output_torque *= get_palisade_center_output_scale(setpoint, CS.vEgo)
|
||||
|
||||
@@ -314,7 +314,7 @@ GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT = 0.14
|
||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05
|
||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0
|
||||
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
|
||||
GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX = 0.08
|
||||
GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX = 0.10
|
||||
GENESIS_G70_CURVE_UNWIND_SPEED = 18.0
|
||||
GENESIS_G70_CURVE_UNWIND_SPEED_WIDTH = 3.0
|
||||
GENESIS_G70_CURVE_UNWIND_LAT = 0.25
|
||||
@@ -629,6 +629,15 @@ KIA_CARNIVAL_UNWIND_FF_OVERSHOOT = 0.08
|
||||
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.06
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK = 0.45
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.20
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_MAX = 0.28
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED = 8.0
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_WIDTH = 2.0
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF = 16.0
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF_WIDTH = 2.5
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT = 0.25
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT_WIDTH = 0.15
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK = 0.45
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK_WIDTH = 0.20
|
||||
|
||||
TUCSON_4TH_GEN_CENTER_TAPER_MAX = 0.44
|
||||
TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28
|
||||
@@ -2854,6 +2863,27 @@ def get_kia_carnival_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
|
||||
return 1.0 - (KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX * speed_weight * overshoot_weight * jerk_weight)
|
||||
|
||||
|
||||
def get_kia_carnival_unwind_output_scale(setpoint: float, measured_lateral_accel: float,
|
||||
desired_lateral_jerk: float, v_ego: float) -> float:
|
||||
if (setpoint * desired_lateral_jerk >= 0.0 or
|
||||
setpoint * measured_lateral_accel <= 0.0):
|
||||
return 1.0
|
||||
|
||||
overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0)
|
||||
if overshoot <= 0.0:
|
||||
return 1.0
|
||||
|
||||
speed_weight = (_sigmoid((v_ego - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED) /
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_WIDTH) *
|
||||
_sigmoid((KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF - v_ego) /
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF_WIDTH))
|
||||
overshoot_weight = _sigmoid((overshoot - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT) /
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT_WIDTH)
|
||||
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK) /
|
||||
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK_WIDTH)
|
||||
return 1.0 - (KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_MAX * speed_weight * overshoot_weight * jerk_weight)
|
||||
|
||||
|
||||
def _tucson_4th_gen_center_weights(desired_lateral_accel: float, v_ego: float) -> tuple[float, float]:
|
||||
speed_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_SPEED_MAX - v_ego) / TUCSON_4TH_GEN_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH)
|
||||
@@ -3126,8 +3156,6 @@ def get_genesis_gv70_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
|
||||
return 1.0
|
||||
|
||||
overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0)
|
||||
if overshoot <= 0.0:
|
||||
return 1.0
|
||||
overshoot_weight = _sigmoid((overshoot - GENESIS_GV70_UNWIND_FF_OVERSHOOT) /
|
||||
GENESIS_GV70_UNWIND_FF_OVERSHOOT_WIDTH)
|
||||
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_GV70_UNWIND_FF_JERK) /
|
||||
|
||||
@@ -38,6 +38,10 @@ HONDA_BOSCH_A_CHALLENGER_STALE_CYCLES = 2
|
||||
HONDA_BOSCH_A_GROSS_DISTANCE_STALE_CYCLES = 3
|
||||
HONDA_BOSCH_A_GROSS_DISTANCE_M = 25.0
|
||||
|
||||
POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES = 3
|
||||
POST_STANDSTILL_RADAR_LEAD_URGENT_TTC = 1.5
|
||||
POST_STANDSTILL_RADAR_LEAD_URGENT_DISTANCE = 1.5
|
||||
|
||||
|
||||
def is_bosch_a_radar_car(CP) -> bool:
|
||||
return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable
|
||||
@@ -242,10 +246,17 @@ def g90_low_speed_radar_lead_sane(track: Track, v_ego: float) -> bool:
|
||||
|
||||
|
||||
def honda_bosch_a_low_speed_radar_lead_sane(track: Track, v_ego: float) -> bool:
|
||||
"""Require a few real Bosch sweeps before a radar-only low-speed takeover."""
|
||||
return track.cnt >= HONDA_BOSCH_A_LOW_SPEED_MIN_COUNT and track.potential_low_speed_lead(v_ego)
|
||||
|
||||
|
||||
def post_standstill_radar_lead_is_urgent(lead: dict[str, Any]) -> bool:
|
||||
d_rel = float(lead.get("dRel", math.inf))
|
||||
v_rel = float(lead.get("vRel", 0.0))
|
||||
closing_speed = max(-v_rel, 0.0)
|
||||
ttc = d_rel / closing_speed if closing_speed > 0.1 else math.inf
|
||||
return d_rel <= POST_STANDSTILL_RADAR_LEAD_URGENT_DISTANCE or ttc <= POST_STANDSTILL_RADAR_LEAD_URGENT_TTC
|
||||
|
||||
|
||||
def track_matches_vision(track: Track, lead: capnp._DynamicStructReader, v_ego: float, *,
|
||||
dist_scale: float, dist_floor: float, vel_limit: float,
|
||||
y_std_scale: float, y_floor: float) -> bool:
|
||||
@@ -454,6 +465,11 @@ class RadarD:
|
||||
self.preferred_stale_track_ids = [-1, -1]
|
||||
self.preferred_challenger_stale_counts = [0, 0]
|
||||
self.preferred_gross_distance_stale_counts = [0, 0]
|
||||
self._was_standstill = False
|
||||
self._standstill_had_lead = False
|
||||
self._post_standstill_gate_active = False
|
||||
self._post_standstill_candidate_id = -1
|
||||
self._post_standstill_candidate_frames = 0
|
||||
|
||||
self.v_ego = 0.0
|
||||
self.v_ego_hist = deque([0.0], maxlen=int(round(delay / DT_MDL)) + 1)
|
||||
@@ -525,9 +541,57 @@ class RadarD:
|
||||
self.prev_lead_track_ids[lead_index] = -1
|
||||
self._reset_preferred_stale_evidence(lead_index)
|
||||
|
||||
def _prepare_post_standstill_gate(self, standstill: bool) -> None:
|
||||
if standstill:
|
||||
self._post_standstill_gate_active = False
|
||||
self._post_standstill_candidate_id = -1
|
||||
self._post_standstill_candidate_frames = 0
|
||||
elif self._was_standstill and not self._standstill_had_lead:
|
||||
self._post_standstill_gate_active = True
|
||||
self._post_standstill_candidate_id = -1
|
||||
self._post_standstill_candidate_frames = 0
|
||||
|
||||
def _filter_post_standstill_lead(self, lead: dict[str, Any]) -> dict[str, Any]:
|
||||
if not self._post_standstill_gate_active:
|
||||
return lead
|
||||
|
||||
model_lead = float(lead.get("modelProb", 0.0)) > float(
|
||||
getattr(self.starpilot_toggles, "lead_detection_probability", 0.35))
|
||||
radar_only = bool(lead.get("status", False) and lead.get("radar", False) and not model_lead)
|
||||
if not radar_only:
|
||||
if lead.get("status", False):
|
||||
self._post_standstill_gate_active = False
|
||||
self._post_standstill_candidate_id = -1
|
||||
self._post_standstill_candidate_frames = 0
|
||||
return lead
|
||||
|
||||
track_id = int(lead.get("radarTrackId", -1))
|
||||
if track_id == self._post_standstill_candidate_id:
|
||||
self._post_standstill_candidate_frames += 1
|
||||
else:
|
||||
self._post_standstill_candidate_id = track_id
|
||||
self._post_standstill_candidate_frames = 1
|
||||
|
||||
persistent = self._post_standstill_candidate_frames >= POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES
|
||||
if persistent or post_standstill_radar_lead_is_urgent(lead):
|
||||
self._post_standstill_gate_active = False
|
||||
return lead
|
||||
|
||||
return {"status": False}
|
||||
|
||||
def _remember_post_standstill_state(self, standstill: bool, lead_status: bool) -> None:
|
||||
if standstill:
|
||||
self._was_standstill = True
|
||||
self._standstill_had_lead |= lead_status
|
||||
else:
|
||||
self._was_standstill = False
|
||||
self._standstill_had_lead = False
|
||||
|
||||
def update(self, sm: messaging.SubMaster, rr: car.RadarData):
|
||||
self.ready = sm.seen['modelV2']
|
||||
self.current_time = 1e-9 * max(sm.logMonoTime.values())
|
||||
standstill = bool(sm['carState'].standstill)
|
||||
self._prepare_post_standstill_gate(standstill)
|
||||
|
||||
if sm.recv_frame['carState'] != self.last_v_ego_frame:
|
||||
self.v_ego = sm['carState'].vEgo
|
||||
@@ -585,11 +649,12 @@ class RadarD:
|
||||
|
||||
self._update_honda_bosch_a_preferred_staleness(i, leads_v3[i], self.lead_prob_filters[i].x)
|
||||
|
||||
self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, sm['modelV2'],
|
||||
sm['carState'].standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=True,
|
||||
g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[0].x,
|
||||
preferred_track_id=self.prev_lead_track_ids[0],
|
||||
honda_bosch_a_radar=self.honda_bosch_a_radar)
|
||||
lead_one = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, sm['modelV2'],
|
||||
standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=True,
|
||||
g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[0].x,
|
||||
preferred_track_id=self.prev_lead_track_ids[0],
|
||||
honda_bosch_a_radar=self.honda_bosch_a_radar)
|
||||
self.radar_state.leadOne = self._filter_post_standstill_lead(lead_one)
|
||||
self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, sm['modelV2'],
|
||||
sm['carState'].standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=False,
|
||||
g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[1].x,
|
||||
@@ -616,6 +681,8 @@ class RadarD:
|
||||
if self.ready:
|
||||
self.starpilot_radar_state.adjacentStopped = get_adjacent_stopped(self.tracks, sm['modelV2'])
|
||||
|
||||
self._remember_post_standstill_state(standstill, bool(self.radar_state.leadOne.status))
|
||||
|
||||
self.starpilot_toggles = get_starpilot_toggles(sm)
|
||||
|
||||
def publish(self, pm: messaging.PubMaster):
|
||||
|
||||
@@ -161,6 +161,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import (
|
||||
get_kia_carnival_friction_threshold,
|
||||
get_kia_carnival_highway_transition_output_scale,
|
||||
get_kia_carnival_unwind_ff_scale,
|
||||
get_kia_carnival_unwind_output_scale,
|
||||
get_kia_stinger_2022_center_taper_scale,
|
||||
get_kia_stinger_2022_friction_threshold,
|
||||
get_tucson_4th_gen_center_taper_scale,
|
||||
@@ -742,6 +743,17 @@ class TestLatControl:
|
||||
low_speed_exit = get_kia_carnival_unwind_ff_scale(0.31, 0.43, -0.88, 11.0)
|
||||
assert low_speed_exit < 0.90
|
||||
|
||||
def test_kia_carnival_unwind_output_scale_is_bounded_and_phase_gated(self):
|
||||
steady_turn = get_kia_carnival_unwind_output_scale(0.80, 0.90, 0.60, 11.0)
|
||||
clean_unwind = get_kia_carnival_unwind_output_scale(0.20, 0.20, -1.5, 11.0)
|
||||
overshooting_unwind = get_kia_carnival_unwind_output_scale(0.20, 0.90, -1.5, 11.0)
|
||||
high_speed_overshoot = get_kia_carnival_unwind_output_scale(0.20, 0.90, -1.5, 25.0)
|
||||
|
||||
assert steady_turn == pytest.approx(1.0)
|
||||
assert clean_unwind == pytest.approx(1.0)
|
||||
assert 0.70 < overshooting_unwind < 1.0
|
||||
assert high_speed_overshoot > overshooting_unwind
|
||||
|
||||
def test_genesis_g90_ff_scale_curve(self):
|
||||
assert get_genesis_g90_ff_scale(0.0, 0.0, 20.0) == 1.0
|
||||
assert get_genesis_g90_ff_scale(0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0)
|
||||
@@ -771,9 +783,13 @@ class TestLatControl:
|
||||
assert base > left_unwind > right_unwind
|
||||
|
||||
def test_genesis_gv70_unwind_ff_scale(self):
|
||||
assert get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, 0.8, 15.0) == 1.0
|
||||
steady_unwind = get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, 0.8, 15.0)
|
||||
assert steady_unwind < 1.0
|
||||
assert get_genesis_gv70_unwind_ff_scale(-0.3, 0.1, 0.8, 15.0) == 1.0
|
||||
assert get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, -0.8, 15.0) == 1.0
|
||||
|
||||
early_unwind = get_genesis_gv70_unwind_ff_scale(-0.7, -0.6, 0.8, 15.0)
|
||||
assert early_unwind < 1.0
|
||||
reduced = get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, 1.0, 20.0)
|
||||
assert 0.6 < reduced < 1.0
|
||||
assert get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, -1.0, 20.0) == 1.0
|
||||
@@ -967,7 +983,7 @@ class TestLatControl:
|
||||
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
|
||||
assert get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 20.0 * 0.44704) > \
|
||||
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
|
||||
assert 0.90 < get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) < 1.0
|
||||
assert 0.88 < get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) < 1.0
|
||||
assert get_genesis_g70_curve_unwind_output_scale(0.7, 0.5, 25.0) == 1.0
|
||||
assert get_genesis_g70_angle_output_scale(55.0, 1.0) > get_genesis_g70_angle_output_scale(85.0, 1.0)
|
||||
assert get_genesis_g70_angle_output_scale(85.0, -1.0) == pytest.approx(1.0)
|
||||
|
||||
@@ -12,6 +12,8 @@ from openpilot.selfdrive.controls.radard import (
|
||||
DT_MDL,
|
||||
HONDA_BOSCH_A_RADAR_TS,
|
||||
RadarD,
|
||||
POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES,
|
||||
post_standstill_radar_lead_is_urgent,
|
||||
g90_low_speed_radar_lead_sane,
|
||||
g90_radar_lead_lateral_sane,
|
||||
has_slow_radar_tracks,
|
||||
@@ -106,6 +108,64 @@ class TestLeads:
|
||||
assert not has_slow_radar_tracks(normal_radar)
|
||||
assert not has_slow_radar_tracks(unavailable_radar)
|
||||
|
||||
@staticmethod
|
||||
def make_radar_only_lead(track_id: int, d_rel: float = 8.0, v_rel: float = 0.0):
|
||||
return {
|
||||
"status": True,
|
||||
"radar": True,
|
||||
"modelProb": 0.0,
|
||||
"radarTrackId": track_id,
|
||||
"dRel": d_rel,
|
||||
"vRel": v_rel,
|
||||
}
|
||||
|
||||
def test_post_standstill_radar_lead_requires_same_track_persistence(self):
|
||||
radar = RadarD()
|
||||
radar._post_standstill_gate_active = True
|
||||
lead = self.make_radar_only_lead(42)
|
||||
|
||||
assert not radar._filter_post_standstill_lead(lead)["status"]
|
||||
assert not radar._filter_post_standstill_lead(lead)["status"]
|
||||
assert radar._filter_post_standstill_lead(lead)["status"]
|
||||
assert radar._post_standstill_candidate_frames == POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES
|
||||
|
||||
def test_post_standstill_radar_lead_resets_for_new_track(self):
|
||||
radar = RadarD()
|
||||
radar._post_standstill_gate_active = True
|
||||
|
||||
assert not radar._filter_post_standstill_lead(self.make_radar_only_lead(42))["status"]
|
||||
assert not radar._filter_post_standstill_lead(self.make_radar_only_lead(43))["status"]
|
||||
assert radar._post_standstill_candidate_frames == 1
|
||||
|
||||
def test_post_standstill_gate_only_arms_after_no_lead_stop(self):
|
||||
radar = RadarD()
|
||||
radar._remember_post_standstill_state(standstill=True, lead_status=False)
|
||||
radar._prepare_post_standstill_gate(standstill=False)
|
||||
assert radar._post_standstill_gate_active
|
||||
|
||||
radar = RadarD()
|
||||
radar._remember_post_standstill_state(standstill=True, lead_status=True)
|
||||
radar._prepare_post_standstill_gate(standstill=False)
|
||||
assert not radar._post_standstill_gate_active
|
||||
|
||||
def test_post_standstill_model_lead_bypasses_gate(self):
|
||||
radar = RadarD()
|
||||
radar._post_standstill_gate_active = True
|
||||
lead = self.make_radar_only_lead(42)
|
||||
lead["modelProb"] = 0.9
|
||||
|
||||
assert radar._filter_post_standstill_lead(lead)["status"]
|
||||
assert not radar._post_standstill_gate_active
|
||||
|
||||
def test_post_standstill_urgent_radar_lead_bypasses_gate(self):
|
||||
radar = RadarD()
|
||||
radar._post_standstill_gate_active = True
|
||||
lead = self.make_radar_only_lead(42, d_rel=3.0, v_rel=-2.1)
|
||||
|
||||
assert post_standstill_radar_lead_is_urgent(lead)
|
||||
assert radar._filter_post_standstill_lead(lead)["status"]
|
||||
assert not radar._post_standstill_gate_active
|
||||
|
||||
@pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd")
|
||||
def test_radar_fault(self):
|
||||
# if there's no radar-related can traffic, radard should either not respond or respond with an error
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.selfdrive.ui import ui_state as ui_state_module
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, **values):
|
||||
self.values = values
|
||||
|
||||
def get_bool(self, key, **_kwargs):
|
||||
return bool(self.values.get(key, False))
|
||||
|
||||
def get_int(self, key, **_kwargs):
|
||||
return int(self.values.get(key, 0))
|
||||
|
||||
|
||||
class PassthroughFilter:
|
||||
def update(self, value):
|
||||
return value
|
||||
|
||||
|
||||
def make_device(monkeypatch, **overrides):
|
||||
values = {
|
||||
"ScreenManagement": True,
|
||||
"ScreenBrightness": 35,
|
||||
"ScreenBrightnessOnroad": 45,
|
||||
"ScreenTimeout": 30,
|
||||
"ScreenTimeoutOnroad": 10,
|
||||
"StandbyMode": False,
|
||||
}
|
||||
values.update(overrides)
|
||||
state = SimpleNamespace(
|
||||
ui_params=FakeParams(**values),
|
||||
status=ui_state_module.UIStatus.DISENGAGED,
|
||||
started=False,
|
||||
ignition=False,
|
||||
light_sensor=-1.0,
|
||||
sm={},
|
||||
)
|
||||
monkeypatch.setattr(ui_state_module, "ui_state", state)
|
||||
monkeypatch.setattr(ui_state_module.gui_app, "big_ui", lambda: False)
|
||||
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [])
|
||||
device = ui_state_module.Device()
|
||||
device._brightness_filter = PassthroughFilter()
|
||||
return device, state
|
||||
|
||||
|
||||
def test_manual_brightness_applies_to_current_device_state(monkeypatch):
|
||||
device, state = make_device(monkeypatch)
|
||||
|
||||
assert device._calculate_brightness() == 35
|
||||
|
||||
state.started = True
|
||||
assert device._calculate_brightness() == 45
|
||||
|
||||
|
||||
def test_auto_brightness_preserves_existing_behavior(monkeypatch):
|
||||
device, state = make_device(monkeypatch, ScreenBrightness=101, ScreenBrightnessOnroad=101)
|
||||
|
||||
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
|
||||
|
||||
state.started = True
|
||||
state.light_sensor = -1.0
|
||||
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
|
||||
|
||||
|
||||
def test_screen_management_off_ignores_custom_values(monkeypatch):
|
||||
device, state = make_device(monkeypatch, ScreenManagement=False, ScreenBrightness=10, ScreenBrightnessOnroad=20,
|
||||
StandbyMode=True)
|
||||
|
||||
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
|
||||
|
||||
state.started = True
|
||||
device._interaction_time = 0
|
||||
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
|
||||
assert device.interactive_timeout == 30
|
||||
|
||||
|
||||
def test_screen_settings_refresh_after_external_param_change(monkeypatch):
|
||||
now = 100.0
|
||||
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
|
||||
device, state = make_device(monkeypatch)
|
||||
state.started = True
|
||||
state.ignition = True
|
||||
device._ignition = True
|
||||
device._interaction_time = now + 10
|
||||
|
||||
state.ui_params.values["ScreenBrightnessOnroad"] = 72
|
||||
state.ui_params.values["ScreenTimeoutOnroad"] = 25
|
||||
now += device.SCREEN_SETTINGS_REFRESH_INTERVAL
|
||||
device._refresh_screen_settings()
|
||||
|
||||
assert device._calculate_brightness() == 72
|
||||
assert device.interactive_timeout == 25
|
||||
assert device._interaction_time == now + 25
|
||||
|
||||
|
||||
def test_standby_blanks_after_timeout_and_touch_wakes(monkeypatch):
|
||||
now = 100.0
|
||||
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
|
||||
device, state = make_device(monkeypatch, StandbyMode=True)
|
||||
state.started = True
|
||||
state.ignition = True
|
||||
device._ignition = True
|
||||
device._interaction_time = now - 1
|
||||
|
||||
assert device._calculate_brightness() == 0
|
||||
|
||||
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [SimpleNamespace(left_down=True)])
|
||||
device._update_wakefulness()
|
||||
|
||||
assert device._interaction_time == now + 10
|
||||
assert device._calculate_brightness() == 45
|
||||
|
||||
|
||||
def test_hide_ui_blanks_after_timeout_and_touch_wakes(monkeypatch):
|
||||
now = 100.0
|
||||
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
|
||||
device, state = make_device(monkeypatch, ScreenBrightnessOnroad=0)
|
||||
state.started = True
|
||||
state.ignition = True
|
||||
device._ignition = True
|
||||
device._interaction_time = now - 1
|
||||
|
||||
assert device._calculate_brightness() == 0
|
||||
|
||||
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [SimpleNamespace(left_down=True)])
|
||||
device._update_wakefulness()
|
||||
|
||||
assert device._interaction_time == now + 10
|
||||
assert device._calculate_brightness() == 5
|
||||
|
||||
|
||||
def test_standby_wakes_for_visible_alert(monkeypatch):
|
||||
now = 100.0
|
||||
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
|
||||
device, state = make_device(monkeypatch, StandbyMode=True)
|
||||
state.started = True
|
||||
state.ignition = True
|
||||
device._ignition = True
|
||||
device._interaction_time = now - 1
|
||||
device._visible_onroad_alert = lambda: True
|
||||
|
||||
device._update_wakefulness()
|
||||
|
||||
assert device._interaction_time == now + 10
|
||||
assert device._calculate_brightness() == 45
|
||||
+101
-10
@@ -297,6 +297,8 @@ class UIState:
|
||||
|
||||
|
||||
class Device:
|
||||
SCREEN_SETTINGS_REFRESH_INTERVAL = 1.0
|
||||
|
||||
def __init__(self):
|
||||
self._ignition = False
|
||||
self._interaction_time: float = -1
|
||||
@@ -306,6 +308,16 @@ class Device:
|
||||
self._awake: bool = True
|
||||
self._params = ui_state.ui_params
|
||||
|
||||
self._screen_settings_refresh_time: float = 0.0
|
||||
self._screen_management = False
|
||||
self._screen_brightness = 101
|
||||
self._screen_brightness_onroad = 101
|
||||
self._screen_timeout = 30
|
||||
self._screen_timeout_onroad = 30
|
||||
self._standby_mode = False
|
||||
self._last_status = ui_state.status
|
||||
self._refresh_screen_settings(force=True)
|
||||
|
||||
self._offroad_brightness: int = BACKLIGHT_OFFROAD
|
||||
self._last_brightness: int = 0
|
||||
self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps)
|
||||
@@ -325,8 +337,8 @@ class Device:
|
||||
if self._override_interactive_timeout is not None:
|
||||
return self._override_interactive_timeout
|
||||
|
||||
timeout_onroad = self._params.get_int("ScreenTimeoutOnroad", return_default=True)
|
||||
timeout_offroad = self._params.get_int("ScreenTimeout", return_default=True)
|
||||
timeout_onroad = self._screen_timeout_onroad
|
||||
timeout_offroad = self._screen_timeout
|
||||
|
||||
if timeout_onroad <= 0:
|
||||
timeout_onroad = 10 if gui_app.big_ui() else 5
|
||||
@@ -345,12 +357,54 @@ class Device:
|
||||
self._interactive_timeout_callbacks.append(callback)
|
||||
|
||||
def update(self):
|
||||
self._refresh_screen_settings()
|
||||
|
||||
# do initial reset
|
||||
if self._interaction_time <= 0:
|
||||
self._reset_interactive_timeout()
|
||||
|
||||
self._update_brightness()
|
||||
self._update_wakefulness()
|
||||
self._update_brightness()
|
||||
|
||||
def _refresh_screen_settings(self, force: bool = False) -> None:
|
||||
now = time.monotonic()
|
||||
if not force and now - self._screen_settings_refresh_time < self.SCREEN_SETTINGS_REFRESH_INTERVAL:
|
||||
return
|
||||
|
||||
previous = (
|
||||
self._screen_management,
|
||||
self._screen_brightness,
|
||||
self._screen_brightness_onroad,
|
||||
self._screen_timeout,
|
||||
self._screen_timeout_onroad,
|
||||
self._standby_mode,
|
||||
)
|
||||
|
||||
self._screen_management = self._params.get_bool("ScreenManagement")
|
||||
if self._screen_management:
|
||||
self._screen_brightness = min(max(self._params.get_int("ScreenBrightness", return_default=True), 0), 101)
|
||||
self._screen_brightness_onroad = min(max(self._params.get_int("ScreenBrightnessOnroad", return_default=True), 0), 101)
|
||||
self._screen_timeout = self._params.get_int("ScreenTimeout", return_default=True)
|
||||
self._screen_timeout_onroad = self._params.get_int("ScreenTimeoutOnroad", return_default=True)
|
||||
self._standby_mode = self._params.get_bool("StandbyMode")
|
||||
else:
|
||||
self._screen_brightness = 101
|
||||
self._screen_brightness_onroad = 101
|
||||
self._screen_timeout = 30
|
||||
self._screen_timeout_onroad = 30
|
||||
self._standby_mode = False
|
||||
|
||||
self._screen_settings_refresh_time = now
|
||||
current = (
|
||||
self._screen_management,
|
||||
self._screen_brightness,
|
||||
self._screen_brightness_onroad,
|
||||
self._screen_timeout,
|
||||
self._screen_timeout_onroad,
|
||||
self._standby_mode,
|
||||
)
|
||||
if previous != current and self._interaction_time > 0:
|
||||
self._reset_interactive_timeout()
|
||||
|
||||
def set_offroad_brightness(self, brightness: int | None):
|
||||
if brightness is None:
|
||||
@@ -358,6 +412,15 @@ class Device:
|
||||
self._offroad_brightness = min(max(brightness, 0), 100)
|
||||
|
||||
def _update_brightness(self):
|
||||
brightness = self._calculate_brightness()
|
||||
|
||||
if brightness != self._last_brightness:
|
||||
if self._brightness_thread is None or not self._brightness_thread.is_alive():
|
||||
self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,))
|
||||
self._brightness_thread.start()
|
||||
self._last_brightness = brightness
|
||||
|
||||
def _calculate_brightness(self) -> int:
|
||||
clipped_brightness = self._offroad_brightness
|
||||
|
||||
if ui_state.started and ui_state.light_sensor >= 0:
|
||||
@@ -374,19 +437,26 @@ class Device:
|
||||
brightness = round(self._brightness_filter.update(clipped_brightness))
|
||||
if not self._awake:
|
||||
brightness = 0
|
||||
elif ui_state.started and self._standby_mode and time.monotonic() > self._interaction_time:
|
||||
brightness = 0
|
||||
elif ui_state.started and self._screen_brightness_onroad != 101:
|
||||
brightness = max(5, self._screen_brightness_onroad) if time.monotonic() <= self._interaction_time else self._screen_brightness_onroad
|
||||
elif not ui_state.started and self._screen_brightness != 101:
|
||||
brightness = self._screen_brightness
|
||||
|
||||
if brightness != self._last_brightness:
|
||||
if self._brightness_thread is None or not self._brightness_thread.is_alive():
|
||||
self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,))
|
||||
self._brightness_thread.start()
|
||||
self._last_brightness = brightness
|
||||
return brightness
|
||||
|
||||
def _update_wakefulness(self):
|
||||
# Handle interactive timeout
|
||||
ignition_just_turned_off = not ui_state.ignition and self._ignition
|
||||
ignition_state_changed = ui_state.ignition != self._ignition
|
||||
self._ignition = ui_state.ignition
|
||||
|
||||
if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events):
|
||||
status_changed = ui_state.status != self._last_status and ui_state.status != UIStatus.OVERRIDE
|
||||
self._last_status = ui_state.status
|
||||
wake_for_onroad_event = (ui_state.started and self._standby_mode and self._screen_brightness_onroad != 0 and
|
||||
(status_changed or self._visible_onroad_alert()))
|
||||
|
||||
if ignition_state_changed or any(ev.left_down for ev in gui_app.mouse_events) or wake_for_onroad_event:
|
||||
self._reset_interactive_timeout()
|
||||
|
||||
interaction_timeout = time.monotonic() > self._interaction_time
|
||||
@@ -397,6 +467,27 @@ class Device:
|
||||
|
||||
self._set_awake(ui_state.ignition or not interaction_timeout or PC)
|
||||
|
||||
@staticmethod
|
||||
def _visible_onroad_alert() -> bool:
|
||||
if not ui_state.started:
|
||||
return False
|
||||
|
||||
sm = ui_state.sm
|
||||
try:
|
||||
selfdrive_state = sm["selfdriveState"]
|
||||
if selfdrive_state.alertSize != log.SelfdriveState.AlertSize.none:
|
||||
return True
|
||||
if selfdrive_state.alertStatus != log.SelfdriveState.AlertStatus.normal:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
starpilot_state = sm["starpilotSelfdriveState"]
|
||||
return getattr(starpilot_state.alertSize, "raw", 0) != 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
self._awake = on
|
||||
|
||||
@@ -37,6 +37,14 @@ CURVATURE_LOOKAHEAD_MAX = 0.40
|
||||
FORD_CURVATURE_LOOKAHEAD = {
|
||||
CAR.FORD_EXPLORER_MK6: 0.20,
|
||||
}
|
||||
FORD_CONSERVATIVE_PREVIEW_CARS = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
})
|
||||
FORD_MANUAL_TURN_LATCH_CARS = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
})
|
||||
MANUAL_TURN_ENTRY_ANGLE_DEG = 12.0
|
||||
MANUAL_TURN_RECOVERY_SECONDS = 0.25
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -101,6 +109,8 @@ class FordLateralController:
|
||||
self.curvature_lane_change_factor = 0.85
|
||||
|
||||
self.human_turn = HumanTurnDetector()
|
||||
self.manual_turn_latched = False
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
self.curvature_samples = deque(maxlen=max(2, round(0.3 / STEER_DT)))
|
||||
self.curvature_last = 0.0
|
||||
self._frame = 0
|
||||
@@ -151,8 +161,13 @@ class FordLateralController:
|
||||
def _current_curvature(CS) -> float:
|
||||
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
|
||||
def _blend_and_scale(self, desired: float, predicted: float, v_ego: float) -> tuple[float, int]:
|
||||
def _blend_and_scale(self, desired: float, predicted: float, v_ego: float, current: float = 0.0) -> tuple[float, int]:
|
||||
blend = float(np.interp(abs(desired), [0.0, 0.001], [self.curvature_blend_low, self.curvature_blend_high]))
|
||||
if self.CP.carFingerprint in FORD_CONSERVATIVE_PREVIEW_CARS:
|
||||
if desired * predicted <= 0.0:
|
||||
blend = 0.0
|
||||
elif current * predicted > 0.0 and abs(current) > abs(desired) and abs(predicted) > abs(desired):
|
||||
blend *= abs(desired) / abs(predicted)
|
||||
requested = predicted * blend + desired * (1.0 - blend)
|
||||
lane_change, direction = self._lane_change()
|
||||
precision = 1
|
||||
@@ -166,26 +181,62 @@ class FordLateralController:
|
||||
def _manual_turn(self, CC, CS) -> bool:
|
||||
if not CC.latActive:
|
||||
self.human_turn.reset()
|
||||
self.manual_turn_latched = False
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
return False
|
||||
return self.human_turn.update(
|
||||
detected = self.human_turn.update(
|
||||
self.human_turn_enabled, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
||||
if self.CP.carFingerprint not in FORD_MANUAL_TURN_LATCH_CARS:
|
||||
return detected
|
||||
|
||||
if not self.human_turn_enabled:
|
||||
self.manual_turn_latched = False
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
return False
|
||||
|
||||
blinker_direction = float(CS.out.rightBlinker) - float(CS.out.leftBlinker)
|
||||
driver_turning_with_signal = (
|
||||
CS.out.steeringPressed and abs(CS.out.steeringAngleDeg) >= MANUAL_TURN_ENTRY_ANGLE_DEG and
|
||||
blinker_direction != 0.0 and not self._lane_change()[0] and
|
||||
CS.out.steeringTorque * blinker_direction < 0.0
|
||||
)
|
||||
if detected or driver_turning_with_signal:
|
||||
self.manual_turn_latched = True
|
||||
|
||||
if not self.manual_turn_latched:
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
return False
|
||||
|
||||
if CS.out.steeringPressed or blinker_direction != 0.0:
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
else:
|
||||
self.manual_turn_recovery_timer += STEER_DT
|
||||
if self.manual_turn_recovery_timer + 1e-9 >= MANUAL_TURN_RECOVERY_SECONDS:
|
||||
self.manual_turn_latched = False
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
|
||||
return self.manual_turn_latched
|
||||
|
||||
def update(self, CC, CS, actuators) -> FordLateralResult:
|
||||
current = self._current_curvature(CS)
|
||||
if not CC.latActive:
|
||||
self.human_turn.reset()
|
||||
self.manual_turn_latched = False
|
||||
self.manual_turn_recovery_timer = 0.0
|
||||
self.curvature_samples.clear()
|
||||
self.curvature_last = 0.0
|
||||
return FordLateralResult()
|
||||
|
||||
if self._manual_turn(CC, CS) or CS.out.vEgoRaw < 0.1:
|
||||
manual_turn = self._manual_turn(CC, CS)
|
||||
if manual_turn or CS.out.vEgoRaw < 0.1:
|
||||
self.curvature_samples.clear()
|
||||
self.curvature_last = 0.0
|
||||
return FordLateralResult(active=True)
|
||||
return FordLateralResult(active=not (
|
||||
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())
|
||||
requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego)
|
||||
requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego, current)
|
||||
|
||||
if v_ego > 9.0:
|
||||
requested = float(np.clip(requested, current - CarControllerParams.CURVATURE_ERROR,
|
||||
|
||||
@@ -32,12 +32,16 @@ def controller(monkeypatch):
|
||||
return controller
|
||||
|
||||
|
||||
def car_state(speed=15.0, curvature=0.0, steering_pressed=False, steering_angle=0.0):
|
||||
def car_state(speed=15.0, curvature=0.0, steering_pressed=False, steering_angle=0.0,
|
||||
steering_torque=0.0, left_blinker=False, right_blinker=False):
|
||||
return SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=speed,
|
||||
yawRate=-curvature * speed,
|
||||
steeringPressed=steering_pressed,
|
||||
steeringAngleDeg=steering_angle,
|
||||
steeringTorque=steering_torque,
|
||||
leftBlinker=left_blinker,
|
||||
rightBlinker=right_blinker,
|
||||
))
|
||||
|
||||
|
||||
@@ -114,6 +118,36 @@ def test_curvature_strategy_uses_learned_lookahead(controller, monkeypatch):
|
||||
assert lookaheads == [pytest.approx(0.38)]
|
||||
|
||||
|
||||
def test_mach_e_preview_does_not_override_opposite_current_path(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
requested, _ = controller._blend_and_scale(-0.0001, 0.002, 20.0)
|
||||
|
||||
assert requested == pytest.approx(-0.0001)
|
||||
|
||||
|
||||
def test_mach_e_preview_is_reduced_when_ahead_of_current_path(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
requested, _ = controller._blend_and_scale(0.0005, 0.002, 20.0, current=0.0015)
|
||||
|
||||
assert requested == pytest.approx(0.00065)
|
||||
|
||||
|
||||
def test_mach_e_preview_remains_available_on_curve_entry(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
requested, _ = controller._blend_and_scale(0.0005, 0.002, 20.0, current=0.0002)
|
||||
|
||||
assert requested == pytest.approx(0.0011)
|
||||
|
||||
|
||||
def test_non_mach_e_preview_blend_is_unchanged(controller):
|
||||
requested, _ = controller._blend_and_scale(-0.0001, 0.002, 20.0, current=0.0015)
|
||||
|
||||
assert requested == pytest.approx(0.00074)
|
||||
|
||||
|
||||
def test_lane_change_accepts_capnp_enum_wrappers(controller):
|
||||
controller.model = SimpleNamespace(meta=SimpleNamespace(
|
||||
laneChangeState=SimpleNamespace(raw=2),
|
||||
@@ -146,3 +180,87 @@ def test_curvature_manual_turn_keeps_session_active_with_neutral_command(control
|
||||
|
||||
assert result.active
|
||||
assert result.curvature == 0.0
|
||||
|
||||
|
||||
def test_mach_e_signaled_manual_turn_yields_until_inputs_settle(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
controller.human_turn_enabled = True
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
actuators = SimpleNamespace(curvature=0.006)
|
||||
|
||||
result = controller.update(CC, car_state(
|
||||
steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
|
||||
right_blinker=True), actuators)
|
||||
assert not result.active
|
||||
assert result.curvature == 0.0
|
||||
|
||||
result = controller.update(CC, car_state(
|
||||
steering_pressed=True, steering_angle=5.0, steering_torque=2.0,
|
||||
right_blinker=True), actuators)
|
||||
assert not result.active
|
||||
|
||||
for _ in range(4):
|
||||
result = controller.update(CC, car_state(), actuators)
|
||||
assert not result.active
|
||||
|
||||
result = controller.update(CC, car_state(), actuators)
|
||||
assert result.active
|
||||
assert result.curvature > 0.0
|
||||
|
||||
|
||||
def test_mach_e_opposite_blinker_correction_does_not_start_manual_turn(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
result = controller.update(
|
||||
SimpleNamespace(latActive=True),
|
||||
car_state(steering_pressed=True, steering_angle=-15.0, steering_torque=2.0,
|
||||
right_blinker=True),
|
||||
SimpleNamespace(curvature=0.001),
|
||||
)
|
||||
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_mach_e_lane_change_nudge_does_not_start_manual_turn(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
controller.model = SimpleNamespace(
|
||||
orientationRate=SimpleNamespace(z=[0.0] * 33),
|
||||
meta=SimpleNamespace(
|
||||
laneChangeState=SimpleNamespace(raw=2),
|
||||
laneChangeDirection=SimpleNamespace(raw=2),
|
||||
),
|
||||
)
|
||||
|
||||
result = controller.update(
|
||||
SimpleNamespace(latActive=True),
|
||||
car_state(steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
|
||||
right_blinker=True),
|
||||
SimpleNamespace(curvature=0.001),
|
||||
)
|
||||
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_mach_e_small_blinker_nudge_does_not_start_manual_turn(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
|
||||
result = controller.update(
|
||||
SimpleNamespace(latActive=True),
|
||||
car_state(steering_pressed=True, steering_angle=-5.0, steering_torque=-2.0,
|
||||
right_blinker=True),
|
||||
SimpleNamespace(curvature=0.001),
|
||||
)
|
||||
|
||||
assert result.active
|
||||
|
||||
|
||||
def test_mach_e_manual_turn_latch_resets_with_lateral_control(controller):
|
||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||
actuators = SimpleNamespace(curvature=0.001)
|
||||
turning = car_state(
|
||||
steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
|
||||
right_blinker=True)
|
||||
|
||||
assert not controller.update(SimpleNamespace(latActive=True), turning, actuators).active
|
||||
assert not controller.update(SimpleNamespace(latActive=False), turning, actuators).active
|
||||
assert controller.update(SimpleNamespace(latActive=True), car_state(), actuators).active
|
||||
|
||||
@@ -16,7 +16,7 @@ from cereal import car, custom, log
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.chrysler.values import JEEPS as CHRYSLER_JEEPS
|
||||
from opendbc.car.gm.values import CAR as GM_CAR, EV_CAR as GM_EV_CAR, GMFlags
|
||||
from opendbc.car.gm.values import CAR as GM_CAR, EV_CAR as GM_EV_CAR, GM_AUTO_HOLD_CARS, GMFlags
|
||||
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, EV_CAR as HYUNDAI_EV_CAR, HyundaiFlags, HyundaiStarPilotSafetyFlags
|
||||
from opendbc.car.interfaces import TORQUE_SUBSTITUTE_PATH, CarInterfaceBase, GearShifter
|
||||
from opendbc.car.mock.values import CAR as MOCK
|
||||
@@ -1406,7 +1406,7 @@ class StarPilotVariables:
|
||||
|
||||
screen_management = self.get_value("ScreenManagement")
|
||||
toggle.screen_brightness = max(self.get_value("ScreenBrightness", cast=float, condition=screen_management), 1)
|
||||
toggle.screen_brightness_onroad = self.get_value("ScreenBrightnessOnroad", cast=float, condition=(screen_management and not toggle.force_onroad), min=1)
|
||||
toggle.screen_brightness_onroad = self.get_value("ScreenBrightnessOnroad", cast=float, condition=(screen_management and not toggle.force_onroad), min=0)
|
||||
toggle.screen_recorder = self.get_value("ScreenRecorder", condition=screen_management) or toggle.debug_mode
|
||||
toggle.screen_timeout = self.get_value("ScreenTimeout", cast=float, condition=screen_management)
|
||||
toggle.screen_timeout_onroad = self.get_value("ScreenTimeoutOnroad", cast=float, condition=screen_management)
|
||||
@@ -1600,9 +1600,9 @@ class StarPilotVariables:
|
||||
)
|
||||
toggle.remote_start_boots_comma = self.get_value("RemoteStartBootsComma", condition=toggle.car_make == "gm")
|
||||
|
||||
gm_auto_hold_supported = toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS
|
||||
gm_auto_hold_supported = toggle.car_model in GM_AUTO_HOLD_CARS
|
||||
toggle.gm_auto_hold = self.get_value("GMAutoHold", condition=gm_auto_hold_supported)
|
||||
toggle.volt_one_pedal_mode = self.get_value("VoltOnePedalMode", condition=gm_auto_hold_supported)
|
||||
toggle.volt_one_pedal_mode = self.get_value("VoltOnePedalMode", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS)
|
||||
|
||||
toggle.volt_sng = self.get_value("VoltSNG", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const VEHICLE_SETTING_MAKES = {
|
||||
RemoteStartBootsComma: GM_MAKES,
|
||||
HKGRemoteStartBootsComma: HKG_MAKES,
|
||||
VoltSNG: ["Chevrolet", "Holden"],
|
||||
GMAutoHold: ["Chevrolet", "Holden"],
|
||||
GMAutoHold: ["Buick", "Chevrolet", "Holden"],
|
||||
VoltOnePedalMode: ["Chevrolet", "Holden"],
|
||||
RemapCancelToDistance: ["Chevrolet", "Holden"],
|
||||
JeepBrakeHold: ["Jeep"],
|
||||
@@ -490,7 +490,7 @@ function numericBounds(param) {
|
||||
return { min: 1, max: 101, step: 1 }
|
||||
}
|
||||
if (param.key === "ScreenBrightnessOnroad") {
|
||||
return { min: 1, max: 101, step: 1 }
|
||||
return { min: 0, max: 101, step: 1 }
|
||||
}
|
||||
|
||||
if (param.key === "LaneCenterOffset") {
|
||||
|
||||
@@ -62,7 +62,7 @@ const VEHICLE_SETTING_MAKES = {
|
||||
RemoteStartBootsComma: ["Buick", "Cadillac", "Chevrolet", "GMC", "Holden"],
|
||||
HKGRemoteStartBootsComma: ["Genesis", "Hyundai", "Kia"],
|
||||
VoltSNG: ["Chevrolet", "Holden"],
|
||||
GMAutoHold: ["Chevrolet", "Holden"],
|
||||
GMAutoHold: ["Buick", "Chevrolet", "Holden"],
|
||||
VoltOnePedalMode: ["Chevrolet", "Holden"],
|
||||
RemapCancelToDistance: ["Chevrolet", "Holden"],
|
||||
JeepBrakeHold: ["Jeep"],
|
||||
@@ -140,9 +140,12 @@ export function numericBounds(param, values = {}) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
if (param.key === "ScreenBrightness" || param.key === "ScreenBrightnessOnroad") {
|
||||
if (param.key === "ScreenBrightness") {
|
||||
return { min: 1, max: 101, step: 1 }
|
||||
}
|
||||
if (param.key === "ScreenBrightnessOnroad") {
|
||||
return { min: 0, max: 101, step: 1 }
|
||||
}
|
||||
if (param.key === "LaneCenterOffset") {
|
||||
return { min: -0.3, max: 0.3, step: 0.01 }
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const SystemTools = {
|
||||
fastStatus: null,
|
||||
checkedForUpdates: false,
|
||||
busy: "",
|
||||
autoUpdateBusy: false,
|
||||
profiles: [],
|
||||
profileBusy: "",
|
||||
}
|
||||
@@ -77,6 +78,7 @@ export const SystemTools = {
|
||||
const status = await api.getUpdateFastStatus()
|
||||
if (!status) throw new Error("Update status unavailable")
|
||||
this.fastStatus = status
|
||||
this.isOnroad = !!status.isOnroad
|
||||
} catch (e) {
|
||||
this.fastStatus = null
|
||||
if (throwOnError) throw e
|
||||
@@ -195,6 +197,21 @@ export const SystemTools = {
|
||||
this.busy = ""
|
||||
}
|
||||
},
|
||||
async setAutomaticUpdates(enabled) {
|
||||
if (this.autoUpdateBusy || this.isOnroad || this.fastStatus?.running || !this.fastStatus) return
|
||||
const previous = !!this.fastStatus.automaticUpdates
|
||||
this.autoUpdateBusy = true
|
||||
this.fastStatus = { ...this.fastStatus, automaticUpdates: !!enabled }
|
||||
try {
|
||||
const payload = await api.updateParam({ key: "AutomaticUpdates", value: !!enabled, label: "Automatic Updates" })
|
||||
showSnackbar(payload?.message || `Automatic updates ${enabled ? "enabled" : "disabled"}.`)
|
||||
} catch (e) {
|
||||
this.fastStatus = { ...this.fastStatus, automaticUpdates: previous }
|
||||
showSnackbar(e?.message || "Failed to update Automatic Updates.", "error")
|
||||
} finally {
|
||||
this.autoUpdateBusy = false
|
||||
}
|
||||
},
|
||||
async applyFastUpdate() {
|
||||
if (this.busy || this.isOnroad) return
|
||||
if (this.fastStatus?.running) { showSnackbar("Fast update is already running."); return }
|
||||
@@ -301,6 +318,22 @@ export const SystemTools = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gx-card" style="margin-bottom:12px;">
|
||||
<div class="gx-row" style="border-top:none;">
|
||||
<div class="gx-row__info">
|
||||
<span class="gx-row__label">Automatically Install Updates</span>
|
||||
<span class="gx-row__desc">Install updates automatically while parked with an active internet connection.</span>
|
||||
</div>
|
||||
<label class="gx-switch">
|
||||
<input type="checkbox" :checked="!!fastStatus?.automaticUpdates"
|
||||
:disabled="!fastStatus || isOnroad || autoUpdateBusy || !!fastStatus?.running"
|
||||
@change="setAutomaticUpdates($event.target.checked)" />
|
||||
<span class="gx-switch__track"></span>
|
||||
<span class="gx-switch__thumb"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gx-card" style="margin-bottom:12px;">
|
||||
<div class="gx-section__header"><i class="bi bi-git-branch"></i><span class="gx-section__title">Switch Branch</span></div>
|
||||
<div style="padding: var(--sp-3);">
|
||||
|
||||
@@ -80,6 +80,14 @@ def test_ui_ports_developer_mode_gating():
|
||||
assert "isAdvancedHiddenByDeveloperMode" in params
|
||||
|
||||
|
||||
def test_ui_exposes_gm_auto_hold_to_buick():
|
||||
params = _read("js/params.js")
|
||||
device_settings = (REPO_ROOT / "starpilot/system/the_galaxy/assets/components/tools/device_settings.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'GMAutoHold: ["Buick", "Chevrolet", "Holden"]' in params
|
||||
assert 'GMAutoHold: ["Buick", "Chevrolet", "Holden"]' in device_settings
|
||||
|
||||
|
||||
def test_ui_restores_hierarchical_sub_toggle_rendering():
|
||||
# Children must nest under parents via the recursive SettingTree, gated on
|
||||
# the parent being enabled AND expanded (classic renderSettingTree contract).
|
||||
@@ -509,6 +517,10 @@ def test_ui_mobile_polish_regressions():
|
||||
assert "checkedForUpdates && !!this.fastStatus?.updateAvailable" in system
|
||||
assert "gx-update-progress__fill" in system
|
||||
assert "linear-gradient(90deg, #5ec8c8 0%, #8b6cc5 100%)" in css
|
||||
assert "Automatically Install Updates" in system
|
||||
assert 'key: "AutomaticUpdates"' in system
|
||||
assert "!!fastStatus?.automaticUpdates" in system
|
||||
assert "isOnroad || autoUpdateBusy || !!fastStatus?.running" in system
|
||||
|
||||
bluetooth = _read("js/components/BluetoothPanel.js")
|
||||
assert "methods: {\n address," in bluetooth
|
||||
|
||||
Reference in New Issue
Block a user