GM Gateway Steering

This commit is contained in:
firestar5683
2026-10-05 04:26:32 -05:00
parent 2709f1c642
commit efa7781dfe
7 changed files with 247 additions and 4 deletions
+12
View File
@@ -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):
@@ -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:
@@ -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)
@@ -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())
@@ -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;
+161 -3
View File
@@ -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
+1
View File
@@ -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: