GM Contracts

Define GM platform groups, startup preferences and CAN messages before their control families are registered.

Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
This commit is contained in:
firestar5683
2026-09-25 14:53:18 -05:00
parent 751d2369c5
commit 5dc6ea7068
6 changed files with 1452 additions and 60 deletions
+59 -6
View File
@@ -1,3 +1,5 @@
import math
from opendbc.car.can_definitions import CanData
from opendbc.car.gm.values import CAR
@@ -19,6 +21,39 @@ def create_buttons(packer, bus, idx, button):
return packer.make_can_msg("ASCMSteeringButton", bus, values)
def create_pedal_command(packer, gas_fraction, idx):
enabled = gas_fraction > 0.001
values = {"ENABLE": enabled, "COUNTER_PEDAL": idx & 0xF}
if enabled:
values["GAS_COMMAND"] = gas_fraction * 255.
values["GAS_COMMAND2"] = gas_fraction * 255.
packed = packer.make_can_msg("GAS_COMMAND", 0, values)
data = bytearray(packed[1])
data[5] = pedal_crc(data)
return CanData(0x200, bytes(data), 0)
def pedal_crc(data):
crc = 0xFF
for byte in reversed(data[:5]):
crc ^= byte
for _ in range(8):
crc = ((crc << 1) ^ 0xD5) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
return crc
def create_bolt_regen_paddle(packer, pressed):
return packer.make_can_msg("EBCMRegenPaddle", 0, {"RegenPaddle": 2 if pressed else 0})
def create_bolt_regen_gear(packer, pressed, gen2):
packed = packer.make_can_msg("ECMPRDNL2", 0, {
"PRNDL2": (5 if gen2 else 7) if pressed else 6,
"ManualMode": int(pressed), "TransmissionState": 1,
})
return CanData(0x1F5, b"\x0c\x0c" + packed[1][2:], 0)
def create_pscm_status(packer, bus, pscm_status):
values = {s: pscm_status[s] for s in [
"HandsOffSWDetectionMode",
@@ -70,17 +105,19 @@ def create_gas_regen_command(packer, bus, throttle, idx, enabled, at_full_stop):
return packer.make_can_msg("ASCMGasRegenCmd", bus, values)
def create_friction_brake_command(packer, bus, apply_brake, idx, enabled, near_stop, at_full_stop, CP):
def create_friction_brake_command(packer, bus, apply_brake, idx, enabled, near_stop, at_full_stop, CP, *, auto_hold=False):
mode = 0x1
# TODO: Understand this better. Volts and ICE Camera ACC cars are 0x1 when enabled with no brake
if enabled and CP.carFingerprint in (CAR.CHEVROLET_BOLT_EUV,):
if enabled and CP.carFingerprint in (CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL):
mode = 0x9
if apply_brake > 0:
mode = 0xa
if at_full_stop:
mode = 0xd
elif auto_hold and near_stop:
mode = 0xb
# TODO: this is to have GM bringing the car to complete stop,
# but currently it conflicts with OP controls, so turned off. Not set by all cars
@@ -100,20 +137,22 @@ def create_friction_brake_command(packer, bus, apply_brake, idx, enabled, near_s
return packer.make_can_msg("EBCMFrictionBrakeCmd", bus, values)
def create_acc_dashboard_command(packer, bus, enabled, target_speed_kph, hud_control, fcw):
def create_acc_dashboard_command(packer, bus, enabled, target_speed_kph, hud_control, fcw, cruise_state=None, *, fcw_alert=None, acc_always_one=1):
target_speed = min(target_speed_kph, 255)
values = {
"ACCAlwaysOne": 1,
"ACCAlwaysOne": acc_always_one,
"ACCResumeButton": 0,
"ACCSpeedSetpoint": target_speed,
"ACCGapLevel": hud_control.leadDistanceBars * enabled, # 3 "far", 0 "inactive"
"ACCCmdActive": enabled,
"ACCAlwaysOne2": 1,
"ACCAlwaysOne2": acc_always_one,
"ACCLeadCar": hud_control.leadVisible,
"FCWAlert": 0x3 if fcw else 0
"FCWAlert": (0x3 if fcw else 0) if fcw_alert is None else int(fcw_alert) & 0x3
}
if cruise_state is not None:
values["ACCCruiseState"] = cruise_state
return packer.make_can_msg("ASCMActiveCruiseControlStatus", bus, values)
@@ -169,3 +208,17 @@ def create_lka_icon_command(bus, active, critical, steer):
else:
dat = b"\x00\x00\x00"
return CanData(0x104c006c, dat, bus)
def create_acc_2cd_command(bus, idx):
return CanData(0x2CD, bytes([idx << 6, 0x2c, 0x03, 0xd3, 0xfd - idx]), bus)
def create_ecm_cruise_control_command(packer, bus, enabled, target_speed_kph):
dat = bytearray(8)
dat[0] = 1
raw = 0
if enabled and math.isfinite(target_speed_kph):
raw = max(0, min(int(round(max(0., target_speed_kph) / .0625)), 0xFFF))
dat[2], dat[3] = raw >> 8, raw & 255
return CanData(0x3D1, bytes(dat), bus)
@@ -0,0 +1,93 @@
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, ALT_ACCS, volt_cc_pedal_profile
"""Finalized GM speed-control choices; existing native envelopes are retained."""
from opendbc.car.gm.values import (is_malibu_cc_f1_profile, MALIBU_CC_F1_STOCK_WORD, is_volt_gateway_profile, is_volt_cc_profile,
is_ordinary_cc_profile, is_ordinary_camera_profile,
is_conventional_cc_pedal_profile, is_silverado_cc_pedal_profile, SILVERADO_CC_PEDAL_WORDS,
CONVENTIONAL_CC_PEDAL_STOCK_WORDS, GMFlags)
from opendbc.car.gm.values import (is_volt_ascm_longitudinal, is_volt_camera_longitudinal, is_volt_sdgm_profile,
is_volt_camera_removed, is_lacrosse_gateway_profile, GMSafetyFlags)
def disable_long_supported(cp) -> bool:
from opendbc.car.gm.values import malibu_hybrid_profile
if malibu_hybrid_profile(cp) is not None:
return True
return (volt_cc_pedal_profile(cp) is not None or camera_acc_pedal_profile(cp) is not None or is_conventional_cc_pedal_profile(cp) or
is_ordinary_cc_profile(cp) or is_ordinary_camera_profile(cp) or is_ordinary_camera_profile(cp, longitudinal=True) or
is_volt_gateway_profile(cp) or is_volt_cc_profile(cp) or
is_volt_ascm_longitudinal(cp) or is_volt_camera_longitudinal(cp) or is_volt_sdgm_profile(cp, longitudinal=True) or
is_volt_camera_removed(cp, longitudinal=True) or is_lacrosse_gateway_profile(cp))
def prepare_disable_longitudinal(cp, requested: bool) -> None:
from opendbc.car.gm.values import malibu_hybrid_profile
hybrid = malibu_hybrid_profile(cp)
if requested and hybrid is not None:
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
cp.autoResumeSng = False
cp.safetyConfigs[0].safetyParam = 0xE804 + int(hybrid.removed)
return
if requested and is_ordinary_camera_profile(cp, longitudinal=True):
cp.safetyConfigs[0].safetyParam = 0xC172 if cp.flags & GMFlags.NO_CAMERA else 0xC171
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
cp.minEnableSpeed = -1. if cp.carFingerprint in ALT_ACCS else 5. / 3.6
return
if requested and is_malibu_cc_f1_profile(cp):
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
cp.autoResumeSng = False
cp.safetyConfigs[0].safetyParam = MALIBU_CC_F1_STOCK_WORD
return
cc_profile = volt_cc_pedal_profile(cp)
if requested and cc_profile is not None:
cp.safetyConfigs[0].safetyParam = 0xE610 + 4 * int(cc_profile.radar) + 2 * int(cc_profile.removed) + int(cc_profile.brake_source.value == "F1")
cp.openpilotLongitudinalControl = False
cp.pcmCruise = False
cp.autoResumeSng = False
return
profile = camera_acc_pedal_profile(cp)
if requested and profile is not None:
stock_words = {0xE100: 0xE110, 0xE101: 0xE111, 0xE102: 0xE112, 0xE103: 0xE113}
if profile.volt:
base = {"camera": 0xE210, "gateway": 0xE310, "ascm": 0xE410, "sdgm": 0xE510}[profile.topology]
starts = (base - 0x10, base + 0x10, base + 0x30, base + 0x50)
stock_words.update({start + index: base + index for start in starts for index in range(4)})
cp.safetyConfigs[0].safetyParam = stock_words.get(int(cp.safetyConfigs[0].safetyParam), int(cp.safetyConfigs[0].safetyParam))
cp.openpilotLongitudinalControl = False
cp.pcmCruise = profile.topology == "camera"
cp.autoResumeSng = False
cp.minEnableSpeed = -1. if profile.volt or cp.carFingerprint in ALT_ACCS else 5. / 3.6
return
if requested and is_volt_one_pedal(cp):
cp.safetyConfigs[0].safetyParam = gm_control_word(cp)
if requested and is_volt_camera_removed(cp, longitudinal=True):
cp.safetyConfigs[0].safetyParam = 0xC150
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
return
if requested and (is_volt_ascm_longitudinal(cp) or is_volt_camera_longitudinal(cp) or is_volt_sdgm_profile(cp, longitudinal=True)):
# Withdraw into the corresponding existing camera stock owner before CI construction.
cp.safetyConfigs[0].safetyParam &= ~int(GMSafetyFlags.HW_CAM_LONG | GMSafetyFlags.VOLT_LONG | GMSafetyFlags.VOLT_AUTO_HOLD)
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
return
if requested and disable_long_supported(cp):
if is_lacrosse_gateway_profile(cp):
cp.safetyConfigs[0].safetyParam = 0
silverado = is_silverado_cc_pedal_profile(cp)
ordinary_pedal = is_conventional_cc_pedal_profile(cp) and not silverado
cp.openpilotLongitudinalControl = False
if silverado:
cp.safetyConfigs[0].safetyParam = SILVERADO_CC_PEDAL_WORDS[1][bool(cp.flags & GMFlags.NO_CAMERA)]
elif ordinary_pedal:
cp.safetyConfigs[0].safetyParam = CONVENTIONAL_CC_PEDAL_STOCK_WORDS[bool(cp.flags & GMFlags.NO_CAMERA)]
def gateway_sources_current(source_ns, now_ns: int) -> bool:
# The selected brake is supplied by the exact finalized gateway profile.
limits = (300_000_000, 300_000_000, 100_000_000, 100_000_000, 100_000_000, 100_000_000, 100_000_000)
return (isinstance(source_ns, tuple) and len(source_ns) == len(limits) and
all(source > 0 and 0 <= now_ns - source <= limit for source, limit in zip(source_ns, limits, strict=True)))
@@ -0,0 +1,79 @@
import ast
import hashlib
import json
from pathlib import Path
from types import SimpleNamespace
import unittest
ROOT = Path(__file__).resolve().parents[3]
BOLT = 'gm.CHEVROLET_BOLT_CC_2018_2021'
def fingerprints(path):
tree = ast.parse(path.read_text())
node = next((item.value for item in tree.body if isinstance(item, ast.Assign) and
any(isinstance(target, ast.Name) and target.id == 'FINGERPRINTS' for target in item.targets)), None)
if node is None:
return {}
uses = [item for item in ast.walk(tree) if isinstance(item, ast.Name) and item.id == "FINGERPRINTS"]
if len(uses) != 1 or not isinstance(node, ast.Dict):
raise ValueError("Fingerprint inventory requires evaluation of dynamic declarations")
return {f'{path.parent.name}.{key.attr}': ast.literal_eval(value) for key, value in zip(node.keys, node.values)}
def production_eliminator(inventory):
tree = ast.parse((ROOT / 'car/fingerprints.py').read_text())
tree.body = [node for node in tree.body if isinstance(node, ast.FunctionDef) and
node.name in ('is_valid_for_fingerprint', 'eliminate_incompatible_cars')]
scope = dict(_FINGERPRINTS=inventory, _DEBUG_ADDRESS={1880: 8})
exec(compile(tree, 'fingerprints.py', 'exec'), scope)
return scope['eliminate_incompatible_cars']
class TestBoltAutoFingerprint(unittest.TestCase):
def setUp(self):
self.inventory = {}
for path in (ROOT / 'car').glob('*/fingerprints.py'):
self.inventory.update(fingerprints(path))
self.eliminate = production_eliminator(self.inventory)
def candidates(self, messages):
candidates = list(self.inventory)
for address, length in messages:
candidates = self.eliminate(SimpleNamespace(address=address, dat=bytes(length)), candidates)
return candidates
def test_expected_bolt_tables(self):
variants = self.inventory[BOLT]
self.assertEqual(len(variants), 8)
digest = hashlib.sha256(json.dumps(variants, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
self.assertEqual(digest, 'e255d49d3b5322c990405ed8e8a9591087a2dc88452ca46fc38ea911518ffda9')
def test_all_variants_uniquely_identify_bolt(self):
self.assertGreaterEqual(len(self.inventory), 22)
for index, variant in enumerate(self.inventory[BOLT]):
with self.subTest(variant=index):
self.assertEqual(self.candidates(variant.items()), [BOLT])
def test_pedal_does_not_eliminate_any_bolt_variant(self):
for index, variant in enumerate(self.inventory[BOLT]):
with self.subTest(variant=index):
self.assertEqual(self.candidates([*variant.items(), (0x201, 6)]), [BOLT])
def test_incorrect_pedal_length_eliminates_bolt(self):
self.assertEqual(self.eliminate(SimpleNamespace(address=0x201, dat=bytes(7)), [BOLT]), [])
def test_existing_variants_do_not_gain_bolt_collision(self):
for car, variants in self.inventory.items():
if car == BOLT:
continue
for index, variant in enumerate(variants):
with self.subTest(car=car, variant=index):
baseline = [candidate for candidate in self.candidates(variant.items()) if candidate != BOLT]
if baseline == [car]:
self.assertEqual(self.candidates(variant.items()), [car])
if __name__ == '__main__':
unittest.main()
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
CM_ "IMPORT _community.dbc";
VERSION ""
+181
View File
@@ -0,0 +1,181 @@
#pragma once
static void gm_cruise_status_clear(void);
#define GM_ALT_EXP_ALWAYS_ON_LATERAL 32U
#define GM_AOL_MAIN_TIMEOUT_US 300000U
static bool gm_aol_enabled = false;
static bool gm_aol_stock_only = false;
static bool gm_aol_stock_gateway = false;
static bool gm_aol_forward_gear = false;
static bool gm_aol_gear_seen = false;
static uint32_t gm_aol_gear_us = 0U;
static bool gm_aol_main = false;
static bool gm_aol_main_seen = false;
static uint32_t gm_aol_main_us = 0U;
static void gm_aol_reset(void) {
gm_aol_enabled = false;
gm_aol_stock_only = false;
gm_aol_stock_gateway = false;
gm_aol_forward_gear = false;
gm_aol_gear_seen = false;
gm_aol_gear_us = 0U;
gm_aol_main = false;
gm_aol_main_seen = false;
gm_aol_main_us = 0U;
}
static uint8_t gm_aol_request_mask(void) {
return (gm_aol_enabled && heartbeat_engaged && !relay_malfunction && !safety_rx_checks_invalid &&
(safety_get_ts_elapsed(microsecond_timer_get(), aol_host_request_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) ?
aol_host_axis_mask : 0U;
}
static uint8_t gm_aol_permission_mask(void) {
uint8_t permission = 0U;
const bool main_current = gm_aol_main_seen && gm_aol_main &&
(safety_get_ts_elapsed(microsecond_timer_get(), gm_aol_main_us) <= GM_AOL_MAIN_TIMEOUT_US);
if (aol_rx_healthy()) {
const uint8_t request = gm_aol_request_mask();
const bool gear_current = !gm_aol_stock_gateway || (gm_aol_gear_seen && gm_aol_forward_gear &&
(safety_get_ts_elapsed(microsecond_timer_get(), gm_aol_gear_us) <= 1000000U));
if (main_current && gear_current) {
permission = request & 0x1U;
}
if (!gm_aol_stock_only && controls_allowed && ((request & 0x2U) != 0U)) {
permission |= 0x2U;
}
}
return permission;
}
static void gm_aol_rx_invalid(void) {
gm_cruise_status_clear();
gm_aol_main_seen = false;
gm_aol_gear_seen = false;
aol_set_host_request(0U);
}
static void gm_aol_observe(const CANPacket_t *msg) {
if (gm_aol_enabled && gm_aol_stock_gateway && msg_matches(msg, 0x1F5U, 0U) && (GET_LEN(msg) == 8U)) {
const uint8_t gear = msg->data[3] & 0xFU;
gm_aol_forward_gear = ((gear == 4U) || (gear == 6U)) && !GET_BIT(msg, 41U);
gm_aol_gear_seen = true;
gm_aol_gear_us = microsecond_timer_get();
if (!gm_aol_forward_gear) { aol_set_host_request(0U); }
}
if (gm_aol_enabled && (msg->addr == 0xC9U) && (msg->bus == 0U) && (GET_LEN(msg) == 8U)) {
gm_aol_main = GET_BIT(msg, 29U);
gm_aol_main_seen = true;
gm_aol_main_us = microsecond_timer_get();
if (!gm_aol_main) {
aol_set_host_request(0U);
}
}
}
// The caller supplies exact profile acceptance after its normal mode initialization.
static void gm_aol_initialize(bool profile_supported) {
gm_aol_reset();
gm_aol_enabled = profile_supported && ((unsigned int)alternative_experience == GM_ALT_EXP_ALWAYS_ON_LATERAL);
if (gm_aol_enabled) {
static const AolSafetyPolicy gm_aol_policy = {
.reset = gm_aol_reset,
.host_request = NULL,
.request_mask = gm_aol_request_mask,
.permission_mask = gm_aol_permission_mask,
.rx_invalid = gm_aol_rx_invalid,
};
aol_policy = &gm_aol_policy;
}
}
static bool gm_aol_profile_word(uint16_t word) {
bool supported = false;
switch (word) {
case 0xD100U: case 0xD101U: case 0xD110U: case 0xD111U:
case 0U: case 0x80U:
case 0x201U: case 0x601U: case 0xA01U: case 0xE01U:
case 0xE610U:
case 0xE611U:
case 0xE612U:
case 0xE613U:
case 0xE614U:
case 0xE615U:
case 0xE616U:
case 0xE617U:
case 0xE510U: case 0xE511U: case 0xE512U: case 0xE513U:
case 0xE410U: case 0xE411U: case 0xE412U: case 0xE413U:
case 0xE310U: case 0xE311U: case 0xE312U: case 0xE313U:
case 0xE210U: case 0xE211U: case 0xE212U: case 0xE213U:
case 0xE110U: case 0xE111U: case 0xE112U: case 0xE113U:
case 0xC171U: case 0xC172U: case 5U:
case 0xBDU: case 0x9DU: case 0x19DU: case 0x1CDU:
case 0xE710U: case 0xE711U: case 0xE712U: case 0xE713U:
case 0xE800U: case 0xE801U: case 0xE802U: case 0xE803U: case 0xE804U: case 0xE805U:
case 0xE700U: case 0xE701U: case 0xE702U: case 0xE703U:
case 0x205U: case 0x605U: case 0xA05U: case 0xE05U:
case 16U: case 0xC160U: case 0xC161U: case 0xC162U: case 0xC180U: case 0xC181U:
case 0xC182U: case 0xC183U: case 0xC184U: case 0xC185U: case 0xC186U: case 0xC187U:
case 0x1001U: case 0x1401U: case 0x3001U: case 0x3401U:
case 0x1005U: case 0x1405U:
case 0x4004U: case 0xC004U:
case 0x4084U: case 0xC084U:
case 0xC110U: case 0xC111U: case 0xC120U: case 0xC121U:
case 0xC130U: case 0xC131U: case 0xC140U: case 0xC141U:
case 0xC150U:
#ifdef ALLOW_DEBUG
case 0xE600U:
case 0xE601U:
case 0xE602U:
case 0xE603U:
case 0xE604U:
case 0xE605U:
case 0xE606U:
case 0xE607U:
case 0xE500U: case 0xE501U: case 0xE502U: case 0xE503U:
case 0xE520U: case 0xE521U: case 0xE522U: case 0xE523U:
case 0xE540U: case 0xE541U: case 0xE542U: case 0xE543U:
case 0xE560U: case 0xE561U: case 0xE562U: case 0xE563U:
case 0xE400U: case 0xE401U: case 0xE402U: case 0xE403U:
case 0xE420U: case 0xE421U: case 0xE422U: case 0xE423U:
case 0xE440U: case 0xE441U: case 0xE442U: case 0xE443U:
case 0xE460U: case 0xE461U: case 0xE462U: case 0xE463U:
case 0xE300U: case 0xE301U: case 0xE302U: case 0xE303U:
case 0xE320U: case 0xE321U: case 0xE322U: case 0xE323U:
case 0xE340U: case 0xE341U: case 0xE342U: case 0xE343U:
case 0xE360U: case 0xE361U: case 0xE362U: case 0xE363U:
case 0xE200U: case 0xE201U: case 0xE202U: case 0xE203U:
case 0xE220U: case 0xE221U: case 0xE222U: case 0xE223U:
case 0xE240U: case 0xE241U: case 0xE242U: case 0xE243U:
case 0xE260U: case 0xE261U: case 0xE262U: case 0xE263U:
case 0xE100U: case 0xE101U: case 0xE102U: case 0xE103U:
case 0xD102U: case 0xD103U: case 0xD104U: case 0xD105U:
case 0xD106U: case 0xD107U: case 0xD108U: case 0xD109U:
case 0xD10AU: case 0xD112U: case 0xD113U: case 0xD114U:
case 0xD115U: case 0xD116U: case 0xD117U: case 0xD118U:
case 0xD119U: case 0xD11AU:
case 0x1003U: case 0x1403U:
case 0x203U: case 0x603U: case 0xA03U: case 0xE03U:
case 0xC170U: case 0xC173U:
case 7U: case 20U:
case 0x4207U: case 0x4607U: case 0x4A07U: case 0x4E07U:
case 0x4007U:
case 0x4287U: case 0x4687U: case 0x4A87U: case 0x4E87U: case 0x4087U:
case 0x5087U: case 0x5487U:
case 0xC1D1U: case 0xC1D3U:
case 0x5007U: case 0x5407U: case 0xC151U:
#endif
supported = true;
break;
default:
break;
}
return supported;
}
static bool gm_aol_lateral_allowed(void) {
return gm_aol_enabled && ((gm_aol_permission_mask() & 0x1U) != 0U);
}