From efa7781dfecbd8549f7f454834868529113fe6d9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:26:32 -0500 Subject: [PATCH] GM Gateway Steering --- opendbc_repo/opendbc/car/gm/aol.py | 12 ++ opendbc_repo/opendbc/safety/modes/gm_aol.h | 1 + .../opendbc/safety/tests/test_gm_aol.py | 46 +++++ .../safety/tests/test_gm_auto_hold_sources.py | 2 +- .../pandad/tests/test_aol_protocol.cc | 25 +++ openpilot/starpilot/aol/tests/test_gm.py | 164 +++++++++++++++++- openpilot/starpilot/car/gm/aol_policy.h | 1 + 7 files changed, 247 insertions(+), 4 deletions(-) diff --git a/opendbc_repo/opendbc/car/gm/aol.py b/opendbc_repo/opendbc/car/gm/aol.py index cc3a726364..695f87c322 100644 --- a/opendbc_repo/opendbc/car/gm/aol.py +++ b/opendbc_repo/opendbc/car/gm/aol.py @@ -12,7 +12,10 @@ from opendbc.car.gm.values import (control_flags, from opendbc.car.gm.lateral import lane_centering_supported GM_AOL_ALTERNATIVE_EXPERIENCE = 32 +GM_BASE_GATEWAY_IDS = frozenset((CAR.GMC_ACADIA, CAR.BUICK_LACROSSE, CAR.CADILLAC_ESCALADE, + CAR.CADILLAC_ESCALADE_ESV, CAR.CADILLAC_ESCALADE_ESV_2019, CAR.CHEVROLET_SUBURBAN)) GM_AOL_WORDS = frozenset(( + 0, 0x80, 0x1001, 0x1401, 0x3001, 0x3401, 0x1003, 0x1403, 0x201, 0x601, 0xA01, 0xE01, 0x203, 0x603, 0xA03, 0xE03, 5, 7, 20, 0xBD, 0x9D, 0x19D, 0x1CD, @@ -44,6 +47,15 @@ def qualified_gm(cp) -> bool: tune = cp.lateralTuning.pid if not all(math.isfinite(value) for value in (*tune.kpBP, *tune.kpV, *tune.kiBP, *tune.kiV, tune.kf)): return False + if cp.carFingerprint in GM_BASE_GATEWAY_IDS: + return (cp.networkLocation == CarParams.NetworkLocation.gateway and + cp.transmissionType == CarParams.TransmissionType.automatic and + not cp.pcmCruise and not cp.alphaLongitudinalAvailable and + not cp.radarUnavailable and control_flags(cp) == 0 and + (int(cp.safetyConfigs[0].safetyParam) == 0 and + (cp.openpilotLongitudinalControl or cp.carFingerprint == CAR.BUICK_LACROSSE) or + int(cp.safetyConfigs[0].safetyParam) == 0x80 and + cp.carFingerprint == CAR.BUICK_LACROSSE and cp.openpilotLongitudinalControl)) if is_ordinary_camera_profile(cp, longitudinal=cp.openpilotLongitudinalControl): return True if is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp): diff --git a/opendbc_repo/opendbc/safety/modes/gm_aol.h b/opendbc_repo/opendbc/safety/modes/gm_aol.h index bee5196652..1e283ba47a 100644 --- a/opendbc_repo/opendbc/safety/modes/gm_aol.h +++ b/opendbc_repo/opendbc/safety/modes/gm_aol.h @@ -72,6 +72,7 @@ static void gm_aol_initialize(bool profile_supported) { static bool gm_aol_profile_word(uint16_t word) { bool supported = false; switch (word) { + case 0U: case 0x80U: case 0x201U: case 0x601U: case 0xA01U: case 0xE01U: case 0xC171U: case 0xC172U: case 5U: case 0xBDU: case 0x9DU: case 0x19DU: case 0x1CDU: diff --git a/opendbc_repo/opendbc/safety/tests/test_gm_aol.py b/opendbc_repo/opendbc/safety/tests/test_gm_aol.py index d0ba6560dd..8a31d6e857 100644 --- a/opendbc_repo/opendbc/safety/tests/test_gm_aol.py +++ b/opendbc_repo/opendbc/safety/tests/test_gm_aol.py @@ -118,3 +118,49 @@ class TestGmAol(unittest.TestCase): self.assertFalse(fixture.tx(button=3)) fixture.safety.set_controls_allowed(False) self.assertFalse(fixture.tx(button=3)) + + def test_base_gateway_typed_axes_and_ordinary_controls(self): + from opendbc.safety.tests.test_gm_volt_auto_hold import TestVoltAutoHold + for word in (0, 0x80): + for alternative in (0, 32): + with self.subTest(word=word, alternative=alternative): + f = TestVoltAutoHold() + f.init(word=word, alternative=alternative) + f.observations(regen_source=False) + f.safety.set_aol_test_heartbeat(True) + f.safety.aol_set_host_request(3) + self.assertFalse(f.safety.get_controls_allowed()) + self.assertEqual(f.safety.aol_get_permission_mask(), 1 if alternative == 32 else 0) + steering = gmcan.create_steering_control(f.packer, 0, 1, 0, True) + self.assertEqual(f.safety.safety_tx_hook(self.packet(steering)), alternative == 32) + self.assertFalse(f.tx(80)) + f.rx(0x1E1, [0, 0, 0, 0, 0, 0x20, 0]) + self.assertTrue(f.safety.get_controls_allowed()) + if alternative == 32: + self.assertEqual(f.safety.aol_get_permission_mask(), 3) + self.assertTrue(f.safety.safety_tx_hook(self.packet(steering))) + self.assertTrue(f.tx(400, 0xA)) + self.assertFalse(f.tx(401, 0xA)) + f.observations(brake=True, regen_source=False) + self.assertFalse(f.safety.get_controls_allowed()) + if alternative == 32: + self.assertEqual(f.safety.aol_get_permission_mask(), 1) + f.observations(main=False, regen_source=False) + self.assertFalse(f.safety.safety_tx_hook(self.packet(steering))) + + def test_base_gateway_main_health_and_unknown_neighbors(self): + from opendbc.safety.tests.test_gm_volt_auto_hold import TestVoltAutoHold + for word in (0, 0x80): + f = TestVoltAutoHold() + f.init(word=word, alternative=32) + f.observations(regen_source=False) + f.safety.set_aol_test_heartbeat(True) + self.assertEqual(self.request(1), 1) + f.safety.set_timer(f.time + 300001) + self.assertEqual(self.request(1), 0) + f.observations(main=False, regen_source=False) + self.assertEqual(self.request(1), 0) + for word in (0x40, 0x81, 0x82, 0x84, 0x180): + self.reset(word) + self.feed() + self.assertEqual(self.request(3), 0) diff --git a/opendbc_repo/opendbc/safety/tests/test_gm_auto_hold_sources.py b/opendbc_repo/opendbc/safety/tests/test_gm_auto_hold_sources.py index 5a9f6c8ff2..45266312c8 100644 --- a/opendbc_repo/opendbc/safety/tests/test_gm_auto_hold_sources.py +++ b/opendbc_repo/opendbc/safety/tests/test_gm_auto_hold_sources.py @@ -142,7 +142,7 @@ class TestGmAutoHoldSources(unittest.TestCase): f.safety.set_aol_test_heartbeat(True) f.safety.aol_set_host_request(1) steering = gmcan.create_steering_control(f.packer, 0, 1, 0, True) - self.assertEqual(f.safety.safety_tx_hook(libsafety_py.make_CANPacket(steering[0], steering[2], steering[1])), alternative == 32 and word != 0x80) + self.assertEqual(f.safety.safety_tx_hook(libsafety_py.make_CANPacket(steering[0], steering[2], steering[1])), alternative == 32) f.observations(speed=100, regen_source=word != 0x80) f.rx(0x1E1, [0, 0, 0, 0, 0, 0x20, 0]) self.assertTrue(f.safety.get_controls_allowed()) diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index 4ae285e1ec..b0400344c8 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -180,6 +180,31 @@ int main() { forte.safety_mode = HYUNDAI_AOL_PROFILE.mode; assert(!aol_capable(forte, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY)); } + for (const uint16_t word : {0U, 0x80U}) { + AolAxisNegotiator negotiator(VEHICLE_REGISTRY); + FakeTransport transport; + auto gateway = status(); + gateway.safety_mode = GM_AOL_PROFILE.mode; + gateway.safety_param = word; + assert(gm_aol_param(word)); + queue(transport, gateway, gateway); + const auto first = run(negotiator, transport, axis("gateway"), NOW, GM_AOL_PROFILE.mode); + assert(first.plan.capable && first.plan.request_mask == 0U && first.outcome.compatible); + auto active = gateway; + active.request_mask = 3U; + active.permission_mask = 1U; + queue(transport, gateway, active); + const auto second = run(negotiator, transport, axis("gateway", true, true), NOW, GM_AOL_PROFILE.mode); + assert(second.plan.capable && second.plan.request_mask == 3U && second.outcome.compatible); + assert(second.outcome.status && second.outcome.status->permission_mask == 1U); + } + for (const uint16_t word : {0x40U, 0x81U, 0x82U, 0x84U, 0x180U}) { + assert(!gm_aol_param(word)); + auto unknown = status(); + unknown.safety_mode = GM_AOL_PROFILE.mode; + unknown.safety_param = word; + assert(!aol_capable(unknown, GM_AOL_PROFILE.mode, VEHICLE_REGISTRY)); + } for (uint32_t word = 0U; word <= 65535U; word++) { auto gm = status(); gm.safety_mode = GM_AOL_PROFILE.mode; diff --git a/openpilot/starpilot/aol/tests/test_gm.py b/openpilot/starpilot/aol/tests/test_gm.py index 5337adb556..86c34dce0a 100644 --- a/openpilot/starpilot/aol/tests/test_gm.py +++ b/openpilot/starpilot/aol/tests/test_gm.py @@ -6,7 +6,8 @@ from unittest.mock import patch from opendbc.can import CANPacker from opendbc.car import Bus, structs -from opendbc.car.gm.aol import GM_AOL_WORDS, qualified_gm +from opendbc.car.gm.aol import GM_AOL_WORDS, GM_BASE_GATEWAY_IDS, qualified_gm +from opendbc.car.gm.lateral import lane_centering_supported from opendbc.car.gm.interface import CarInterface from opendbc.car.gm.tests.test_bolt_cc import params as bolt_params, feed as feed_car, control, native from opendbc.safety.tests.libsafety import libsafety_py @@ -33,7 +34,17 @@ BOLT_IDS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL) -def configurations(): +def _configurations(): + for identity in GM_BASE_GATEWAY_IDS: + cp = ordinary_params(identity, radar=True) + yield cp + if identity == CAR.BUICK_LACROSSE: + disabled = cp.as_reader().as_builder() + VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled) + yield disabled + held = cp.as_reader().as_builder() + VehicleStartupPreferences(gm_auto_hold=True).prepare(held) + yield held for identity in BOLT_IDS: for pedal in (False, True): for removed in (False, True): @@ -49,6 +60,7 @@ def configurations(): yield factory_params(alpha=alpha) yield camera_params(alpha=alpha) yield removed_params(alpha=alpha) + yield removed_params(alpha=alpha, alternate=True) for c9 in (False, True): yield sdgm_params(alpha=alpha, brake_c9=c9) for c9 in (False, True): @@ -87,6 +99,15 @@ def configurations(): yield silverado_pedal(removed=removed, disabled=disabled) +def configurations(): + for cp in _configurations(): + yield cp + selected = cp.as_reader().as_builder() + VehicleStartupPreferences(gm_auto_hold=True).prepare(selected) + if selected.safetyConfigs[0].safetyParam != cp.safetyConfigs[0].safetyParam: + yield selected + + class TestGmAol(unittest.TestCase): def test_actual_final_cp_registry_and_isolation(self): words = set() @@ -105,7 +126,29 @@ class TestGmAol(unittest.TestCase): denied = cp.as_reader().as_builder() denied.alternativeExperience = 33 self.assertFalse(qualified_gm(denied)) - self.assertEqual(words, GM_AOL_WORDS) + self.assertEqual(words, GM_AOL_WORDS, {"missing": GM_AOL_WORDS - words, "unexpected": words - GM_AOL_WORDS}) + + def test_base_gateway_aol_preserves_tuning_and_lane_capability(self): + for identity in GM_BASE_GATEWAY_IDS: + cp = ordinary_params(identity, radar=True) + with self.subTest(identity=identity): + self.assertTrue(qualified_gm(cp)) + self.assertFalse(lane_centering_supported(cp)) + selected = cp.as_reader().as_builder() + selected.alternativeExperience = 32 + self.assertTrue(qualified_gm(selected)) + self.assertFalse(lane_centering_supported(selected)) + for field, value in (('pcmCruise', True), ('radarUnavailable', True), ('alphaLongitudinalAvailable', True), + ('networkLocation', 'fwdCamera'), ('transmissionType', 'direct'), ('flags', 1)): + denied = cp.as_reader().as_builder() + setattr(denied, field, value) + self.assertFalse(qualified_gm(denied)) + denied = cp.as_reader().as_builder() + denied.safetyConfigs[0].safetyParam = 0x80 + self.assertEqual(qualified_gm(denied), identity == CAR.BUICK_LACROSSE) + denied = cp.as_reader().as_builder() + denied.openpilotLongitudinalControl = False + self.assertEqual(qualified_gm(denied), identity == CAR.BUICK_LACROSSE) @staticmethod def card(cp, settings): @@ -207,6 +250,121 @@ class TestGmAol(unittest.TestCase): self.assertFalse(command.longActive) + def test_base_gateway_actual_card_controls_parser_and_native_axes(self): + cases = [(identity, False, False) for identity in GM_BASE_GATEWAY_IDS] + cases += [(CAR.BUICK_LACROSSE, True, False), (CAR.BUICK_LACROSSE, False, True)] + safety = libsafety_py.libsafety + for identity, held, disabled in cases: + with self.subTest(identity=identity, held=held, disabled=disabled), OpenpilotPrefix(), \ + patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}): + settings = Params() + settings.put_bool('GMAutoHold', held, block=True) + settings.put_bool('DisableOpenpilotLongitudinal', disabled, block=True) + factory = ordinary_params(identity, radar=True) + default = self.card(factory, settings) + self.assertIsNone(default.aol_card_intent) + self.assertEqual(default.CP.alternativeExperience, 0) + settings.put_bool('AlwaysOnLateral', True, block=True) + selected = self.card(factory, settings) + self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, default.CP.safetyConfigs[0].safetyParam) + self.assertEqual(selected.CP.lateralTuning.to_dict(), default.CP.lateralTuning.to_dict()) + self.assertEqual(selected.CP.alternativeExperience, 32) + self.assertEqual(selected.CP.openpilotLongitudinalControl, not disabled) + self.assertFalse(selected.CP.pcmCruise) + self.assertEqual(selected.CP.safetyConfigs[0].safetyParam, 0x80 if held else 0) + self.assertEqual(selected.CP.lateralTuning.to_dict(), factory.lateralTuning.to_dict()) + self.assertFalse(lane_centering_supported(selected.CP)) + controls = Controls() + self.assertEqual(controls.CP.lateralTuning.to_dict(), factory.lateralTuning.to_dict()) + self.assertEqual(controls.CP.lateralTuning.which(), factory.lateralTuning.which()) + ci = selected.CI + packer = CANPacker(DBC[identity][Bus.pt]) + safety.set_alternative_experience(32) + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, selected.CP.safetyConfigs[0].safetyParam), 0) + safety.init_tests() + seen_lateral = seen_longitudinal = False + for tick in range(150): + now = 1_000_000_000 + tick * 10_000_000 + ordinary = 41 <= tick < 60 and not disabled + main = not 20 <= tick < 30 + gas, brake, reverse = 70 <= tick < 80, 80 <= tick < 90, 90 <= tick < 100 + _, packets = feed_car(SimpleNamespace(update=lambda _: None), packer, now, counter=tick % 4, + active=ordinary, speed=20., camera=False) + packets = [packet for packet in packets if packet[0] not in (0xC9, 0x1C4, 0xBE, 0x1E1, 0x1F5, 0xBD)] + packets += [packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 2 if ordinary else 0, + 'AcceleratorPedal2': 30 if gas else 0}), + packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 20 if brake else 0}), + packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 2 if reverse else 4}), + packer.make_can_msg('ASCMSteeringButton', 0, {'ACCButtons': 3 if tick == 40 else 6 if tick == 60 else 1, + 'RollingCounter': tick % 4})] + if not 100 <= tick < 140: + packets.append(packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)})) + ci.update([(now - 1_000_000, packets)]) + cs = ci.update([(now, packets)]) + for packet in packets: + native('rx', packet, now // 1000) + safety.safety_tick() + safety.set_aol_test_heartbeat(True) + selected.aol_card_intent.update(cs, now_ns=now, standard_enabled=ordinary) + intent = IntentState('card', tick + 1, now, now, now + 30_000_000, + selected.aol_card_intent.allowed_latch, False, False, True, True) + wanted = decide_axes(standard_lateral=ordinary, standard_longitudinal=ordinary, intent=intent, native=None, + car_state=cs, initialized=True, model_ready=True, no_entry=False, + immediate_disable=False, dm_lockout=False, pause_brake_mps=25.) + safety.aol_set_host_request(int(wanted.desired_lateral) | (int(wanted.desired_longitudinal) << 1)) + safety.set_timer(now // 1000 + 1) + mask = safety.aol_get_permission_mask() + receipt_state = SafetyState(1, True, now, now + 200_000_000, int(structs.CarParams.SafetyModel.gm), + selected.CP.safetyConfigs[0].safetyParam, + bool(mask & 1), bool(mask & 2), + wanted.desired_lateral, wanted.desired_longitudinal, 'panda', 'gm-gateway-test') + decision = decide_axes(standard_lateral=ordinary, standard_longitudinal=ordinary, intent=intent, native=receipt_state, + car_state=cs, initialized=True, model_ready=True, no_entry=False, + immediate_disable=False, dm_lockout=False, pause_brake_mps=25.) + feed(controls, now, tick, active=ordinary, enabled=ordinary) + axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) + axis.aolAxisState.qualified = True + axis.aolAxisState.nativeAcknowledged = True + axis.aolAxisState.desiredLateral = decision.desired_lateral + axis.aolAxisState.desiredLongitudinal = decision.desired_longitudinal + axis.aolAxisState.lateralActive = decision.lateral_active + axis.aolAxisState.longitudinalActive = decision.longitudinal_active + axis.aolAxisState.sessionId = 'gm-gateway-test' + axis.aolAxisState.observedMonoTime = now + axis.aolAxisState.validUntilMonoTime = now + 30_000_000 + receipt = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) + from dataclasses import replace + transport_receipt = receipt_state + if tick == 11: + transport_receipt = replace(receipt_state, safetyParam=5) + elif tick == 12: + transport_receipt = replace(receipt_state, axisSessionId='wrong-session') + elif tick == 13: + transport_receipt = replace(receipt_state, observedMonoTime=now + 1) + elif tick == 14: + transport_receipt = replace(receipt_state, validUntilMonoTime=now - 1) + receipt.aolSafetyWire = encode_safety(transport_receipt) + state = messaging.new_message('carState', valid=True, logMonoTime=now) + state.carState = cs + controls.sm.update_msgs((now + 1_000) / 1e9, [axis.as_reader(), receipt.as_reader(), state.as_reader()]) + command, _ = controls.state_control() + self.assertEqual(command.latActive, decision.lateral_active and tick not in (11, 12, 13, 14)) + self.assertEqual(command.longActive, ordinary) + self.assertEqual(bool(safety.get_controls_allowed()), 41 <= tick < 60) + if not main or brake or reverse or 131 <= tick < 140: + self.assertFalse(command.latActive) + if tick == 10: + self.assertTrue(command.latActive) + command.actuators.torque = .02 if command.latActive else 0. + _, messages = ci.apply(command.as_reader(), now + 2) + for message in messages: + self.assertTrue(native('tx', message, now // 1000 + 1), (tick, message)) + seen_lateral |= command.latActive and not command.longActive + seen_longitudinal |= command.longActive + self.assertTrue(seen_lateral) + self.assertEqual(seen_longitudinal, not disabled) + safety.set_alternative_experience(0) + def test_actual_card_forwards_shared_disarming_events(self): class IntentUpdated(Exception): pass diff --git a/openpilot/starpilot/car/gm/aol_policy.h b/openpilot/starpilot/car/gm/aol_policy.h index 3259f420cd..fc42cb3d34 100644 --- a/openpilot/starpilot/car/gm/aol_policy.h +++ b/openpilot/starpilot/car/gm/aol_policy.h @@ -4,6 +4,7 @@ inline bool gm_aol_param(uint16_t param) { switch (param) { + case 0U: case 0x80U: case 0x201U: case 0x601U: case 0xA01U: case 0xE01U: case 0x203U: case 0x603U: case 0xA03U: case 0xE03U: case 0xC172U: case 0xC173U: case 0xC171U: case 5U: case 7U: case 20U: