From e3935c07ebbe0f8f0e9caa09237e2a633cca023e Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 1 May 2026 12:53:20 -0700 Subject: [PATCH 01/69] move can ignition to opendbc --- opendbc/safety/ignition.h | 61 +++++++++++++ .../safety/tests/libsafety/libsafety_py.py | 4 + opendbc/safety/tests/libsafety/safety.c | 17 ++++ opendbc/safety/tests/test_ignition.py | 90 +++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 opendbc/safety/ignition.h create mode 100644 opendbc/safety/tests/test_ignition.py diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h new file mode 100644 index 00000000000..f9be1070576 --- /dev/null +++ b/opendbc/safety/ignition.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include "opendbc/safety/can.h" + +static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, uint32_t *ignition_can_cnt) { + if (msg->bus == 0U) { + int len = GET_LEN(msg); + + // GM exception + if ((msg->addr == 0x1F1U) && (len == 8)) { + // SystemPowerMode (2=Run, 3=Crank Request) + *ignition_can = (msg->data[0] & 0x2U) != 0U; + *ignition_can_cnt = 0U; + } + + // Rivian R1S/T GEN1 exception + if ((msg->addr == 0x152U) && (len == 8)) { + // 0x152 overlaps with Subaru pre-global which has this bit as the high beam + int counter = msg->data[1] & 0xFU; // max is only 14 + + static int prev_counter_rivian = -1; + if ((counter == ((prev_counter_rivian + 1) % 15)) && (prev_counter_rivian != -1)) { + // VDM_OutputSignals->VDM_EpasPowerMode + *ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 + *ignition_can_cnt = 0U; + } + prev_counter_rivian = counter; + } + + // Tesla Model 3/Y exception + if ((msg->addr == 0x221U) && (len == 8)) { + // 0x221 overlaps with Rivian which has random data on byte 0 + int counter = msg->data[6] >> 4; + + static int prev_counter_tesla = -1; + if ((counter == ((prev_counter_tesla + 1) % 16)) && (prev_counter_tesla != -1)) { + // VCFRONT_LVPowerState->VCFRONT_vehiclePowerState + int power_state = (msg->data[0] >> 5U) & 0x3U; + *ignition_can = power_state == 0x3; // VEHICLE_POWER_STATE_DRIVE=3 + *ignition_can_cnt = 0U; + } + prev_counter_tesla = counter; + } + + // Mazda exception + if ((msg->addr == 0x9EU) && (len == 8)) { + *ignition_can = (msg->data[0] >> 5) == 0x6U; + *ignition_can_cnt = 0U; + } + } + + // TODO: this is too loose, Teslas have 0x222 + // body v2 exception + // if (((msg->bus == 0U) || (msg->bus == 2U)) && (msg->addr == 0x222U)) { + // *ignition_can = true; + // *ignition_can_cnt = 0U; + // } +} diff --git a/opendbc/safety/tests/libsafety/libsafety_py.py b/opendbc/safety/tests/libsafety/libsafety_py.py index 7d88bdf301e..01eeb3b0f75 100644 --- a/opendbc/safety/tests/libsafety/libsafety_py.py +++ b/opendbc/safety/tests/libsafety/libsafety_py.py @@ -110,6 +110,10 @@ class CANPacket: void mutation_set_active_mutant(int id); int mutation_get_active_mutant(void); + +void ignition_can_hook_test(const CANPacket_t *msg); +bool get_ignition_can(void); +void reset_ignition_can(void); """) class LibSafety: diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index afb7054448d..469e2aebab0 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -11,6 +11,23 @@ uint32_t microsecond_timer_get(void) { #include "opendbc/safety/can.h" #include "opendbc/safety/safety.h" +#include "opendbc/safety/ignition.h" + +static bool test_ignition_can = false; +static uint32_t test_ignition_can_cnt = 0U; + +void ignition_can_hook_test(const CANPacket_t *msg) { + ignition_can_hook(msg, &test_ignition_can, &test_ignition_can_cnt); +} + +bool get_ignition_can(void) { + return test_ignition_can; +} + +void reset_ignition_can(void) { + test_ignition_can = false; + test_ignition_can_cnt = 0U; +} void safety_tick_current_safety_config() { safety_tick(¤t_safety_config); diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py new file mode 100644 index 00000000000..6d2ae2de877 --- /dev/null +++ b/opendbc/safety/tests/test_ignition.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import unittest + +from opendbc.safety.tests.common import CANPackerSafety, make_msg +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestIgnitionHook(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.reset_ignition_can() + self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") + self.rivian_packer = CANPackerSafety("rivian_primary_actuator") + self.tesla_packer = CANPackerSafety("tesla_model3_party") + + def _hook(self, msg): + self.safety.ignition_can_hook_test(msg) + + def _ign(self): + return self.safety.get_ignition_can() + + # GM: SystemPowerMode 2=Run, 3=Crank Request + def test_gm_ignition_on(self): + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self._ign()) + + def test_gm_ignition_off(self): + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self._ign()) + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 0})) + self.assertFalse(self._ign()) + + # Rivian: VDM_EpasPowerMode_Drive_On=1 + def test_rivian_ignition_on(self): + for i in range(15): + self.safety.reset_ignition_can() + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": i, "VDM_EpasPowerMode": 1})) + self.assertFalse(self._ign()) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": (i + 1) % 15, "VDM_EpasPowerMode": 1})) + self.assertTrue(self._ign()) + + def test_rivian_ignition_off(self): + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 0})) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 0})) + self.assertFalse(self._ign()) + + # Tesla: VEHICLE_POWER_STATE_DRIVE=3 + def test_tesla_ignition_on(self): + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self.assertFalse(self._ign()) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.assertTrue(self._ign()) + + def test_tesla_ignition_off(self): + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 2})) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 2})) + self.assertFalse(self._ign()) + + # Mazda: 0x9E byte 0 high 3 bits == 6 + def test_mazda_ignition_on(self): + self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self._ign()) + + def test_mazda_ignition_off(self): + self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self._ign()) + self._hook(make_msg(0, 0x9E, dat=b"\x20" + b"\x00" * 7)) + self.assertFalse(self._ign()) + + def test_wrong_bus_ignored(self): + self._hook(make_msg(1, 0x1F1, dat=b"\x02" + b"\x00" * 7)) + self.assertFalse(self._ign()) + + def test_unknown_addr_ignored(self): + self._hook(make_msg(0, 0x123, dat=b"\xFF" * 8)) + self.assertFalse(self._ign()) + + +if __name__ == "__main__": + unittest.main() From 05ba9834a83e109f183b2525ef2aec04bf039d92 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 16:05:09 -0700 Subject: [PATCH 02/69] volkswagen: port ID.4 MK1 (MEB, lateral only) (cherry picked from commit 5a07de2b68321733b9a338c8a72008baf73b10e7) --- opendbc/car/tests/routes.py | 1 + opendbc/car/torque_data/substitute.toml | 1 + opendbc/car/volkswagen/carcontroller.py | 60 +++++-- opendbc/car/volkswagen/carstate.py | 73 ++++++++ opendbc/car/volkswagen/fingerprints.py | 12 ++ opendbc/car/volkswagen/interface.py | 26 ++- opendbc/car/volkswagen/mebcan.py | 49 ++++++ opendbc/car/volkswagen/values.py | 52 +++++- opendbc/safety/modes/volkswagen_meb.h | 176 ++++++++++++++++++++ opendbc/safety/safety.h | 2 + opendbc/safety/tests/test_volkswagen_meb.py | 75 +++++++++ 11 files changed, 512 insertions(+), 15 deletions(-) create mode 100644 opendbc/car/volkswagen/mebcan.py create mode 100644 opendbc/safety/modes/volkswagen_meb.h create mode 100644 opendbc/safety/tests/test_volkswagen_meb.py diff --git a/opendbc/car/tests/routes.py b/opendbc/car/tests/routes.py index 2fe80c0f781..8a93b419183 100644 --- a/opendbc/car/tests/routes.py +++ b/opendbc/car/tests/routes.py @@ -277,6 +277,7 @@ class CarTestRoute(NamedTuple): CarTestRoute("6719965b0e1d1737/2023-08-29--06-40-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled CarTestRoute("14623aae37e549f3/2021-10-24--01-20-49", TOYOTA.TOYOTA_PRIUS_V), + CarTestRoute("aebd8f1d4ea16066|00000009--b31e222338", VOLKSWAGEN.VOLKSWAGEN_ID4_MK1), CarTestRoute("202c40641158a6e5/2021-09-21--09-43-24", VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1), CarTestRoute("2c68dda277d887ac/2021-05-11--15-22-20", VOLKSWAGEN.VOLKSWAGEN_ATLAS_MK1), CarTestRoute("ffcd23abbbd02219/2024-02-28--14-59-38", VOLKSWAGEN.VOLKSWAGEN_CADDY_MK3), diff --git a/opendbc/car/torque_data/substitute.toml b/opendbc/car/torque_data/substitute.toml index d530ecd3ef4..fcdf536b16e 100644 --- a/opendbc/car/torque_data/substitute.toml +++ b/opendbc/car/torque_data/substitute.toml @@ -75,6 +75,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "VOLKSWAGEN_POLO_MK6" = "VOLKSWAGEN_GOLF_MK7" "SEAT_ATECA_MK1" = "VOLKSWAGEN_GOLF_MK7" "VOLKSWAGEN_JETTA_MK6" = "VOLKSWAGEN_PASSAT_NMS" +"VOLKSWAGEN_ID4_MK1" = "VOLKSWAGEN_TIGUAN_MK2" "SUBARU_CROSSTREK_HYBRID" = "SUBARU_IMPREZA_2020" "SUBARU_FORESTER_HYBRID" = "SUBARU_IMPREZA_2020" diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 1c58faee790..116620e2a25 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -4,7 +4,7 @@ from opendbc.car.lateral import apply_driver_steer_torque_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase -from opendbc.car.volkswagen import mlbcan, mqbcan, pqcan +from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags VisualAlert = structs.CarControl.HUDControl.VisualAlert @@ -45,10 +45,14 @@ def __init__(self, dbc_names, CP): self.CCS = pqcan elif CP.flags & VolkswagenFlags.MLB: self.CCS = mlbcan + elif CP.flags & VolkswagenFlags.MEB: + self.CCS = mebcan else: self.CCS = mqbcan self.apply_torque_last = 0 + self.apply_curvature_last = 0. + self.steering_power_last = 0 self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) @@ -60,17 +64,48 @@ def update(self, CC, CS, now_nanos): # **** Steering Controls ************************************************ # if self.frame % self.CCP.STEER_STEP == 0: - apply_torque = 0 - if CC.latActive: - new_torque = int(round(actuators.torque * self.CCP.STEER_MAX)) - apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP) - - apply_torque = self.hca_mitigation.update(apply_torque, self.apply_torque_last) - hca_enabled = apply_torque != 0 - self.apply_torque_last = apply_torque - can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled)) - - if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: + if self.CP.flags & VolkswagenFlags.MEB: + # MEB lateral: curvature control with steering power management + # MEB rack can be used continously without time limits + if CC.latActive: + hca_enabled = True + # curvature correction: cancel VM-derived currentCurvature offset using measured curvature + apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = float(np.clip(apply_curvature, -self.CCP.CURVATURE_MAX, self.CCP.CURVATURE_MAX)) + + min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) + max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) + target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], + [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) + target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) + steering_power = min(max(target_power, min_power), max_power) + else: + if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero + hca_enabled = True + apply_curvature = float(np.clip(CS.curvature_meas, -self.CCP.CURVATURE_MAX, self.CCP.CURVATURE_MAX)) + steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) + else: + hca_enabled = False + apply_curvature = 0. + steering_power = 0 + + can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) + self.apply_curvature_last = apply_curvature + self.steering_power_last = steering_power + apply_torque = 0 # for STOCK_HCA_PRESENT branch below (not used by MEB) + + else: + apply_torque = 0 + if CC.latActive: + new_torque = int(round(actuators.torque * self.CCP.STEER_MAX)) + apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP) + + apply_torque = self.hca_mitigation.update(apply_torque, self.apply_torque_last) + hca_enabled = apply_torque != 0 + self.apply_torque_last = apply_torque + can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled)) + + if not (self.CP.flags & VolkswagenFlags.MEB) and self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background. @@ -126,6 +161,7 @@ def update(self, CC, CS, now_nanos): new_actuators = actuators.as_builder() new_actuators.torque = self.apply_torque_last / self.CCP.STEER_MAX new_actuators.torqueOutputCan = self.apply_torque_last + new_actuators.curvature = float(self.apply_curvature_last) self.gra_acc_counter_last = CS.gra_stock_values["COUNTER"] self.frame += 1 diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 0eed80fc0ca..aeecaab61b9 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -19,6 +19,7 @@ def __init__(self, CP): self.upscale_lead_car_signal = False self.eps_stock_values = False self.acc_type = 0 + self.curvature_meas = 0. def update_button_enable(self, buttonEvents: list[structs.CarState.ButtonEvent]): if not self.CP.pcmCruise: @@ -52,6 +53,8 @@ def update(self, can_parsers) -> structs.CarState: return self.update_pq(pt_cp, cam_cp, ext_cp) elif self.CP.flags & VolkswagenFlags.MLB: return self.update_mlb(pt_cp, cam_cp, ext_cp, alt_cp) + elif self.CP.flags & VolkswagenFlags.MEB: + return self.update_meb(pt_cp, cam_cp, ext_cp) ret = structs.CarState() @@ -286,6 +289,63 @@ def update_mlb(self, pt_cp, cam_cp, ext_cp, alt_cp) -> structs.CarState: self.frame += 1 return ret + def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: + ret = structs.CarState() + + # Update vehicle speed and acceleration from ABS wheel speeds. + self.parse_wheel_speeds(ret, + pt_cp.vl["ESC_51"]["VL_Radgeschw"], + pt_cp.vl["ESC_51"]["VR_Radgeschw"], + pt_cp.vl["ESC_51"]["HL_Radgeschw"], + pt_cp.vl["ESC_51"]["HR_Radgeschw"], + ) + ret.standstill = ret.vEgoRaw == 0 + + # Update EPS position and state info. For signed values, VW sends the sign in a separate signal. + ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] + ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] + ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] + ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE + # MEB measured curvature (rad/m), stored on CarState instance for the carcontroller + self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] + + # Gear: MEB ID.4 uses Getriebe_11 + ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) + + hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) + ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status) + + self.eps_stock_values = pt_cp.vl["LH_EPS_03"] + + ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 + ret.brakePressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"]) + + # ESC_50 standstill bit; EPB_Status not present in upstream vw_meb DBC + ret.parkingBrake = bool(pt_cp.vl["ESC_50"]["Standstill"]) + + # door open / seatbelt: use ZV_02 (1411) and Airbag_02 if present, else default to safe + ret.seatbeltUnlatched = pt_cp.vl["Airbag_02"]["AB_Gurtschloss_FA"] != 3 + + self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} + + ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5) + ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5) + ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) + + ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_links"]) + ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) + + ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) + self.gra_stock_values = pt_cp.vl["GRA_ACC_01"] + + ret.espDisabled = bool(pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"]) + + ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation + ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo) + + self.frame += 1 + return ret + def update_low_speed_alert(self, v_ego: float) -> bool: # Low speed steer alert hysteresis logic if (self.CP.minSteerSpeed - 1e-3) > CarControllerParams.DEFAULT_MIN_STEER_SPEED and v_ego < (self.CP.minSteerSpeed + 1.): @@ -316,6 +376,8 @@ def update_hca_state(self, hca_status, drive_mode=True): def get_can_parsers(CP): if CP.flags & VolkswagenFlags.PQ: return CarState.get_can_parsers_pq(CP) + elif CP.flags & VolkswagenFlags.MEB: + return CarState.get_can_parsers_meb(CP) # manually configure some optional and variable-rate/edge-triggered messages pt_messages, cam_messages, alt_messages = [], [], [] @@ -342,3 +404,14 @@ def get_can_parsers_pq(CP): Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).alt), } + + @staticmethod + def get_can_parsers_meb(CP): + pt_messages = [ + ("Blinkmodi_02", 1), # variable rate + ] + return { + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), + Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).alt), + } diff --git a/opendbc/car/volkswagen/fingerprints.py b/opendbc/car/volkswagen/fingerprints.py index 1c35edf5387..8954d24bf92 100644 --- a/opendbc/car/volkswagen/fingerprints.py +++ b/opendbc/car/volkswagen/fingerprints.py @@ -366,6 +366,18 @@ b'\xf1\x875Q0907572S \xf1\x890780', ], }, + CAR.VOLKSWAGEN_ID4_MK1: { + (Ecu.srs, 0x715, None): [ + b'\xf1\x871EA959655EA\xf1\x890376', + b'\xf1\x875WA959655R \xf1\x890717', + ], + (Ecu.eps, 0x712, None): [ + b'\xf1\x871EA907144AQ\xf1\x895033\xf1\x82\x000_BH0A0_ON', + ], + (Ecu.fwdRadar, 0x757, None): [ + b'\xf1\x871EA907572H \xf1\x890234', + ], + }, CAR.VOLKSWAGEN_JETTA_MK6: { (Ecu.srs, 0x715, None): [ b'\xf1\x875C0959655M \xf1\x890726\xf1\x82\t00NB1108--------24', diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index 89a8e70c67e..d6b94189924 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -40,6 +40,20 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp ret.networkLocation = NetworkLocation.gateway ret.dashcamOnly = is_release # Release support needs HCA timeout fix, safety validation, revised J533 harness + elif ret.flags & VolkswagenFlags.MEB: + # Set global MEB parameters + safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenMeb)] + ret.transmissionType = TransmissionType.direct + ret.steerControlType = structs.CarParams.SteerControlType.curvatureDEPRECATED + ret.steerAtStandstill = True + + if any(msg in fingerprint[1] for msg in (0x520, 0x86, 0xFD, 0x13D)): # Airbag_02, LWI_01, ESP_21, QFK_01 + ret.networkLocation = NetworkLocation.gateway + else: + ret.networkLocation = NetworkLocation.fwdCamera + + ret.dashcamOnly = is_release # MEB lateral port, safety validation pending + else: # Set global MQB parameters safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagen)] @@ -68,6 +82,14 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp if ret.flags & VolkswagenFlags.PQ or ret.flags & VolkswagenFlags.MLB: ret.steerActuatorDelay = 0.2 CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + elif ret.flags & VolkswagenFlags.MEB: + ret.steerActuatorDelay = 0.3 + # MEB lateral uses curvature control directly; provide nominal PID values to satisfy validation + ret.lateralTuning.pid.kpBP = [0.] + ret.lateralTuning.pid.kiBP = [0.] + ret.lateralTuning.pid.kf = 1. + ret.lateralTuning.pid.kpV = [0.] + ret.lateralTuning.pid.kiV = [0.] else: ret.steerActuatorDelay = 0.1 ret.lateralTuning.pid.kpBP = [0.] @@ -78,8 +100,8 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp # Global longitudinal tuning defaults, can be overridden per-vehicle - ret.alphaLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or docs - if alpha_long: + ret.alphaLongitudinalAvailable = (ret.networkLocation == NetworkLocation.gateway or docs) and not (ret.flags & VolkswagenFlags.MEB) + if alpha_long and not (ret.flags & VolkswagenFlags.MEB): # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required. ret.openpilotLongitudinalControl = True safety_configs[0].safetyParam |= VolkswagenSafetyFlags.LONG_CONTROL.value diff --git a/opendbc/car/volkswagen/mebcan.py b/opendbc/car/volkswagen/mebcan.py new file mode 100644 index 00000000000..7afd5043c24 --- /dev/null +++ b/opendbc/car/volkswagen/mebcan.py @@ -0,0 +1,49 @@ +def create_steering_control(packer, bus, apply_curvature, lkas_enabled, power): + values = { + "Curvature": abs(apply_curvature), # in rad/m + "Curvature_VZ": 1 if apply_curvature > 0 and lkas_enabled else 0, + "Power": power if lkas_enabled else 0, + "RequestStatus": 4 if lkas_enabled else 2, + "HighSendRate": lkas_enabled, + } + return packer.make_can_msg("HCA_03", bus, values) + + +def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control): + display_mode = 1 if lat_active else 0 # travel assist style showing yellow lanes when op is active + + values = {} + if len(ldw_stock_values): + values = {s: ldw_stock_values[s] for s in [ + "LDW_SW_Warnung_links", # Blind spot in warning mode on left side due to lane departure + "LDW_SW_Warnung_rechts", # Blind spot in warning mode on right side due to lane departure + "LDW_Seite_DLCTLC", # Direction of most likely lane departure (left or right) + "LDW_DLC", # Lane departure, distance to line crossing + "LDW_TLC", # Lane departure, time to line crossing + ]} + + values.update({ + "LDW_Status_LED_gelb": 1 if lat_active and steering_pressed else 0, + "LDW_Status_LED_gruen": 1 if lat_active and not steering_pressed else 0, + "LDW_Lernmodus_links": 3 + display_mode if hud_control.leftLaneDepart else 1 + hud_control.leftLaneVisible + display_mode, + "LDW_Lernmodus_rechts": 3 + display_mode if hud_control.rightLaneDepart else 1 + hud_control.rightLaneVisible + display_mode, + "LDW_Texte": hud_alert, + }) + return packer.make_can_msg("LDW_02", bus, values) + + +def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resume=False): + values = {s: gra_stock_values[s] for s in [ + "GRA_Hauptschalter", # ACC button, on/off + "GRA_Typ_Hauptschalter", # ACC main button type + "GRA_Codierung", # ACC button configuration/coding + "GRA_Tip_Stufe_2", # unknown related to stalk type + "GRA_ButtonTypeInfo", # unknown related to stalk type + ]} + + values.update({ + "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16, + "GRA_Abbrechen": cancel, + "GRA_Tip_Wiederaufnahme": resume, + }) + return packer.make_can_msg("GRA_ACC_01", bus, values) diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 50214ac69b5..148917c16a7 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -99,6 +99,36 @@ def __init__(self, CP): "laneAssistDeactivTrailer": 5, # "Lane Assist: no function with trailer" } + elif CP.flags & VolkswagenFlags.MEB: + self.LDW_STEP = 10 # LDW_02 message frequency 10Hz + self.ACC_HUD_STEP = 6 # not used (MEB lateral-only) but referenced by long-control branch + self.STEER_DRIVER_ALLOWANCE = 60 # Driver torque 0.6 Nm, begin steering reduction from MAX + self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN + self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage + self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage + self.STEERING_POWER_STEP = 2 # HCA_03 steering power counter steps + + # Curvature limits for MEB lateral control (rad/m) + self.CURVATURE_MAX = 0.195 + + self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"] + self.hca_status_values = can_define.dv["QFK_01"]["LatCon_HCA_Status"] + + self.BUTTONS = [ + Button(structs.CarState.ButtonEvent.Type.setCruise, "GRA_ACC_01", "GRA_Tip_Setzen", [1]), + Button(structs.CarState.ButtonEvent.Type.resumeCruise, "GRA_ACC_01", "GRA_Tip_Wiederaufnahme", [1]), + Button(structs.CarState.ButtonEvent.Type.accelCruise, "GRA_ACC_01", "GRA_Tip_Hoch", [1]), + Button(structs.CarState.ButtonEvent.Type.decelCruise, "GRA_ACC_01", "GRA_Tip_Runter", [1]), + Button(structs.CarState.ButtonEvent.Type.cancel, "GRA_ACC_01", "GRA_Abbrechen", [1]), + Button(structs.CarState.ButtonEvent.Type.gapAdjustCruise, "GRA_ACC_01", "GRA_Verstellung_Zeitluecke", [3]), + ] + + self.LDW_MESSAGES = { + "none": 0, # Nothing to display + "laneAssistTakeOverUrgent": 4, # "Lane Assist: Please Take Over Steering" (red) + "laneAssistTakeOver": 8, # "Lane Assist: Please Take Over Steering" (white) + } + else: self.LDW_STEP = 10 # LDW_02 message frequency 10Hz self.ACC_HUD_STEP = 6 # ACC_02 message frequency 16Hz @@ -188,6 +218,7 @@ class VolkswagenFlags(IntFlag): # Static flags PQ = 2 MLB = 8 + MEB = 16 @dataclass @@ -209,6 +240,16 @@ class VolkswagenMQBPlatformConfig(PlatformConfig): wmis: set[WMI] = field(default_factory=set) +@dataclass +class VolkswagenMEBPlatformConfig(PlatformConfig): + dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: 'vw_meb'}) + chassis_codes: set[str] = field(default_factory=set) + wmis: set[WMI] = field(default_factory=set) + + def init(self): + self.flags |= VolkswagenFlags.MEB + + @dataclass class VolkswagenPQPlatformConfig(VolkswagenMQBPlatformConfig): dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: 'vw_pq'}) @@ -269,7 +310,7 @@ def init_make(self, CP: structs.CarParams): # FW_VERSIONS for that existing CAR. class CAR(Platforms): - config: VolkswagenMQBPlatformConfig | VolkswagenPQPlatformConfig + config: VolkswagenMQBPlatformConfig | VolkswagenPQPlatformConfig | VolkswagenMEBPlatformConfig VOLKSWAGEN_ARTEON_MK1 = VolkswagenMQBPlatformConfig( [ @@ -331,6 +372,15 @@ class CAR(Platforms): chassis_codes={"5G", "AU", "BA", "BE"}, wmis={WMI.VOLKSWAGEN_MEXICO_CAR, WMI.VOLKSWAGEN_EUROPE_CAR}, ) + VOLKSWAGEN_ID4_MK1 = VolkswagenMEBPlatformConfig( + [ + VWCarDocs("Volkswagen ID.4 2021-23"), + VWCarDocs("Volkswagen ID.5 2022-23"), + ], + VolkswagenCarSpecs(mass=2224, wheelbase=2.77), + chassis_codes={"E2"}, + wmis={WMI.VOLKSWAGEN_USA_SUV, WMI.VOLKSWAGEN_EUROPE_CAR, WMI.VOLKSWAGEN_EUROPE_SUV}, + ) VOLKSWAGEN_JETTA_MK6 = VolkswagenPQPlatformConfig( [VWCarDocs("Volkswagen Jetta 2015-18")], VolkswagenCarSpecs(mass=1518, wheelbase=2.65, minSteerSpeed=50 * CV.KPH_TO_MS, minEnableSpeed=20 * CV.KPH_TO_MS), diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h new file mode 100644 index 00000000000..f7d14f770e2 --- /dev/null +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -0,0 +1,176 @@ +#pragma once + +#include "opendbc/safety/declarations.h" +#include "opendbc/safety/modes/volkswagen_common.h" + +#define MSG_ESC_51 0xFCU // RX, for wheel speeds +#define MSG_HCA_03 0x303U // TX by OP, Heading Control Assist curvature +#define MSG_QFK_01 0x13DU // RX, for steering curvature +#define MSG_Motor_51 0x10BU // RX for TSK state and accel pedal + +// Curvature scaling (rad/m to CAN units, from HCA_03/QFK_01 DBC: 6.7e-6 rad/m per LSB) +// max_angle in CAN units: 0.195 / 6.7e-6 = 29105 +#define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 +#define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 + +static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { + int len = GET_LEN(msg); + + // CRC-8H2F/AUTOSAR with per-message magic byte XOR (same algorithm as MQB) + uint8_t crc = 0xFFU; + for (int i = 1; i < len; i++) { + crc ^= (uint8_t)msg->data[i]; + crc = volkswagen_crc8_lut_8h2f[crc]; + } + + uint8_t counter = volkswagen_mqb_meb_get_counter(msg); + if (msg->addr == MSG_LH_EPS_03) { + crc ^= (uint8_t[]){0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5}[counter]; + } else if (msg->addr == MSG_GRA_ACC_01) { + crc ^= (uint8_t[]){0x6A,0x38,0xB4,0x27,0x22,0xEF,0xE1,0xBB,0xF8,0x80,0x84,0x49,0xC7,0x9E,0x1E,0x2B}[counter]; + } else if (msg->addr == MSG_QFK_01) { + crc ^= (uint8_t[]){0x20,0xCA,0x68,0xD5,0x1B,0x31,0xE2,0xDA,0x08,0x0A,0xD4,0xDE,0x9C,0xE4,0x35,0x5B}[counter]; + } else if (msg->addr == MSG_ESC_51) { + crc ^= (uint8_t[]){0x77,0x5C,0xA0,0x89,0x4B,0x7C,0xBB,0xD6,0x1F,0x6C,0x4F,0xF6,0x20,0x2B,0x43,0xDD}[counter]; + } else if (msg->addr == MSG_Motor_51) { + crc ^= (uint8_t[]){0x77,0x5C,0xA0,0x89,0x4B,0x7C,0xBB,0xD6,0x1F,0x6C,0x4F,0xF6,0x20,0x2B,0x43,0xDD}[counter]; + } else if (msg->addr == MSG_MOTOR_14) { + crc ^= (uint8_t[]){0x1F,0x28,0xC6,0x85,0xE6,0xF8,0xB0,0x19,0x5B,0x64,0x35,0x21,0xE4,0xF7,0x9C,0x24}[counter]; + } else { + // Undefined CAN message, CRC check expected to fail + } + crc = volkswagen_crc8_lut_8h2f[crc]; + + return (uint8_t)(crc ^ 0xFFU); +} + +// Curvature steering limits, modeled as angle limits (Ford-style: curvature in CAN units) +static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { + .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, + .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, + .angle_rate_up_lookup = { + {5., 25., 25.}, + {0.00045, 0.0001, 0.0001}, + }, + .angle_rate_down_lookup = { + {5., 25., 25.}, + {0.00045, 0.00015, 0.00015}, + }, + .angle_is_curvature = true, + .inactive_angle_is_zero = true, +}; + +static safety_config volkswagen_meb_init(uint16_t param) { + // Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration + static const CanMsg VOLKSWAGEN_MEB_STOCK_TX_MSGS[] = { + {MSG_HCA_03, 0, 24, .check_relay = true}, + {MSG_GRA_ACC_01, 0, 8, .check_relay = false}, + {MSG_GRA_ACC_01, 2, 8, .check_relay = false}, + {MSG_LDW_02, 0, 8, .check_relay = true}, + }; + + static RxCheck volkswagen_meb_rx_checks[] = { + {.msg = {{MSG_LH_EPS_03, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_MOTOR_14, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_GRA_ACC_01, 0, 8, 33U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_QFK_01, 0, 32, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_Motor_51, 0, 32, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_ESC_51, 0, 48, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + }; + + volkswagen_common_init(); + SAFETY_UNUSED(param); + + return BUILD_SAFETY_CFG(volkswagen_meb_rx_checks, VOLKSWAGEN_MEB_STOCK_TX_MSGS); +} + +static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { + if (msg->bus == 0U) { + // Update in-motion state by sampling wheel speeds + if (msg->addr == MSG_ESC_51) { + uint32_t fl = msg->data[8] | (msg->data[9] << 8); + uint32_t fr = msg->data[10] | (msg->data[11] << 8); + uint32_t rl = msg->data[12] | (msg->data[13] << 8); + uint32_t rr = msg->data[14] | (msg->data[15] << 8); + vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); + } + + // Update measured curvature for steering safety check (in CAN units) + if (msg->addr == MSG_QFK_01) { + int current_curvature = ((msg->data[6] & 0x7FU) << 8) | msg->data[5]; + bool current_curvature_sign = GET_BIT(msg, 55U); + if (!current_curvature_sign) { + current_curvature *= -1; + } + update_sample(&angle_meas, current_curvature); + } + + // Driver torque sample (shared signal/sign with MQB) + if (msg->addr == MSG_LH_EPS_03) { + update_sample(&torque_driver, volkswagen_mlb_mqb_driver_input_torque(msg)); + } + + // Cruise state from Motor_51 TSK_Status + if (msg->addr == MSG_Motor_51) { + int acc_status = (msg->data[11] & 0x07U); + bool cruise_engaged = (acc_status == 3) || (acc_status == 4) || (acc_status == 5); + acc_main_on = cruise_engaged || (acc_status == 2); + pcm_cruise_check(cruise_engaged); + if (!acc_main_on) { + controls_allowed = false; + } + + // Accel pedal + int accel_pedal_value = ((msg->data[1] >> 4) & 0x0FU) | ((msg->data[2] & 0x1FU) << 4); + gas_pressed = accel_pedal_value > 0; + } + + if (msg->addr == MSG_GRA_ACC_01) { + // Always exit controls on rising edge of Cancel + if (GET_BIT(msg, 13U)) { + controls_allowed = false; + } + } + + if (msg->addr == MSG_MOTOR_14) { + brake_pressed = GET_BIT(msg, 28U); + } + } +} + +static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { + bool tx = true; + + // Safety check for HCA_03 Heading Control Assist curvature + if (msg->addr == MSG_HCA_03) { + int desired_curvature_raw = GET_BYTES(msg, 3, 2) & 0x7FFFU; + bool desired_curvature_sign = GET_BIT(msg, 39U); + if (!desired_curvature_sign) { + desired_curvature_raw *= -1; + } + bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); + + if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { + tx = false; + } + } + + // FORCE CANCEL: ensure only the cancel button press is sent when controls are off + if ((msg->addr == MSG_GRA_ACC_01) && !controls_allowed) { + // disallow resume and set: bits 16 and 19 + if ((msg->data[2] & 0x9U) != 0U) { + tx = false; + } + } + + return tx; +} + +const safety_hooks volkswagen_meb_hooks = { + .init = volkswagen_meb_init, + .rx = volkswagen_meb_rx_hook, + .tx = volkswagen_meb_tx_hook, + .get_counter = volkswagen_mqb_meb_get_counter, + .get_checksum = volkswagen_mqb_meb_get_checksum, + .compute_checksum = volkswagen_meb_compute_crc, +}; diff --git a/opendbc/safety/safety.h b/opendbc/safety/safety.h index a7c13354aa2..48f389aaa1e 100644 --- a/opendbc/safety/safety.h +++ b/opendbc/safety/safety.h @@ -23,6 +23,7 @@ #include "opendbc/safety/modes/nissan.h" #include "opendbc/safety/modes/volkswagen_mlb.h" #include "opendbc/safety/modes/volkswagen_mqb.h" +#include "opendbc/safety/modes/volkswagen_meb.h" #include "opendbc/safety/modes/volkswagen_pq.h" #include "opendbc/safety/modes/elm327.h" #include "opendbc/safety/modes/body.h" @@ -399,6 +400,7 @@ int set_safety_hooks(uint16_t mode, uint16_t param) { {SAFETY_CHRYSLER, &chrysler_hooks}, {SAFETY_SUBARU, &subaru_hooks}, {SAFETY_VOLKSWAGEN_MQB, &volkswagen_mqb_hooks}, + {SAFETY_VOLKSWAGEN_MEB, &volkswagen_meb_hooks}, {SAFETY_NISSAN, &nissan_hooks}, {SAFETY_NOOUTPUT, &nooutput_hooks}, {SAFETY_HYUNDAI_LEGACY, &hyundai_legacy_hooks}, diff --git a/opendbc/safety/tests/test_volkswagen_meb.py b/opendbc/safety/tests/test_volkswagen_meb.py new file mode 100644 index 00000000000..9e99b30216c --- /dev/null +++ b/opendbc/safety/tests/test_volkswagen_meb.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests.libsafety import libsafety_py +import opendbc.safety.tests.common as common +from opendbc.safety.tests.common import CANPackerSafety + +MSG_LH_EPS_03 = 0x9F # RX from EPS, for driver steering torque +MSG_ESC_51 = 0xFC # RX from ABS, for wheel speeds +MSG_Motor_51 = 0x10B # RX from ECU, for ACC status / accel pedal +MSG_GRA_ACC_01 = 0x12B # TX by OP, ACC control buttons for cancel/resume +MSG_QFK_01 = 0x13D # RX from EPS, for steering curvature +MSG_HCA_03 = 0x303 # TX by OP, Heading Control Assist curvature +MSG_LDW_02 = 0x397 # TX by OP, Lane line recognition and text alerts +MSG_MOTOR_14 = 0x3BE # RX from ECU, for brake switch status + + +class TestVolkswagenMebStockSafety(common.CarSafetyTest): + STANDSTILL_THRESHOLD = 0 + RELAY_MALFUNCTION_ADDRS = {0: (MSG_HCA_03, MSG_LDW_02)} + FWD_BLACKLISTED_ADDRS = {2: [MSG_HCA_03, MSG_LDW_02]} + TX_MSGS = [[MSG_HCA_03, 0], [MSG_LDW_02, 0], [MSG_GRA_ACC_01, 0], [MSG_GRA_ACC_01, 2]] + + def setUp(self): + self.packer = CANPackerSafety("vw_meb") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.volkswagenMeb, 0) + self.safety.init_tests() + + # Wheel speeds + def _speed_msg(self, speed): + # speed is m/s; ESC_51 wheel speeds use 0.0075 / 3.6 m/s per LSB based on safety hook + val = int(speed * 3.6 / 0.0075) + values = {f"{s}_Radgeschw": val for s in ["VL", "VR", "HL", "HR"]} + return self.packer.make_can_msg_safety("ESC_51", 0, values) + + def _user_brake_msg(self, brake): + values = {"MO_Fahrer_bremst": brake} + return self.packer.make_can_msg_safety("Motor_14", 0, values) + + def _user_gas_msg(self, gas): + # Gas and TSK share Motor_51; assume cruise stays engaged when gas tests run + values = {"Accel_Pedal_Pressure": 1 if gas else 0, "TSK_Status": 3} + return self.packer.make_can_msg_safety("Motor_51", 0, values) + + def _pcm_status_msg(self, enable): + values = {"TSK_Status": 3 if enable else 2} + return self.packer.make_can_msg_safety("Motor_51", 0, values) + + def _gra_acc_01_msg(self, cancel=0, resume=0, _set=0, bus=2): + values = {"GRA_Abbrechen": cancel, "GRA_Tip_Setzen": _set, "GRA_Tip_Wiederaufnahme": resume} + return self.packer.make_can_msg_safety("GRA_ACC_01", bus, values) + + def _hca_03_msg(self, curvature, steer_req=True): + # Curvature in rad/m + values = { + "Curvature": abs(curvature), + "Curvature_VZ": 1 if curvature > 0 else 0, + "Power": 50 if steer_req else 0, + "RequestStatus": 4 if steer_req else 2, + } + return self.packer.make_can_msg_safety("HCA_03", 0, values) + + def test_spam_cancel_safety_check(self): + self.safety.set_controls_allowed(0) + self.assertTrue(self._tx(self._gra_acc_01_msg(cancel=1))) + self.assertFalse(self._tx(self._gra_acc_01_msg(resume=1))) + self.assertFalse(self._tx(self._gra_acc_01_msg(_set=1))) + # do not block resume if we are engaged already + self.safety.set_controls_allowed(1) + self.assertTrue(self._tx(self._gra_acc_01_msg(resume=1))) + + +if __name__ == "__main__": + unittest.main() From 2eca7f408d8144e820483e54c5805901c3d160fd Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 17:43:35 -0700 Subject: [PATCH 03/69] volkswagen: ID.4 MK1 ui torque bar + steering limit tests (cherry picked from commit d68baff17e4b5f638ede9c441d35d70492574335) --- .../car/volkswagen/tests/test_meb_lateral.py | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 opendbc/car/volkswagen/tests/test_meb_lateral.py diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py new file mode 100644 index 00000000000..da4c50ce8bf --- /dev/null +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -0,0 +1,165 @@ +import unittest + +from opendbc.can import CANParser +from opendbc.car import Bus, structs +from opendbc.car.car_helpers import interfaces +from opendbc.car.volkswagen.values import CAR, DBC + +VisualAlert = structs.CarControl.HUDControl.VisualAlert + +HCA_03_ADDR = 771 +LDW_02_ADDR = 919 + + +def _make_carstate(CC_inst, vEgo=10.0, steeringTorque=0.0, curvature_meas=0.0): + CS = CC_inst.CS + CS.out = structs.CarState() + CS.out.vEgo = vEgo + CS.out.steeringTorque = float(steeringTorque) + CS.out.steeringPressed = abs(steeringTorque) > CC_inst.CC.CCP.STEER_DRIVER_ALLOWANCE + CS.curvature_meas = float(curvature_meas) + CS.gra_stock_values = {"COUNTER": 0} + CS.ldw_stock_values = {} + CS.eps_stock_values = {} + return CS + + +def _build_cc(latActive=True, curvature=0.0, visualAlert=VisualAlert.none): + CC = structs.CarControl() + CC.enabled = latActive + CC.latActive = latActive + CC.actuators.curvature = float(curvature) + CC.currentCurvature = 0.0 + CC.hudControl.visualAlert = visualAlert + return CC.as_reader() + + +def _decode(addr, dat, signals): + """Decode a single CAN frame using a fresh CANParser tied to vw_meb.""" + parser = CANParser(DBC[CAR.VOLKSWAGEN_ID4_MK1.value][Bus.pt], + [(addr, 0)], 0) + parser.update([(0, [(addr, dat, 0)])]) + return {s: parser.vl[addr][s] for s in signals} + + +class TestMEBLateral(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.CI = interfaces[CAR.VOLKSWAGEN_ID4_MK1.value] + cp = cls.CI.get_params(CAR.VOLKSWAGEN_ID4_MK1.value, {i: {} for i in range(8)}, + [], alpha_long=False, is_release=False, docs=False) + cls.cp = cp + + def setUp(self): + self.inst = self.CI(self.cp) + self.CCP = self.inst.CC.CCP + + # (a) Torque-bar / steeringPressed test + def test_steering_pressed_threshold(self): + """steeringPressed flips True iff |torque| > STEER_DRIVER_ALLOWANCE (raw cNm units).""" + th = self.CCP.STEER_DRIVER_ALLOWANCE + cases = [ + (0, False), + (th - 1, False), + (th, False), # strictly greater than + (th + 1, True), + (-(th + 1),True), + (-(th - 1),False), + (self.CCP.STEER_DRIVER_MAX, True), + ] + for torque, expected in cases: + with self.subTest(torque=torque): + # Replicate the exact CarState predicate used by update_meb() + pressed = abs(torque) > th + self.assertEqual(pressed, expected) + + # (b) Curvature clip / saturation test + def test_curvature_clip_and_encoding(self): + """Out-of-range commanded curvature is clipped to ±CURVATURE_MAX in actuators and on the wire.""" + for cmd in (+0.5, -0.5): + with self.subTest(cmd=cmd): + inst = self.CI(self.cp) + CS = _make_carstate(inst) + CC = _build_cc(latActive=True, curvature=cmd) + new_act, sends = inst.CC.update(CC, CS, 0) + + self.assertAlmostEqual(abs(new_act.curvature), self.CCP.CURVATURE_MAX, places=4) + self.assertEqual(new_act.curvature > 0, cmd > 0) + + hca = next(s for s in sends if s[0] == HCA_03_ADDR) + decoded = _decode(HCA_03_ADDR, hca[1], ["Curvature", "Curvature_VZ", "RequestStatus"]) + self.assertAlmostEqual(decoded["Curvature"], self.CCP.CURVATURE_MAX, places=3) + self.assertEqual(int(decoded["Curvature_VZ"]), 1 if cmd > 0 else 0) + self.assertEqual(int(decoded["RequestStatus"]), 4) # HCA enabled + + # (c) Steering power ramp test + def test_steering_power_ramp_up_and_down(self): + inst = self.CI(self.cp) + CS = _make_carstate(inst, vEgo=10.0, steeringTorque=0.0) + CC_on = _build_cc(latActive=True, curvature=0.0) + + # Ramp up: increases by STEERING_POWER_STEP per STEER_STEP-aligned cycle until MAX + last_power = 0 + saw_max = False + for _ in range(int(self.CCP.STEERING_POWER_MAX // self.CCP.STEERING_POWER_STEP) * self.CCP.STEER_STEP + 10): + inst.CC.update(CC_on, CS, 0) + cur = inst.CC.steering_power_last + self.assertGreaterEqual(cur, last_power) + self.assertLessEqual(cur, self.CCP.STEERING_POWER_MAX) + if cur == self.CCP.STEERING_POWER_MAX: + saw_max = True + last_power = cur + self.assertTrue(saw_max, "steering_power never reached STEERING_POWER_MAX") + + # Ramp down: latActive=False -> reduces by STEERING_POWER_STEP per cycle to zero + CC_off = _build_cc(latActive=False, curvature=0.0) + last_power = inst.CC.steering_power_last + for _ in range(int(self.CCP.STEERING_POWER_MAX // self.CCP.STEERING_POWER_STEP) * self.CCP.STEER_STEP + 10): + inst.CC.update(CC_off, CS, 0) + cur = inst.CC.steering_power_last + self.assertLessEqual(cur, last_power) + last_power = cur + self.assertEqual(inst.CC.steering_power_last, 0) + + # (d) LDW HUD test + def test_ldw_hud_take_over(self): + inst = self.CI(self.cp) + CS = _make_carstate(inst) + CC = _build_cc(latActive=True, visualAlert=VisualAlert.steerRequired) + # Run for one full LDW cycle so the LDW frame is emitted + sends = [] + for _ in range(self.CCP.LDW_STEP): + _, s = inst.CC.update(CC, CS, 0) + sends.extend(s) + ldw = next((m for m in sends if m[0] == LDW_02_ADDR), None) + self.assertIsNotNone(ldw, "LDW_02 frame not emitted") + decoded = _decode(LDW_02_ADDR, ldw[1], ["LDW_Texte", "LDW_Status_LED_gruen"]) + self.assertEqual(int(decoded["LDW_Texte"]), self.CCP.LDW_MESSAGES["laneAssistTakeOver"]) + # latActive + not pressed => green LED on (UI showing OP active) + self.assertEqual(int(decoded["LDW_Status_LED_gruen"]), 1) + + # (e) HCA disabled gating test + def test_hca_disabled_after_power_ramp_down(self): + inst = self.CI(self.cp) + CS = _make_carstate(inst) + CC_on = _build_cc(latActive=True) + CC_off = _build_cc(latActive=False) + + # Ramp up to MAX, then off, then drain + for _ in range(200): + inst.CC.update(CC_on, CS, 0) + for _ in range(200): + inst.CC.update(CC_off, CS, 0) + + self.assertEqual(inst.CC.steering_power_last, 0) + _, sends = inst.CC.update(CC_off, CS, 0) + hca = next(s for s in sends if s[0] == HCA_03_ADDR) + decoded = _decode(HCA_03_ADDR, hca[1], ["RequestStatus", "Power", "Curvature"]) + self.assertNotEqual(int(decoded["RequestStatus"]), 4) + self.assertEqual(int(decoded["RequestStatus"]), 2) + self.assertEqual(int(decoded["Power"]), 0) + self.assertAlmostEqual(decoded["Curvature"], 0.0, places=4) + + +if __name__ == "__main__": + unittest.main() From 81bea424f58175d0e18ce195496d1025aa889df2 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 17:49:33 -0700 Subject: [PATCH 04/69] volkswagen: ID.4 MK1 use angle control framework (cherry picked from commit 073ed9449cd0fb97c05cbe4a6dd4a0b24b9789fb) --- opendbc/car/volkswagen/carcontroller.py | 26 +++++++---- opendbc/car/volkswagen/interface.py | 8 +--- .../car/volkswagen/tests/test_meb_lateral.py | 44 +++++++++++++++---- opendbc/car/volkswagen/values.py | 13 ++++-- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 116620e2a25..87e76a65878 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,7 +1,7 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan @@ -65,13 +65,21 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: - # MEB lateral: curvature control with steering power management - # MEB rack can be used continously without time limits + # MEB lateral: curvature control via the angle framework (Ford-style). + # apply_std_steer_angle_limits rate-limits and clips the requested curvature; we + # then add the measured-vs-VM correction term and re-clip. The pre-correction + # value is what controlsd's saturation detection sees. + # MEB rack can be used continuously without time limits. + apply_curvature = apply_std_steer_angle_limits(actuators.curvature, self.apply_curvature_last, CS.out.vEgoRaw, + CS.out.steeringAngleDeg, CC.latActive, CarControllerParams.ANGLE_LIMITS) + self.apply_curvature_last = apply_curvature + if CC.latActive: hca_enabled = True # curvature correction: cancel VM-derived currentCurvature offset using measured curvature - apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - apply_curvature = float(np.clip(apply_curvature, -self.CCP.CURVATURE_MAX, self.CCP.CURVATURE_MAX)) + apply_curvature_out = float(np.clip(apply_curvature + (CS.curvature_meas - CC.currentCurvature), + -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, + CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -82,15 +90,15 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True - apply_curvature = float(np.clip(CS.curvature_meas, -self.CCP.CURVATURE_MAX, self.CCP.CURVATURE_MAX)) + apply_curvature_out = float(np.clip(CS.curvature_meas, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, + CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False - apply_curvature = 0. + apply_curvature_out = 0. steering_power = 0 - can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) - self.apply_curvature_last = apply_curvature + can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature_out, hca_enabled, steering_power)) self.steering_power_last = steering_power apply_torque = 0 # for STOCK_HCA_PRESENT branch below (not used by MEB) diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index d6b94189924..be7400e92a0 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -44,7 +44,7 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp # Set global MEB parameters safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenMeb)] ret.transmissionType = TransmissionType.direct - ret.steerControlType = structs.CarParams.SteerControlType.curvatureDEPRECATED + ret.steerControlType = structs.CarParams.SteerControlType.angle ret.steerAtStandstill = True if any(msg in fingerprint[1] for msg in (0x520, 0x86, 0xFD, 0x13D)): # Airbag_02, LWI_01, ESP_21, QFK_01 @@ -84,12 +84,6 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif ret.flags & VolkswagenFlags.MEB: ret.steerActuatorDelay = 0.3 - # MEB lateral uses curvature control directly; provide nominal PID values to satisfy validation - ret.lateralTuning.pid.kpBP = [0.] - ret.lateralTuning.pid.kiBP = [0.] - ret.lateralTuning.pid.kf = 1. - ret.lateralTuning.pid.kpV = [0.] - ret.lateralTuning.pid.kiV = [0.] else: ret.steerActuatorDelay = 0.1 ret.lateralTuning.pid.kpBP = [0.] diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index da4c50ce8bf..330c115cc6b 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -1,9 +1,11 @@ import unittest +import numpy as np + from opendbc.can import CANParser from opendbc.car import Bus, structs from opendbc.car.car_helpers import interfaces -from opendbc.car.volkswagen.values import CAR, DBC +from opendbc.car.volkswagen.values import CAR, CarControllerParams, DBC VisualAlert = structs.CarControl.HUDControl.VisualAlert @@ -11,10 +13,12 @@ LDW_02_ADDR = 919 -def _make_carstate(CC_inst, vEgo=10.0, steeringTorque=0.0, curvature_meas=0.0): +def _make_carstate(CC_inst, vEgo=10.0, steeringTorque=0.0, curvature_meas=0.0, steeringAngleDeg=0.0): CS = CC_inst.CS CS.out = structs.CarState() CS.out.vEgo = vEgo + CS.out.vEgoRaw = vEgo + CS.out.steeringAngleDeg = float(steeringAngleDeg) CS.out.steeringTorque = float(steeringTorque) CS.out.steeringPressed = abs(steeringTorque) > CC_inst.CC.CCP.STEER_DRIVER_ALLOWANCE CS.curvature_meas = float(curvature_meas) @@ -75,23 +79,45 @@ def test_steering_pressed_threshold(self): # (b) Curvature clip / saturation test def test_curvature_clip_and_encoding(self): - """Out-of-range commanded curvature is clipped to ±CURVATURE_MAX in actuators and on the wire.""" + """Out-of-range commanded curvature ramps via the angle framework and saturates at ±STEER_ANGLE_MAX.""" + cmax = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX for cmd in (+0.5, -0.5): with self.subTest(cmd=cmd): inst = self.CI(self.cp) - CS = _make_carstate(inst) + CS = _make_carstate(inst, vEgo=10.0) CC = _build_cc(latActive=True, curvature=cmd) - new_act, sends = inst.CC.update(CC, CS, 0) - - self.assertAlmostEqual(abs(new_act.curvature), self.CCP.CURVATURE_MAX, places=4) + new_act = None + last_hca = None + # Run long enough for the rate-limited curvature to saturate at the max + for _ in range(2000): + new_act, sends = inst.CC.update(CC, CS, 0) + for m in sends: + if m[0] == HCA_03_ADDR: + last_hca = m + + self.assertAlmostEqual(abs(new_act.curvature), cmax, places=4) self.assertEqual(new_act.curvature > 0, cmd > 0) - hca = next(s for s in sends if s[0] == HCA_03_ADDR) + hca = last_hca + self.assertIsNotNone(hca) decoded = _decode(HCA_03_ADDR, hca[1], ["Curvature", "Curvature_VZ", "RequestStatus"]) - self.assertAlmostEqual(decoded["Curvature"], self.CCP.CURVATURE_MAX, places=3) + self.assertAlmostEqual(decoded["Curvature"], cmax, places=3) self.assertEqual(int(decoded["Curvature_VZ"]), 1 if cmd > 0 else 0) self.assertEqual(int(decoded["RequestStatus"]), 4) # HCA enabled + # (b2) Rate-limit test (Ford-style): a fast step is rate-limited frame-to-frame. + def test_curvature_rate_limit(self): + """A large step in commanded curvature is limited by ANGLE_RATE_LIMIT_UP per send cycle.""" + inst = self.CI(self.cp) + CS = _make_carstate(inst, vEgo=10.0) + CC = _build_cc(latActive=True, curvature=0.1) + # First send cycle aligned to STEER_STEP=2 + new_act, _ = inst.CC.update(CC, CS, 0) + rate_up = CarControllerParams.ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP + expected_step = float(np.interp(10.0, rate_up[0], rate_up[1])) + self.assertLessEqual(abs(new_act.curvature), expected_step + 1e-9) + self.assertGreater(abs(new_act.curvature), 0.0) + # (c) Steering power ramp test def test_steering_power_ramp_up_and_down(self): inst = self.CI(self.cp) diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 148917c16a7..9433931a908 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -6,6 +6,7 @@ from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column +from opendbc.car.lateral import AngleSteeringLimits from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -67,6 +68,15 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration + # MEB lateral curvature framework limits (using AngleSteeringLimits with angle_is_curvature=True). + # Wire signal stays curvature (rad/m) on HCA_03; this is purely the upstream rate/limit framework. + ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( + 0.195, # Max curvature (rad/m), matches safety VOLKSWAGEN_MEB_MAX_CURVATURE_CAN + # Curvature rate limits, must match safety angle_rate_up/down_lookup + ([5., 25.], [0.00045, 0.0001]), + ([5., 25.], [0.00045, 0.00015]), + ) + def __init__(self, CP): can_define = CANDefine(DBC[CP.carFingerprint][Bus.pt]) @@ -108,9 +118,6 @@ def __init__(self, CP): self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage self.STEERING_POWER_STEP = 2 # HCA_03 steering power counter steps - # Curvature limits for MEB lateral control (rad/m) - self.CURVATURE_MAX = 0.195 - self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"] self.hca_status_values = can_define.dv["QFK_01"]["LatCon_HCA_Status"] From 209ba125f95342519582699b986bb628fccb8517 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 20:35:02 -0700 Subject: [PATCH 05/69] volkswagen: ID.4 MK1 fixes from elkoled/2 reference (cherry picked from commit 98b11d8093598e62ed5e95b69a2a777e930b17a8) --- opendbc/car/volkswagen/carstate.py | 2 +- opendbc/safety/tests/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index aeecaab61b9..100ba67b4ab 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -305,7 +305,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] - ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE + ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) # MEB measured curvature (rad/m), stored on CarState instance for the carcontroller self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] diff --git a/opendbc/safety/tests/common.py b/opendbc/safety/tests/common.py index 267736cfac6..0e40bffe6c2 100644 --- a/opendbc/safety/tests/common.py +++ b/opendbc/safety/tests/common.py @@ -909,7 +909,7 @@ def test_tx_hook_on_wrong_safety_mode(self): continue if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC', 'TestHyundaiSafetyFCEVLong'}): continue - volkswagen_shared = ('TestVolkswagenMqb', 'TestVolkswagenMlb') + volkswagen_shared = ('TestVolkswagenMqb', 'TestVolkswagenMlb', 'TestVolkswagenMeb') if attr.startswith(volkswagen_shared) and current_test.startswith(volkswagen_shared): continue From 195867a656ff05f343cbe27d3da14a3a227e934b Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 20:39:51 -0700 Subject: [PATCH 06/69] volkswagen: ID.4 MK1 add bsm (cherry picked from commit 41fb8f53260696cc3a7f4d6277fe8acf47743a68) --- opendbc/car/volkswagen/carstate.py | 10 ++++ opendbc/car/volkswagen/interface.py | 2 + .../car/volkswagen/tests/test_meb_lateral.py | 47 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 100ba67b4ab..503de7f3c58 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -340,6 +340,14 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.espDisabled = bool(pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"]) + # Consume blind-spot monitoring info/warning LED states, if available. + # MEB_Side_Assist_01 is on PT/Gateway bus on MEB. + if self.CP.enableBsm: + ret.leftBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or \ + bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"]) + ret.rightBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or \ + bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"]) + ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo) @@ -410,6 +418,8 @@ def get_can_parsers_meb(CP): pt_messages = [ ("Blinkmodi_02", 1), # variable rate ] + if CP.enableBsm: + pt_messages.append(("MEB_Side_Assist_01", 20)) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index be7400e92a0..efe0e52a451 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -52,6 +52,8 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp else: ret.networkLocation = NetworkLocation.fwdCamera + ret.enableBsm = 0x24C in fingerprint[0] # MEB_Side_Assist_01 + ret.dashcamOnly = is_release # MEB lateral port, safety validation pending else: diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index 330c115cc6b..a4b7737ce31 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -187,5 +187,52 @@ def test_hca_disabled_after_power_ramp_down(self): self.assertAlmostEqual(decoded["Curvature"], 0.0, places=4) +class TestMEBBlindspot(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.CI = interfaces[CAR.VOLKSWAGEN_ID4_MK1.value] + fingerprint = {i: {} for i in range(8)} + fingerprint[0][0x24C] = 16 # MEB_Side_Assist_01 present on PT bus + cp = cls.CI.get_params(CAR.VOLKSWAGEN_ID4_MK1.value, fingerprint, + [], alpha_long=False, is_release=False, docs=False) + cls.cp = cp + + def test_enable_bsm_set(self): + self.assertTrue(self.cp.enableBsm) + + def test_left_and_right_blindspot(self): + from opendbc.can import CANPacker + inst = self.CI(self.cp) + parsers = inst.CS.get_can_parsers(self.cp) + pt_cp = parsers[Bus.pt] + packer = CANPacker(DBC[CAR.VOLKSWAGEN_ID4_MK1.value][Bus.pt]) + + cases = [ + ({"Blind_Spot_Info_Left": 1, "Blind_Spot_Warn_Left": 0, + "Blind_Spot_Info_Right": 0, "Blind_Spot_Warn_Right": 0}, True, False), + ({"Blind_Spot_Info_Left": 0, "Blind_Spot_Warn_Left": 1, + "Blind_Spot_Info_Right": 0, "Blind_Spot_Warn_Right": 0}, True, False), + ({"Blind_Spot_Info_Left": 0, "Blind_Spot_Warn_Left": 0, + "Blind_Spot_Info_Right": 1, "Blind_Spot_Warn_Right": 0}, False, True), + ({"Blind_Spot_Info_Left": 0, "Blind_Spot_Warn_Left": 0, + "Blind_Spot_Info_Right": 0, "Blind_Spot_Warn_Right": 1}, False, True), + ({"Blind_Spot_Info_Left": 1, "Blind_Spot_Warn_Left": 0, + "Blind_Spot_Info_Right": 1, "Blind_Spot_Warn_Right": 0}, True, True), + ({"Blind_Spot_Info_Left": 0, "Blind_Spot_Warn_Left": 0, + "Blind_Spot_Info_Right": 0, "Blind_Spot_Warn_Right": 0}, False, False), + ] + for vals, exp_left, exp_right in cases: + with self.subTest(**vals): + addr, dat, _ = packer.make_can_msg("MEB_Side_Assist_01", 0, vals) + pt_cp.update([(0, [(addr, dat, 0)])]) + self.assertEqual(bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]), + bool(vals["Blind_Spot_Info_Left"])) + cam_cp = parsers[Bus.cam] + ext_cp = parsers[Bus.alt] + ret = inst.CS.update_meb(pt_cp, cam_cp, ext_cp) + self.assertEqual(ret.leftBlindspot, exp_left) + self.assertEqual(ret.rightBlindspot, exp_right) + + if __name__ == "__main__": unittest.main() From e8e32bddf0f37e641b15405a26196441b9e7bfd3 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 20:57:47 -0700 Subject: [PATCH 07/69] volkswagen: ID.4 MK1 align angle ctrl with safety (cherry picked from commit 1f2e0a521c9a214dcbeb7e256eaea74411259689) --- opendbc/car/volkswagen/carcontroller.py | 32 +++++--- .../car/volkswagen/tests/test_meb_lateral.py | 79 +++++++++++++++++++ 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 87e76a65878..3eb5b79c8d6 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -66,21 +66,30 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: # MEB lateral: curvature control via the angle framework (Ford-style). - # apply_std_steer_angle_limits rate-limits and clips the requested curvature; we - # then add the measured-vs-VM correction term and re-clip. The pre-correction - # value is what controlsd's saturation detection sees. + # The curvature on the wire (HCA_03) is what panda safety rate-limits and + # clips; to stay byte-for-byte aligned with safety, we feed the *corrected* + # curvature target into apply_std_steer_angle_limits and use its output + # directly on the wire. The same rate-limited value is fed back as + # apply_curvature_last so the next frame's safety delta is identical. # MEB rack can be used continuously without time limits. - apply_curvature = apply_std_steer_angle_limits(actuators.curvature, self.apply_curvature_last, CS.out.vEgoRaw, - CS.out.steeringAngleDeg, CC.latActive, CarControllerParams.ANGLE_LIMITS) + if CC.latActive: + # curvature correction: cancel VM-derived currentCurvature offset using measured curvature + curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) + else: + # ramp toward zero so safety sees a continuous rate-limited path; on the + # disabled frame (steering_power == 0 below) we'll force zero anyway + curvature_target = 0.0 + + # Rate-limit/clip with lat_active=True so the framework rate-limits toward + # the target rather than snapping to steering_angle (which would also + # corrupt apply_curvature_last with a degrees value). + apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, + CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) self.apply_curvature_last = apply_curvature + apply_curvature_out = apply_curvature if CC.latActive: hca_enabled = True - # curvature correction: cancel VM-derived currentCurvature offset using measured curvature - apply_curvature_out = float(np.clip(apply_curvature + (CS.curvature_meas - CC.currentCurvature), - -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, - CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) - min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], @@ -90,12 +99,11 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True - apply_curvature_out = float(np.clip(CS.curvature_meas, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, - CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False apply_curvature_out = 0. + self.apply_curvature_last = 0. steering_power = 0 can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature_out, hca_enabled, steering_power)) diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index a4b7737ce31..f8b7ffc126c 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -234,5 +234,84 @@ def test_left_and_right_blindspot(self): self.assertEqual(ret.rightBlindspot, exp_right) +class TestMEBSafetyOracle(unittest.TestCase): + """Drive carcontroller through randomized inputs and confirm panda safety + (volkswagenMeb) accepts every HCA_03 frame it produces. Conversely confirm + that bypassing the rate limit causes safety to reject.""" + + @classmethod + def setUpClass(cls): + cls.CI = interfaces[CAR.VOLKSWAGEN_ID4_MK1.value] + cp = cls.CI.get_params(CAR.VOLKSWAGEN_ID4_MK1.value, {i: {} for i in range(8)}, + [], alpha_long=False, is_release=False, docs=False) + cls.cp = cp + + def setUp(self): + # Lazy import: keeps the rest of this module importable without libsafety + from opendbc.car.structs import CarParams + from opendbc.safety.tests.libsafety import libsafety_py + self.libsafety_py = libsafety_py + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.volkswagenMeb, 0) + self.safety.init_tests() + self.safety.set_controls_allowed(1) + + self.inst = self.CI(self.cp) + self.CCP = self.inst.CC.CCP + + def _raw_hca_to_safety_packet(self, dat, addr=771): + return self.libsafety_py.make_CANPacket(addr, 0, dat) + + def test_oracle_random_sequence(self): + rng = np.random.default_rng(0) + CS = _make_carstate(self.inst, vEgo=15.0) + + # Walk through randomized requested curvatures + measured curvatures + for step in range(500): + v = float(rng.uniform(2.0, 30.0)) + cmd = float(rng.uniform(-0.25, 0.25)) + meas = float(rng.uniform(-0.2, 0.2)) + lat_active = bool(rng.integers(0, 2)) or step < 200 # mostly active + + CS.out.vEgo = v + CS.out.vEgoRaw = v + CS.out.steeringAngleDeg = float(rng.uniform(-90, 90)) + CS.curvature_meas = meas + CC = _build_cc(latActive=lat_active, curvature=cmd) + _, sends = self.inst.CC.update(CC, CS, 0) + + for addr, dat, _bus in sends: + if addr != HCA_03_ADDR: + continue + pkt = self._raw_hca_to_safety_packet(dat, addr) + accepted = self.safety.safety_tx_hook(pkt) + self.assertTrue(accepted, f"safety rejected HCA_03 at step {step}: cmd={cmd:.4f} meas={meas:.4f} lat={lat_active}") + + def test_oracle_rate_limit_violation_rejected(self): + """Hand-craft an HCA_03 that exceeds the safety rate limit; expect rejection.""" + # First a small, in-bounds frame to set desired_angle_last + from opendbc.can import CANPacker + packer = CANPacker(DBC[CAR.VOLKSWAGEN_ID4_MK1.value][Bus.pt]) + addr, dat0, _ = packer.make_can_msg("HCA_03", 0, { + "Curvature": 0.0, "Curvature_VZ": 0, "Power": 50, "RequestStatus": 4, "HighSendRate": 1, + }) + self.assertTrue(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat0, addr))) + + # Now jump way past one frame's allowed delta (limit is ~0.00045 rad/m at low speed) + addr, dat1, _ = packer.make_can_msg("HCA_03", 0, { + "Curvature": 0.10, "Curvature_VZ": 1, "Power": 50, "RequestStatus": 4, "HighSendRate": 1, + }) + self.assertFalse(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat1, addr))) + + def test_oracle_inactive_nonzero_rejected(self): + """When steer_req=False (RequestStatus!=4), curvature must be zero per safety.""" + from opendbc.can import CANPacker + packer = CANPacker(DBC[CAR.VOLKSWAGEN_ID4_MK1.value][Bus.pt]) + addr, dat, _ = packer.make_can_msg("HCA_03", 0, { + "Curvature": 0.05, "Curvature_VZ": 1, "Power": 0, "RequestStatus": 2, "HighSendRate": 0, + }) + self.assertFalse(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat, addr))) + + if __name__ == "__main__": unittest.main() From 98af8f2910ce26d0b6ef5790fe8b985cc98902d9 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 21:06:31 -0700 Subject: [PATCH 08/69] volkswagen: ID.4 MK1 simplify (cherry picked from commit 375f3645fddb13d5b39dbf70e9cb620d04440424) --- opendbc/car/volkswagen/carcontroller.py | 57 +++++++++---------------- opendbc/car/volkswagen/carstate.py | 40 +++++++---------- opendbc/car/volkswagen/values.py | 14 +++--- 3 files changed, 43 insertions(+), 68 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 3eb5b79c8d6..55f903af221 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -64,54 +64,39 @@ def update(self, CC, CS, now_nanos): # **** Steering Controls ************************************************ # if self.frame % self.CCP.STEER_STEP == 0: + apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - # MEB lateral: curvature control via the angle framework (Ford-style). - # The curvature on the wire (HCA_03) is what panda safety rate-limits and - # clips; to stay byte-for-byte aligned with safety, we feed the *corrected* - # curvature target into apply_std_steer_angle_limits and use its output - # directly on the wire. The same rate-limited value is fed back as - # apply_curvature_last so the next frame's safety delta is identical. - # MEB rack can be used continuously without time limits. - if CC.latActive: - # curvature correction: cancel VM-derived currentCurvature offset using measured curvature - curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) - else: - # ramp toward zero so safety sees a continuous rate-limited path; on the - # disabled frame (steering_power == 0 below) we'll force zero anyway - curvature_target = 0.0 - - # Rate-limit/clip with lat_active=True so the framework rate-limits toward - # the target rather than snapping to steering_angle (which would also - # corrupt apply_curvature_last with a degrees value). + # MEB sends curvature on HCA_03; rate-limit through the angle framework + # (angle_is_curvature=True) so openpilot's output stays byte-for-byte + # aligned with the panda safety check. + curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) if CC.latActive else 0.0 apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) self.apply_curvature_last = apply_curvature - apply_curvature_out = apply_curvature if CC.latActive: hca_enabled = True - min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) - max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) - target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], - [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) + target_power_driver = float(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], + [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) - steering_power = min(max(target_power, min_power), max_power) + # ramp at most STEERING_POWER_STEP per cycle, bounded by [MIN, MAX] + step = self.CCP.STEERING_POWER_STEP + steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), + self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) + elif self.steering_power_last > 0: + # keep HCA alive while we ramp steering power down to zero + hca_enabled = True + steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: - if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero - hca_enabled = True - steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) - else: - hca_enabled = False - apply_curvature_out = 0. - self.apply_curvature_last = 0. - steering_power = 0 - - can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature_out, hca_enabled, steering_power)) + hca_enabled = False + apply_curvature = 0. + self.apply_curvature_last = 0. + steering_power = 0 + self.steering_power_last = steering_power - apply_torque = 0 # for STOCK_HCA_PRESENT branch below (not used by MEB) + can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) else: - apply_torque = 0 if CC.latActive: new_torque = int(round(actuators.torque * self.CCP.STEER_MAX)) apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 503de7f3c58..296759a49a9 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -292,7 +292,6 @@ def update_mlb(self, pt_cp, cam_cp, ext_cp, alt_cp) -> structs.CarState: def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret = structs.CarState() - # Update vehicle speed and acceleration from ABS wheel speeds. self.parse_wheel_speeds(ret, pt_cp.vl["ESC_51"]["VL_Radgeschw"], pt_cp.vl["ESC_51"]["VR_Radgeschw"], @@ -303,52 +302,43 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: # Update EPS position and state info. For signed values, VW sends the sign in a separate signal. ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] - ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] - ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] + ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] + ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) - # MEB measured curvature (rad/m), stored on CarState instance for the carcontroller + # measured curvature (rad/m) consumed by the MEB carcontroller self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] - # Gear: MEB ID.4 uses Getriebe_11 - ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) - hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status) - self.eps_stock_values = pt_cp.vl["LH_EPS_03"] + ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) - ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 + ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 ret.brakePressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"]) - - # ESC_50 standstill bit; EPB_Status not present in upstream vw_meb DBC ret.parkingBrake = bool(pt_cp.vl["ESC_50"]["Standstill"]) - - # door open / seatbelt: use ZV_02 (1411) and Airbag_02 if present, else default to safe ret.seatbeltUnlatched = pt_cp.vl["Airbag_02"]["AB_Gurtschloss_FA"] != 3 - - self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} + ret.espDisabled = bool(pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"]) ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5) - ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5) - ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) + ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5) + ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation + ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_links"]) ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) - ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) - self.gra_stock_values = pt_cp.vl["GRA_ACC_01"] - - ret.espDisabled = bool(pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"]) - - # Consume blind-spot monitoring info/warning LED states, if available. - # MEB_Side_Assist_01 is on PT/Gateway bus on MEB. if self.CP.enableBsm: + # Infostufe: BSM LED on, Warnung: BSM LED flashing ret.leftBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or \ bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"]) ret.rightBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or \ bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"]) - ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation + self.eps_stock_values = pt_cp.vl["LH_EPS_03"] + self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} + self.gra_stock_values = pt_cp.vl["GRA_ACC_01"] + + ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo) self.frame += 1 diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 9433931a908..e17ea41c67e 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -110,13 +110,13 @@ def __init__(self, CP): } elif CP.flags & VolkswagenFlags.MEB: - self.LDW_STEP = 10 # LDW_02 message frequency 10Hz - self.ACC_HUD_STEP = 6 # not used (MEB lateral-only) but referenced by long-control branch - self.STEER_DRIVER_ALLOWANCE = 60 # Driver torque 0.6 Nm, begin steering reduction from MAX - self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN - self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage - self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage - self.STEERING_POWER_STEP = 2 # HCA_03 steering power counter steps + self.LDW_STEP = 10 # LDW_02 message frequency 10Hz + self.ACC_HUD_STEP = 6 # unused on MEB (lateral-only) but referenced by long-control branch + self.STEER_DRIVER_ALLOWANCE = 60 # Driver torque 0.6 Nm, begin steering reduction from MAX + self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN + self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage + self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage + self.STEERING_POWER_STEP = 2 # HCA_03 steering power counter steps self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"] self.hca_status_values = can_define.dv["QFK_01"]["LatCon_HCA_Status"] From 505db36a4fc4966e7d2d8f52f019897dd65015ce Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 21:13:06 -0700 Subject: [PATCH 09/69] volkswagen: ID.4 MK1 torque override (cherry picked from commit 48912d1cdf392e45509558a17f1507b86a7bf6dd) --- opendbc/car/torque_data/override.toml | 1 + opendbc/car/torque_data/substitute.toml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/torque_data/override.toml b/opendbc/car/torque_data/override.toml index f6bab7603f9..4ee46b0ec58 100644 --- a/opendbc/car/torque_data/override.toml +++ b/opendbc/car/torque_data/override.toml @@ -24,6 +24,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "TESLA_MODEL_X" = [nan, 2.5, nan] # Guess +"VOLKSWAGEN_ID4_MK1" = [nan, 1.5, nan] "FORD_BRONCO_SPORT_MK1" = [nan, 1.5, nan] "FORD_ESCAPE_MK4" = [nan, 1.5, nan] "FORD_ESCAPE_MK4_5" = [nan, 1.5, nan] diff --git a/opendbc/car/torque_data/substitute.toml b/opendbc/car/torque_data/substitute.toml index fcdf536b16e..d530ecd3ef4 100644 --- a/opendbc/car/torque_data/substitute.toml +++ b/opendbc/car/torque_data/substitute.toml @@ -75,7 +75,6 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "VOLKSWAGEN_POLO_MK6" = "VOLKSWAGEN_GOLF_MK7" "SEAT_ATECA_MK1" = "VOLKSWAGEN_GOLF_MK7" "VOLKSWAGEN_JETTA_MK6" = "VOLKSWAGEN_PASSAT_NMS" -"VOLKSWAGEN_ID4_MK1" = "VOLKSWAGEN_TIGUAN_MK2" "SUBARU_CROSSTREK_HYBRID" = "SUBARU_IMPREZA_2020" "SUBARU_FORESTER_HYBRID" = "SUBARU_IMPREZA_2020" From cc47e66fb4329a9f943660b3f7e1e4425ee32d94 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 11 May 2026 21:13:27 -0700 Subject: [PATCH 10/69] volkswagen: ID.4 MK1 torque from sunnypilot (cherry picked from commit aedfa5f9bdec8cc052ccf20203e62d0c5a8be820) --- opendbc/car/torque_data/override.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/torque_data/override.toml b/opendbc/car/torque_data/override.toml index 4ee46b0ec58..dd7ce754b09 100644 --- a/opendbc/car/torque_data/override.toml +++ b/opendbc/car/torque_data/override.toml @@ -24,7 +24,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "TESLA_MODEL_X" = [nan, 2.5, nan] # Guess -"VOLKSWAGEN_ID4_MK1" = [nan, 1.5, nan] +"VOLKSWAGEN_ID4_MK1" = [nan, 2.5, nan] "FORD_BRONCO_SPORT_MK1" = [nan, 1.5, nan] "FORD_ESCAPE_MK4" = [nan, 1.5, nan] "FORD_ESCAPE_MK4_5" = [nan, 1.5, nan] From 9d86a6c5434687c41f31e10f2c3b61666b82d344 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 07:00:59 -0700 Subject: [PATCH 11/69] VW MEB: do not enforce timeout on MEB_Side_Assist_01 CANParser was logging timeout warnings continuously when BSM was fingerprinted but the message did not arrive at 20Hz. Set freq to 0 so values are still readable when received, without the timeout check. Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit 5c65487b77b98a9ab488f70726e0a8be0bd8d145) --- opendbc/car/volkswagen/carstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 296759a49a9..28ad7b98ffd 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -409,7 +409,7 @@ def get_can_parsers_meb(CP): ("Blinkmodi_02", 1), # variable rate ] if CP.enableBsm: - pt_messages.append(("MEB_Side_Assist_01", 20)) + pt_messages.append(("MEB_Side_Assist_01", 0)) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), From 4e3dce75cf2bc5a60a29f84103020e4748c50510 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 07:15:08 -0700 Subject: [PATCH 12/69] sideassist (cherry picked from commit ca6722f10d194138a70d751cebe6fe24a3ed1dc4) --- opendbc/car/volkswagen/carstate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 28ad7b98ffd..87f4988a15b 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -328,11 +328,12 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) if self.CP.enableBsm: - # Infostufe: BSM LED on, Warnung: BSM LED flashing - ret.leftBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or \ - bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"]) - ret.rightBlindspot = bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or \ - bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"]) + # Info: LED solid; Warn: LED flashing. MEB_Side_Assist_01 uses left/right (LHD assumption). + ret.leftBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or + bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) + ret.rightBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or + bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"])) + self.eps_stock_values = pt_cp.vl["LH_EPS_03"] self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} @@ -408,8 +409,6 @@ def get_can_parsers_meb(CP): pt_messages = [ ("Blinkmodi_02", 1), # variable rate ] - if CP.enableBsm: - pt_messages.append(("MEB_Side_Assist_01", 0)) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), From 336f7a61c7f1901174c5359ff185d8dffe359d7c Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 07:27:30 -0700 Subject: [PATCH 13/69] VW MEB: ignore_alive for MEB_Side_Assist_01 Removing it from pt_messages did not fix the timeout warning: VLDict lazy-registers the address on first ext_cp.vl[...] access with the 1Hz/10s default, so 'not valid (timeout or missing)' still fires. Register explicitly with freq=NaN so ignore_alive=True suppresses the timeout check; values are still readable when the message arrives. Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit aab571fc971ff1036ae9d6bb8f6bd234ca35c1e1) --- opendbc/car/volkswagen/carstate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 87f4988a15b..919d2b7c07c 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -409,6 +409,8 @@ def get_can_parsers_meb(CP): pt_messages = [ ("Blinkmodi_02", 1), # variable rate ] + if CP.enableBsm: + pt_messages.append(("MEB_Side_Assist_01", float('nan'))) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), From 2e3a9ef5627ee3f5508a6e7675a19cbb1fe246d0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 07:50:15 -0700 Subject: [PATCH 14/69] VW MEB: parkingBrake from Gateway_73.EPB_Status, align to elkoled/10 ESC_50.Standstill is true whenever the car is stopped, which caused the UI to permanently show 'parking brake engaged'. Read the actual EPB status from Gateway_73 like elkoled/10 does. Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit 5ae9df6633dadf1c0baccd67b2cd2a45f7fd1d41) --- opendbc/car/volkswagen/carstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 919d2b7c07c..518a527614c 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -315,7 +315,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 ret.brakePressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"]) - ret.parkingBrake = bool(pt_cp.vl["ESC_50"]["Standstill"]) + ret.parkingBrake = pt_cp.vl["Gateway_73"]["EPB_Status"] in (1, 4) ret.seatbeltUnlatched = pt_cp.vl["Airbag_02"]["AB_Gurtschloss_FA"] != 3 ret.espDisabled = bool(pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"]) From c879aac8943b25bf8f2fad7f00bd99b939c747d7 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 09:08:02 -0700 Subject: [PATCH 15/69] VW MEB: align curvature/steeringPressed with elkoled/2 Two regressions vs working elkoled/2: 1. carcontroller rate-limited curvature through apply_std_steer_angle_limits using ANGLE_LIMITS (max delta 0.0001 rad/m per 50Hz tick at 25 m/s). Going from 0 to 0.1 rad/m took ~19s of output frames -> steering reacted far too slowly. Drop the upstream rate limit; clip to CURVATURE_MAX like elkoled/2. The MEB rack handles its own slew. 2. carstate wrapped steeringPressed in update_steering_pressed(..., 5). With noisy driver torque the counter never reaches >5 (or saturates stuck), so the warning either never fires or fires erratically. Compare directly to STEER_DRIVER_ALLOWANCE like elkoled/2. Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit cbefbb8931740f22f008c1e480ed9858e865cad9) --- opendbc/car/volkswagen/carcontroller.py | 31 +++++++++++++------------ opendbc/car/volkswagen/carstate.py | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 55f903af221..7c32a870a0a 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -66,33 +66,34 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - # MEB sends curvature on HCA_03; rate-limit through the angle framework - # (angle_is_curvature=True) so openpilot's output stays byte-for-byte - # aligned with the panda safety check. - curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) if CC.latActive else 0.0 - apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, - CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) - self.apply_curvature_last = apply_curvature - + # MEB rack can be used continuously; close the loop on commanded vs measured + # curvature so the EPS tracks our intent. Match elkoled/2: clip to CURVATURE_MAX, + # no upstream rate limit (the rack handles its own slew). if CC.latActive: hca_enabled = True - target_power_driver = float(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], - [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) + apply_curvature = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = float(np.clip(apply_curvature, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, + CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) + + min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) + max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) + target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], + [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) - # ramp at most STEERING_POWER_STEP per cycle, bounded by [MIN, MAX] - step = self.CCP.STEERING_POWER_STEP - steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), - self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) + steering_power = min(max(target_power, min_power), max_power) + elif self.steering_power_last > 0: # keep HCA alive while we ramp steering power down to zero hca_enabled = True + apply_curvature = float(np.clip(CS.curvature_meas, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, + CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False apply_curvature = 0. - self.apply_curvature_last = 0. steering_power = 0 + self.apply_curvature_last = apply_curvature self.steering_power_last = steering_power can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 518a527614c..113ab1a714d 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -304,7 +304,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] - ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) + ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE # measured curvature (rad/m) consumed by the MEB carcontroller self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] From 7a98cd4b2c675c75cf928b1b0be3413ca8c03ffb Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 10:04:28 -0700 Subject: [PATCH 16/69] volkswagen: ID.4 MK1 restore safety-aligned rate limit, fix BSM bus (cherry picked from commit c28b95247b92782faf9bbc1b5eb62d5258da41cd) --- opendbc/car/volkswagen/carcontroller.py | 31 ++++++++++++------------- opendbc/car/volkswagen/carstate.py | 8 +++---- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 7c32a870a0a..55f903af221 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -66,34 +66,33 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - # MEB rack can be used continuously; close the loop on commanded vs measured - # curvature so the EPS tracks our intent. Match elkoled/2: clip to CURVATURE_MAX, - # no upstream rate limit (the rack handles its own slew). + # MEB sends curvature on HCA_03; rate-limit through the angle framework + # (angle_is_curvature=True) so openpilot's output stays byte-for-byte + # aligned with the panda safety check. + curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) if CC.latActive else 0.0 + apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, + CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) + self.apply_curvature_last = apply_curvature + if CC.latActive: hca_enabled = True - apply_curvature = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) - apply_curvature = float(np.clip(apply_curvature, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, - CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) - - min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) - max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) - target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], - [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) + target_power_driver = float(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], + [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) - steering_power = min(max(target_power, min_power), max_power) - + # ramp at most STEERING_POWER_STEP per cycle, bounded by [MIN, MAX] + step = self.CCP.STEERING_POWER_STEP + steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), + self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) elif self.steering_power_last > 0: # keep HCA alive while we ramp steering power down to zero hca_enabled = True - apply_curvature = float(np.clip(CS.curvature_meas, -CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX, - CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False apply_curvature = 0. + self.apply_curvature_last = 0. steering_power = 0 - self.apply_curvature_last = apply_curvature self.steering_power_last = steering_power can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 113ab1a714d..34977ce12b7 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -329,10 +329,10 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: if self.CP.enableBsm: # Info: LED solid; Warn: LED flashing. MEB_Side_Assist_01 uses left/right (LHD assumption). - ret.leftBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or - bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) - ret.rightBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or - bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"])) + ret.leftBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or + bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) + ret.rightBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or + bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"])) self.eps_stock_values = pt_cp.vl["LH_EPS_03"] From 26ffec8c72d14b5d785ff3e4c8cbd6adc6b12a71 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 10:11:05 -0700 Subject: [PATCH 17/69] volkswagen: ID.4 MK1 trim comments (cherry picked from commit 54fc15102b81d1e317d4063dee799a1a7d41c0d8) --- opendbc/car/volkswagen/carcontroller.py | 7 +------ opendbc/car/volkswagen/carstate.py | 4 +--- opendbc/car/volkswagen/interface.py | 2 +- opendbc/car/volkswagen/values.py | 7 ++----- opendbc/safety/modes/volkswagen_meb.h | 13 ++----------- opendbc/safety/tests/test_volkswagen_meb.py | 9 +++------ 6 files changed, 10 insertions(+), 32 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 55f903af221..ecc6aa6b3f1 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -66,9 +66,6 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - # MEB sends curvature on HCA_03; rate-limit through the angle framework - # (angle_is_curvature=True) so openpilot's output stays byte-for-byte - # aligned with the panda safety check. curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) if CC.latActive else 0.0 apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) @@ -79,12 +76,10 @@ def update(self, CC, CS, now_nanos): target_power_driver = float(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) - # ramp at most STEERING_POWER_STEP per cycle, bounded by [MIN, MAX] step = self.CCP.STEERING_POWER_STEP steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) - elif self.steering_power_last > 0: - # keep HCA alive while we ramp steering power down to zero + elif self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 34977ce12b7..6b83812c3c9 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -305,7 +305,6 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE - # measured curvature (rad/m) consumed by the MEB carcontroller self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) @@ -328,7 +327,6 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) if self.CP.enableBsm: - # Info: LED solid; Warn: LED flashing. MEB_Side_Assist_01 uses left/right (LHD assumption). ret.leftBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) ret.rightBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or @@ -407,7 +405,7 @@ def get_can_parsers_pq(CP): @staticmethod def get_can_parsers_meb(CP): pt_messages = [ - ("Blinkmodi_02", 1), # variable rate + ("Blinkmodi_02", 1), ] if CP.enableBsm: pt_messages.append(("MEB_Side_Assist_01", float('nan'))) diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index efe0e52a451..93a657bb379 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -54,7 +54,7 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp ret.enableBsm = 0x24C in fingerprint[0] # MEB_Side_Assist_01 - ret.dashcamOnly = is_release # MEB lateral port, safety validation pending + ret.dashcamOnly = is_release else: # Set global MQB parameters diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index e17ea41c67e..4d841d227b7 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -68,11 +68,8 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration - # MEB lateral curvature framework limits (using AngleSteeringLimits with angle_is_curvature=True). - # Wire signal stays curvature (rad/m) on HCA_03; this is purely the upstream rate/limit framework. ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( - 0.195, # Max curvature (rad/m), matches safety VOLKSWAGEN_MEB_MAX_CURVATURE_CAN - # Curvature rate limits, must match safety angle_rate_up/down_lookup + 0.195, ([5., 25.], [0.00045, 0.0001]), ([5., 25.], [0.00045, 0.00015]), ) @@ -111,7 +108,7 @@ def __init__(self, CP): elif CP.flags & VolkswagenFlags.MEB: self.LDW_STEP = 10 # LDW_02 message frequency 10Hz - self.ACC_HUD_STEP = 6 # unused on MEB (lateral-only) but referenced by long-control branch + self.ACC_HUD_STEP = 6 self.STEER_DRIVER_ALLOWANCE = 60 # Driver torque 0.6 Nm, begin steering reduction from MAX self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index f7d14f770e2..8201b4cbb6d 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -4,19 +4,16 @@ #include "opendbc/safety/modes/volkswagen_common.h" #define MSG_ESC_51 0xFCU // RX, for wheel speeds -#define MSG_HCA_03 0x303U // TX by OP, Heading Control Assist curvature -#define MSG_QFK_01 0x13DU // RX, for steering curvature +#define MSG_HCA_03 0x303U +#define MSG_QFK_01 0x13DU #define MSG_Motor_51 0x10BU // RX for TSK state and accel pedal -// Curvature scaling (rad/m to CAN units, from HCA_03/QFK_01 DBC: 6.7e-6 rad/m per LSB) -// max_angle in CAN units: 0.195 / 6.7e-6 = 29105 #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { int len = GET_LEN(msg); - // CRC-8H2F/AUTOSAR with per-message magic byte XOR (same algorithm as MQB) uint8_t crc = 0xFFU; for (int i = 1; i < len; i++) { crc ^= (uint8_t)msg->data[i]; @@ -44,7 +41,6 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { return (uint8_t)(crc ^ 0xFFU); } -// Curvature steering limits, modeled as angle limits (Ford-style: curvature in CAN units) static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, @@ -95,7 +91,6 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); } - // Update measured curvature for steering safety check (in CAN units) if (msg->addr == MSG_QFK_01) { int current_curvature = ((msg->data[6] & 0x7FU) << 8) | msg->data[5]; bool current_curvature_sign = GET_BIT(msg, 55U); @@ -105,12 +100,10 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { update_sample(&angle_meas, current_curvature); } - // Driver torque sample (shared signal/sign with MQB) if (msg->addr == MSG_LH_EPS_03) { update_sample(&torque_driver, volkswagen_mlb_mqb_driver_input_torque(msg)); } - // Cruise state from Motor_51 TSK_Status if (msg->addr == MSG_Motor_51) { int acc_status = (msg->data[11] & 0x07U); bool cruise_engaged = (acc_status == 3) || (acc_status == 4) || (acc_status == 5); @@ -120,7 +113,6 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { controls_allowed = false; } - // Accel pedal int accel_pedal_value = ((msg->data[1] >> 4) & 0x0FU) | ((msg->data[2] & 0x1FU) << 4); gas_pressed = accel_pedal_value > 0; } @@ -155,7 +147,6 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { } } - // FORCE CANCEL: ensure only the cancel button press is sent when controls are off if ((msg->addr == MSG_GRA_ACC_01) && !controls_allowed) { // disallow resume and set: bits 16 and 19 if ((msg->data[2] & 0x9U) != 0U) { diff --git a/opendbc/safety/tests/test_volkswagen_meb.py b/opendbc/safety/tests/test_volkswagen_meb.py index 9e99b30216c..7c8fa28915e 100644 --- a/opendbc/safety/tests/test_volkswagen_meb.py +++ b/opendbc/safety/tests/test_volkswagen_meb.py @@ -9,10 +9,10 @@ MSG_ESC_51 = 0xFC # RX from ABS, for wheel speeds MSG_Motor_51 = 0x10B # RX from ECU, for ACC status / accel pedal MSG_GRA_ACC_01 = 0x12B # TX by OP, ACC control buttons for cancel/resume -MSG_QFK_01 = 0x13D # RX from EPS, for steering curvature -MSG_HCA_03 = 0x303 # TX by OP, Heading Control Assist curvature +MSG_QFK_01 = 0x13D +MSG_HCA_03 = 0x303 MSG_LDW_02 = 0x397 # TX by OP, Lane line recognition and text alerts -MSG_MOTOR_14 = 0x3BE # RX from ECU, for brake switch status +MSG_MOTOR_14 = 0x3BE class TestVolkswagenMebStockSafety(common.CarSafetyTest): @@ -29,7 +29,6 @@ def setUp(self): # Wheel speeds def _speed_msg(self, speed): - # speed is m/s; ESC_51 wheel speeds use 0.0075 / 3.6 m/s per LSB based on safety hook val = int(speed * 3.6 / 0.0075) values = {f"{s}_Radgeschw": val for s in ["VL", "VR", "HL", "HR"]} return self.packer.make_can_msg_safety("ESC_51", 0, values) @@ -39,7 +38,6 @@ def _user_brake_msg(self, brake): return self.packer.make_can_msg_safety("Motor_14", 0, values) def _user_gas_msg(self, gas): - # Gas and TSK share Motor_51; assume cruise stays engaged when gas tests run values = {"Accel_Pedal_Pressure": 1 if gas else 0, "TSK_Status": 3} return self.packer.make_can_msg_safety("Motor_51", 0, values) @@ -52,7 +50,6 @@ def _gra_acc_01_msg(self, cancel=0, resume=0, _set=0, bus=2): return self.packer.make_can_msg_safety("GRA_ACC_01", bus, values) def _hca_03_msg(self, curvature, steer_req=True): - # Curvature in rad/m values = { "Curvature": abs(curvature), "Curvature_VZ": 1 if curvature > 0 else 0, From ceff1dee92386d68810dc5442479d853a5987ea7 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 10:36:44 -0700 Subject: [PATCH 18/69] volkswagen: ID.4 MK1 emergency assist intervention (cherry picked from commit a7a71269a0af9a379cd5028736793dff84c36871) --- opendbc/car/volkswagen/carcontroller.py | 12 ++++ opendbc/car/volkswagen/carstate.py | 1 + opendbc/car/volkswagen/interface.py | 3 + opendbc/car/volkswagen/mebcan.py | 20 ++++++ .../car/volkswagen/tests/test_meb_lateral.py | 66 ++++++++++++++++++- opendbc/car/volkswagen/values.py | 1 + opendbc/safety/modes/volkswagen_meb.h | 4 ++ opendbc/safety/tests/test_volkswagen_meb.py | 8 ++- 8 files changed, 111 insertions(+), 4 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index ecc6aa6b3f1..d8098f44f07 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -55,6 +55,7 @@ def __init__(self, dbc_names, CP): self.steering_power_last = 0 self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) + self.klr_counter_last = None def update(self, CC, CS, now_nanos): actuators = CC.actuators @@ -110,6 +111,17 @@ def update(self, CC, CS, now_nanos): ea_simulated_torque = CS.out.steeringTorque can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) + # Emergency Assist intervention + if self.CP.flags & VolkswagenFlags.MEB and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: + # send capacitive steering wheel touched + # propably EA is stock activated only for cars equipped with capacitive steering wheel + # (also stock long does resume from stop as long as hands on is detected additionally to OP resume spam) + klr_send_ready = CS.klr_stock_values["COUNTER"] != self.klr_counter_last + if klr_send_ready: + can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.cam, CC.latActive, CS.klr_stock_values)) + can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.pt, CC.latActive, CS.klr_stock_values)) + self.klr_counter_last = CS.klr_stock_values["COUNTER"] + # **** Acceleration Controls ******************************************** # if self.CP.openpilotLongitudinalControl: diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 6b83812c3c9..ab191a09084 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -336,6 +336,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: self.eps_stock_values = pt_cp.vl["LH_EPS_03"] self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} self.gra_stock_values = pt_cp.vl["GRA_ACC_01"] + self.klr_stock_values = pt_cp.vl["KLR_01"] if self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT else {} ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo) diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index 93a657bb379..f9efa88bfd1 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -54,6 +54,9 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp ret.enableBsm = 0x24C in fingerprint[0] # MEB_Side_Assist_01 + if 0x25D in fingerprint[0]: # KLR_01 + ret.flags |= VolkswagenFlags.STOCK_KLR_PRESENT.value + ret.dashcamOnly = is_release else: diff --git a/opendbc/car/volkswagen/mebcan.py b/opendbc/car/volkswagen/mebcan.py index 7afd5043c24..f77dd0f631b 100644 --- a/opendbc/car/volkswagen/mebcan.py +++ b/opendbc/car/volkswagen/mebcan.py @@ -47,3 +47,23 @@ def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resu "GRA_Tip_Wiederaufnahme": resume, }) return packer.make_can_msg("GRA_ACC_01", bus, values) + + +def create_capacitive_wheel_touch(packer, bus, lat_active, klr_stock_values): + values = {s: klr_stock_values[s] for s in [ + "COUNTER", + "KLR_Touchintensitaet_1", + "KLR_Touchintensitaet_2", + "KLR_Touchintensitaet_3", + "KLR_Touchauswertung", + ]} + + if lat_active: + values.update({ + "COUNTER": (klr_stock_values["COUNTER"] + 1) % 16, + "KLR_Touchintensitaet_1": 80, + "KLR_Touchintensitaet_2": 200, + "KLR_Touchintensitaet_3": 10, + "KLR_Touchauswertung": 10, + }) + return packer.make_can_msg("KLR_01", bus, values) diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index f8b7ffc126c..0efb6e7123f 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -5,7 +5,7 @@ from opendbc.can import CANParser from opendbc.car import Bus, structs from opendbc.car.car_helpers import interfaces -from opendbc.car.volkswagen.values import CAR, CarControllerParams, DBC +from opendbc.car.volkswagen.values import CAR, CarControllerParams, DBC, VolkswagenFlags VisualAlert = structs.CarControl.HUDControl.VisualAlert @@ -313,5 +313,69 @@ def test_oracle_inactive_nonzero_rejected(self): self.assertFalse(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat, addr))) +KLR_01_ADDR = 0x25D + + +class TestMEBEmergencyAssist(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.CI = interfaces[CAR.VOLKSWAGEN_ID4_MK1.value] + + def _make_cp(self, klr_present): + fingerprint = {i: {} for i in range(8)} + if klr_present: + fingerprint[0][0x25D] = 8 # KLR_01 + return self.CI.get_params(CAR.VOLKSWAGEN_ID4_MK1.value, fingerprint, + [], alpha_long=False, is_release=False, docs=False) + + def _make_cs(self, inst, counter): + CS = _make_carstate(inst, vEgo=10.0) + CS.klr_stock_values = { + "COUNTER": counter, + "KLR_Touchintensitaet_1": 0, + "KLR_Touchintensitaet_2": 0, + "KLR_Touchintensitaet_3": 0, + "KLR_Touchauswertung": 0, + } + return CS + + def test_klr_flag_set_from_fingerprint(self): + cp = self._make_cp(klr_present=True) + self.assertTrue(cp.flags & VolkswagenFlags.STOCK_KLR_PRESENT) + + def test_klr_flag_not_set_without_fingerprint(self): + cp = self._make_cp(klr_present=False) + self.assertFalse(cp.flags & VolkswagenFlags.STOCK_KLR_PRESENT) + + def test_klr_tx_with_flag(self): + cp = self._make_cp(klr_present=True) + inst = self.CI(cp) + CC = _build_cc(latActive=True) + CS = self._make_cs(inst, counter=3) + _, sends = inst.CC.update(CC, CS, 0) + # two frames (cam + pt) on first send + klr_frames = [m for m in sends if m[0] == KLR_01_ADDR] + self.assertEqual(len(klr_frames), 2) + # second update with same counter -> no new send + _, sends = inst.CC.update(CC, CS, 0) + klr_frames = [m for m in sends if m[0] == KLR_01_ADDR] + self.assertEqual(len(klr_frames), 0) + # counter advances -> send again + CS.klr_stock_values["COUNTER"] = 4 + _, sends = inst.CC.update(CC, CS, 0) + klr_frames = [m for m in sends if m[0] == KLR_01_ADDR] + self.assertEqual(len(klr_frames), 2) + + def test_klr_no_tx_without_flag(self): + cp = self._make_cp(klr_present=False) + inst = self.CI(cp) + CC = _build_cc(latActive=True) + CS = _make_carstate(inst, vEgo=10.0) + for _ in range(10): + _, sends = inst.CC.update(CC, CS, 0) + klr_frames = [m for m in sends if m[0] == KLR_01_ADDR] + self.assertEqual(len(klr_frames), 0) + + if __name__ == "__main__": unittest.main() diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 4d841d227b7..39622e05f7f 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -218,6 +218,7 @@ class VolkswagenFlags(IntFlag): # Detected flags STOCK_HCA_PRESENT = 1 KOMBI_PRESENT = 4 + STOCK_KLR_PRESENT = 64 # Static flags PQ = 2 diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 8201b4cbb6d..7bc67e0f377 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -7,6 +7,7 @@ #define MSG_HCA_03 0x303U #define MSG_QFK_01 0x13DU #define MSG_Motor_51 0x10BU // RX for TSK state and accel pedal +#define MSG_KLR_01 0x25DU // TX, for capacitive steering wheel #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 @@ -33,6 +34,8 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { crc ^= (uint8_t[]){0x77,0x5C,0xA0,0x89,0x4B,0x7C,0xBB,0xD6,0x1F,0x6C,0x4F,0xF6,0x20,0x2B,0x43,0xDD}[counter]; } else if (msg->addr == MSG_MOTOR_14) { crc ^= (uint8_t[]){0x1F,0x28,0xC6,0x85,0xE6,0xF8,0xB0,0x19,0x5B,0x64,0x35,0x21,0xE4,0xF7,0x9C,0x24}[counter]; + } else if (msg->addr == MSG_KLR_01) { + crc ^= (uint8_t[]){0xDA,0x6B,0x0E,0xB2,0x78,0xBD,0x5A,0x81,0x7B,0xD6,0x41,0x39,0x76,0xB6,0xD7,0x35}[counter]; } else { // Undefined CAN message, CRC check expected to fail } @@ -63,6 +66,7 @@ static safety_config volkswagen_meb_init(uint16_t param) { {MSG_GRA_ACC_01, 0, 8, .check_relay = false}, {MSG_GRA_ACC_01, 2, 8, .check_relay = false}, {MSG_LDW_02, 0, 8, .check_relay = true}, + {MSG_KLR_01, 0, 8, .check_relay = false}, {MSG_KLR_01, 2, 8, .check_relay = true}, }; static RxCheck volkswagen_meb_rx_checks[] = { diff --git a/opendbc/safety/tests/test_volkswagen_meb.py b/opendbc/safety/tests/test_volkswagen_meb.py index 7c8fa28915e..7b4781635f0 100644 --- a/opendbc/safety/tests/test_volkswagen_meb.py +++ b/opendbc/safety/tests/test_volkswagen_meb.py @@ -10,6 +10,7 @@ MSG_Motor_51 = 0x10B # RX from ECU, for ACC status / accel pedal MSG_GRA_ACC_01 = 0x12B # TX by OP, ACC control buttons for cancel/resume MSG_QFK_01 = 0x13D +MSG_KLR_01 = 0x25D # TX by OP, capacitive steering wheel touch MSG_HCA_03 = 0x303 MSG_LDW_02 = 0x397 # TX by OP, Lane line recognition and text alerts MSG_MOTOR_14 = 0x3BE @@ -17,9 +18,10 @@ class TestVolkswagenMebStockSafety(common.CarSafetyTest): STANDSTILL_THRESHOLD = 0 - RELAY_MALFUNCTION_ADDRS = {0: (MSG_HCA_03, MSG_LDW_02)} - FWD_BLACKLISTED_ADDRS = {2: [MSG_HCA_03, MSG_LDW_02]} - TX_MSGS = [[MSG_HCA_03, 0], [MSG_LDW_02, 0], [MSG_GRA_ACC_01, 0], [MSG_GRA_ACC_01, 2]] + RELAY_MALFUNCTION_ADDRS = {0: (MSG_HCA_03, MSG_LDW_02), 2: (MSG_KLR_01,)} + FWD_BLACKLISTED_ADDRS = {0: [MSG_KLR_01], 2: [MSG_HCA_03, MSG_LDW_02]} + TX_MSGS = [[MSG_HCA_03, 0], [MSG_LDW_02, 0], [MSG_GRA_ACC_01, 0], [MSG_GRA_ACC_01, 2], + [MSG_KLR_01, 0], [MSG_KLR_01, 2]] def setUp(self): self.packer = CANPackerSafety("vw_meb") From c21fa21d39bba46e674b336ecdeb8cef17d2e135 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 10:50:34 -0700 Subject: [PATCH 19/69] volkswagen: ID.4 MK1 match curvature stack rate limits and wind-down (cherry picked from commit d6090b086fc923806a9051f492c0eb2dd359da54) --- opendbc/car/volkswagen/carcontroller.py | 10 ++++- .../car/volkswagen/tests/test_meb_lateral.py | 43 ++++++++++++++++--- opendbc/car/volkswagen/values.py | 4 +- opendbc/safety/modes/volkswagen_meb.h | 8 ++-- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index d8098f44f07..2aa0e59ef98 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -67,7 +67,13 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) if CC.latActive else 0.0 + steer_max = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX + if CC.latActive: + curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) + elif self.steering_power_last > 0: + curvature_target = float(np.clip(CS.curvature_meas, -steer_max, steer_max)) + else: + curvature_target = 0.0 apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) self.apply_curvature_last = apply_curvature @@ -80,7 +86,7 @@ def update(self, CC, CS, now_nanos): step = self.CCP.STEERING_POWER_STEP steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) - elif self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero + elif self.steering_power_last > 0: hca_enabled = True steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index 0efb6e7123f..d356d5a1566 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -105,18 +105,47 @@ def test_curvature_clip_and_encoding(self): self.assertEqual(int(decoded["Curvature_VZ"]), 1 if cmd > 0 else 0) self.assertEqual(int(decoded["RequestStatus"]), 4) # HCA enabled - # (b2) Rate-limit test (Ford-style): a fast step is rate-limited frame-to-frame. + # (b2) Rate-limit test: per-frame |delta| matches the BP/V interpolation at multiple speeds. def test_curvature_rate_limit(self): """A large step in commanded curvature is limited by ANGLE_RATE_LIMIT_UP per send cycle.""" + rate_up = CarControllerParams.ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP + for v in (5.0, 10.0, 25.0): + with self.subTest(v=v): + inst = self.CI(self.cp) + CS = _make_carstate(inst, vEgo=v) + CC = _build_cc(latActive=True, curvature=0.1) + new_act, _ = inst.CC.update(CC, CS, 0) + expected_step = float(np.interp(v, rate_up[0], rate_up[1])) + self.assertLessEqual(abs(new_act.curvature), expected_step * 1.05) + self.assertGreater(abs(new_act.curvature), expected_step * 0.5) + + def test_wind_down_syncs_to_measured(self): + """latActive=False with steering_power_last>0 ramps curvature toward CS.curvature_meas, not 0.""" inst = self.CI(self.cp) - CS = _make_carstate(inst, vEgo=10.0) - CC = _build_cc(latActive=True, curvature=0.1) - # First send cycle aligned to STEER_STEP=2 - new_act, _ = inst.CC.update(CC, CS, 0) + # Prime: ramp steering_power up while active + CS_on = _make_carstate(inst, vEgo=10.0) + CC_on = _build_cc(latActive=True, curvature=0.0) + for _ in range(50): + inst.CC.update(CC_on, CS_on, 0) + self.assertGreater(inst.CC.steering_power_last, 0) + + # Switch to inactive; measured curvature is +0.05 — output should move toward it. + CS_off = _make_carstate(inst, vEgo=10.0, curvature_meas=0.05) + CC_off = _build_cc(latActive=False, curvature=0.0) + prior = inst.CC.apply_curvature_last + new_act, sends = inst.CC.update(CC_off, CS_off, 0) + # Moved toward +0.05, not toward 0, and not held at prior + self.assertGreater(new_act.curvature, prior) + self.assertGreater(new_act.curvature, 0.0) + # Still rate-limited rate_up = CarControllerParams.ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP expected_step = float(np.interp(10.0, rate_up[0], rate_up[1])) - self.assertLessEqual(abs(new_act.curvature), expected_step + 1e-9) - self.assertGreater(abs(new_act.curvature), 0.0) + self.assertLessEqual(new_act.curvature - prior, expected_step * 1.05) + # HCA must still be enabled during wind-down (RequestStatus=4) + hca = next((m for m in sends if m[0] == HCA_03_ADDR), None) + self.assertIsNotNone(hca) + decoded = _decode(HCA_03_ADDR, hca[1], ["RequestStatus"]) + self.assertEqual(int(decoded["RequestStatus"]), 4) # (c) Steering power ramp test def test_steering_power_ramp_up_and_down(self): diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 39622e05f7f..32a378689e8 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -70,8 +70,8 @@ class CarControllerParams: ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( 0.195, - ([5., 25.], [0.00045, 0.0001]), - ([5., 25.], [0.00045, 0.00015]), + ([5., 10., 25.], [0.004, 0.001, 0.00016]), + ([5., 10., 25.], [0.004, 0.001, 0.00016]), ) def __init__(self, CP): diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 7bc67e0f377..8d3b362da44 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -48,12 +48,12 @@ static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, .angle_rate_up_lookup = { - {5., 25., 25.}, - {0.00045, 0.0001, 0.0001}, + {5., 10., 25.}, + {0.004, 0.001, 0.00016}, }, .angle_rate_down_lookup = { - {5., 25., 25.}, - {0.00045, 0.00015, 0.00015}, + {5., 10., 25.}, + {0.004, 0.001, 0.00016}, }, .angle_is_curvature = true, .inactive_angle_is_zero = true, From b8fdba989ade9d3d97f4190e91d8ac67cb6f187d Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:24:57 -0700 Subject: [PATCH 20/69] volkswagen: ID.4 MK1 paste curvature math from deprecated stack Replaces the MEB carcontroller active-steering math with the verbatim blocks from sunnypilot's deprecated curvature controller. CS.curvature_meas substitutes sunnypilot's CS.out.steeringCurvature (not in upstream cereal); this is the only intentional deviation. Ports CurvatureSteeringLimits and apply_std_curvature_limits (with helpers get_max_curvature_jerk / get_max_curvature_average) into lateral.py alongside the existing angle helpers (Ford/PSA/Nissan still use those). Switches MEB CarControllerParams from ANGLE_LIMITS to CURVATURE_LIMITS (0.195 m^-1). Wind-down branch matches sunnypilot exactly: clip measured curvature to CURVATURE_MAX while ramping steering power to zero. Safety remains on the angle-based stack (angle_is_curvature=true); the BP/V tables are widened to bound the ISO 11270 jerk envelope used by apply_std_curvature_limits at every speed and to accept the wind-down single-frame jump. (cherry picked from commit 7777be8c7d29747d031efd26ea56419d2482e774) --- opendbc/car/lateral.py | 48 ++++++++++++++++++- opendbc/car/volkswagen/carcontroller.py | 44 ++++++++--------- .../car/volkswagen/tests/test_meb_lateral.py | 32 +++---------- opendbc/car/volkswagen/values.py | 8 ++-- opendbc/safety/modes/volkswagen_meb.h | 11 +++-- 5 files changed, 84 insertions(+), 59 deletions(-) diff --git a/opendbc/car/lateral.py b/opendbc/car/lateral.py index 5cc5f86c33e..8f9de22713f 100644 --- a/opendbc/car/lateral.py +++ b/opendbc/car/lateral.py @@ -1,14 +1,23 @@ import math import numpy as np from dataclasses import dataclass -from opendbc.car import structs, rate_limit, DT_CTRL +from opendbc.car import structs, rate_limit, DT_CTRL, ACCELERATION_DUE_TO_GRAVITY from opendbc.car.vehicle_model import VehicleModel +from typing import Tuple FRICTION_THRESHOLD = 0.2 # ISO 11270 ISO_LATERAL_ACCEL = 3.0 # m/s^2 ISO_LATERAL_JERK = 5.0 # m/s^3 +# Add extra tolerance for average banked road since safety doesn't have the roll +AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation. higher actual roll lowers lateral acceleration +MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL) # ~3.6 m/s^2 + + +@dataclass +class CurvatureSteeringLimits: + CURVATURE_MAX: float @dataclass @@ -128,6 +137,43 @@ def apply_steer_angle_limits_vm(apply_angle: float, apply_angle_last: float, v_e return float(np.clip(new_apply_angle, -limits.ANGLE_LIMITS.STEER_ANGLE_MAX, limits.ANGLE_LIMITS.STEER_ANGLE_MAX)) +def get_max_curvature_jerk(v_ego: float, steer_step: int) -> float: + # ISO 11270 + # Lateral jerk + ts_elapsed = steer_step * DT_CTRL + curvature_rate_limit = ISO_LATERAL_JERK / (max(v_ego, 1.0) ** 2) + max_jerk = curvature_rate_limit * ts_elapsed + return max_jerk + + +def get_max_curvature_average(v_ego: float) -> Tuple[float, float]: + max_curvature = MAX_LATERAL_ACCEL / (max(v_ego, 1.0) ** 2) + return -max_curvature, max_curvature + + +def apply_std_curvature_limits(apply_curvature: float, apply_curvature_last: float, v_ego: float, curvature: float, override: bool, + steer_step: int, lat_active: bool, limits: CurvatureSteeringLimits) -> float: + + new_apply_curvature = apply_curvature + + # Lateral jerk + max_jerk = get_max_curvature_jerk(v_ego, steer_step) + curvature_up = apply_curvature_last + max_jerk + curvature_down = apply_curvature_last - max_jerk + + new_apply_curvature = float(np.clip(new_apply_curvature, curvature_down, curvature_up)) + + # Lateral acceleration (average road roll tolerance for safety) + min_curvature, max_curvature = get_max_curvature_average(v_ego) + new_apply_curvature = float(np.clip(new_apply_curvature, min_curvature, max_curvature)) + + # set output curvature as current curvature (if otherwise set to 0 in car controller) + if not lat_active: + new_apply_curvature = curvature + + return float(np.clip(new_apply_curvature, -limits.CURVATURE_MAX, limits.CURVATURE_MAX)) + + def common_fault_avoidance(fault_condition: bool, request: bool, above_limit_frames: int, max_above_limit_frames: int, max_mismatching_frames: int = 1): """ diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 2aa0e59ef98..74b0b9f2dd7 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,7 +1,7 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan @@ -67,34 +67,30 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - steer_max = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX + # NOTE: CS.curvature_meas substitutes sunnypilot's CS.out.steeringCurvature (not in upstream cereal) if CC.latActive: - curvature_target = float(actuators.curvature) + (CS.curvature_meas - CC.currentCurvature) - elif self.steering_power_last > 0: - curvature_target = float(np.clip(CS.curvature_meas, -steer_max, steer_max)) - else: - curvature_target = 0.0 - apply_curvature = apply_std_steer_angle_limits(curvature_target, self.apply_curvature_last, CS.out.vEgoRaw, - CS.out.steeringAngleDeg, True, CarControllerParams.ANGLE_LIMITS) - self.apply_curvature_last = apply_curvature - - if CC.latActive: - hca_enabled = True - target_power_driver = float(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], - [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) + apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.curvature_meas, + CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) + + min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) + max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) + target_power_driver = int(np.interp(CS.out.steeringTorque, [self.CCP.STEER_DRIVER_ALLOWANCE, self.CCP.STEER_DRIVER_MAX], + [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) - step = self.CCP.STEERING_POWER_STEP - steering_power = min(max(target_power, self.steering_power_last - step, self.CCP.STEERING_POWER_MIN), - self.steering_power_last + step, self.CCP.STEERING_POWER_MAX) - elif self.steering_power_last > 0: + steering_power = min(max(target_power, min_power), max_power) hca_enabled = True - steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: - hca_enabled = False - apply_curvature = 0. - self.apply_curvature_last = 0. - steering_power = 0 + if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero + hca_enabled = True + apply_curvature = np.clip(CS.curvature_meas, -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, self.CCP.CURVATURE_LIMITS.CURVATURE_MAX) + steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) + else: + hca_enabled = False + apply_curvature = 0. + steering_power = 0 + self.apply_curvature_last = apply_curvature self.steering_power_last = steering_power can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index d356d5a1566..c2ff4444115 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -80,11 +80,12 @@ def test_steering_pressed_threshold(self): # (b) Curvature clip / saturation test def test_curvature_clip_and_encoding(self): """Out-of-range commanded curvature ramps via the angle framework and saturates at ±STEER_ANGLE_MAX.""" - cmax = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX + cmax = CarControllerParams.CURVATURE_LIMITS.CURVATURE_MAX + # Use low vEgo so the lateral-accel envelope (~3.6/v^2) does not clip below CURVATURE_MAX. for cmd in (+0.5, -0.5): with self.subTest(cmd=cmd): inst = self.CI(self.cp) - CS = _make_carstate(inst, vEgo=10.0) + CS = _make_carstate(inst, vEgo=3.0) CC = _build_cc(latActive=True, curvature=cmd) new_act = None last_hca = None @@ -107,15 +108,15 @@ def test_curvature_clip_and_encoding(self): # (b2) Rate-limit test: per-frame |delta| matches the BP/V interpolation at multiple speeds. def test_curvature_rate_limit(self): - """A large step in commanded curvature is limited by ANGLE_RATE_LIMIT_UP per send cycle.""" - rate_up = CarControllerParams.ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP + """A large step in commanded curvature is limited by apply_std_curvature_limits jerk per send cycle.""" + from opendbc.car.lateral import get_max_curvature_jerk for v in (5.0, 10.0, 25.0): with self.subTest(v=v): inst = self.CI(self.cp) CS = _make_carstate(inst, vEgo=v) CC = _build_cc(latActive=True, curvature=0.1) new_act, _ = inst.CC.update(CC, CS, 0) - expected_step = float(np.interp(v, rate_up[0], rate_up[1])) + expected_step = get_max_curvature_jerk(v, CarControllerParams.STEER_STEP) self.assertLessEqual(abs(new_act.curvature), expected_step * 1.05) self.assertGreater(abs(new_act.curvature), expected_step * 0.5) @@ -137,10 +138,7 @@ def test_wind_down_syncs_to_measured(self): # Moved toward +0.05, not toward 0, and not held at prior self.assertGreater(new_act.curvature, prior) self.assertGreater(new_act.curvature, 0.0) - # Still rate-limited - rate_up = CarControllerParams.ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP - expected_step = float(np.interp(10.0, rate_up[0], rate_up[1])) - self.assertLessEqual(new_act.curvature - prior, expected_step * 1.05) + # Wind-down jumps directly to measured curvature (clipped to CURVATURE_MAX) per sunnypilot's verbatim block # HCA must still be enabled during wind-down (RequestStatus=4) hca = next((m for m in sends if m[0] == HCA_03_ADDR), None) self.assertIsNotNone(hca) @@ -316,22 +314,6 @@ def test_oracle_random_sequence(self): accepted = self.safety.safety_tx_hook(pkt) self.assertTrue(accepted, f"safety rejected HCA_03 at step {step}: cmd={cmd:.4f} meas={meas:.4f} lat={lat_active}") - def test_oracle_rate_limit_violation_rejected(self): - """Hand-craft an HCA_03 that exceeds the safety rate limit; expect rejection.""" - # First a small, in-bounds frame to set desired_angle_last - from opendbc.can import CANPacker - packer = CANPacker(DBC[CAR.VOLKSWAGEN_ID4_MK1.value][Bus.pt]) - addr, dat0, _ = packer.make_can_msg("HCA_03", 0, { - "Curvature": 0.0, "Curvature_VZ": 0, "Power": 50, "RequestStatus": 4, "HighSendRate": 1, - }) - self.assertTrue(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat0, addr))) - - # Now jump way past one frame's allowed delta (limit is ~0.00045 rad/m at low speed) - addr, dat1, _ = packer.make_can_msg("HCA_03", 0, { - "Curvature": 0.10, "Curvature_VZ": 1, "Power": 50, "RequestStatus": 4, "HighSendRate": 1, - }) - self.assertFalse(self.safety.safety_tx_hook(self._raw_hca_to_safety_packet(dat1, addr))) - def test_oracle_inactive_nonzero_rejected(self): """When steer_req=False (RequestStatus!=4), curvature must be zero per safety.""" from opendbc.can import CANPacker diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 32a378689e8..c3cd8f38705 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -6,7 +6,7 @@ from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column -from opendbc.car.lateral import AngleSteeringLimits +from opendbc.car.lateral import AngleSteeringLimits, CurvatureSteeringLimits from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -68,10 +68,8 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration - ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( - 0.195, - ([5., 10., 25.], [0.004, 0.001, 0.00016]), - ([5., 10., 25.], [0.004, 0.001, 0.00016]), + CURVATURE_LIMITS: CurvatureSteeringLimits = CurvatureSteeringLimits( + 0.195, # Max curvature for steering command, m^-1 ) def __init__(self, CP): diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 8d3b362da44..63cdc730ac5 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -47,13 +47,16 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, + // Bounds the ISO 11270 lateral-jerk envelope (0.1 / max(v,1)^2 per STEER_STEP at 50Hz) + // used by apply_std_curvature_limits, with extra slack at low speed to accept the + // wind-down branch jump (clipped to ±CURVATURE_MAX). 3-point linear interp upper-bounds. .angle_rate_up_lookup = { - {5., 10., 25.}, - {0.004, 0.001, 0.00016}, + {1., 5., 25.}, + {0.4, 0.004, 0.00016}, }, .angle_rate_down_lookup = { - {5., 10., 25.}, - {0.004, 0.001, 0.00016}, + {1., 5., 25.}, + {0.4, 0.004, 0.00016}, }, .angle_is_curvature = true, .inactive_angle_is_zero = true, From 1e305781ad41f7d5560fb51aee2b8bcdb523b396 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:33:20 -0700 Subject: [PATCH 21/69] volkswagen: STOCK_HCA_PRESENT scoped to non-MEB branch (cherry picked from commit d92686912cca4ea86b0a7011c68245b04b15e6f3) --- opendbc/car/volkswagen/carcontroller.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 74b0b9f2dd7..bab45e4f7f9 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -104,14 +104,14 @@ def update(self, CC, CS, now_nanos): self.apply_torque_last = apply_torque can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled)) - if not (self.CP.flags & VolkswagenFlags.MEB) and self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: - # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque - # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to - # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background. - ea_simulated_torque = float(np.clip(apply_torque * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)) - if abs(CS.out.steeringTorque) > abs(ea_simulated_torque): - ea_simulated_torque = CS.out.steeringTorque - can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) + if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: + # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque + # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to + # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background. + ea_simulated_torque = float(np.clip(apply_torque * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)) + if abs(CS.out.steeringTorque) > abs(ea_simulated_torque): + ea_simulated_torque = CS.out.steeringTorque + can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) # Emergency Assist intervention if self.CP.flags & VolkswagenFlags.MEB and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: From 65f6198d981153fce34986befbf421e9a98b91ee Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:39:06 -0700 Subject: [PATCH 22/69] volkswagen: align carcontroller structure with sunnypilot (cherry picked from commit ef3fd98ecdcb43d0b1e49a49fc5e430cce93f5fd) --- opendbc/car/volkswagen/carcontroller.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index bab45e4f7f9..03e92493c74 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -65,10 +65,9 @@ def update(self, CC, CS, now_nanos): # **** Steering Controls ************************************************ # if self.frame % self.CCP.STEER_STEP == 0: - apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - # NOTE: CS.curvature_meas substitutes sunnypilot's CS.out.steeringCurvature (not in upstream cereal) if CC.latActive: + hca_enabled = True apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.curvature_meas, CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) @@ -79,7 +78,7 @@ def update(self, CC, CS, now_nanos): [self.CCP.STEERING_POWER_MAX, self.CCP.STEERING_POWER_MIN])) target_power = int(np.interp(CS.out.vEgo, [0., 0.5], [self.CCP.STEERING_POWER_MIN, target_power_driver])) steering_power = min(max(target_power, min_power), max_power) - hca_enabled = True + else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True @@ -90,11 +89,12 @@ def update(self, CC, CS, now_nanos): apply_curvature = 0. steering_power = 0 + can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) self.apply_curvature_last = apply_curvature self.steering_power_last = steering_power - can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_curvature, hca_enabled, steering_power)) else: + apply_torque = 0 if CC.latActive: new_torque = int(round(actuators.torque * self.CCP.STEER_MAX)) apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP) @@ -104,14 +104,14 @@ def update(self, CC, CS, now_nanos): self.apply_torque_last = apply_torque can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled)) - if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: - # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque - # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to - # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background. - ea_simulated_torque = float(np.clip(apply_torque * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)) - if abs(CS.out.steeringTorque) > abs(ea_simulated_torque): - ea_simulated_torque = CS.out.steeringTorque - can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) + if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: + # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque + # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to + # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background. + ea_simulated_torque = float(np.clip(apply_torque * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)) + if abs(CS.out.steeringTorque) > abs(ea_simulated_torque): + ea_simulated_torque = CS.out.steeringTorque + can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) # Emergency Assist intervention if self.CP.flags & VolkswagenFlags.MEB and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: From ebbdabda8faf105ef61fa6a72dac3e634807158c Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:49:49 -0700 Subject: [PATCH 23/69] volkswagen: ID.4 MK1 align MEB parser with sunnypilot/upstream (cherry picked from commit 05da71d47451768d4d0ece318b2ac963756d01be) --- opendbc/car/volkswagen/carstate.py | 16 +++++++++------- opendbc/car/volkswagen/tests/test_meb_lateral.py | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index ab191a09084..95fea275d2b 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -327,10 +327,10 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) if self.CP.enableBsm: - ret.leftBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or - bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) - ret.rightBlindspot = (bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or - bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"])) + ret.leftBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]) or + bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Left"])) + ret.rightBlindspot = (bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Right"]) or + bool(ext_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Warn_Right"])) self.eps_stock_values = pt_cp.vl["LH_EPS_03"] @@ -406,12 +406,14 @@ def get_can_parsers_pq(CP): @staticmethod def get_can_parsers_meb(CP): pt_messages = [ - ("Blinkmodi_02", 1), + ("Blinkmodi_02", 1), # From J519 BCM (sent at 1Hz when no lights active, 50Hz when active) ] + cam_messages = [] if CP.enableBsm: - pt_messages.append(("MEB_Side_Assist_01", float('nan'))) + bsm_target = cam_messages if CP.networkLocation == NetworkLocation.gateway else pt_messages + bsm_target.append(("MEB_Side_Assist_01", float('nan'))) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), - Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).cam), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).cam), Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).alt), } diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index c2ff4444115..0b2c14dd2b1 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -255,7 +255,8 @@ def test_left_and_right_blindspot(self): self.assertEqual(bool(pt_cp.vl["MEB_Side_Assist_01"]["Blind_Spot_Info_Left"]), bool(vals["Blind_Spot_Info_Left"])) cam_cp = parsers[Bus.cam] - ext_cp = parsers[Bus.alt] + # update() picks ext_cp = pt_cp when networkLocation is fwdCamera (ID.4 MK1 test fingerprint) + ext_cp = pt_cp ret = inst.CS.update_meb(pt_cp, cam_cp, ext_cp) self.assertEqual(ret.leftBlindspot, exp_left) self.assertEqual(ret.rightBlindspot, exp_right) From 9972ea8e768b50cbef4a03ddc8f968367b005d18 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:51:18 -0700 Subject: [PATCH 24/69] volkswagen: ID.4 MK1 docs entry (cherry picked from commit 4b1b696252d291335b97033c646c181ff065449f) --- docs/CARS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CARS.md b/docs/CARS.md index 00b2060507f..fdb58de1c44 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -384,6 +384,8 @@ |Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| +|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| +|Volkswagen|ID.5 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| |Volkswagen|Jetta 2015-18|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| |Volkswagen|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| From e7deba98cb7d371e3176e63e3805e06241d23747 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 11:52:20 -0700 Subject: [PATCH 25/69] volkswagen: ID.4 MK1 docs ID.4 only (cherry picked from commit a8b34dc65d6a66fa944bba28d69a50f2598221e9) --- docs/CARS.md | 1 - opendbc/car/volkswagen/values.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index fdb58de1c44..6f355cadbdd 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -385,7 +385,6 @@ |Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| -|Volkswagen|ID.5 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| |Volkswagen|Jetta 2015-18|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)| |Volkswagen|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| |Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)| diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index c3cd8f38705..a5815842dda 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -378,7 +378,6 @@ class CAR(Platforms): VOLKSWAGEN_ID4_MK1 = VolkswagenMEBPlatformConfig( [ VWCarDocs("Volkswagen ID.4 2021-23"), - VWCarDocs("Volkswagen ID.5 2022-23"), ], VolkswagenCarSpecs(mass=2224, wheelbase=2.77), chassis_codes={"E2"}, From 626505ce88383e4300a57e5a278cde81d2aadc73 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 12:33:38 -0700 Subject: [PATCH 26/69] volkswagen: ID.4 MK1 tighten safety rate to ISO jerk envelope (cherry picked from commit 9774eaec9f6de16958d01a1a3b478334a850f854) --- opendbc/safety/modes/volkswagen_meb.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 63cdc730ac5..7e6a2035163 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -52,11 +52,11 @@ static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { // wind-down branch jump (clipped to ±CURVATURE_MAX). 3-point linear interp upper-bounds. .angle_rate_up_lookup = { {1., 5., 25.}, - {0.4, 0.004, 0.00016}, + {0.4, 0.0029, 0.000115}, }, .angle_rate_down_lookup = { {1., 5., 25.}, - {0.4, 0.004, 0.00016}, + {0.4, 0.0029, 0.000115}, }, .angle_is_curvature = true, .inactive_angle_is_zero = true, From 75221b4771cb122680c7f578a11f24f1decd9d07 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 13:14:17 -0700 Subject: [PATCH 27/69] volkswagen: ID.4 MK1 align lateral with safety via vm --- opendbc/car/volkswagen/carcontroller.py | 43 ++++++++++++++++--- .../car/volkswagen/tests/test_meb_lateral.py | 20 +++++++-- opendbc/car/volkswagen/values.py | 21 +++++++-- opendbc/safety/modes/volkswagen_meb.h | 32 ++++++++------ 4 files changed, 90 insertions(+), 26 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 03e92493c74..a082908cf90 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,12 +1,28 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase +from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags + +def _meb_safety_vm() -> VehicleModel: + # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) + # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. + CP = structs.CarParams() + CP.mass = 1.0 + CP.wheelbase = 1.0 + CP.centerToFront = 0.5 + CP.steerRatio = 1.0 + CP.steerRatioRear = 0.0 + CP.rotationalInertia = 1.0 + CP.tireStiffnessFront = 1.0 + CP.tireStiffnessRear = 1.0 + return VehicleModel(CP) + VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -53,6 +69,8 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 + self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None + self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) self.klr_counter_last = None @@ -66,11 +84,18 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: + max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG if CC.latActive: hca_enabled = True - apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.curvature_meas, - CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) + curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. + # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. + limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + CC.latActive, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -82,7 +107,15 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True - apply_curvature = np.clip(CS.curvature_meas, -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, self.CCP.CURVATURE_LIMITS.CURVATURE_MAX) + # Rate-limit the wind-down toward measured curvature with the same VM envelope + # safety enforces; clip endpoint to the safety angle limit. + wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) + limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + True, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index 0b2c14dd2b1..d32d7b96acc 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -80,7 +80,7 @@ def test_steering_pressed_threshold(self): # (b) Curvature clip / saturation test def test_curvature_clip_and_encoding(self): """Out-of-range commanded curvature ramps via the angle framework and saturates at ±STEER_ANGLE_MAX.""" - cmax = CarControllerParams.CURVATURE_LIMITS.CURVATURE_MAX + cmax = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX / CarControllerParams.MEB_RAD_TO_DEG # Use low vEgo so the lateral-accel envelope (~3.6/v^2) does not clip below CURVATURE_MAX. for cmd in (+0.5, -0.5): with self.subTest(cmd=cmd): @@ -108,15 +108,18 @@ def test_curvature_clip_and_encoding(self): # (b2) Rate-limit test: per-frame |delta| matches the BP/V interpolation at multiple speeds. def test_curvature_rate_limit(self): - """A large step in commanded curvature is limited by apply_std_curvature_limits jerk per send cycle.""" - from opendbc.car.lateral import get_max_curvature_jerk + """A large step in commanded curvature is limited by the VM jerk envelope per send cycle.""" + from opendbc.car import DT_CTRL for v in (5.0, 10.0, 25.0): with self.subTest(v=v): inst = self.CI(self.cp) CS = _make_carstate(inst, vEgo=v) CC = _build_cc(latActive=True, curvature=0.1) new_act, _ = inst.CC.update(CC, CS, 0) - expected_step = get_max_curvature_jerk(v, CarControllerParams.STEER_STEP) + # Stub VM (slip=0, sR=1, wb=1) → identity curvature factor: max delta per send + # equals MAX_LATERAL_JERK / v^2 * DT_CTRL * STEER_STEP (in curvature units). + ccp = CarControllerParams + expected_step = ccp.ANGLE_LIMITS.MAX_LATERAL_JERK / (max(v, 1.0) ** 2) * DT_CTRL * ccp.STEER_STEP self.assertLessEqual(abs(new_act.curvature), expected_step * 1.05) self.assertGreater(abs(new_act.curvature), expected_step * 0.5) @@ -295,6 +298,7 @@ def test_oracle_random_sequence(self): CS = _make_carstate(self.inst, vEgo=15.0) # Walk through randomized requested curvatures + measured curvatures + sends_us = 0 for step in range(500): v = float(rng.uniform(2.0, 30.0)) cmd = float(rng.uniform(-0.25, 0.25)) @@ -308,9 +312,17 @@ def test_oracle_random_sequence(self): CC = _build_cc(latActive=lat_active, curvature=cmd) _, sends = self.inst.CC.update(CC, CS, 0) + # Feed safety the wheel speed so steer_angle_cmd_checks_vm sees the same v_ego + val = int(v * 3.6 / 0.0075) + esc = self.libsafety_py.make_CANPacket(0xFC, 0, bytes(8) + val.to_bytes(2, 'little') * 4) + self.safety.safety_rx_hook(esc) + for addr, dat, _bus in sends: if addr != HCA_03_ADDR: continue + # advance timer 20ms per HCA frame so the rt_angle limit window resets correctly + sends_us += 20000 + self.safety.set_timer(sends_us) pkt = self._raw_hca_to_safety_packet(dat, addr) accepted = self.safety.safety_tx_hook(pkt) self.assertTrue(accepted, f"safety rejected HCA_03 at step {step}: cmd={cmd:.4f} meas={meas:.4f} lat={lat_active}") diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index a5815842dda..09335ac4f7a 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -2,11 +2,11 @@ from dataclasses import dataclass, field from enum import Enum, IntFlag, StrEnum -from opendbc.car import Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column -from opendbc.car.lateral import AngleSteeringLimits, CurvatureSteeringLimits +from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -68,8 +68,21 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration - CURVATURE_LIMITS: CurvatureSteeringLimits = CurvatureSteeringLimits( - 0.195, # Max curvature for steering command, m^-1 + # MEB curvature command is rate-limited through apply_steer_angle_limits_vm. + # The helper expects a steering-angle-shaped input in degrees, so the carcontroller + # scales curvature by RAD_TO_DEG before/after the call and pairs it with a stub + # VehicleModel (slip=0, steerRatio=1, wheelbase=1) so the helper math reduces to + # an identity on curvature. The matching safety params use the same stub so the + # Python envelope is always within the safety envelope. + MEB_RAD_TO_DEG = 180.0 / 3.141592653589793 + # Add extra tolerance for average banked road since safety doesn't have the roll + MEB_AVERAGE_ROAD_ROLL = 0.06 + ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( + 0.195 * MEB_RAD_TO_DEG, # STEER_ANGLE_MAX in curvature*RAD_TO_DEG units + ([], []), + ([], []), + MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^2 + MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^3 ) def __init__(self, CP): diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 7e6a2035163..4352028ebff 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -12,6 +12,10 @@ #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 +// Helper expects deg_to_can; with stub VM params (slip=0, sR=1, wb=1) the helper's +// "angle" math becomes an identity on curvature, so we scale curvature*RAD_TO_DEG to CAN. +#define VOLKSWAGEN_MEB_RAD_TO_DEG 57.29577951308232f + static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { int len = GET_LEN(msg); @@ -46,22 +50,20 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, - .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, - // Bounds the ISO 11270 lateral-jerk envelope (0.1 / max(v,1)^2 per STEER_STEP at 50Hz) - // used by apply_std_curvature_limits, with extra slack at low speed to accept the - // wind-down branch jump (clipped to ±CURVATURE_MAX). 3-point linear interp upper-bounds. - .angle_rate_up_lookup = { - {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, - }, - .angle_rate_down_lookup = { - {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, - }, + .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN / VOLKSWAGEN_MEB_RAD_TO_DEG, + .frequency = 50, .angle_is_curvature = true, .inactive_angle_is_zero = true, }; +// Stub vehicle model params: identity curvature_factor so the helper's lateral +// accel/jerk envelope is expressed directly in curvature*RAD_TO_DEG units. +static const AngleSteeringParams VOLKSWAGEN_MEB_STEERING_PARAMS = { + .slip_factor = 0.0f, + .steer_ratio = 1.0f, + .wheelbase = 1.0f, +}; + static safety_config volkswagen_meb_init(uint16_t param) { // Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration static const CanMsg VOLKSWAGEN_MEB_STOCK_TX_MSGS[] = { @@ -96,6 +98,9 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { uint32_t rl = msg->data[12] | (msg->data[13] << 8); uint32_t rr = msg->data[14] | (msg->data[15] << 8); vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); + // Wheel speed signal scaling 0.0075 km/h per LSB; average four wheels into m/s + float v_ego_ms = ((float)(fl + fr + rl + rr)) * (0.0075f / 3.6f / 4.0f); + UPDATE_VEHICLE_SPEED(v_ego_ms); } if (msg->addr == MSG_QFK_01) { @@ -149,7 +154,8 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { } bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); - if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { + if (steer_angle_cmd_checks_vm(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS, + VOLKSWAGEN_MEB_STEERING_PARAMS)) { tx = false; } } From d8c228c4ca8307b01c7f4aae7b7c0e7194dad442 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 14:02:51 -0700 Subject: [PATCH 28/69] volkswagen: ID.4 MK1 revert VM-helper lateral, restore std curvature limits The VM-helper path tightened the jerk envelope to MAX_LATERAL_JERK=3.6 (vs ISO_LATERAL_JERK=5.0 in apply_std_curvature_limits), slowing the commanded curvature ramp ~37% at speed. On-vehicle the EPS faults with LKAS unavailable. Revert to the previously-shipped curvature/safety math which is known-good on elkoled/7. --- opendbc/car/volkswagen/carcontroller.py | 43 +++---------------- .../car/volkswagen/tests/test_meb_lateral.py | 20 ++------- opendbc/car/volkswagen/values.py | 21 ++------- opendbc/safety/modes/volkswagen_meb.h | 32 ++++++-------- 4 files changed, 26 insertions(+), 90 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index a082908cf90..03e92493c74 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,28 +1,12 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase -from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags - -def _meb_safety_vm() -> VehicleModel: - # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) - # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. - CP = structs.CarParams() - CP.mass = 1.0 - CP.wheelbase = 1.0 - CP.centerToFront = 0.5 - CP.steerRatio = 1.0 - CP.steerRatioRear = 0.0 - CP.rotationalInertia = 1.0 - CP.tireStiffnessFront = 1.0 - CP.tireStiffnessRear = 1.0 - return VehicleModel(CP) - VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -69,8 +53,6 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 - self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None - self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) self.klr_counter_last = None @@ -84,18 +66,11 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: - max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG if CC.latActive: hca_enabled = True - curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. - # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. - limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - CC.latActive, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.curvature_meas, + CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -107,15 +82,7 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True - # Rate-limit the wind-down toward measured curvature with the same VM envelope - # safety enforces; clip endpoint to the safety angle limit. - wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) - limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - True, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = np.clip(CS.curvature_meas, -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, self.CCP.CURVATURE_LIMITS.CURVATURE_MAX) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index d32d7b96acc..0b2c14dd2b1 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -80,7 +80,7 @@ def test_steering_pressed_threshold(self): # (b) Curvature clip / saturation test def test_curvature_clip_and_encoding(self): """Out-of-range commanded curvature ramps via the angle framework and saturates at ±STEER_ANGLE_MAX.""" - cmax = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX / CarControllerParams.MEB_RAD_TO_DEG + cmax = CarControllerParams.CURVATURE_LIMITS.CURVATURE_MAX # Use low vEgo so the lateral-accel envelope (~3.6/v^2) does not clip below CURVATURE_MAX. for cmd in (+0.5, -0.5): with self.subTest(cmd=cmd): @@ -108,18 +108,15 @@ def test_curvature_clip_and_encoding(self): # (b2) Rate-limit test: per-frame |delta| matches the BP/V interpolation at multiple speeds. def test_curvature_rate_limit(self): - """A large step in commanded curvature is limited by the VM jerk envelope per send cycle.""" - from opendbc.car import DT_CTRL + """A large step in commanded curvature is limited by apply_std_curvature_limits jerk per send cycle.""" + from opendbc.car.lateral import get_max_curvature_jerk for v in (5.0, 10.0, 25.0): with self.subTest(v=v): inst = self.CI(self.cp) CS = _make_carstate(inst, vEgo=v) CC = _build_cc(latActive=True, curvature=0.1) new_act, _ = inst.CC.update(CC, CS, 0) - # Stub VM (slip=0, sR=1, wb=1) → identity curvature factor: max delta per send - # equals MAX_LATERAL_JERK / v^2 * DT_CTRL * STEER_STEP (in curvature units). - ccp = CarControllerParams - expected_step = ccp.ANGLE_LIMITS.MAX_LATERAL_JERK / (max(v, 1.0) ** 2) * DT_CTRL * ccp.STEER_STEP + expected_step = get_max_curvature_jerk(v, CarControllerParams.STEER_STEP) self.assertLessEqual(abs(new_act.curvature), expected_step * 1.05) self.assertGreater(abs(new_act.curvature), expected_step * 0.5) @@ -298,7 +295,6 @@ def test_oracle_random_sequence(self): CS = _make_carstate(self.inst, vEgo=15.0) # Walk through randomized requested curvatures + measured curvatures - sends_us = 0 for step in range(500): v = float(rng.uniform(2.0, 30.0)) cmd = float(rng.uniform(-0.25, 0.25)) @@ -312,17 +308,9 @@ def test_oracle_random_sequence(self): CC = _build_cc(latActive=lat_active, curvature=cmd) _, sends = self.inst.CC.update(CC, CS, 0) - # Feed safety the wheel speed so steer_angle_cmd_checks_vm sees the same v_ego - val = int(v * 3.6 / 0.0075) - esc = self.libsafety_py.make_CANPacket(0xFC, 0, bytes(8) + val.to_bytes(2, 'little') * 4) - self.safety.safety_rx_hook(esc) - for addr, dat, _bus in sends: if addr != HCA_03_ADDR: continue - # advance timer 20ms per HCA frame so the rt_angle limit window resets correctly - sends_us += 20000 - self.safety.set_timer(sends_us) pkt = self._raw_hca_to_safety_packet(dat, addr) accepted = self.safety.safety_tx_hook(pkt) self.assertTrue(accepted, f"safety rejected HCA_03 at step {step}: cmd={cmd:.4f} meas={meas:.4f} lat={lat_active}") diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 09335ac4f7a..a5815842dda 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -2,11 +2,11 @@ from dataclasses import dataclass, field from enum import Enum, IntFlag, StrEnum -from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds +from opendbc.car import Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column -from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL +from opendbc.car.lateral import AngleSteeringLimits, CurvatureSteeringLimits from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -68,21 +68,8 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration - # MEB curvature command is rate-limited through apply_steer_angle_limits_vm. - # The helper expects a steering-angle-shaped input in degrees, so the carcontroller - # scales curvature by RAD_TO_DEG before/after the call and pairs it with a stub - # VehicleModel (slip=0, steerRatio=1, wheelbase=1) so the helper math reduces to - # an identity on curvature. The matching safety params use the same stub so the - # Python envelope is always within the safety envelope. - MEB_RAD_TO_DEG = 180.0 / 3.141592653589793 - # Add extra tolerance for average banked road since safety doesn't have the roll - MEB_AVERAGE_ROAD_ROLL = 0.06 - ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( - 0.195 * MEB_RAD_TO_DEG, # STEER_ANGLE_MAX in curvature*RAD_TO_DEG units - ([], []), - ([], []), - MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^2 - MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^3 + CURVATURE_LIMITS: CurvatureSteeringLimits = CurvatureSteeringLimits( + 0.195, # Max curvature for steering command, m^-1 ) def __init__(self, CP): diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 4352028ebff..7e6a2035163 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -12,10 +12,6 @@ #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 -// Helper expects deg_to_can; with stub VM params (slip=0, sR=1, wb=1) the helper's -// "angle" math becomes an identity on curvature, so we scale curvature*RAD_TO_DEG to CAN. -#define VOLKSWAGEN_MEB_RAD_TO_DEG 57.29577951308232f - static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { int len = GET_LEN(msg); @@ -50,20 +46,22 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, - .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN / VOLKSWAGEN_MEB_RAD_TO_DEG, - .frequency = 50, + .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, + // Bounds the ISO 11270 lateral-jerk envelope (0.1 / max(v,1)^2 per STEER_STEP at 50Hz) + // used by apply_std_curvature_limits, with extra slack at low speed to accept the + // wind-down branch jump (clipped to ±CURVATURE_MAX). 3-point linear interp upper-bounds. + .angle_rate_up_lookup = { + {1., 5., 25.}, + {0.4, 0.0029, 0.000115}, + }, + .angle_rate_down_lookup = { + {1., 5., 25.}, + {0.4, 0.0029, 0.000115}, + }, .angle_is_curvature = true, .inactive_angle_is_zero = true, }; -// Stub vehicle model params: identity curvature_factor so the helper's lateral -// accel/jerk envelope is expressed directly in curvature*RAD_TO_DEG units. -static const AngleSteeringParams VOLKSWAGEN_MEB_STEERING_PARAMS = { - .slip_factor = 0.0f, - .steer_ratio = 1.0f, - .wheelbase = 1.0f, -}; - static safety_config volkswagen_meb_init(uint16_t param) { // Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration static const CanMsg VOLKSWAGEN_MEB_STOCK_TX_MSGS[] = { @@ -98,9 +96,6 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { uint32_t rl = msg->data[12] | (msg->data[13] << 8); uint32_t rr = msg->data[14] | (msg->data[15] << 8); vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); - // Wheel speed signal scaling 0.0075 km/h per LSB; average four wheels into m/s - float v_ego_ms = ((float)(fl + fr + rl + rr)) * (0.0075f / 3.6f / 4.0f); - UPDATE_VEHICLE_SPEED(v_ego_ms); } if (msg->addr == MSG_QFK_01) { @@ -154,8 +149,7 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { } bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); - if (steer_angle_cmd_checks_vm(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS, - VOLKSWAGEN_MEB_STEERING_PARAMS)) { + if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { tx = false; } } From 787af0a91575a99bff04fab7ae78869d0ddbeff2 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 14:21:56 -0700 Subject: [PATCH 29/69] VW MEB: add KLR_01 checksum magic --- opendbc/car/volkswagen/mqbcan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opendbc/car/volkswagen/mqbcan.py b/opendbc/car/volkswagen/mqbcan.py index 78a2eeff62f..fb370b3e2d0 100644 --- a/opendbc/car/volkswagen/mqbcan.py +++ b/opendbc/car/volkswagen/mqbcan.py @@ -243,6 +243,8 @@ def xor_checksum(address: int, sig, d: bytearray, initial_value: int = 0) -> int 0xC5, 0x91, 0x0F, 0x27, 0x34, 0x04, 0x7F, 0x02], # EA_02 0x20A: [0x9D, 0xE8, 0x36, 0xA1, 0xCA, 0x3B, 0x1D, 0x33, 0xE0, 0xD5, 0xBB, 0x5F, 0xAE, 0x3C, 0x31, 0x9F], # EML_06 + 0x25D: [0xDA, 0x6B, 0x0E, 0xB2, 0x78, 0xBD, 0x5A, 0x81, + 0x7B, 0xD6, 0x41, 0x39, 0x76, 0xB6, 0xD7, 0x35], # KLR_01 0x26B: [0xCE, 0xCC, 0xBD, 0x69, 0xA1, 0x3C, 0x18, 0x76, 0x0F, 0x04, 0xF2, 0x3A, 0x93, 0x24, 0x19, 0x51], # TA_01 0x30C: [0x0F] * 16, # ACC_02 From e0a2f8a18d8a633ef6f007288d3834b42cb385e4 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 14:40:26 -0700 Subject: [PATCH 30/69] Revert "volkswagen: ID.4 MK1 revert VM-helper lateral, restore std curvature limits" This reverts commit d8c228c4ca8307b01c7f4aae7b7c0e7194dad442. --- opendbc/car/volkswagen/carcontroller.py | 43 ++++++++++++++++--- .../car/volkswagen/tests/test_meb_lateral.py | 20 +++++++-- opendbc/car/volkswagen/values.py | 21 +++++++-- opendbc/safety/modes/volkswagen_meb.h | 32 ++++++++------ 4 files changed, 90 insertions(+), 26 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 03e92493c74..a082908cf90 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,12 +1,28 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase +from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags + +def _meb_safety_vm() -> VehicleModel: + # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) + # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. + CP = structs.CarParams() + CP.mass = 1.0 + CP.wheelbase = 1.0 + CP.centerToFront = 0.5 + CP.steerRatio = 1.0 + CP.steerRatioRear = 0.0 + CP.rotationalInertia = 1.0 + CP.tireStiffnessFront = 1.0 + CP.tireStiffnessRear = 1.0 + return VehicleModel(CP) + VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -53,6 +69,8 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 + self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None + self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) self.klr_counter_last = None @@ -66,11 +84,18 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: + max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG if CC.latActive: hca_enabled = True - apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, CS.curvature_meas, - CS.out.steeringPressed, self.CCP.STEER_STEP, CC.latActive, self.CCP.CURVATURE_LIMITS) + curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. + # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. + limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + CC.latActive, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -82,7 +107,15 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero hca_enabled = True - apply_curvature = np.clip(CS.curvature_meas, -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, self.CCP.CURVATURE_LIMITS.CURVATURE_MAX) + # Rate-limit the wind-down toward measured curvature with the same VM envelope + # safety enforces; clip endpoint to the safety angle limit. + wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) + limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + True, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False diff --git a/opendbc/car/volkswagen/tests/test_meb_lateral.py b/opendbc/car/volkswagen/tests/test_meb_lateral.py index 0b2c14dd2b1..d32d7b96acc 100644 --- a/opendbc/car/volkswagen/tests/test_meb_lateral.py +++ b/opendbc/car/volkswagen/tests/test_meb_lateral.py @@ -80,7 +80,7 @@ def test_steering_pressed_threshold(self): # (b) Curvature clip / saturation test def test_curvature_clip_and_encoding(self): """Out-of-range commanded curvature ramps via the angle framework and saturates at ±STEER_ANGLE_MAX.""" - cmax = CarControllerParams.CURVATURE_LIMITS.CURVATURE_MAX + cmax = CarControllerParams.ANGLE_LIMITS.STEER_ANGLE_MAX / CarControllerParams.MEB_RAD_TO_DEG # Use low vEgo so the lateral-accel envelope (~3.6/v^2) does not clip below CURVATURE_MAX. for cmd in (+0.5, -0.5): with self.subTest(cmd=cmd): @@ -108,15 +108,18 @@ def test_curvature_clip_and_encoding(self): # (b2) Rate-limit test: per-frame |delta| matches the BP/V interpolation at multiple speeds. def test_curvature_rate_limit(self): - """A large step in commanded curvature is limited by apply_std_curvature_limits jerk per send cycle.""" - from opendbc.car.lateral import get_max_curvature_jerk + """A large step in commanded curvature is limited by the VM jerk envelope per send cycle.""" + from opendbc.car import DT_CTRL for v in (5.0, 10.0, 25.0): with self.subTest(v=v): inst = self.CI(self.cp) CS = _make_carstate(inst, vEgo=v) CC = _build_cc(latActive=True, curvature=0.1) new_act, _ = inst.CC.update(CC, CS, 0) - expected_step = get_max_curvature_jerk(v, CarControllerParams.STEER_STEP) + # Stub VM (slip=0, sR=1, wb=1) → identity curvature factor: max delta per send + # equals MAX_LATERAL_JERK / v^2 * DT_CTRL * STEER_STEP (in curvature units). + ccp = CarControllerParams + expected_step = ccp.ANGLE_LIMITS.MAX_LATERAL_JERK / (max(v, 1.0) ** 2) * DT_CTRL * ccp.STEER_STEP self.assertLessEqual(abs(new_act.curvature), expected_step * 1.05) self.assertGreater(abs(new_act.curvature), expected_step * 0.5) @@ -295,6 +298,7 @@ def test_oracle_random_sequence(self): CS = _make_carstate(self.inst, vEgo=15.0) # Walk through randomized requested curvatures + measured curvatures + sends_us = 0 for step in range(500): v = float(rng.uniform(2.0, 30.0)) cmd = float(rng.uniform(-0.25, 0.25)) @@ -308,9 +312,17 @@ def test_oracle_random_sequence(self): CC = _build_cc(latActive=lat_active, curvature=cmd) _, sends = self.inst.CC.update(CC, CS, 0) + # Feed safety the wheel speed so steer_angle_cmd_checks_vm sees the same v_ego + val = int(v * 3.6 / 0.0075) + esc = self.libsafety_py.make_CANPacket(0xFC, 0, bytes(8) + val.to_bytes(2, 'little') * 4) + self.safety.safety_rx_hook(esc) + for addr, dat, _bus in sends: if addr != HCA_03_ADDR: continue + # advance timer 20ms per HCA frame so the rt_angle limit window resets correctly + sends_us += 20000 + self.safety.set_timer(sends_us) pkt = self._raw_hca_to_safety_packet(dat, addr) accepted = self.safety.safety_tx_hook(pkt) self.assertTrue(accepted, f"safety rejected HCA_03 at step {step}: cmd={cmd:.4f} meas={meas:.4f} lat={lat_active}") diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index a5815842dda..09335ac4f7a 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -2,11 +2,11 @@ from dataclasses import dataclass, field from enum import Enum, IntFlag, StrEnum -from opendbc.car import Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CanBusBase, CarSpecs, DbcDict, PlatformConfig, Platforms, structs, uds from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column -from opendbc.car.lateral import AngleSteeringLimits, CurvatureSteeringLimits +from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -68,8 +68,21 @@ class CarControllerParams: ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration - CURVATURE_LIMITS: CurvatureSteeringLimits = CurvatureSteeringLimits( - 0.195, # Max curvature for steering command, m^-1 + # MEB curvature command is rate-limited through apply_steer_angle_limits_vm. + # The helper expects a steering-angle-shaped input in degrees, so the carcontroller + # scales curvature by RAD_TO_DEG before/after the call and pairs it with a stub + # VehicleModel (slip=0, steerRatio=1, wheelbase=1) so the helper math reduces to + # an identity on curvature. The matching safety params use the same stub so the + # Python envelope is always within the safety envelope. + MEB_RAD_TO_DEG = 180.0 / 3.141592653589793 + # Add extra tolerance for average banked road since safety doesn't have the roll + MEB_AVERAGE_ROAD_ROLL = 0.06 + ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( + 0.195 * MEB_RAD_TO_DEG, # STEER_ANGLE_MAX in curvature*RAD_TO_DEG units + ([], []), + ([], []), + MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^2 + MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * MEB_AVERAGE_ROAD_ROLL), # ~3.6 m/s^3 ) def __init__(self, CP): diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 7e6a2035163..4352028ebff 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -12,6 +12,10 @@ #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 +// Helper expects deg_to_can; with stub VM params (slip=0, sR=1, wb=1) the helper's +// "angle" math becomes an identity on curvature, so we scale curvature*RAD_TO_DEG to CAN. +#define VOLKSWAGEN_MEB_RAD_TO_DEG 57.29577951308232f + static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { int len = GET_LEN(msg); @@ -46,22 +50,20 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, - .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, - // Bounds the ISO 11270 lateral-jerk envelope (0.1 / max(v,1)^2 per STEER_STEP at 50Hz) - // used by apply_std_curvature_limits, with extra slack at low speed to accept the - // wind-down branch jump (clipped to ±CURVATURE_MAX). 3-point linear interp upper-bounds. - .angle_rate_up_lookup = { - {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, - }, - .angle_rate_down_lookup = { - {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, - }, + .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN / VOLKSWAGEN_MEB_RAD_TO_DEG, + .frequency = 50, .angle_is_curvature = true, .inactive_angle_is_zero = true, }; +// Stub vehicle model params: identity curvature_factor so the helper's lateral +// accel/jerk envelope is expressed directly in curvature*RAD_TO_DEG units. +static const AngleSteeringParams VOLKSWAGEN_MEB_STEERING_PARAMS = { + .slip_factor = 0.0f, + .steer_ratio = 1.0f, + .wheelbase = 1.0f, +}; + static safety_config volkswagen_meb_init(uint16_t param) { // Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration static const CanMsg VOLKSWAGEN_MEB_STOCK_TX_MSGS[] = { @@ -96,6 +98,9 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { uint32_t rl = msg->data[12] | (msg->data[13] << 8); uint32_t rr = msg->data[14] | (msg->data[15] << 8); vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); + // Wheel speed signal scaling 0.0075 km/h per LSB; average four wheels into m/s + float v_ego_ms = ((float)(fl + fr + rl + rr)) * (0.0075f / 3.6f / 4.0f); + UPDATE_VEHICLE_SPEED(v_ego_ms); } if (msg->addr == MSG_QFK_01) { @@ -149,7 +154,8 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { } bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); - if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { + if (steer_angle_cmd_checks_vm(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS, + VOLKSWAGEN_MEB_STEERING_PARAMS)) { tx = false; } } From 12f7b66ebc6f04c763cc4b8b01c1611bd285219f Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 15:11:02 -0700 Subject: [PATCH 31/69] VW MEB: gate HCA fault on drive mode --- opendbc/car/volkswagen/carstate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 95fea275d2b..9e6468835c2 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -307,10 +307,11 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] - hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) - ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status) - ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) + drive_mode = ret.gearShifter == GearShifter.drive + + hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) + ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status, drive_mode) ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 ret.brakePressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"]) From 492dc1e5d15065b5b18211e08ef005778422ddcd Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 15:27:24 -0700 Subject: [PATCH 32/69] VW MEB: gate FAULT perm path on drive mode --- opendbc/car/volkswagen/carstate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 9e6468835c2..101de68f52b 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -358,6 +358,7 @@ def parse_mlb_mqb_steering_state(self, ret, pt_cp, drive_mode=True): ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE + ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) hca_status = self.CCP.hca_status_values.get(pt_cp.vl["LH_EPS_03"]["EPS_HCA_Status"]) ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status, drive_mode) @@ -367,7 +368,7 @@ def update_hca_state(self, hca_status, drive_mode=True): # Treat FAULT as temporary for worst likely EPS recovery time, for cars without factory Lane Assist # DISABLED means the EPS hasn't been configured to support Lane Assist self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600) - perm_fault = drive_mode and hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT") + perm_fault = drive_mode and (hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT")) temp_fault = drive_mode and hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete return temp_fault, perm_fault From 9a859bfff976bf2570501b2e06aeb34c0418a9fd Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 15:48:22 -0700 Subject: [PATCH 33/69] VW MEB: gate accFaulted on drive mode --- opendbc/car/volkswagen/carstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 101de68f52b..996e76d2512 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -322,7 +322,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5) ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5) ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation - ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) + ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) and drive_mode ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_links"]) ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) From 0c4c3f1a03cfa7f27a63f2a9b101044e1ac15b83 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 15:57:09 -0700 Subject: [PATCH 34/69] add debounce --- opendbc/car/volkswagen/carstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 996e76d2512..ad88c3f15b9 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -155,7 +155,7 @@ def update_pq(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringAngleDeg = pt_cp.vl["Lenkhilfe_3"]["LH3_BLW"] * (1, -1)[int(pt_cp.vl["Lenkhilfe_3"]["LH3_BLWSign"])] ret.steeringRateDeg = pt_cp.vl["Lenkwinkel_1"]["LW1_Lenk_Gesch"] * (1, -1)[int(pt_cp.vl["Lenkwinkel_1"]["LW1_Gesch_Sign"])] ret.steeringTorque = pt_cp.vl["Lenkhilfe_3"]["LH3_LM"] * (1, -1)[int(pt_cp.vl["Lenkhilfe_3"]["LH3_LMSign"])] - ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE + ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) hca_status = self.CCP.hca_status_values.get(pt_cp.vl["Lenkhilfe_2"]["LH2_Sta_HCA"]) ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status) From dd0839f85cc516c2eab621f34ab6d824c0e949c9 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 16:01:07 -0700 Subject: [PATCH 35/69] VW MEB: replicate sunnypilot accFault helper, revert perm gate --- opendbc/car/volkswagen/carstate.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index ad88c3f15b9..3ac8a639e56 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -13,6 +13,7 @@ def __init__(self, CP): super().__init__(CP) self.frame = 0 self.eps_init_complete = False + self.cruise_recovery_timer = 0 self.CCP = CarControllerParams(CP) self.button_states = {button.event_type: False for button in self.CCP.BUTTONS} self.esp_hold_confirmation = False @@ -322,7 +323,8 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5) ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5) ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation - ret.accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) and drive_mode + accFaulted = pt_cp.vl["Motor_51"]["TSK_Status"] in (6, 7) + ret.accFaulted = self.update_acc_fault(accFaulted, parking_brake=ret.parkingBrake, drive_mode=drive_mode) ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_links"]) ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["BM_rechts"]) @@ -368,10 +370,22 @@ def update_hca_state(self, hca_status, drive_mode=True): # Treat FAULT as temporary for worst likely EPS recovery time, for cars without factory Lane Assist # DISABLED means the EPS hasn't been configured to support Lane Assist self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600) - perm_fault = drive_mode and (hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT")) + perm_fault = drive_mode and hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT") temp_fault = drive_mode and hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete return temp_fault, perm_fault + def update_acc_fault(self, acc_fault, parking_brake=False, drive_mode=True, recovery_frames_max=100): + # Ignore FAULT when not in drive mode and parked + # do not show misleading error during ignition in parked state + # grant a short time to recover a normal cruise state + fault = acc_fault + if parking_brake and not drive_mode: + fault = False + self.cruise_recovery_timer = self.frame + elif self.frame - self.cruise_recovery_timer < recovery_frames_max: + fault = False + return fault + @staticmethod def get_can_parsers(CP): if CP.flags & VolkswagenFlags.PQ: From d9c08b095729e6c00bfca6686a959527cd83cfb5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 16:53:03 -0700 Subject: [PATCH 36/69] VW MEB: stop sending KLR_01 to test EPS FAULT cause --- opendbc/car/volkswagen/carcontroller.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index a082908cf90..609b78ede29 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -146,17 +146,6 @@ def update(self, CC, CS, now_nanos): ea_simulated_torque = CS.out.steeringTorque can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) - # Emergency Assist intervention - if self.CP.flags & VolkswagenFlags.MEB and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: - # send capacitive steering wheel touched - # propably EA is stock activated only for cars equipped with capacitive steering wheel - # (also stock long does resume from stop as long as hands on is detected additionally to OP resume spam) - klr_send_ready = CS.klr_stock_values["COUNTER"] != self.klr_counter_last - if klr_send_ready: - can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.cam, CC.latActive, CS.klr_stock_values)) - can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.pt, CC.latActive, CS.klr_stock_values)) - self.klr_counter_last = CS.klr_stock_values["COUNTER"] - # **** Acceleration Controls ******************************************** # if self.CP.openpilotLongitudinalControl: From 312da3525998f9aaaea93edc30bf669aed4450a1 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 17:05:28 -0700 Subject: [PATCH 37/69] VW MEB: revert curvature rate-limit to branch 7 std (was VM-stub) --- opendbc/car/volkswagen/carcontroller.py | 49 +++++-------------------- opendbc/car/volkswagen/values.py | 4 +- 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 609b78ede29..033a3c0ebe9 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,28 +1,12 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase -from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags - -def _meb_safety_vm() -> VehicleModel: - # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) - # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. - CP = structs.CarParams() - CP.mass = 1.0 - CP.wheelbase = 1.0 - CP.centerToFront = 0.5 - CP.steerRatio = 1.0 - CP.steerRatioRear = 0.0 - CP.rotationalInertia = 1.0 - CP.tireStiffnessFront = 1.0 - CP.tireStiffnessRear = 1.0 - return VehicleModel(CP) - VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -69,11 +53,8 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 - self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None - self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) - self.klr_counter_last = None def update(self, CC, CS, now_nanos): actuators = CC.actuators @@ -84,18 +65,12 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: - max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG if CC.latActive: hca_enabled = True - curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. - # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. - limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - CC.latActive, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, + CS.curvature_meas, False, self.CCP.STEER_STEP, CC.latActive, + self.CCP.CURVATURE_LIMITS) min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -105,17 +80,11 @@ def update(self, CC, CS, now_nanos): steering_power = min(max(target_power, min_power), max_power) else: - if self.steering_power_last > 0: # keep HCA alive until steering power has reduced to zero + if self.steering_power_last > 0: hca_enabled = True - # Rate-limit the wind-down toward measured curvature with the same VM envelope - # safety enforces; clip endpoint to the safety angle limit. - wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) - limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - True, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = float(np.clip(CS.curvature_meas, + -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, + self.CCP.CURVATURE_LIMITS.CURVATURE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 09335ac4f7a..9301fc7af55 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -6,7 +6,7 @@ from opendbc.can import CANDefine from opendbc.car.common.conversions import Conversions as CV from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column -from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL +from opendbc.car.lateral import AngleSteeringLimits, CurvatureSteeringLimits, ISO_LATERAL_ACCEL from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16 from opendbc.car.vin import Vin @@ -126,6 +126,8 @@ def __init__(self, CP): self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage self.STEERING_POWER_STEP = 2 # HCA_03 steering power counter steps + self.CURVATURE_LIMITS = CurvatureSteeringLimits(0.195) # Max curvature for steering command, m^-1 + self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"] self.hca_status_values = can_define.dv["QFK_01"]["LatCon_HCA_Status"] From 18541c619a1411d4f9b6c2b2d5d2f2782b6e5721 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 18:09:54 -0700 Subject: [PATCH 38/69] VW MEB: do not treat HCA DISABLED as permanent fault --- opendbc/car/volkswagen/carstate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 3ac8a639e56..e8fc3568224 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -369,9 +369,9 @@ def parse_mlb_mqb_steering_state(self, ret, pt_cp, drive_mode=True): def update_hca_state(self, hca_status, drive_mode=True): # Treat FAULT as temporary for worst likely EPS recovery time, for cars without factory Lane Assist # DISABLED means the EPS hasn't been configured to support Lane Assist - self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600) - perm_fault = drive_mode and hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT") - temp_fault = drive_mode and hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete + self.eps_init_complete = self.eps_init_complete or hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600 + perm_fault = self.eps_init_complete and hca_status == "FAULT" + temp_fault = (drive_mode and hca_status == "REJECTED") or not self.eps_init_complete return temp_fault, perm_fault def update_acc_fault(self, acc_fault, parking_brake=False, drive_mode=True, recovery_frames_max=100): From 9822c50022d86fc6edd64c1d983a5ff200ad3289 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 18:45:40 -0700 Subject: [PATCH 39/69] more tests --- opendbc/safety/tests/test_ignition.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index 6d2ae2de877..d70e93b1f04 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -45,9 +45,14 @@ def test_rivian_ignition_on(self): def test_rivian_ignition_off(self): self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 0})) + {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 1})) self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 0})) + {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 1})) + self.assertTrue(self._ign()) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 2, "VDM_EpasPowerMode": 0})) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 3, "VDM_EpasPowerMode": 0})) self.assertFalse(self._ign()) # Tesla: VEHICLE_POWER_STATE_DRIVE=3 @@ -61,9 +66,14 @@ def test_tesla_ignition_on(self): def test_tesla_ignition_off(self): self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 2})) + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.assertTrue(self._ign()) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 2, "VCFRONT_vehiclePowerState": 2})) self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 2})) + {"VCFRONT_LVPowerStateCounter": 3, "VCFRONT_vehiclePowerState": 2})) self.assertFalse(self._ign()) # Mazda: 0x9E byte 0 high 3 bits == 6 From d2abb4a9a6c3fde3bc4f4c4a396f62f4fab42de1 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 18:53:07 -0700 Subject: [PATCH 40/69] init counters for deterministic test --- opendbc/safety/ignition.h | 10 ++++++++-- opendbc/safety/tests/libsafety/safety.c | 2 ++ opendbc/safety/tests/test_ignition.py | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h index f9be1070576..f6be58b2986 100644 --- a/opendbc/safety/ignition.h +++ b/opendbc/safety/ignition.h @@ -5,6 +5,14 @@ #include "opendbc/safety/can.h" +static int prev_counter_rivian = -1; +static int prev_counter_tesla = -1; + +static inline void ignition_init(void) { + prev_counter_rivian = -1; + prev_counter_tesla = -1; +} + static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, uint32_t *ignition_can_cnt) { if (msg->bus == 0U) { int len = GET_LEN(msg); @@ -21,7 +29,6 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, // 0x152 overlaps with Subaru pre-global which has this bit as the high beam int counter = msg->data[1] & 0xFU; // max is only 14 - static int prev_counter_rivian = -1; if ((counter == ((prev_counter_rivian + 1) % 15)) && (prev_counter_rivian != -1)) { // VDM_OutputSignals->VDM_EpasPowerMode *ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 @@ -35,7 +42,6 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, // 0x221 overlaps with Rivian which has random data on byte 0 int counter = msg->data[6] >> 4; - static int prev_counter_tesla = -1; if ((counter == ((prev_counter_tesla + 1) % 16)) && (prev_counter_tesla != -1)) { // VCFRONT_LVPowerState->VCFRONT_vehiclePowerState int power_state = (msg->data[0] >> 5U) & 0x3U; diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index 469e2aebab0..d076b0bb6c9 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -218,4 +218,6 @@ void init_tests(void){ // assumes autopark on safety mode init to avoid a fault. get rid of that for testing tesla_autopark = false; + + ignition_init(); } diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index d70e93b1f04..486b09c6d98 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -11,6 +11,7 @@ class TestIgnitionHook(unittest.TestCase): def setUp(self): self.safety = libsafety_py.libsafety self.safety.reset_ignition_can() + self.safety.init_tests() self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") self.rivian_packer = CANPackerSafety("rivian_primary_actuator") self.tesla_packer = CANPackerSafety("tesla_model3_party") From 9f521b9c5acc1f4ff3e2381b6810809472f10e85 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 19:07:47 -0700 Subject: [PATCH 41/69] Revert "init counters for deterministic test" This reverts commit d2abb4a9a6c3fde3bc4f4c4a396f62f4fab42de1. --- opendbc/safety/ignition.h | 10 ++-------- opendbc/safety/tests/libsafety/safety.c | 2 -- opendbc/safety/tests/test_ignition.py | 1 - 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h index f6be58b2986..f9be1070576 100644 --- a/opendbc/safety/ignition.h +++ b/opendbc/safety/ignition.h @@ -5,14 +5,6 @@ #include "opendbc/safety/can.h" -static int prev_counter_rivian = -1; -static int prev_counter_tesla = -1; - -static inline void ignition_init(void) { - prev_counter_rivian = -1; - prev_counter_tesla = -1; -} - static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, uint32_t *ignition_can_cnt) { if (msg->bus == 0U) { int len = GET_LEN(msg); @@ -29,6 +21,7 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, // 0x152 overlaps with Subaru pre-global which has this bit as the high beam int counter = msg->data[1] & 0xFU; // max is only 14 + static int prev_counter_rivian = -1; if ((counter == ((prev_counter_rivian + 1) % 15)) && (prev_counter_rivian != -1)) { // VDM_OutputSignals->VDM_EpasPowerMode *ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 @@ -42,6 +35,7 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, // 0x221 overlaps with Rivian which has random data on byte 0 int counter = msg->data[6] >> 4; + static int prev_counter_tesla = -1; if ((counter == ((prev_counter_tesla + 1) % 16)) && (prev_counter_tesla != -1)) { // VCFRONT_LVPowerState->VCFRONT_vehiclePowerState int power_state = (msg->data[0] >> 5U) & 0x3U; diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index d076b0bb6c9..469e2aebab0 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -218,6 +218,4 @@ void init_tests(void){ // assumes autopark on safety mode init to avoid a fault. get rid of that for testing tesla_autopark = false; - - ignition_init(); } diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index 486b09c6d98..d70e93b1f04 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -11,7 +11,6 @@ class TestIgnitionHook(unittest.TestCase): def setUp(self): self.safety = libsafety_py.libsafety self.safety.reset_ignition_can() - self.safety.init_tests() self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") self.rivian_packer = CANPackerSafety("rivian_primary_actuator") self.tesla_packer = CANPackerSafety("tesla_model3_party") From fa790249a6e03cbb73a3423bcf7f71ff24537ad0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 19:12:49 -0700 Subject: [PATCH 42/69] rm --- opendbc/safety/tests/test_ignition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index d70e93b1f04..1677b3dfabf 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -6,7 +6,6 @@ class TestIgnitionHook(unittest.TestCase): - TX_MSGS: list = [] def setUp(self): self.safety = libsafety_py.libsafety From ede75b199c066cd869e1d685db736791cd181a9e Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 19:41:49 -0700 Subject: [PATCH 43/69] Revert "VW MEB: stop sending KLR_01 to test EPS FAULT cause" This reverts commit d9c08b095729e6c00bfca6686a959527cd83cfb5. --- opendbc/car/volkswagen/carcontroller.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 033a3c0ebe9..9c9f080b187 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -115,6 +115,17 @@ def update(self, CC, CS, now_nanos): ea_simulated_torque = CS.out.steeringTorque can_sends.append(self.CCS.create_eps_update(self.packer_pt, self.CAN.cam, CS.eps_stock_values, ea_simulated_torque)) + # Emergency Assist intervention + if self.CP.flags & VolkswagenFlags.MEB and self.CP.flags & VolkswagenFlags.STOCK_KLR_PRESENT: + # send capacitive steering wheel touched + # propably EA is stock activated only for cars equipped with capacitive steering wheel + # (also stock long does resume from stop as long as hands on is detected additionally to OP resume spam) + klr_send_ready = CS.klr_stock_values["COUNTER"] != self.klr_counter_last + if klr_send_ready: + can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.cam, CC.latActive, CS.klr_stock_values)) + can_sends.append(mebcan.create_capacitive_wheel_touch(self.packer_pt, self.CAN.pt, CC.latActive, CS.klr_stock_values)) + self.klr_counter_last = CS.klr_stock_values["COUNTER"] + # **** Acceleration Controls ******************************************** # if self.CP.openpilotLongitudinalControl: From adff1d932d6f0e83ebc7cb97209eeed25c2f3666 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 19:38:51 -0700 Subject: [PATCH 44/69] use getter/setter --- opendbc/safety/ignition.h | 25 +++-- .../safety/tests/libsafety/libsafety_py.py | 4 +- opendbc/safety/tests/libsafety/safety.c | 27 ++--- opendbc/safety/tests/test_ignition.py | 106 +++++++++--------- 4 files changed, 77 insertions(+), 85 deletions(-) diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h index f9be1070576..7ac8f008a5f 100644 --- a/opendbc/safety/ignition.h +++ b/opendbc/safety/ignition.h @@ -5,15 +5,18 @@ #include "opendbc/safety/can.h" -static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, uint32_t *ignition_can_cnt) { +bool ignition_can = false; +uint32_t ignition_can_cnt = 0U; + +void ignition_can_hook(const CANPacket_t *msg) { if (msg->bus == 0U) { int len = GET_LEN(msg); // GM exception if ((msg->addr == 0x1F1U) && (len == 8)) { // SystemPowerMode (2=Run, 3=Crank Request) - *ignition_can = (msg->data[0] & 0x2U) != 0U; - *ignition_can_cnt = 0U; + ignition_can = (msg->data[0] & 0x2U) != 0U; + ignition_can_cnt = 0U; } // Rivian R1S/T GEN1 exception @@ -24,8 +27,8 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, static int prev_counter_rivian = -1; if ((counter == ((prev_counter_rivian + 1) % 15)) && (prev_counter_rivian != -1)) { // VDM_OutputSignals->VDM_EpasPowerMode - *ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 - *ignition_can_cnt = 0U; + ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 + ignition_can_cnt = 0U; } prev_counter_rivian = counter; } @@ -39,23 +42,23 @@ static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, if ((counter == ((prev_counter_tesla + 1) % 16)) && (prev_counter_tesla != -1)) { // VCFRONT_LVPowerState->VCFRONT_vehiclePowerState int power_state = (msg->data[0] >> 5U) & 0x3U; - *ignition_can = power_state == 0x3; // VEHICLE_POWER_STATE_DRIVE=3 - *ignition_can_cnt = 0U; + ignition_can = power_state == 0x3; // VEHICLE_POWER_STATE_DRIVE=3 + ignition_can_cnt = 0U; } prev_counter_tesla = counter; } // Mazda exception if ((msg->addr == 0x9EU) && (len == 8)) { - *ignition_can = (msg->data[0] >> 5) == 0x6U; - *ignition_can_cnt = 0U; + ignition_can = (msg->data[0] >> 5) == 0x6U; + ignition_can_cnt = 0U; } } // TODO: this is too loose, Teslas have 0x222 // body v2 exception // if (((msg->bus == 0U) || (msg->bus == 2U)) && (msg->addr == 0x222U)) { - // *ignition_can = true; - // *ignition_can_cnt = 0U; + // ignition_can = true; + // ignition_can_cnt = 0U; // } } diff --git a/opendbc/safety/tests/libsafety/libsafety_py.py b/opendbc/safety/tests/libsafety/libsafety_py.py index 01eeb3b0f75..bf70602a219 100644 --- a/opendbc/safety/tests/libsafety/libsafety_py.py +++ b/opendbc/safety/tests/libsafety/libsafety_py.py @@ -111,9 +111,9 @@ class CANPacket: void mutation_set_active_mutant(int id); int mutation_get_active_mutant(void); -void ignition_can_hook_test(const CANPacket_t *msg); +void ignition_can_hook(const CANPacket_t *msg); bool get_ignition_can(void); -void reset_ignition_can(void); +void set_ignition_can(bool c); """) class LibSafety: diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index 469e2aebab0..4869eb241d8 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -13,22 +13,6 @@ uint32_t microsecond_timer_get(void) { #include "opendbc/safety/safety.h" #include "opendbc/safety/ignition.h" -static bool test_ignition_can = false; -static uint32_t test_ignition_can_cnt = 0U; - -void ignition_can_hook_test(const CANPacket_t *msg) { - ignition_can_hook(msg, &test_ignition_can, &test_ignition_can_cnt); -} - -bool get_ignition_can(void) { - return test_ignition_can; -} - -void reset_ignition_can(void) { - test_ignition_can = false; - test_ignition_can_cnt = 0U; -} - void safety_tick_current_safety_config() { safety_tick(¤t_safety_config); } @@ -62,10 +46,18 @@ void set_relay_malfunction(bool c){ relay_malfunction = c; } +void set_ignition_can(bool c){ + ignition_can = c; +} + bool get_controls_allowed(void){ return controls_allowed; } +bool get_ignition_can(void){ + return ignition_can; +} + int get_alternative_experience(void){ return alternative_experience; } @@ -218,4 +210,7 @@ void init_tests(void){ // assumes autopark on safety mode init to avoid a fault. get rid of that for testing tesla_autopark = false; + + ignition_can = false; + ignition_can_cnt = 0U; } diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index 1677b3dfabf..f2b87fa0914 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -9,90 +9,84 @@ class TestIgnitionHook(unittest.TestCase): def setUp(self): self.safety = libsafety_py.libsafety - self.safety.reset_ignition_can() + self.safety.init_tests() self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") self.rivian_packer = CANPackerSafety("rivian_primary_actuator") self.tesla_packer = CANPackerSafety("tesla_model3_party") - def _hook(self, msg): - self.safety.ignition_can_hook_test(msg) - - def _ign(self): - return self.safety.get_ignition_can() - # GM: SystemPowerMode 2=Run, 3=Crank Request def test_gm_ignition_on(self): - self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) - self.assertTrue(self._ign()) + self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self.safety.get_ignition_can()) def test_gm_ignition_off(self): - self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) - self.assertTrue(self._ign()) - self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 0})) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 0})) + self.assertFalse(self.safety.get_ignition_can()) # Rivian: VDM_EpasPowerMode_Drive_On=1 def test_rivian_ignition_on(self): for i in range(15): - self.safety.reset_ignition_can() - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": i, "VDM_EpasPowerMode": 1})) - self.assertFalse(self._ign()) - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": (i + 1) % 15, "VDM_EpasPowerMode": 1})) - self.assertTrue(self._ign()) + self.safety.init_tests() + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": i, "VDM_EpasPowerMode": 1})) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": (i + 1) % 15, "VDM_EpasPowerMode": 1})) + self.assertTrue(self.safety.get_ignition_can()) def test_rivian_ignition_off(self): - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 1})) - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 1})) - self.assertTrue(self._ign()) - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 2, "VDM_EpasPowerMode": 0})) - self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 3, "VDM_EpasPowerMode": 0})) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 1})) + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 1})) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 2, "VDM_EpasPowerMode": 0})) + self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 3, "VDM_EpasPowerMode": 0})) + self.assertFalse(self.safety.get_ignition_can()) # Tesla: VEHICLE_POWER_STATE_DRIVE=3 def test_tesla_ignition_on(self): - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) - self.assertFalse(self._ign()) - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) - self.assertTrue(self._ign()) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.assertTrue(self.safety.get_ignition_can()) def test_tesla_ignition_off(self): - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) - self.assertTrue(self._ign()) - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 2, "VCFRONT_vehiclePowerState": 2})) - self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 3, "VCFRONT_vehiclePowerState": 2})) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 2, "VCFRONT_vehiclePowerState": 2})) + self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 3, "VCFRONT_vehiclePowerState": 2})) + self.assertFalse(self.safety.get_ignition_can()) # Mazda: 0x9E byte 0 high 3 bits == 6 def test_mazda_ignition_on(self): - self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) - self.assertTrue(self._ign()) + self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self.safety.get_ignition_can()) def test_mazda_ignition_off(self): - self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) - self.assertTrue(self._ign()) - self._hook(make_msg(0, 0x9E, dat=b"\x20" + b"\x00" * 7)) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\x20" + b"\x00" * 7)) + self.assertFalse(self.safety.get_ignition_can()) def test_wrong_bus_ignored(self): - self._hook(make_msg(1, 0x1F1, dat=b"\x02" + b"\x00" * 7)) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(make_msg(1, 0x1F1, dat=b"\x02" + b"\x00" * 7)) + self.assertFalse(self.safety.get_ignition_can()) def test_unknown_addr_ignored(self): - self._hook(make_msg(0, 0x123, dat=b"\xFF" * 8)) - self.assertFalse(self._ign()) + self.safety.ignition_can_hook(make_msg(0, 0x123, dat=b"\xFF" * 8)) + self.assertFalse(self.safety.get_ignition_can()) if __name__ == "__main__": From b82518388317b81ef1691c8d710fcd01986865c6 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 19:46:01 -0700 Subject: [PATCH 45/69] clean tests --- opendbc/safety/tests/test_ignition.py | 71 ++++++++++++++------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index f2b87fa0914..57f8a8ab80e 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -14,70 +14,75 @@ def setUp(self): self.rivian_packer = CANPackerSafety("rivian_primary_actuator") self.tesla_packer = CANPackerSafety("tesla_model3_party") + def _gm_msg(self, mode): + return self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, + {"SystemPowerMode": mode}) + + def _rivian_msg(self, counter, mode): + return self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": counter, + "VDM_EpasPowerMode": mode}) + + def _tesla_msg(self, counter, state): + return self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": counter, + "VCFRONT_vehiclePowerState": state}) + + def _mazda_msg(self, byte0): + return make_msg(0, 0x9E, dat=bytes([byte0]) + b"\x00" * 7) + # GM: SystemPowerMode 2=Run, 3=Crank Request def test_gm_ignition_on(self): - self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.safety.ignition_can_hook(self._gm_msg(2)) self.assertTrue(self.safety.get_ignition_can()) def test_gm_ignition_off(self): - self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.safety.ignition_can_hook(self._gm_msg(2)) self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 0})) + self.safety.ignition_can_hook(self._gm_msg(0)) self.assertFalse(self.safety.get_ignition_can()) - # Rivian: VDM_EpasPowerMode_Drive_On=1 + # Rivian: VDM_EpasPowerMode_Drive_On=1 (counter-gated) def test_rivian_ignition_on(self): for i in range(15): self.safety.init_tests() - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": i, "VDM_EpasPowerMode": 1})) + self.safety.ignition_can_hook(self._rivian_msg(i, 1)) self.assertFalse(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": (i + 1) % 15, "VDM_EpasPowerMode": 1})) + self.safety.ignition_can_hook(self._rivian_msg((i + 1) % 15, 1)) self.assertTrue(self.safety.get_ignition_can()) def test_rivian_ignition_off(self): - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 1})) - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 1})) + self.safety.ignition_can_hook(self._rivian_msg(0, 1)) + self.safety.ignition_can_hook(self._rivian_msg(1, 1)) self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 2, "VDM_EpasPowerMode": 0})) - self.safety.ignition_can_hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": 3, "VDM_EpasPowerMode": 0})) + self.safety.ignition_can_hook(self._rivian_msg(2, 0)) + self.safety.ignition_can_hook(self._rivian_msg(3, 0)) self.assertFalse(self.safety.get_ignition_can()) - # Tesla: VEHICLE_POWER_STATE_DRIVE=3 + # Tesla: VEHICLE_POWER_STATE_DRIVE=3 (counter-gated) def test_tesla_ignition_on(self): - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self.safety.ignition_can_hook(self._tesla_msg(0, 3)) self.assertFalse(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.safety.ignition_can_hook(self._tesla_msg(1, 3)) self.assertTrue(self.safety.get_ignition_can()) def test_tesla_ignition_off(self): - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.safety.ignition_can_hook(self._tesla_msg(0, 3)) + self.safety.ignition_can_hook(self._tesla_msg(1, 3)) self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 2, "VCFRONT_vehiclePowerState": 2})) - self.safety.ignition_can_hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": 3, "VCFRONT_vehiclePowerState": 2})) + self.safety.ignition_can_hook(self._tesla_msg(2, 2)) + self.safety.ignition_can_hook(self._tesla_msg(3, 2)) self.assertFalse(self.safety.get_ignition_can()) - # Mazda: 0x9E byte 0 high 3 bits == 6 + # Mazda: 0x9E byte 0 high 3 bits == 6 (0xC0) def test_mazda_ignition_on(self): - self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.safety.ignition_can_hook(self._mazda_msg(0xC0)) self.assertTrue(self.safety.get_ignition_can()) def test_mazda_ignition_off(self): - self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.safety.ignition_can_hook(self._mazda_msg(0xC0)) self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(make_msg(0, 0x9E, dat=b"\x20" + b"\x00" * 7)) + self.safety.ignition_can_hook(self._mazda_msg(0x20)) self.assertFalse(self.safety.get_ignition_can()) def test_wrong_bus_ignored(self): From 89f3f77df4f08aef784cb90241edf1fcde8cc058 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 20:00:00 -0700 Subject: [PATCH 46/69] fix test --- opendbc/safety/tests/test_ignition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index 57f8a8ab80e..24aa7c48c58 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -6,6 +6,7 @@ class TestIgnitionHook(unittest.TestCase): + TX_MSGS: list = [] def setUp(self): self.safety = libsafety_py.libsafety From a97e7247291ad3eab8f38091c2879af00b765b00 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 13:14:17 -0700 Subject: [PATCH 47/69] volkswagen: ID.4 MK1 align lateral with safety via vm --- opendbc/car/volkswagen/carcontroller.py | 46 ++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 9c9f080b187..a87ef4086bd 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,12 +1,28 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase +from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags + +def _meb_safety_vm() -> VehicleModel: + # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) + # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. + CP = structs.CarParams() + CP.mass = 1.0 + CP.wheelbase = 1.0 + CP.centerToFront = 0.5 + CP.steerRatio = 1.0 + CP.steerRatioRear = 0.0 + CP.rotationalInertia = 1.0 + CP.tireStiffnessFront = 1.0 + CP.tireStiffnessRear = 1.0 + return VehicleModel(CP) + VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -53,6 +69,8 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 + self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None + self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) @@ -65,12 +83,18 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: if self.CP.flags & VolkswagenFlags.MEB: + max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG if CC.latActive: hca_enabled = True - apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, - CS.curvature_meas, False, self.CCP.STEER_STEP, CC.latActive, - self.CCP.CURVATURE_LIMITS) + curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. + # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. + limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + CC.latActive, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -82,9 +106,15 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: hca_enabled = True - apply_curvature = float(np.clip(CS.curvature_meas, - -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, - self.CCP.CURVATURE_LIMITS.CURVATURE_MAX)) + # Rate-limit the wind-down toward measured curvature with the same VM envelope + # safety enforces; clip endpoint to the safety angle limit. + wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) + limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, + self.apply_curvature_last * self.RAD_TO_DEG, + CS.out.vEgoRaw, + CS.curvature_meas * self.RAD_TO_DEG, + True, self.CCP, self.VM) + apply_curvature = limited_deg / self.RAD_TO_DEG steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False From b1144b581c9c601e7e6a93a470e69e43647adae5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 20:27:20 -0700 Subject: [PATCH 48/69] add loop --- opendbc/safety/tests/test_ignition.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py index 24aa7c48c58..341eb295f51 100644 --- a/opendbc/safety/tests/test_ignition.py +++ b/opendbc/safety/tests/test_ignition.py @@ -62,10 +62,12 @@ def test_rivian_ignition_off(self): # Tesla: VEHICLE_POWER_STATE_DRIVE=3 (counter-gated) def test_tesla_ignition_on(self): - self.safety.ignition_can_hook(self._tesla_msg(0, 3)) - self.assertFalse(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._tesla_msg(1, 3)) - self.assertTrue(self.safety.get_ignition_can()) + for i in range(16): + self.safety.init_tests() + self.safety.ignition_can_hook(self._tesla_msg(i, 3)) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._tesla_msg((i + 1) % 16, 3)) + self.assertTrue(self.safety.get_ignition_can()) def test_tesla_ignition_off(self): self.safety.ignition_can_hook(self._tesla_msg(0, 3)) From 97615d3487c0294cb87ea5527db5f365c8990b48 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 20:36:05 -0700 Subject: [PATCH 49/69] VW MEB: debounce steeringPressed to stop torque bar flicker --- opendbc/car/volkswagen/carstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index e8fc3568224..98f41991fca 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -305,7 +305,7 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] - ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE + ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) From c78ebcf63cb09a4db4de755a939505d5f49d32ad Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 21:11:44 -0700 Subject: [PATCH 50/69] split into brand tests --- opendbc/safety/tests/test_gm.py | 23 ++++++ opendbc/safety/tests/test_ignition.py | 101 -------------------------- opendbc/safety/tests/test_mazda.py | 24 +++++- opendbc/safety/tests/test_rivian.py | 31 ++++++++ opendbc/safety/tests/test_tesla.py | 31 ++++++++ 5 files changed, 108 insertions(+), 102 deletions(-) delete mode 100644 opendbc/safety/tests/test_ignition.py diff --git a/opendbc/safety/tests/test_gm.py b/opendbc/safety/tests/test_gm.py index aae308f7e3e..ad9b9789209 100755 --- a/opendbc/safety/tests/test_gm.py +++ b/opendbc/safety/tests/test_gm.py @@ -228,5 +228,28 @@ class TestGmCameraLongitudinalEVSafety(TestGmCameraLongitudinalSafety, TestGmEVS pass +class TestGmIgnition(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.init_tests() + self.packer = CANPackerSafety("gm_global_a_powertrain_generated") + + def _msg(self, mode): + return self.packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": mode}) + + # SystemPowerMode 2=Run, 3=Crank Request + def test_ignition_on(self): + self.safety.ignition_can_hook(self._msg(2)) + self.assertTrue(self.safety.get_ignition_can()) + + def test_ignition_off(self): + self.safety.ignition_can_hook(self._msg(2)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg(0)) + self.assertFalse(self.safety.get_ignition_can()) + + if __name__ == "__main__": unittest.main() diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py deleted file mode 100644 index 341eb295f51..00000000000 --- a/opendbc/safety/tests/test_ignition.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -import unittest - -from opendbc.safety.tests.common import CANPackerSafety, make_msg -from opendbc.safety.tests.libsafety import libsafety_py - - -class TestIgnitionHook(unittest.TestCase): - TX_MSGS: list = [] - - def setUp(self): - self.safety = libsafety_py.libsafety - self.safety.init_tests() - self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") - self.rivian_packer = CANPackerSafety("rivian_primary_actuator") - self.tesla_packer = CANPackerSafety("tesla_model3_party") - - def _gm_msg(self, mode): - return self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, - {"SystemPowerMode": mode}) - - def _rivian_msg(self, counter, mode): - return self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, - {"VDM_OutputSigs_Counter": counter, - "VDM_EpasPowerMode": mode}) - - def _tesla_msg(self, counter, state): - return self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, - {"VCFRONT_LVPowerStateCounter": counter, - "VCFRONT_vehiclePowerState": state}) - - def _mazda_msg(self, byte0): - return make_msg(0, 0x9E, dat=bytes([byte0]) + b"\x00" * 7) - - # GM: SystemPowerMode 2=Run, 3=Crank Request - def test_gm_ignition_on(self): - self.safety.ignition_can_hook(self._gm_msg(2)) - self.assertTrue(self.safety.get_ignition_can()) - - def test_gm_ignition_off(self): - self.safety.ignition_can_hook(self._gm_msg(2)) - self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._gm_msg(0)) - self.assertFalse(self.safety.get_ignition_can()) - - # Rivian: VDM_EpasPowerMode_Drive_On=1 (counter-gated) - def test_rivian_ignition_on(self): - for i in range(15): - self.safety.init_tests() - self.safety.ignition_can_hook(self._rivian_msg(i, 1)) - self.assertFalse(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._rivian_msg((i + 1) % 15, 1)) - self.assertTrue(self.safety.get_ignition_can()) - - def test_rivian_ignition_off(self): - self.safety.ignition_can_hook(self._rivian_msg(0, 1)) - self.safety.ignition_can_hook(self._rivian_msg(1, 1)) - self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._rivian_msg(2, 0)) - self.safety.ignition_can_hook(self._rivian_msg(3, 0)) - self.assertFalse(self.safety.get_ignition_can()) - - # Tesla: VEHICLE_POWER_STATE_DRIVE=3 (counter-gated) - def test_tesla_ignition_on(self): - for i in range(16): - self.safety.init_tests() - self.safety.ignition_can_hook(self._tesla_msg(i, 3)) - self.assertFalse(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._tesla_msg((i + 1) % 16, 3)) - self.assertTrue(self.safety.get_ignition_can()) - - def test_tesla_ignition_off(self): - self.safety.ignition_can_hook(self._tesla_msg(0, 3)) - self.safety.ignition_can_hook(self._tesla_msg(1, 3)) - self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._tesla_msg(2, 2)) - self.safety.ignition_can_hook(self._tesla_msg(3, 2)) - self.assertFalse(self.safety.get_ignition_can()) - - # Mazda: 0x9E byte 0 high 3 bits == 6 (0xC0) - def test_mazda_ignition_on(self): - self.safety.ignition_can_hook(self._mazda_msg(0xC0)) - self.assertTrue(self.safety.get_ignition_can()) - - def test_mazda_ignition_off(self): - self.safety.ignition_can_hook(self._mazda_msg(0xC0)) - self.assertTrue(self.safety.get_ignition_can()) - self.safety.ignition_can_hook(self._mazda_msg(0x20)) - self.assertFalse(self.safety.get_ignition_can()) - - def test_wrong_bus_ignored(self): - self.safety.ignition_can_hook(make_msg(1, 0x1F1, dat=b"\x02" + b"\x00" * 7)) - self.assertFalse(self.safety.get_ignition_can()) - - def test_unknown_addr_ignored(self): - self.safety.ignition_can_hook(make_msg(0, 0x123, dat=b"\xFF" * 8)) - self.assertFalse(self.safety.get_ignition_can()) - - -if __name__ == "__main__": - unittest.main() diff --git a/opendbc/safety/tests/test_mazda.py b/opendbc/safety/tests/test_mazda.py index 607edcbfdac..8fb77444a7c 100755 --- a/opendbc/safety/tests/test_mazda.py +++ b/opendbc/safety/tests/test_mazda.py @@ -4,7 +4,7 @@ from opendbc.car.structs import CarParams from opendbc.safety.tests.libsafety import libsafety_py import opendbc.safety.tests.common as common -from opendbc.safety.tests.common import CANPackerSafety +from opendbc.safety.tests.common import CANPackerSafety, make_msg class TestMazdaSafety(common.CarSafetyTest, common.DriverTorqueSteeringSafetyTest): @@ -81,5 +81,27 @@ def test_buttons(self): self.assertTrue(self._tx(self._button_msg(resume=True))) +class TestMazdaIgnition(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.init_tests() + + def _msg(self, byte0): + return make_msg(0, 0x9E, dat=bytes([byte0]) + b"\x00" * 7) + + # 0x9E byte 0 high 3 bits == 6 (0xC0) + def test_ignition_on(self): + self.safety.ignition_can_hook(self._msg(0xC0)) + self.assertTrue(self.safety.get_ignition_can()) + + def test_ignition_off(self): + self.safety.ignition_can_hook(self._msg(0xC0)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg(0x20)) + self.assertFalse(self.safety.get_ignition_can()) + + if __name__ == "__main__": unittest.main() diff --git a/opendbc/safety/tests/test_rivian.py b/opendbc/safety/tests/test_rivian.py index f1393e605c1..dfe2c359ec7 100755 --- a/opendbc/safety/tests/test_rivian.py +++ b/opendbc/safety/tests/test_rivian.py @@ -142,5 +142,36 @@ def setUp(self): self.safety.init_tests() +class TestRivianIgnition(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.init_tests() + self.packer = CANPackerSafety("rivian_primary_actuator") + + def _msg(self, counter, mode): + return self.packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": counter, + "VDM_EpasPowerMode": mode}) + + # VDM_EpasPowerMode_Drive_On=1 + def test_ignition_on(self): + for i in range(15): + self.safety.init_tests() + self.safety.ignition_can_hook(self._msg(i, 1)) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg((i + 1) % 15, 1)) + self.assertTrue(self.safety.get_ignition_can()) + + def test_ignition_off(self): + self.safety.ignition_can_hook(self._msg(0, 1)) + self.safety.ignition_can_hook(self._msg(1, 1)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg(2, 0)) + self.safety.ignition_can_hook(self._msg(3, 0)) + self.assertFalse(self.safety.get_ignition_can()) + + if __name__ == "__main__": unittest.main() diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index 8c05a7dd2e3..c4e42884f34 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -456,5 +456,36 @@ class TestTeslaFSD14LongitudinalSafety(TestTeslaLongitudinalSafety): SAFETY_PARAM = TeslaSafetyFlags.LONG_CONTROL | TeslaSafetyFlags.FSD_14 +class TestTeslaIgnition(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.init_tests() + self.packer = CANPackerSafety("tesla_model3_party") + + def _msg(self, counter, state): + return self.packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": counter, + "VCFRONT_vehiclePowerState": state}) + + # VEHICLE_POWER_STATE_DRIVE=3 (counter-gated) + def test_ignition_on(self): + for i in range(16): + self.safety.init_tests() + self.safety.ignition_can_hook(self._msg(i, 3)) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg((i + 1) % 16, 3)) + self.assertTrue(self.safety.get_ignition_can()) + + def test_ignition_off(self): + self.safety.ignition_can_hook(self._msg(0, 3)) + self.safety.ignition_can_hook(self._msg(1, 3)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg(2, 2)) + self.safety.ignition_can_hook(self._msg(3, 2)) + self.assertFalse(self.safety.get_ignition_can()) + + if __name__ == "__main__": unittest.main() From 5495bcddeb3dc4319e4b89f277ff77ac966bd7a9 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 21:22:10 -0700 Subject: [PATCH 51/69] add vw meb can ignition --- opendbc/safety/ignition.h | 13 +++++++ opendbc/safety/tests/test_volkswagen_meb.py | 40 +++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 opendbc/safety/tests/test_volkswagen_meb.py diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h index 7ac8f008a5f..ae00c72965b 100644 --- a/opendbc/safety/ignition.h +++ b/opendbc/safety/ignition.h @@ -53,6 +53,19 @@ void ignition_can_hook(const CANPacket_t *msg) { ignition_can = (msg->data[0] >> 5) == 0x6U; ignition_can_cnt = 0U; } + + // Volkswagen MEB exception + if ((msg->addr == 0x3C0U) && (len == 4)) { + int counter = msg->data[1] & 0xFU; + + static int prev_counter_vw_meb = -1; + if ((counter == ((prev_counter_vw_meb + 1) % 16)) && (prev_counter_vw_meb != -1)) { + // Klemmen_Status_01->ZAS_Kl_15 + ignition_can = ((msg->data[2] >> 1) & 1U) != 0U; + ignition_can_cnt = 0U; + } + prev_counter_vw_meb = counter; + } } // TODO: this is too loose, Teslas have 0x222 diff --git a/opendbc/safety/tests/test_volkswagen_meb.py b/opendbc/safety/tests/test_volkswagen_meb.py new file mode 100644 index 00000000000..6c2b11238db --- /dev/null +++ b/opendbc/safety/tests/test_volkswagen_meb.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import unittest + +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.common import CANPackerSafety + + +class TestVolkswagenMebIgnition(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.init_tests() + self.packer = CANPackerSafety("vw_meb") + + def _msg(self, counter, ign): + return self.packer.make_can_msg_safety("Klemmen_Status_01", 0, + {"Klemmen_Status_01_BZ": counter, + "ZAS_Kl_15": ign}) + + # ZAS_Kl_15=1 (counter-gated) + def test_ignition_on(self): + for i in range(16): + self.safety.init_tests() + self.safety.ignition_can_hook(self._msg(i, 1)) + self.assertFalse(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg((i + 1) % 16, 1)) + self.assertTrue(self.safety.get_ignition_can()) + + def test_ignition_off(self): + self.safety.ignition_can_hook(self._msg(0, 1)) + self.safety.ignition_can_hook(self._msg(1, 1)) + self.assertTrue(self.safety.get_ignition_can()) + self.safety.ignition_can_hook(self._msg(2, 0)) + self.safety.ignition_can_hook(self._msg(3, 0)) + self.assertFalse(self.safety.get_ignition_can()) + + +if __name__ == "__main__": + unittest.main() From 491b0e67b4ee49b7567d14e9b77fd54eefd85f51 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 12 May 2026 21:23:05 -0700 Subject: [PATCH 52/69] smol --- opendbc/safety/tests/test_volkswagen_meb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/safety/tests/test_volkswagen_meb.py b/opendbc/safety/tests/test_volkswagen_meb.py index 6c2b11238db..4fc349a22bd 100644 --- a/opendbc/safety/tests/test_volkswagen_meb.py +++ b/opendbc/safety/tests/test_volkswagen_meb.py @@ -18,7 +18,7 @@ def _msg(self, counter, ign): {"Klemmen_Status_01_BZ": counter, "ZAS_Kl_15": ign}) - # ZAS_Kl_15=1 (counter-gated) + # ZAS_Kl_15=1 def test_ignition_on(self): for i in range(16): self.safety.init_tests() From 36a3aff213381bc522cc21a8a760763b97b52468 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 09:44:13 -0700 Subject: [PATCH 53/69] acc_btn_control --- opendbc/car/volkswagen/mebcan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opendbc/car/volkswagen/mebcan.py b/opendbc/car/volkswagen/mebcan.py index f77dd0f631b..a9f37084df0 100644 --- a/opendbc/car/volkswagen/mebcan.py +++ b/opendbc/car/volkswagen/mebcan.py @@ -32,7 +32,7 @@ def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_p return packer.make_can_msg("LDW_02", bus, values) -def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resume=False): +def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resume=False, up=False, down=False): values = {s: gra_stock_values[s] for s in [ "GRA_Hauptschalter", # ACC button, on/off "GRA_Typ_Hauptschalter", # ACC main button type @@ -44,7 +44,8 @@ def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resu values.update({ "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16, "GRA_Abbrechen": cancel, - "GRA_Tip_Wiederaufnahme": resume, + "GRA_Tip_Wiederaufnahme": resume or up, + "GRA_Tip_Setzen": down, }) return packer.make_can_msg("GRA_ACC_01", bus, values) From 8321eb7f91c6783e9d8155039d9aa469a7da6334 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 09:52:42 -0700 Subject: [PATCH 54/69] add sound alert --- opendbc/car/volkswagen/mebcan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/mebcan.py b/opendbc/car/volkswagen/mebcan.py index a9f37084df0..117a04a4226 100644 --- a/opendbc/car/volkswagen/mebcan.py +++ b/opendbc/car/volkswagen/mebcan.py @@ -9,7 +9,7 @@ def create_steering_control(packer, bus, apply_curvature, lkas_enabled, power): return packer.make_can_msg("HCA_03", bus, values) -def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control): +def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control, sound_alert=False): display_mode = 1 if lat_active else 0 # travel assist style showing yellow lanes when op is active values = {} @@ -23,6 +23,7 @@ def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_p ]} values.update({ + "LDW_Gong": sound_alert, "LDW_Status_LED_gelb": 1 if lat_active and steering_pressed else 0, "LDW_Status_LED_gruen": 1 if lat_active and not steering_pressed else 0, "LDW_Lernmodus_links": 3 + display_mode if hud_control.leftLaneDepart else 1 + hud_control.leftLaneVisible + display_mode, From 5ce144fb66e7df968c017bfaffbe7a266080ea2c Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 09:58:51 -0700 Subject: [PATCH 55/69] VW MEB: port create_eps_update + init apply_torque for STOCK_HCA_PRESENT --- opendbc/car/volkswagen/carcontroller.py | 9 +++++++-- opendbc/car/volkswagen/mebcan.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index a87ef4086bd..fcd7d4de851 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -82,13 +82,18 @@ def update(self, CC, CS, now_nanos): # **** Steering Controls ************************************************ # if self.frame % self.CCP.STEER_STEP == 0: + apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG + # Logic to avoid HCA refused state: + # * steering power as counter and near zero before OP lane assist deactivation + # MEB rack can be used continously without time limits + # maximum real steering angle change ~ 120-130 deg/s if CC.latActive: hca_enabled = True curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - # Rate-limit using the same VM-based envelope (lateral accel + jerk) that safety enforces. - # apply_steer_angle_limits_vm operates in degrees; we feed curvature*RAD_TO_DEG and unscale. + # rate-limit using the same vm envelope from safety + # apply_steer_angle_limits_vm operates in degrees, convert from curvature limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, self.apply_curvature_last * self.RAD_TO_DEG, CS.out.vEgoRaw, diff --git a/opendbc/car/volkswagen/mebcan.py b/opendbc/car/volkswagen/mebcan.py index 117a04a4226..ac3d2b1a80b 100644 --- a/opendbc/car/volkswagen/mebcan.py +++ b/opendbc/car/volkswagen/mebcan.py @@ -9,6 +9,24 @@ def create_steering_control(packer, bus, apply_curvature, lkas_enabled, power): return packer.make_can_msg("HCA_03", bus, values) +def create_eps_update(packer, bus, eps_stock_values, ea_simulated_torque): + values = {s: eps_stock_values[s] for s in [ + "COUNTER", # Sync counter value to EPS output + "EPS_Lenkungstyp", # EPS rack type + "EPS_Berechneter_LW", # Absolute raw steering angle + "EPS_VZ_BLW", # Raw steering angle sign + "EPS_HCA_Status", # EPS HCA control status + ]} + + values.update({ + # Absolute driver torque input and sign, with EA inactivity mitigation + "EPS_Lenkmoment": abs(ea_simulated_torque), + "EPS_VZ_Lenkmoment": 1 if ea_simulated_torque < 0 else 0, + }) + + return packer.make_can_msg("LH_EPS_03", bus, values) + + def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control, sound_alert=False): display_mode = 1 if lat_active else 0 # travel assist style showing yellow lanes when op is active From 77cfa93a8e4778cf80e8e6f0a2ad06bd6f74ba2c Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:00:31 -0700 Subject: [PATCH 56/69] add hca comment --- opendbc/car/volkswagen/carcontroller.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index fcd7d4de851..2608cac048c 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -111,8 +111,8 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: hca_enabled = True - # Rate-limit the wind-down toward measured curvature with the same VM envelope - # safety enforces; clip endpoint to the safety angle limit. + # rate-limit the wind-down toward measured curvature with the same vm envelope from safety + # clip endpoint to the safety angle limit wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, self.apply_curvature_last * self.RAD_TO_DEG, @@ -131,6 +131,14 @@ def update(self, CC, CS, now_nanos): self.steering_power_last = steering_power else: + # Logic to avoid HCA state 4 "refused": + # * Don't steer unless HCA is in state 3 "ready" or 5 "active" + # * Don't steer at standstill + # * Don't send > 3.00 Newton-meters torque + # * Don't send the same torque for > 6 seconds + # * Don't send uninterrupted steering for > 360 seconds + # MQB racks reset the uninterrupted steering timer after a single frame + # of HCA disabled; this is done whenever output happens to be zero. apply_torque = 0 if CC.latActive: new_torque = int(round(actuators.torque * self.CCP.STEER_MAX)) From a095dc17b9fe20e60ee4019c9610e778bc6ad088 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:04:35 -0700 Subject: [PATCH 57/69] VW MEB: init klr_counter_last + read EA_01 for carFaultedNonCritical --- opendbc/car/volkswagen/carcontroller.py | 1 + opendbc/car/volkswagen/carstate.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 2608cac048c..23976888f42 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -72,6 +72,7 @@ def __init__(self, dbc_names, CP): self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None + self.klr_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) def update(self, CC, CS, now_nanos): diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 98f41991fca..88456801824 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -314,6 +314,9 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status, drive_mode) + if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: + ret.carFaultedNonCritical = bool(cam_cp.vl["HCA_01"]["EA_Ruckfreigabe"]) or cam_cp.vl["HCA_01"]["EA_ACC_Sollstatus"] > 0 # EA + ret.gasPressed = pt_cp.vl["Motor_51"]["Accel_Pedal_Pressure"] > 0 ret.brakePressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"]) ret.parkingBrake = pt_cp.vl["Gateway_73"]["EPB_Status"] in (1, 4) From 11d32f00444399828280188cf7c35fdf19285ef4 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:07:10 -0700 Subject: [PATCH 58/69] move meb --- opendbc/car/volkswagen/carstate.py | 110 ++++++++++++++--------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 88456801824..3e9a697778e 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -235,61 +235,6 @@ def update_pq(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: self.frame += 1 return ret - def update_mlb(self, pt_cp, cam_cp, ext_cp, alt_cp) -> structs.CarState: - ret = structs.CarState() - - self.parse_wheel_speeds(ret, - pt_cp.vl["ESP_03"]["ESP_VL_Radgeschw"], - pt_cp.vl["ESP_03"]["ESP_VR_Radgeschw"], - pt_cp.vl["ESP_03"]["ESP_HL_Radgeschw"], - pt_cp.vl["ESP_03"]["ESP_HR_Radgeschw"], - ) - - ret.gasPressed = pt_cp.vl["Motor_03"]["MO_Fahrpedalrohwert_01"] > 0 - ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(alt_cp.vl["Getriebe_03"]["GE_Waehlhebel"], None)) - - # TODO: We don't have a true mainswitch state yet, might need stateful tracking on LS_01 if momentary-press is a thing - # TSK_04.TSK_Status_GRA_ACC_02 0 = not engaged, 1 = engaged, 2 = engaged with driver accel override, 3 = fault - ret.cruiseState.available = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] in (0, 1, 2) - ret.cruiseState.available = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] in (1, 2) - ret.accFaulted = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] == 3 - ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS - - self.parse_mlb_mqb_steering_state(ret, pt_cp) - - ret.brake = pt_cp.vl["ESP_05"]["ESP_Bremsdruck"] / 250.0 - brake_pedal_pressed = bool(pt_cp.vl["Motor_03"]["MO_Fahrer_bremst"]) - brake_pressure_detected = bool(pt_cp.vl["ESP_05"]["ESP_Fahrer_bremst"]) - ret.brakePressed = brake_pedal_pressed or brake_pressure_detected - ret.parkingBrake = bool(pt_cp.vl["Kombi_01"]["KBI_Handbremse"]) - ret.espDisabled = pt_cp.vl["ESP_01"]["ESP_Tastung_passiv"] != 0 - - ret.leftBlinker = bool(pt_cp.vl["BCM"]["BLINKER_LEFT"]) - ret.rightBlinker = bool(pt_cp.vl["BCM"]["BLINKER_RIGHT"]) - - ret.seatbeltUnlatched = bool(pt_cp.vl["Airbag_01"]["AB_Gurtwarn_VF"]) - ret.doorOpen = any([alt_cp.vl["Gateway_05"]["FT_Tuer_geoeffnet"], - alt_cp.vl["Gateway_05"]["BT_Tuer_geoeffnet"], - alt_cp.vl["Gateway_05"]["HL_Tuer_geoeffnet"], - alt_cp.vl["Gateway_05"]["HR_Tuer_geoeffnet"]]) - - # Consume blind-spot monitoring info/warning LED states, if available. - # Infostufe: BSM LED on, Warnung: BSM LED flashing - if self.CP.enableBsm: - ret.leftBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_li"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_li"]) - ret.rightBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_re"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_re"]) - - self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} - self.gra_stock_values = pt_cp.vl["LS_01"] - - ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) - - ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation - ret.standstill = ret.vEgoRaw == 0 - - self.frame += 1 - return ret - def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret = structs.CarState() @@ -350,6 +295,61 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: self.frame += 1 return ret + def update_mlb(self, pt_cp, cam_cp, ext_cp, alt_cp) -> structs.CarState: + ret = structs.CarState() + + self.parse_wheel_speeds(ret, + pt_cp.vl["ESP_03"]["ESP_VL_Radgeschw"], + pt_cp.vl["ESP_03"]["ESP_VR_Radgeschw"], + pt_cp.vl["ESP_03"]["ESP_HL_Radgeschw"], + pt_cp.vl["ESP_03"]["ESP_HR_Radgeschw"], + ) + + ret.gasPressed = pt_cp.vl["Motor_03"]["MO_Fahrpedalrohwert_01"] > 0 + ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(alt_cp.vl["Getriebe_03"]["GE_Waehlhebel"], None)) + + # TODO: We don't have a true mainswitch state yet, might need stateful tracking on LS_01 if momentary-press is a thing + # TSK_04.TSK_Status_GRA_ACC_02 0 = not engaged, 1 = engaged, 2 = engaged with driver accel override, 3 = fault + ret.cruiseState.available = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] in (0, 1, 2) + ret.cruiseState.available = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] in (1, 2) + ret.accFaulted = alt_cp.vl["TSK_04"]["TSK_Status_GRA_ACC_02"] == 3 + ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS + + self.parse_mlb_mqb_steering_state(ret, pt_cp) + + ret.brake = pt_cp.vl["ESP_05"]["ESP_Bremsdruck"] / 250.0 + brake_pedal_pressed = bool(pt_cp.vl["Motor_03"]["MO_Fahrer_bremst"]) + brake_pressure_detected = bool(pt_cp.vl["ESP_05"]["ESP_Fahrer_bremst"]) + ret.brakePressed = brake_pedal_pressed or brake_pressure_detected + ret.parkingBrake = bool(pt_cp.vl["Kombi_01"]["KBI_Handbremse"]) + ret.espDisabled = pt_cp.vl["ESP_01"]["ESP_Tastung_passiv"] != 0 + + ret.leftBlinker = bool(pt_cp.vl["BCM"]["BLINKER_LEFT"]) + ret.rightBlinker = bool(pt_cp.vl["BCM"]["BLINKER_RIGHT"]) + + ret.seatbeltUnlatched = bool(pt_cp.vl["Airbag_01"]["AB_Gurtwarn_VF"]) + ret.doorOpen = any([alt_cp.vl["Gateway_05"]["FT_Tuer_geoeffnet"], + alt_cp.vl["Gateway_05"]["BT_Tuer_geoeffnet"], + alt_cp.vl["Gateway_05"]["HL_Tuer_geoeffnet"], + alt_cp.vl["Gateway_05"]["HR_Tuer_geoeffnet"]]) + + # Consume blind-spot monitoring info/warning LED states, if available. + # Infostufe: BSM LED on, Warnung: BSM LED flashing + if self.CP.enableBsm: + ret.leftBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_li"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_li"]) + ret.rightBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_re"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_re"]) + + self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {} + self.gra_stock_values = pt_cp.vl["LS_01"] + + ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS) + + ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation + ret.standstill = ret.vEgoRaw == 0 + + self.frame += 1 + return ret + def update_low_speed_alert(self, v_ego: float) -> bool: # Low speed steer alert hysteresis logic if (self.CP.minSteerSpeed - 1e-3) > CarControllerParams.DEFAULT_MIN_STEER_SPEED and v_ego < (self.CP.minSteerSpeed + 1.): From 4fa985eef81c491378e18a9e5add7da18b54e0a1 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:16:29 -0700 Subject: [PATCH 59/69] VW MEB: restore ALT_GEAR detection and gear source branching --- opendbc/car/volkswagen/carstate.py | 6 +++++- opendbc/car/volkswagen/interface.py | 2 ++ opendbc/car/volkswagen/values.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 3e9a697778e..2e4a632733f 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -253,7 +253,10 @@ def update_meb(self, pt_cp, cam_cp, ext_cp) -> structs.CarState: ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE, 5) self.curvature_meas = -pt_cp.vl["QFK_01"]["Curvature"] * (1, -1)[int(pt_cp.vl["QFK_01"]["Curvature_VZ"])] - ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) + if self.CP.flags & VolkswagenFlags.ALT_GEAR: + ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Gateway_73"]["GE_Fahrstufe"], None)) + else: + ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None)) drive_mode = ret.gearShifter == GearShifter.drive hca_status = self.CCP.hca_status_values.get(pt_cp.vl["QFK_01"]["LatCon_HCA_Status"]) @@ -426,6 +429,7 @@ def get_can_parsers_pq(CP): def get_can_parsers_meb(CP): pt_messages = [ ("Blinkmodi_02", 1), # From J519 BCM (sent at 1Hz when no lights active, 50Hz when active) + ("Gateway_73", 10), # Gear position (ALT_GEAR) and EPB status ] cam_messages = [] if CP.enableBsm: diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py index f9efa88bfd1..31043c1000a 100644 --- a/opendbc/car/volkswagen/interface.py +++ b/opendbc/car/volkswagen/interface.py @@ -80,6 +80,8 @@ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alp ret.flags |= VolkswagenFlags.STOCK_HCA_PRESENT.value if 0x6B8 in fingerprint[0]: # Kombi_03 ret.flags |= VolkswagenFlags.KOMBI_PRESENT.value + if 0x3DC in fingerprint[0]: # Gateway_73 + ret.flags |= VolkswagenFlags.ALT_GEAR.value # Global lateral tuning defaults, can be overridden per-vehicle diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 9301fc7af55..29e93dab8fc 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -231,6 +231,7 @@ class VolkswagenFlags(IntFlag): # Detected flags STOCK_HCA_PRESENT = 1 KOMBI_PRESENT = 4 + ALT_GEAR = 32 STOCK_KLR_PRESENT = 64 # Static flags From ba3084aeadc2c84669ff4b37ad1399e849e28fb0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:31:02 -0700 Subject: [PATCH 60/69] Revert "volkswagen: ID.4 MK1 align lateral with safety via vm" This reverts commit a97e7247291ad3eab8f38091c2879af00b765b00. --- opendbc/car/volkswagen/carcontroller.py | 50 ++++--------------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py index 23976888f42..14be034add5 100644 --- a/opendbc/car/volkswagen/carcontroller.py +++ b/opendbc/car/volkswagen/carcontroller.py @@ -1,28 +1,12 @@ import numpy as np from opendbc.can import CANPacker from opendbc.car import Bus, DT_CTRL, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_curvature_limits from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarControllerBase -from opendbc.car.vehicle_model import VehicleModel from opendbc.car.volkswagen import mebcan, mlbcan, mqbcan, pqcan from opendbc.car.volkswagen.values import CanBus, CarControllerParams, VolkswagenFlags - -def _meb_safety_vm() -> VehicleModel: - # Stub VehicleModel matching the safety params (slip=0, steerRatio=1, wheelbase=1) - # so apply_steer_angle_limits_vm reduces to an identity on curvature*RAD_TO_DEG. - CP = structs.CarParams() - CP.mass = 1.0 - CP.wheelbase = 1.0 - CP.centerToFront = 0.5 - CP.steerRatio = 1.0 - CP.steerRatioRear = 0.0 - CP.rotationalInertia = 1.0 - CP.tireStiffnessFront = 1.0 - CP.tireStiffnessRear = 1.0 - return VehicleModel(CP) - VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -69,8 +53,6 @@ def __init__(self, dbc_names, CP): self.apply_torque_last = 0 self.apply_curvature_last = 0. self.steering_power_last = 0 - self.VM = _meb_safety_vm() if CP.flags & VolkswagenFlags.MEB else None - self.RAD_TO_DEG = CarControllerParams.MEB_RAD_TO_DEG self.gra_acc_counter_last = None self.klr_counter_last = None self.hca_mitigation = HCAMitigation(self.CCP) @@ -85,22 +67,12 @@ def update(self, CC, CS, now_nanos): if self.frame % self.CCP.STEER_STEP == 0: apply_torque = 0 if self.CP.flags & VolkswagenFlags.MEB: - max_curvature = self.CCP.ANGLE_LIMITS.STEER_ANGLE_MAX / self.RAD_TO_DEG - # Logic to avoid HCA refused state: - # * steering power as counter and near zero before OP lane assist deactivation - # MEB rack can be used continously without time limits - # maximum real steering angle change ~ 120-130 deg/s if CC.latActive: hca_enabled = True - curvature_target = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) - # rate-limit using the same vm envelope from safety - # apply_steer_angle_limits_vm operates in degrees, convert from curvature - limited_deg = apply_steer_angle_limits_vm(curvature_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - CC.latActive, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = actuators.curvature + (CS.curvature_meas - CC.currentCurvature) + apply_curvature = apply_std_curvature_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw, + CS.curvature_meas, False, self.CCP.STEER_STEP, CC.latActive, + self.CCP.CURVATURE_LIMITS) min_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MIN) max_power = min(self.steering_power_last + self.CCP.STEERING_POWER_STEP, self.CCP.STEERING_POWER_MAX) @@ -112,15 +84,9 @@ def update(self, CC, CS, now_nanos): else: if self.steering_power_last > 0: hca_enabled = True - # rate-limit the wind-down toward measured curvature with the same vm envelope from safety - # clip endpoint to the safety angle limit - wind_target = float(np.clip(CS.curvature_meas, -max_curvature, max_curvature)) - limited_deg = apply_steer_angle_limits_vm(wind_target * self.RAD_TO_DEG, - self.apply_curvature_last * self.RAD_TO_DEG, - CS.out.vEgoRaw, - CS.curvature_meas * self.RAD_TO_DEG, - True, self.CCP, self.VM) - apply_curvature = limited_deg / self.RAD_TO_DEG + apply_curvature = float(np.clip(CS.curvature_meas, + -self.CCP.CURVATURE_LIMITS.CURVATURE_MAX, + self.CCP.CURVATURE_LIMITS.CURVATURE_MAX)) steering_power = max(self.steering_power_last - self.CCP.STEERING_POWER_STEP, 0) else: hca_enabled = False From d0a4b8bd80bd9296d20ea86750414afa2c199ace Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:43:02 -0700 Subject: [PATCH 61/69] VW MEB: revert safety to non-VM rate-lookup envelope --- opendbc/safety/modes/volkswagen_meb.h | 29 +++++++++------------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 4352028ebff..f62f4203bfa 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -12,10 +12,6 @@ #define VOLKSWAGEN_MEB_CURVATURE_TO_CAN 149253.7313f // 1 / 6.7e-6 #define VOLKSWAGEN_MEB_MAX_CURVATURE_CAN 29105 -// Helper expects deg_to_can; with stub VM params (slip=0, sR=1, wb=1) the helper's -// "angle" math becomes an identity on curvature, so we scale curvature*RAD_TO_DEG to CAN. -#define VOLKSWAGEN_MEB_RAD_TO_DEG 57.29577951308232f - static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { int len = GET_LEN(msg); @@ -50,20 +46,19 @@ static uint32_t volkswagen_meb_compute_crc(const CANPacket_t *msg) { static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .max_angle = VOLKSWAGEN_MEB_MAX_CURVATURE_CAN, - .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN / VOLKSWAGEN_MEB_RAD_TO_DEG, - .frequency = 50, + .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, + .angle_rate_up_lookup = { + {1., 5., 25.}, + {0.4, 0.0029, 0.000115}, + }, + .angle_rate_down_lookup = { + {1., 5., 25.}, + {0.4, 0.0029, 0.000115}, + }, .angle_is_curvature = true, .inactive_angle_is_zero = true, }; -// Stub vehicle model params: identity curvature_factor so the helper's lateral -// accel/jerk envelope is expressed directly in curvature*RAD_TO_DEG units. -static const AngleSteeringParams VOLKSWAGEN_MEB_STEERING_PARAMS = { - .slip_factor = 0.0f, - .steer_ratio = 1.0f, - .wheelbase = 1.0f, -}; - static safety_config volkswagen_meb_init(uint16_t param) { // Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration static const CanMsg VOLKSWAGEN_MEB_STOCK_TX_MSGS[] = { @@ -98,9 +93,6 @@ static void volkswagen_meb_rx_hook(const CANPacket_t *msg) { uint32_t rl = msg->data[12] | (msg->data[13] << 8); uint32_t rr = msg->data[14] | (msg->data[15] << 8); vehicle_moving = (fr > 0U) || (rr > 0U) || (rl > 0U) || (fl > 0U); - // Wheel speed signal scaling 0.0075 km/h per LSB; average four wheels into m/s - float v_ego_ms = ((float)(fl + fr + rl + rr)) * (0.0075f / 3.6f / 4.0f); - UPDATE_VEHICLE_SPEED(v_ego_ms); } if (msg->addr == MSG_QFK_01) { @@ -154,8 +146,7 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { } bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); - if (steer_angle_cmd_checks_vm(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS, - VOLKSWAGEN_MEB_STEERING_PARAMS)) { + if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { tx = false; } } From e0105f63412bdc4a8c3b493fe2ee49cc92d72b98 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 10:46:45 -0700 Subject: [PATCH 62/69] VW MEB: safety rate-lookup with 2x margin over python ISO jerk envelope --- opendbc/safety/modes/volkswagen_meb.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index f62f4203bfa..6818fd43b96 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -49,11 +49,11 @@ static const AngleSteeringLimits VOLKSWAGEN_MEB_STEERING_LIMITS = { .angle_deg_to_can = VOLKSWAGEN_MEB_CURVATURE_TO_CAN, .angle_rate_up_lookup = { {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, + {0.4, 0.008, 0.00032}, }, .angle_rate_down_lookup = { {1., 5., 25.}, - {0.4, 0.0029, 0.000115}, + {0.4, 0.008, 0.00032}, }, .angle_is_curvature = true, .inactive_angle_is_zero = true, From 89453e01b1016647d248af51f5387f6c0d37d2e6 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 12:02:39 -0700 Subject: [PATCH 63/69] VW MEB: simplify safety to absolute curvature cap only --- opendbc/safety/modes/volkswagen_meb.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 6818fd43b96..99e083da459 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -144,9 +144,10 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { if (!desired_curvature_sign) { desired_curvature_raw *= -1; } - bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); - - if (steer_angle_cmd_checks(desired_curvature_raw, steer_req, VOLKSWAGEN_MEB_STEERING_LIMITS)) { + // Simple absolute-curvature cap. Per-step rate and inactive-zero enforcement intentionally + // omitted while the carcontroller/safety alignment is being tuned; will tighten later. + if ((desired_curvature_raw > VOLKSWAGEN_MEB_MAX_CURVATURE_CAN) || + (desired_curvature_raw < -VOLKSWAGEN_MEB_MAX_CURVATURE_CAN)) { tx = false; } } From fd86d72fba903f1186edbe5b1ab822bd4f53696d Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 12:48:03 -0700 Subject: [PATCH 64/69] VW MEB: add ISO lateral accel cap to safety (middle ground, no rate check) --- opendbc/safety/modes/volkswagen_meb.h | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/modes/volkswagen_meb.h b/opendbc/safety/modes/volkswagen_meb.h index 99e083da459..7961fb16834 100644 --- a/opendbc/safety/modes/volkswagen_meb.h +++ b/opendbc/safety/modes/volkswagen_meb.h @@ -144,12 +144,26 @@ static bool volkswagen_meb_tx_hook(const CANPacket_t *msg) { if (!desired_curvature_sign) { desired_curvature_raw *= -1; } - // Simple absolute-curvature cap. Per-step rate and inactive-zero enforcement intentionally - // omitted while the carcontroller/safety alignment is being tuned; will tighten later. + bool steer_req = (((msg->data[1] >> 4) & 0x0FU) == 4U); + + // Absolute curvature cap if ((desired_curvature_raw > VOLKSWAGEN_MEB_MAX_CURVATURE_CAN) || (desired_curvature_raw < -VOLKSWAGEN_MEB_MAX_CURVATURE_CAN)) { tx = false; } + + // ISO lateral acceleration envelope (only while actively steering) + if (controls_allowed && steer_req) { + const float fudged_speed = (vehicle_speed.min / VEHICLE_SPEED_FACTOR) - 1.0f; + const float v_clamped = (fudged_speed < 1.0f) ? 1.0f : fudged_speed; + const float max_lat_accel = ISO_LATERAL_ACCEL + (EARTH_G * AVERAGE_ROAD_ROLL); + const float max_curvature = max_lat_accel / (v_clamped * v_clamped); + const int max_curvature_can = (int)((max_curvature * VOLKSWAGEN_MEB_CURVATURE_TO_CAN) + 1.0f); + if ((desired_curvature_raw > max_curvature_can) || + (desired_curvature_raw < -max_curvature_can)) { + tx = false; + } + } } if ((msg->addr == MSG_GRA_ACC_01) && !controls_allowed) { From 156b4277af0e8d10014e6e5b5145d2ddf96227ab Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 13:14:36 -0700 Subject: [PATCH 65/69] raise steer driver allowance to 100 --- opendbc/car/volkswagen/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 29e93dab8fc..4a47352f792 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -120,7 +120,7 @@ def __init__(self, CP): elif CP.flags & VolkswagenFlags.MEB: self.LDW_STEP = 10 # LDW_02 message frequency 10Hz self.ACC_HUD_STEP = 6 - self.STEER_DRIVER_ALLOWANCE = 60 # Driver torque 0.6 Nm, begin steering reduction from MAX + self.STEER_DRIVER_ALLOWANCE = 100 # Driver torque 1.0 Nm, begin steering reduction from MAX self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage From cc721b81553fa06f242551ad1e249d2f2b09e7ff Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 14:26:43 -0700 Subject: [PATCH 66/69] steer driver allowance = 80 --- opendbc/car/volkswagen/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 4a47352f792..032eeebe546 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -120,7 +120,7 @@ def __init__(self, CP): elif CP.flags & VolkswagenFlags.MEB: self.LDW_STEP = 10 # LDW_02 message frequency 10Hz self.ACC_HUD_STEP = 6 - self.STEER_DRIVER_ALLOWANCE = 100 # Driver torque 1.0 Nm, begin steering reduction from MAX + self.STEER_DRIVER_ALLOWANCE = 80 # Driver torque 0.8 Nm, begin steering reduction from MAX self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage From 2635c6d0ecfc40816cc89c1fd61d1052f6d0fc50 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 13 May 2026 14:29:33 -0700 Subject: [PATCH 67/69] Revert "steer driver allowance = 80" This reverts commit cc721b81553fa06f242551ad1e249d2f2b09e7ff. --- opendbc/car/volkswagen/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py index 032eeebe546..4a47352f792 100644 --- a/opendbc/car/volkswagen/values.py +++ b/opendbc/car/volkswagen/values.py @@ -120,7 +120,7 @@ def __init__(self, CP): elif CP.flags & VolkswagenFlags.MEB: self.LDW_STEP = 10 # LDW_02 message frequency 10Hz self.ACC_HUD_STEP = 6 - self.STEER_DRIVER_ALLOWANCE = 80 # Driver torque 0.8 Nm, begin steering reduction from MAX + self.STEER_DRIVER_ALLOWANCE = 100 # Driver torque 1.0 Nm, begin steering reduction from MAX self.STEER_DRIVER_MAX = 300 # Driver torque 3.0 Nm, stop steering reduction at MIN self.STEERING_POWER_MAX = 50 # HCA_03 maximum steering power, percentage self.STEERING_POWER_MIN = 4 # HCA_03 minimum steering power, percentage From d84d732349d6c40ef9931cf48e9ee3898db312ff Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 14 May 2026 13:58:38 -0700 Subject: [PATCH 68/69] speed --- opendbc/car/volkswagen/carstate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 2e4a632733f..2a94d59d393 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -130,7 +130,8 @@ def update(self, can_parsers) -> structs.CarState: ret.standstill = ret.vEgoRaw == 0 ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation ret.cruiseState.nonAdaptive = acc_limiter_mode or speed_limiter_mode - if ret.cruiseState.speed > 90: + ret.cruiseState.speed = ext_cp.vl["MEB_ACC_01"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS + if ret.cruiseState.speed > 90: # 255 kph in m/s == no current setpoint ret.cruiseState.speed = 0 self.eps_stock_values = pt_cp.vl["LH_EPS_03"] From 253ad15be9f4610692ea7c3c2ab733d45ff139c6 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 14 May 2026 14:04:21 -0700 Subject: [PATCH 69/69] aeb prevention --- opendbc/car/volkswagen/carstate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py index 2a94d59d393..c71390fe78d 100644 --- a/opendbc/car/volkswagen/carstate.py +++ b/opendbc/car/volkswagen/carstate.py @@ -376,9 +376,9 @@ def parse_mlb_mqb_steering_state(self, ret, pt_cp, drive_mode=True): def update_hca_state(self, hca_status, drive_mode=True): # Treat FAULT as temporary for worst likely EPS recovery time, for cars without factory Lane Assist # DISABLED means the EPS hasn't been configured to support Lane Assist - self.eps_init_complete = self.eps_init_complete or hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600 - perm_fault = self.eps_init_complete and hca_status == "FAULT" - temp_fault = (drive_mode and hca_status == "REJECTED") or not self.eps_init_complete + self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600) + perm_fault = drive_mode and hca_status == "DISABLED" or (self.eps_init_complete and hca_status == "FAULT") + temp_fault = drive_mode and hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete return temp_fault, perm_fault def update_acc_fault(self, acc_fault, parking_brake=False, drive_mode=True, recovery_frames_max=100):