diff --git a/common/params.cc b/common/params.cc index 283cf2a0c..0430c5c3d 100644 --- a/common/params.cc +++ b/common/params.cc @@ -478,6 +478,7 @@ std::unordered_map keys = { {"RoadName", CLEAR_ON_MANAGER_START}, {"RoadNameUI", PERSISTENT}, {"RedPanda", PERSISTENT}, + {"RemapCancelToDistance", PERSISTENT}, {"RemoteStartBootsComma", PERSISTENT}, {"RotatingWheel", PERSISTENT}, {"ScreenBrightness", PERSISTENT}, diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index ac7da01f0..8f552ddc5 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -239,6 +239,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [ ("FullMap", "0", 2, "0"), ("GasRegenCmd", "1", 2, "0"), ("GMPedalLongitudinal", "1", 2, "1"), + ("RemapCancelToDistance", "0", 2, "0"), ("RedPanda", "0", 3, "0"), ("RemoteStartBootsComma", "0", 3, "0"), ("GithubSshKeys", "", 0, ""), @@ -828,6 +829,9 @@ class FrogPilotVariables: toggle.red_panda = toggle.car_make == "gm" and (params.get_bool("RedPanda") if tuning_level >= level["RedPanda"] else default.get_bool("RedPanda")) toggle.remote_start_boots_comma = toggle.car_make == "gm" and (params.get_bool("RemoteStartBootsComma") if tuning_level >= level["RemoteStartBootsComma"] else default.get_bool("RemoteStartBootsComma")) + toggle.remap_cancel_to_distance = toggle.car_make == "gm" and toggle.openpilot_longitudinal and ( + params.get_bool("RemapCancelToDistance") if tuning_level >= level["RemapCancelToDistance"] else default.get_bool("RemapCancelToDistance") + ) toggle.force_fingerprint = (params.get_bool("ForceFingerprint") if tuning_level >= level["ForceFingerprint"] else default.get_bool("ForceFingerprint")) and toggle.car_model is not None diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.cc b/frogpilot/ui/qt/offroad/vehicle_settings.cc index 31551dd71..1b130769f 100644 --- a/frogpilot/ui/qt/offroad/vehicle_settings.cc +++ b/frogpilot/ui/qt/offroad/vehicle_settings.cc @@ -171,6 +171,7 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent) std::vector> vehicleToggles { {"GMToggles", tr("General Motors Settings"), tr("FrogPilot features for General Motors vehicles."), ""}, {"GMPedalLongitudinal", tr("Use Pedal for Longitudinal Control"), tr("Use the pedal interceptor for longitudinal control instead of camera ACC/Redneck when available."), ""}, + {"RemapCancelToDistance", tr("Remap cancel to distance"), tr("Treat CANCEL as distance-button input on supported pedal-long GM platforms. Toggle requires a reboot to take effect."), ""}, {"LongPitch", tr("Smooth Pedal Response on Hills"), tr("Smoothen acceleration and braking when driving downhill/uphill."), ""}, {"RedPanda", tr("Red Panda"), tr("Enable Red Panda behavior for GM (alternate safety config and bus numbering). Requires a reboot to take effect."), ""}, {"RemoteStartBootsComma", tr("Remote Start Boots Comma"), tr("Use GM C9 SystemPowerMode for ignition detection. Toggle requires a panda firmware update and a reboot to take effect."), ""}, @@ -287,7 +288,7 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent) static_cast(toggles["LockDoorsTimer"])->setWarning("Warning: openpilot can't detect if keys are still inside the car, so ensure you have a spare key to prevent accidental lockouts!"); - QSet rebootKeys = {"NewLongAPI", "TacoTuneHacks"}; + QSet rebootKeys = {"NewLongAPI", "RemapCancelToDistance", "TacoTuneHacks"}; for (const QString &key : rebootKeys) { QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { if (started) { @@ -430,7 +431,7 @@ void FrogPilotVehiclesPanel::updateToggles() { setVisible &= isHKGCanFd; } - else if (key == "GMPedalLongitudinal") { + else if (key == "GMPedalLongitudinal" || key == "RemapCancelToDistance") { setVisible &= hasPedal; } diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.h b/frogpilot/ui/qt/offroad/vehicle_settings.h index 9e8056fd0..8dab98474 100644 --- a/frogpilot/ui/qt/offroad/vehicle_settings.h +++ b/frogpilot/ui/qt/offroad/vehicle_settings.h @@ -36,9 +36,9 @@ private: std::map toggles; - QSet gmKeys = {"GMPedalLongitudinal", "LongPitch", "RedPanda", "RemoteStartBootsComma", "VoltSNG"}; + QSet gmKeys = {"GMPedalLongitudinal", "LongPitch", "RedPanda", "RemapCancelToDistance", "RemoteStartBootsComma", "VoltSNG"}; QSet hkgKeys = {"NewLongAPI", "TacoTuneHacks"}; - QSet longitudinalKeys = {"FrogsGoMoosTweak", "LongPitch", "NewLongAPI", "SNGHack", "VoltSNG"}; + QSet longitudinalKeys = {"FrogsGoMoosTweak", "LongPitch", "NewLongAPI", "RemapCancelToDistance", "SNGHack", "VoltSNG"}; QSet toyotaKeys = {"ClusterOffset", "FrogsGoMoosTweak", "LockDoorsTimer", "SNGHack", "ToyotaDoors"}; QSet vehicleInfoKeys = {"BlindSpotSupport", "HardwareDetected", "OpenpilotLongitudinal", "PedalSupport", "RadarSupport", "SASCMSupport", "SDSUSupport", "SNGSupport"}; diff --git a/panda/board/safety/safety_gm.h b/panda/board/safety/safety_gm.h index eff88ba6f..91c9437f5 100644 --- a/panda/board/safety/safety_gm.h +++ b/panda/board/safety/safety_gm.h @@ -263,9 +263,13 @@ static void gm_rx_hook(const CANPacket_t *to_push) { // ACC steering wheel buttons (GM_CAM is tied to the PCM) if ((addr == 0x1E1) && (!gm_pcm_cruise || gm_cc_long)) { int button = (GET_BYTE(to_push, 5) & 0x70U) >> 4; + bool remap_cancel_to_distance = (alternative_experience & ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE) != 0; // Malibu Hybrid pedal-long observed format uses byte3 bit0 set (typically 0x01/0x41) on ASCMSteeringButton. // Don't treat that physical CANCEL frame as OP disengage. - bool malibu_cancel_passthrough = gm_bolt_2022_pedal && gm_pedal_long && ((GET_BYTE(to_push, 3) & 0x1U) != 0U); + bool malibu_cancel_passthrough = remap_cancel_to_distance && gm_bolt_2022_pedal && gm_pedal_long && ((GET_BYTE(to_push, 3) & 0x1U) != 0U); + // Gen1 Bolt pedal-long behavior: let wheel CANCEL act as an in-drive aux button (personality cycling), + // so safety should not force a controls drop on this RX cancel edge. + bool bolt_cancel_passthrough = remap_cancel_to_distance && gm_pedal_long && !gm_has_acc && !gm_bolt_2022_pedal; // enter controls on falling edge of set or rising edge of resume (avoids fault) bool set = (button != GM_BTN_SET) && (cruise_button_prev == GM_BTN_SET); @@ -275,7 +279,7 @@ static void gm_rx_hook(const CANPacket_t *to_push) { } // exit controls on cancel press - if ((button == GM_BTN_CANCEL) && !malibu_cancel_passthrough) { + if ((button == GM_BTN_CANCEL) && !(malibu_cancel_passthrough || bolt_cancel_passthrough)) { controls_allowed = false; } diff --git a/panda/board/safety_declarations.h b/panda/board/safety_declarations.h index 09671e96f..b722cf0cb 100644 --- a/panda/board/safety_declarations.h +++ b/panda/board/safety_declarations.h @@ -275,3 +275,6 @@ const uint32_t RELAY_TRNS_TIMEOUT = 1U; // Always on Lateral #define ALT_EXP_ALWAYS_ON_LATERAL 32 + +// GM pedal-long option: interpret wheel CANCEL as an auxiliary distance-style button. +#define ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE 64 diff --git a/panda/python/__init__.py b/panda/python/__init__.py index cc224baf5..3db29d6ce 100644 --- a/panda/python/__init__.py +++ b/panda/python/__init__.py @@ -112,6 +112,7 @@ class ALTERNATIVE_EXPERIENCE: RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX = 8 ALLOW_AEB = 16 ALWAYS_ON_LATERAL = 32 + GM_REMAP_CANCEL_TO_DISTANCE = 64 class Panda: diff --git a/panda/tests/safety/test_gm.py b/panda/tests/safety/test_gm.py index ab08ce2fa..d64592c77 100644 --- a/panda/tests/safety/test_gm.py +++ b/panda/tests/safety/test_gm.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import unittest -from panda import Panda +from panda import ALTERNATIVE_EXPERIENCE, Panda from panda.tests.libpanda import libpanda_py import panda.tests.safety.common as common from panda.tests.safety.common import CANPackerPanda @@ -344,6 +344,13 @@ class TestGmInterceptorSafety(common.GasInterceptorSafetyTest, TestGmCameraSafet self._rx(_acc_pcm_msg(False)) self.assertEqual(enable, self.safety.get_controls_allowed()) + def test_cancel_button_passthrough(self): + # Gen1 Bolt-style pedal-long path: wheel cancel shouldn't force controls off. + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.GM_REMAP_CANCEL_TO_DISTANCE) + self.safety.set_controls_allowed(1) + self._rx(self._button_msg(Buttons.CANCEL)) + self.assertTrue(self.safety.get_controls_allowed()) + def test_buttons(self): self._rx(self._interceptor_user_gas(0)) # Only CANCEL button is allowed while cruise is enabled diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index bf43487da..f25a1d580 100644 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -56,8 +56,9 @@ class Car: # set alternative experiences from parameters self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") - self.CP.alternativeExperience = 0 - if not self.disengage_on_accelerator: + if self.disengage_on_accelerator: + self.CP.alternativeExperience &= ~ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS + else: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 4cfed386f..9b9e18ca2 100644 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -2,7 +2,7 @@ from cereal import car, custom from math import fabs, exp import numpy as np -from panda import Panda +from panda import ALTERNATIVE_EXPERIENCE, Panda from openpilot.common.conversions import Conversions as CV from openpilot.common.numpy_fast import interp @@ -48,6 +48,13 @@ BOLT_PEDAL_LONG_CARS = { CAR.CHEVROLET_MALIBU_HYBRID_CC, } +# Cancel-to-personality mapping target: gen1 Bolt pedal-long paths only. +BOLT_GEN1_CANCEL_PERSONALITY_CARS = { + CAR.CHEVROLET_BOLT_CC_2017, + CAR.CHEVROLET_BOLT_CC_2019_2021, +} +CANCEL_REMAP_DISTANCE_CARS = BOLT_GEN1_CANCEL_PERSONALITY_CARS | {CAR.CHEVROLET_MALIBU_HYBRID_CC} + NON_LINEAR_TORQUE_PARAMS = { CAR.CHEVROLET_BOLT_ACC_2022_2023: { "left": [2.6531724862969748, 1.1, 0.1919764879840985, 0.0], @@ -602,29 +609,76 @@ class CarInterface(CarInterfaceBase): if use_panda_paddle_sched: gm_safety_cfg.safetyParam |= Panda.FLAG_GM_PANDA_PADDLE_SCHED + remap_cancel_to_distance = ( + getattr(frogpilot_toggles, "remap_cancel_to_distance", False) and + ret.openpilotLongitudinalControl and + bool(ret.flags & GMFlags.PEDAL_LONG.value) and + candidate in CANCEL_REMAP_DISTANCE_CARS + ) + if remap_cancel_to_distance: + ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.GM_REMAP_CANCEL_TO_DISTANCE + return ret # returns a car.CarState def _update(self, c, frogpilot_toggles): ret, fp_ret = self.CS.update(self.cp, self.cp_cam, self.cp_loopback, frogpilot_toggles) + remap_cancel_to_distance = bool(self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.GM_REMAP_CANCEL_TO_DISTANCE) + malibu_cancel_passthrough = ( + remap_cancel_to_distance and + self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC and + self.CP.openpilotLongitudinalControl + ) + bolt_cancel_personality = ( + remap_cancel_to_distance and + self.CP.carFingerprint in BOLT_GEN1_CANCEL_PERSONALITY_CARS and + self.CP.openpilotLongitudinalControl and + bool(self.CP.flags & GMFlags.PEDAL_LONG.value) + ) + # Don't add event if transitioning from INIT, unless it's to an actual button cruise_button_map = BUTTONS_DICT - if self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC and self.CP.openpilotLongitudinalControl: - # Malibu Hybrid only: keep all wheel-button functionality, but don't map CANCEL - # to ButtonType.cancel so OP long isn't disengaged by the physical cancel button. + if malibu_cancel_passthrough: + # Keep pedal-long cancel presses from creating a buttonCancel disengage event. cruise_button_map = {k: v for k, v in BUTTONS_DICT.items() if k != CruiseButtons.CANCEL} + elif bolt_cancel_personality: + # Keep pedal-long cancel presses from creating a buttonCancel disengage event. + cruise_button_map = {k: v for k, v in BUTTONS_DICT.items() if k != CruiseButtons.CANCEL} + + cruise_events = create_button_events(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, cruise_button_map, + unpressed_btn=CruiseButtons.UNPRESS) + cancel_gap_events = [] + if bolt_cancel_personality: + # Gen1 Bolt pedal-long: treat CANCEL as a distance-style button for personality cycling. + cancel_gap_events = create_button_events(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, + {CruiseButtons.CANCEL: ButtonType.gapAdjustCruise}, + unpressed_btn=CruiseButtons.UNPRESS) + + # Malibu pedal-long cancel can alias wheel-button bits on 0x1E1; ignore those side effects. + suppress_malibu_side_buttons = malibu_cancel_passthrough and ( + self.CS.cruise_buttons in (CruiseButtons.CANCEL, CruiseButtons.MAIN) or + self.CS.prev_cruise_buttons in (CruiseButtons.CANCEL, CruiseButtons.MAIN) + ) + distance_events = [] if suppress_malibu_side_buttons else create_button_events( + self.CS.distance_button, self.CS.prev_distance_button, {1: ButtonType.gapAdjustCruise} + ) + lkas_events = [] if suppress_malibu_side_buttons else create_button_events( + self.CS.lkas_enabled, self.CS.lkas_previously_enabled, {1: FrogPilotButtonType.lkas} + ) if self.CS.cruise_buttons != CruiseButtons.UNPRESS or self.CS.prev_cruise_buttons != CruiseButtons.INIT: ret.buttonEvents = [ - *create_button_events(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, cruise_button_map, - unpressed_btn=CruiseButtons.UNPRESS), - *create_button_events(self.CS.distance_button, self.CS.prev_distance_button, - {1: ButtonType.gapAdjustCruise}), - *create_button_events(self.CS.lkas_enabled, self.CS.lkas_previously_enabled, - {1: FrogPilotButtonType.lkas}), + *cruise_events, + *cancel_gap_events, + *distance_events, + *lkas_events, ] + if bolt_cancel_personality and self.CS.cruise_buttons == CruiseButtons.CANCEL: + # Feed long-press logic (traffic mode, etc.) as if distance button is being held. + fp_ret.distancePressed = True + # The ECM allows enabling on falling edge of set, but only rising edge of resume events = self.create_common_events(ret, extra_gears=[GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic], diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 5061b843c..34124d3e6 100644 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -9,6 +9,7 @@ from typing import SupportsFloat import cereal.messaging as messaging from cereal import car, custom, log +from panda import ALTERNATIVE_EXPERIENCE from msgq.visionipc import VisionIpcClient, VisionStreamType @@ -20,7 +21,7 @@ from openpilot.common.realtime import config_realtime_process, Priority, Ratekee from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.car.car_helpers import get_car_interface, get_startup_event -from openpilot.selfdrive.car.gm.values import CC_ONLY_CAR, GMFlags +from openpilot.selfdrive.car.gm.values import CAR, CC_ONLY_CAR, GMFlags from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, clip_curvature from openpilot.selfdrive.controls.lib.events import Events, ET @@ -64,6 +65,10 @@ CSID_MAP = {"1": EventName.roadCameraError, "2": EventName.wideRoadCameraError, ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys()) ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) ENABLED_STATES = (State.preEnabled, *ACTIVE_STATES) +BOLT_GEN1_CANCEL_PERSONALITY_CARS = { + CAR.CHEVROLET_BOLT_CC_2017, + CAR.CHEVROLET_BOLT_CC_2019_2021, +} class Controls: @@ -773,10 +778,19 @@ class Controls: # decrement personality on distance button press if self.CP.openpilotLongitudinalControl: distance_pressed = params_memory.get_bool("OnroadDistanceButtonPressed") + bolt_cancel_personality = ( + self.CP.carName == "gm" and + bool(self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.GM_REMAP_CANCEL_TO_DISTANCE) and + bool(self.CP.flags & GMFlags.PEDAL_LONG.value) and + self.CP.carFingerprint in BOLT_GEN1_CANCEL_PERSONALITY_CARS + ) if self.frogpilot_toggles.personality_profile_via_distance: distance_pressed |= any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents) distance_pressed &= not (self.sm['frogpilotCarState'].distanceLongPressed or self.sm['frogpilotCarState'].distanceVeryLongPressed) + elif bolt_cancel_personality: + distance_pressed |= any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents) + distance_pressed &= not (self.sm['frogpilotCarState'].distanceLongPressed or self.sm['frogpilotCarState'].distanceVeryLongPressed) if self.frogpilot_toggles.personality_profile_via_distance_long: distance_pressed |= self.sm['frogpilotCarState'].distanceLongPressed if self.frogpilot_toggles.personality_profile_via_distance_very_long: