mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 09:03:46 +08:00
Bolt Pedal
Implement Bolt Pedal command generation and mode selection with dedicated native safety coverage. Keep pedal actuation separate from the factory cruise families.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Bolt pedal and conventional-cruise configurations
|
||||
|
||||
The 2017, 2018–21 and 2022–23 conventional-cruise Bolt configurations and the 2022–23 ACC-pedal configuration have separate owners. Normal CAN/firmware identification remains available. A supported live pedal sensor (`0x201`, bus 0) selects the interceptor branch; an identified Bolt EUV or 2022–23 ACC Bolt with that sensor is mapped to `CHEVROLET_BOLT_ACC_2022_2023_PEDAL`. The current interface does not require a saved `GMPedalLongitudinal` opt-in. `DisableOpenpilotLongitudinal` is a separate startup preference that defaults off; stock restoration depends on the actual selected topology and camera evidence.
|
||||
|
||||
The interceptor branch uses paired pedal tracks for acceleration and a regenerative paddle/PRNDL scheduler for braking. Its active pedal and scheduler commands require fresh stock L with ManualMode clear, healthy current sensor/source packets, active longitudinal controls and no driver gas, brake or paddle override. This restriction belongs to the interceptor/scheduler branch. The separate conventional-cruise controller, including the 2018–21 `0xC120`/`0xC121` profiles, accepts Drive or Low and controls speed through stock cruise buttons rather than pedal actuation.
|
||||
|
||||
The ACC-pedal branch additionally requires fresh confirmation that stock ACC is disengaged and retains stock cancellation. It can send qualified friction-brake commands on the powertrain bus, including low-speed braking and a bounded zero-brake unwind. The non-ACC interceptor branches use regenerative braking without that friction sender. None of these branches sends ASCM gas/regen commands. A fault, stale sensor, stock takeover or control release withdraws active pedal requests; scheduler release feeds remain conditional on actual L ownership. Driver paddle or gear changes discard pending active scheduler feeds.
|
||||
|
||||
Native safety independently checks the selected profile, pedal pairing/CRC/counter, source freshness, scheduler payload and ownership, and applicable numeric command bounds. Existing tests join generated DBC parsing, controller output and native safety. The separate gateway Volt Auto Hold feature does not change these Bolt branches or their regenerative stopping behavior.
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Mode-transition output shaping for admitted Bolt longitudinal owners."""
|
||||
|
||||
|
||||
class BoltModeTransition:
|
||||
def __init__(self):
|
||||
self.prev_mode = 'acc'
|
||||
self.current_mode = 'acc'
|
||||
self.timer = 0.0
|
||||
self.duration = 1.0
|
||||
self.transitioning = False
|
||||
|
||||
def update(self, experimental_mode, dt):
|
||||
# Lost mode telemetry cannot create a new transition. A current transition
|
||||
# still advances under the caller's active PID ownership.
|
||||
if type(experimental_mode) is bool:
|
||||
mode = 'blended' if experimental_mode else 'acc'
|
||||
if mode != self.current_mode:
|
||||
self.prev_mode = self.current_mode
|
||||
self.current_mode = mode
|
||||
self.transitioning = True
|
||||
self.timer = 0.0
|
||||
if self.transitioning:
|
||||
self.timer += dt
|
||||
if self.timer >= self.duration:
|
||||
self.transitioning = False
|
||||
|
||||
@property
|
||||
def leaving_experimental(self):
|
||||
return self.transitioning and self.prev_mode == 'blended' and self.current_mode == 'acc'
|
||||
|
||||
def shape(self, output, last_output):
|
||||
if self.leaving_experimental:
|
||||
if output > last_output:
|
||||
progress = min(1.0, self.timer / max(self.duration, 1e-3))
|
||||
output = last_output + (output - last_output) * progress
|
||||
elif self.transitioning and self.prev_mode == 'acc' and self.current_mode == 'blended':
|
||||
if output < 0.0 and output < last_output:
|
||||
progress = min(1.0, self.timer / self.duration)
|
||||
urgency = abs(output / -4.0)
|
||||
urgency_smooth = min(1.0, urgency ** 0.4)
|
||||
blend = 1.0 - (1.0 - progress) * (1.0 - urgency_smooth)
|
||||
output = last_output + (output - last_output) * blend
|
||||
return output
|
||||
|
||||
|
||||
def policy_for(cp):
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.gm.values import (CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_euv_longitudinal,
|
||||
is_ordinary_cc_profile, is_ordinary_camera_profile, is_conventional_cc_pedal_profile)
|
||||
from opendbc.car.gm.longitudinal import policy_for as pedal_policy_for
|
||||
|
||||
if not cp.openpilotLongitudinalControl:
|
||||
return None
|
||||
if (is_conventional_cc_pedal_profile(cp) or is_ordinary_camera_profile(cp, longitudinal=True) or
|
||||
is_ordinary_cc_profile(cp) or is_bolt_cc_profile(cp) or is_bolt_euv_longitudinal(cp)):
|
||||
return BoltModeTransition()
|
||||
from opendbc.car.gm.values import gm_control_word
|
||||
base = GMSafetyFlags.HW_CAM | GMSafetyFlags.EV | GMSafetyFlags.PEDAL_LONG | GMSafetyFlags.PADDLE_SCHED
|
||||
words = {
|
||||
CAR.CHEVROLET_BOLT_CC_2017: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017,
|
||||
CAR.CHEVROLET_BOLT_CC_2018_2021: base | GMSafetyFlags.NO_ACC,
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2,
|
||||
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL: base | GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2,
|
||||
}
|
||||
if (pedal_policy_for(cp) is not None and not cp.flags & GMFlags.CC_LONG.value and
|
||||
len(cp.safetyConfigs) == 1 and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
|
||||
gm_control_word(cp) == words.get(cp.carFingerprint)):
|
||||
return BoltModeTransition()
|
||||
return None
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Ordinary camera acceleration owner in normalized GM wire units."""
|
||||
from opendbc.car.gm.values import CAR, is_ordinary_camera_profile
|
||||
from opendbc.car.gm.longitudinal import GMOrdinaryLongitudinalPolicy, _GMDefaultStopPolicy, AscmStopEvidence
|
||||
from opendbc.car.gm.values import CAR, is_ordinary_camera_profile, camera_acc_pedal_profile, volt_cc_pedal_profile
|
||||
from opendbc.car.gm.longitudinal import GMOrdinaryLongitudinalPolicy, GMVoltLongitudinalPolicy, _GMDefaultStopPolicy, AscmStopEvidence
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car.gm.truck_longitudinal import GMTruckLongitudinalPolicy
|
||||
from opendbc.car.gm.conventional_pedal import GMConventionalPedalPolicy
|
||||
|
||||
|
||||
class CameraLongitudinalPolicy(GMOrdinaryLongitudinalPolicy):
|
||||
@@ -14,7 +17,25 @@ class TruckCameraLongitudinalPolicy(GMTruckLongitudinalPolicy):
|
||||
return _GMDefaultStopPolicy(.25, AscmStopEvidence)
|
||||
|
||||
|
||||
class CameraPedalLongitudinalPolicy(CameraLongitudinalPolicy):
|
||||
stopping_decel_rate = float(np.float32(.8))
|
||||
kp = ((0., 5., 15., 35.), tuple(float(np.float32(value)) for value in (.09, .08, .06, .045)))
|
||||
|
||||
def feedforward(self, target, speed, last_output):
|
||||
return target * float(np.float32(.25))
|
||||
|
||||
|
||||
def policy_for(cp):
|
||||
cc_profile = volt_cc_pedal_profile(cp)
|
||||
if cc_profile is not None and cc_profile.longitudinal:
|
||||
policy = GMConventionalPedalPolicy(False)
|
||||
policy.starting_speed = .75
|
||||
return policy
|
||||
profile = camera_acc_pedal_profile(cp)
|
||||
if profile is not None and profile.longitudinal:
|
||||
return (GMVoltLongitudinalPolicy(gateway=profile.topology == "gateway")
|
||||
if profile.topology in ("gateway", "ascm", "sdgm") else CameraPedalLongitudinalPolicy())
|
||||
|
||||
if not is_ordinary_camera_profile(cp, longitudinal=True):
|
||||
return None
|
||||
return TruckCameraLongitudinalPolicy() if cp.carFingerprint == CAR.CHEVROLET_SILVERADO else CameraLongitudinalPolicy()
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Acceleration tuning for conventional-cruise GM pedal configurations."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car.gm.values import CAR, is_conventional_cc_pedal_profile, is_silverado_cc_pedal_profile
|
||||
from opendbc.car.gm.longitudinal import GMPedalLongitudinalPolicy, _GMDefaultStopPolicy, _moving_stop_target_follow
|
||||
from opendbc.car.gm.cc_longitudinal import VoltCcEvidence
|
||||
|
||||
|
||||
def stop_start_speed(malibu: bool) -> float:
|
||||
"""Default stop/start speed for the conventional-cruise pedal policy."""
|
||||
return .75 if malibu else .5
|
||||
|
||||
|
||||
class GMConventionalPedalPolicy(GMPedalLongitudinalPolicy):
|
||||
def __init__(self, malibu: bool):
|
||||
super().__init__(friction_variant=False)
|
||||
# The reached original default reads this rate through Float32 CarParams storage.
|
||||
self.stopping_decel_rate = float(np.float32(.8))
|
||||
self.kp = (((0.0, 5.0, 35.0), (0.06, 0.05, 0.04)) if malibu else
|
||||
((0.0, 5.0, 15.0, 35.0), (0.09, 0.08, 0.06, 0.045)))
|
||||
self.kp = (self.kp[0], tuple(float(np.float32(value)) for value in self.kp[1]))
|
||||
self.starting_speed = stop_start_speed(malibu)
|
||||
self.feedforward_gain = float(np.float32(0.15 if malibu else 0.25))
|
||||
|
||||
def stop_policy(self):
|
||||
return _GMDefaultStopPolicy(self.starting_speed, VoltCcEvidence)
|
||||
|
||||
def stopping_output(self, output, target, should_stop, cs):
|
||||
return _moving_stop_target_follow(output, target, should_stop, cs, max(1.5, self.starting_speed + 1.))
|
||||
|
||||
def feedforward(self, target: float, speed: float, last_output: float) -> float:
|
||||
return target * self.feedforward_gain
|
||||
|
||||
|
||||
def policy_for(cp):
|
||||
if not is_conventional_cc_pedal_profile(cp) or not cp.openpilotLongitudinalControl:
|
||||
return None
|
||||
return GMConventionalPedalPolicy(cp.carFingerprint == CAR.CHEVROLET_MALIBU_CC)
|
||||
|
||||
|
||||
class CancelCredit:
|
||||
DRIVER_OVERRIDE_LENGTHS = {0xBD: 7, 0xC9: 8, 0x1C4: 8, 0x1F5: 8}
|
||||
|
||||
"""One cancellation per sequential, byte-valid neutral wheel-button packet."""
|
||||
|
||||
def __init__(self, *, neutral_interval_ns=300_000_000, allow_non_acc_neutral=False):
|
||||
self.neutral_interval_ns = neutral_interval_ns
|
||||
self.allow_non_acc_neutral = allow_non_acc_neutral
|
||||
self.counter = None
|
||||
self.packet_ns = self.source_ns = self.credit_ns = 0
|
||||
self.main_ns = self.stock_ns = 0
|
||||
self.gear_ns = self.gear_packet_ns = 0
|
||||
self.main = self.stock_active = False
|
||||
self.override_ns = {}
|
||||
|
||||
def observe(self, can_packets, *, clear_on_main_off=False, clear_on_driver_override=False, observe_gear=False):
|
||||
# Decoded signals omit reserved bits. Observe ordered physical packets so
|
||||
# the sender cannot grant credit for a button frame Panda would reject.
|
||||
for stamp, packets in can_packets:
|
||||
for address, raw, bus in packets:
|
||||
if bus != 0:
|
||||
continue
|
||||
if observe_gear and address == 0x1F5:
|
||||
if len(raw) == 8 and stamp > max(0, self.gear_packet_ns):
|
||||
self.gear_ns = stamp
|
||||
elif len(raw) != 8 or stamp != self.gear_ns or stamp <= 0:
|
||||
# Same-event valid observations retain a qualified clock, never refresh it.
|
||||
self.gear_ns = 0
|
||||
self.gear_packet_ns = max(self.gear_packet_ns, stamp)
|
||||
if not self.gear_ns:
|
||||
self.credit_ns = 0
|
||||
if clear_on_driver_override and stamp > self.override_ns.get(address, 0):
|
||||
if address in self.DRIVER_OVERRIDE_LENGTHS and len(raw) == self.DRIVER_OVERRIDE_LENGTHS[address]:
|
||||
self.override_ns[address] = stamp
|
||||
override = ((address == 0xBD and raw[0] >> 4 != 0) or
|
||||
(address == 0xC9 and raw[5] & 1 != 0) or
|
||||
(address == 0x1C4 and raw[5] != 0) or
|
||||
(address == 0x1F5 and (raw[3] & 15 not in (4, 6) or raw[5] & 2 != 0)))
|
||||
if override:
|
||||
self.credit_ns = 0
|
||||
if address == 0xC9:
|
||||
self.main_ns = stamp if len(raw) == 8 and stamp > 0 else 0
|
||||
self.main = bool(self.main_ns and raw[3] & 0x20)
|
||||
if clear_on_main_off and not self.main:
|
||||
self.credit_ns = 0
|
||||
elif address == 0x3D1:
|
||||
self.stock_ns = stamp if len(raw) == 8 and stamp > 0 else 0
|
||||
self.stock_active = bool(self.stock_ns and raw[4] & 0x80)
|
||||
if not self.stock_active:
|
||||
self.credit_ns = 0
|
||||
elif address == 0x1E1:
|
||||
self._observe_button(raw, stamp)
|
||||
|
||||
def _observe_button(self, raw, stamp):
|
||||
if len(raw) != 7:
|
||||
self.credit_ns = 0
|
||||
return
|
||||
counter = raw[4] & 3
|
||||
if stamp <= 0 or stamp < self.packet_ns:
|
||||
# Panda still observes the physical counter even if its host timestamp is unusable.
|
||||
self.counter = counter
|
||||
self.credit_ns = self.source_ns = 0
|
||||
return
|
||||
checksum = 0xFF + counter * 0x4EF
|
||||
neutral = raw == bytes((0, 0, 0, 1, counter, 0x10 | (checksum >> 8), checksum & 0xFF))
|
||||
if self.allow_non_acc_neutral:
|
||||
non_acc_checksum = 0xF0 + counter * 0x3F0
|
||||
neutral |= raw == bytes((0, 0, 0, 0, counter, 0x10 | (non_acc_checksum >> 8), non_acc_checksum & 0xFF))
|
||||
first = self.counter is None
|
||||
timely = self.source_ns > 0 and 0 <= stamp - self.source_ns <= self.neutral_interval_ns
|
||||
if neutral and (first or timely and counter == (self.counter + 1) % 4):
|
||||
self.credit_ns = stamp
|
||||
self.source_ns = stamp
|
||||
elif not neutral or not timely or counter != self.counter:
|
||||
self.credit_ns = 0
|
||||
if first or counter != self.counter:
|
||||
self.source_ns = stamp
|
||||
self.counter, self.packet_ns = counter, stamp
|
||||
|
||||
def available(self, now_ns, used_ns):
|
||||
return (self.credit_ns > used_ns and self.main and self.stock_active and
|
||||
all(stamp > 0 and 0 <= now_ns - stamp <= 300_000_000
|
||||
for stamp in (self.main_ns, self.stock_ns, self.source_ns)))
|
||||
|
||||
|
||||
def cancel_sources_current(cs, now_ns):
|
||||
"""Keep the parser's process-health checks alongside raw cancellation credit."""
|
||||
return cs.out.canValid and not cs.out.canTimeout
|
||||
|
||||
|
||||
def sources_current(cs, now_ns):
|
||||
if is_silverado_cc_pedal_profile(cs.CP):
|
||||
stamps = cs.silverado_pedal_sources
|
||||
limits = (300_000_000, 100_000_000, 300_000_000, 300_000_000,
|
||||
100_000_000, 100_000_000, 300_000_000, 100_000_000)
|
||||
return (len(stamps) == len(limits) and cs.out.canValid and not cs.out.canTimeout and
|
||||
all(stamp > 0 and 0 <= now_ns - stamp <= limit for stamp, limit in zip(stamps, limits, strict=True)))
|
||||
stamps = cs.conventional_pedal_sources
|
||||
limits = (300_000_000, 300_000_000, 300_000_000, 300_000_000,
|
||||
300_000_000, 100_000_000, 300_000_000, 100_000_000)
|
||||
if len(stamps) == 9:
|
||||
limits += (100_000_000,)
|
||||
return (len(stamps) == len(limits) and cs.out.canValid and not cs.out.canTimeout and
|
||||
all(stamp > 0 and 0 <= now_ns - stamp <= limit for stamp, limit in zip(stamps, limits, strict=True)))
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Conventional-cruise GM pedal command and Malibu physical limits."""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
|
||||
from opendbc.car.gm.values import CAR
|
||||
from opendbc.car.gm.conventional_pedal import stop_start_speed
|
||||
from opendbc.car.gm.silverado_cc import pedal_fraction, pedal_slew
|
||||
|
||||
# The no-paddle interpolation and slew are shared source arithmetic. Physical
|
||||
# gas/brake limits, pitch and launch thresholds remain bound to this identity.
|
||||
|
||||
class ConventionalPedalCommand:
|
||||
def __init__(self, cp):
|
||||
self.mass = cp.mass
|
||||
self.wheelbase = cp.wheelbase
|
||||
self.stop_accel = cp.stopAccel
|
||||
malibu = cp.carFingerprint == CAR.CHEVROLET_MALIBU_CC
|
||||
self.stop_speed = self.start_speed = stop_start_speed(malibu)
|
||||
self.max_gas = 8191 if malibu else 7168
|
||||
self.brake_bp = [-4., -.1] if malibu else [-4., 0.]
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.steady = 0.
|
||||
self.active_last = False
|
||||
self.recovering = False
|
||||
self.recovery_emitted = 0.
|
||||
|
||||
def prime_recovery(self):
|
||||
# Invalid input revokes actuation. Recovery resumes through the original slew from zero.
|
||||
self.steady = 0.
|
||||
self.active_last = True
|
||||
self.recovering = True
|
||||
self.recovery_emitted = 0.
|
||||
|
||||
def pause_recovery(self):
|
||||
if self.recovering:
|
||||
self.recovery_emitted = 0.
|
||||
|
||||
def update(self, accel, long_active, cs, *, stopping, resume, orientation=None):
|
||||
# The original outer caller bypasses calc on these branches and retains its memory.
|
||||
if not long_active:
|
||||
self.pause_recovery()
|
||||
return 0., -650., 0
|
||||
if cs.vEgo < .25 and stopping and not resume:
|
||||
self.pause_recovery()
|
||||
return 0., -650., int(min(-100 * self.stop_accel, 400))
|
||||
target = pedal_fraction(accel, cs.vEgo)
|
||||
self.steady = pedal_slew(target, self.steady, accel, cs.vEgo) if self.active_last else target
|
||||
self.active_last = True
|
||||
command = self.steady
|
||||
|
||||
pitch = 0.
|
||||
if orientation is not None and len(orientation) == 3 and cs.vEgo > self.stop_speed and math.isfinite(orientation[1]):
|
||||
pitch = math.sin(orientation[1]) * ACCELERATION_DUE_TO_GRAVITY
|
||||
pitch = 0. if pitch > 0. and accel > 0. else min(pitch, .20)
|
||||
radius = .075 * self.wheelbase + .1453
|
||||
drag = .5 * .30 * (1.05 * self.wheelbase + .0679) * 1.225 * cs.vEgo ** 2
|
||||
scaled = radius * (self.mass * float(np.clip(accel + pitch, -4., 2.)) + drag) + 6150
|
||||
gas = int(round(np.clip(scaled, 5500, self.max_gas)))
|
||||
brake = int(round(np.interp(min((scaled - 6150) / (radius * self.mass), 0.), self.brake_bp, [400., 0.])))
|
||||
if brake > 0 or stopping:
|
||||
gas = 5500
|
||||
if gas > 5500 and cs.cruiseState.standstill and (cs.standstill or cs.vEgo < max(self.start_speed, .3)):
|
||||
command = 18. / 255.
|
||||
gas, brake = 5500, 0
|
||||
if self.recovering:
|
||||
desired = command
|
||||
# Bound the final command, including SNG, after invalid input. Zero and decreases are immediate.
|
||||
command = min(desired, pedal_slew(desired, self.recovery_emitted, accel, cs.vEgo)) if desired > .001 else 0.
|
||||
self.recovery_emitted = command
|
||||
if command == desired:
|
||||
self.recovering = False
|
||||
return command, gas - 6150, brake
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,74 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.gm.gmcan import create_friction_brake_command
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_bolt_pedal import params
|
||||
from opendbc.car.gm.values import CAR, DBC, PEDAL_BOLT_CAR
|
||||
|
||||
|
||||
class TestBoltAccPedalParams(unittest.TestCase):
|
||||
def test_stop_target_and_delay_follow_final_control_owner(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
for alpha in (False, True):
|
||||
for setting in (False, True):
|
||||
for observed in (False, True):
|
||||
with self.subTest(candidate=candidate, alpha=alpha, setting=setting, observed=observed):
|
||||
cp = params(candidate, setting, observed, alpha_long=alpha)
|
||||
self.assertAlmostEqual(cp.stopAccel, -0.25)
|
||||
expected_delay = 0.6 if setting and observed else 1.0
|
||||
self.assertAlmostEqual(cp.longitudinalActuatorDelay, expected_delay)
|
||||
for candidate in (CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_BOLT_ACC_2022_2023):
|
||||
for alpha in (False, True):
|
||||
with self.subTest(candidate=candidate, alpha=alpha):
|
||||
cp = params(candidate, True, True, alpha_long=alpha)
|
||||
expected_stop = -0.25 if candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023 or alpha else -2.0
|
||||
self.assertAlmostEqual(cp.stopAccel, expected_stop)
|
||||
self.assertAlmostEqual(cp.longitudinalActuatorDelay, 0.5)
|
||||
|
||||
def test_final_stop_target_for_all_bolt_cc_camera_profiles(self):
|
||||
for candidate in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021,
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL):
|
||||
for alpha in (False, True):
|
||||
for camera in (False, True):
|
||||
for setting in (False, True):
|
||||
for observed in (False, True):
|
||||
with self.subTest(candidate=candidate, alpha=alpha, camera=camera, setting=setting, observed=observed):
|
||||
cp = params(candidate, setting, observed, alpha_long=alpha, camera=camera)
|
||||
self.assertEqual(cp.stopAccel, -0.25)
|
||||
self.assertAlmostEqual(cp.longitudinalActuatorDelay, 0.6 if setting and observed else 1.0)
|
||||
self.assertTrue(cp.openpilotLongitudinalControl)
|
||||
|
||||
def test_friction_braking_and_speed_shaped_launch_limits(self):
|
||||
for alpha in (False, True):
|
||||
cp = params(CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, True, True, alpha_long=alpha)
|
||||
for speed, maximum in ((0., .54), (.75, .64), (1.5, .74), (4., 1.03), (8., 1.46), (15., 2.), (40., 2.)):
|
||||
with self.subTest(alpha=alpha, speed=speed):
|
||||
minimum, actual_maximum = CarInterface.get_pid_accel_limits(cp, speed, 30.)
|
||||
self.assertEqual(minimum, -4.)
|
||||
self.assertAlmostEqual(actual_maximum, maximum)
|
||||
|
||||
def test_non_acc_pedal_keeps_regen_only_limits(self):
|
||||
cp = params(CAR.CHEVROLET_BOLT_CC_2022_2023, True, True)
|
||||
for speed, minimum, maximum in ((0., -.93, .54), (1.5, -1.28, .74), (4., -1.98, 1.03),
|
||||
(8., -2.58, 1.46), (15., -2.86, 2.), (30., -2.95, 2.)):
|
||||
self.assertEqual(CarInterface.get_pid_accel_limits(cp, speed, 30.), (minimum, maximum))
|
||||
|
||||
def test_friction_payload_modes_counter_and_checksum(self):
|
||||
cp = params(CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, True, True)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.chassis])
|
||||
for counter in range(4):
|
||||
for brake, enabled, full_stop, mode in ((0, False, False, 1), (0, True, False, 9),
|
||||
(100, True, False, 10), (400, True, True, 13)):
|
||||
with self.subTest(counter=counter, brake=brake, enabled=enabled, full_stop=full_stop):
|
||||
message = create_friction_brake_command(packer, 0, brake, counter, enabled, False, full_stop, cp)
|
||||
encoded = (-brake) & 0xfff
|
||||
checksum = (-(mode << 12) - encoded - counter) & 0xffff
|
||||
expected = bytes(((mode << 4) | (encoded >> 8), encoded & 0xff,
|
||||
checksum >> 8, checksum & 0xff, counter))
|
||||
self.assertEqual(message, (0x315, expected, 0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,208 @@
|
||||
from unittest.mock import patch
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
from opendbc.car import Bus, gen_empty_fingerprint, structs
|
||||
from opendbc.car.gm.carcontroller import CarController
|
||||
from opendbc.car.gm.carstate import CarState
|
||||
from opendbc.car.gm.gmcan import pedal_crc
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.values import CAR, DBC, GMFlags, GMSafetyFlags, NO_ACC_BOLT_CAR, PEDAL_BOLT_CAR, BOLT_CC_WORDS
|
||||
|
||||
|
||||
class SavedPedalSetting:
|
||||
def __init__(self, enabled=False, missing=False):
|
||||
self.enabled = enabled
|
||||
self.missing = missing
|
||||
|
||||
def get_bool(self, key):
|
||||
assert key == "GMPedalLongitudinal"
|
||||
if self.missing:
|
||||
from openpilot.common.params import UnknownKeyName
|
||||
raise UnknownKeyName(key)
|
||||
return self.enabled
|
||||
|
||||
|
||||
def params(candidate, setting=False, pedal=False, alpha_long=False, missing_key=False, camera=False):
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
if camera:
|
||||
fingerprint[2][0x180] = 4
|
||||
if pedal:
|
||||
fingerprint[0][0x201] = 6
|
||||
with patch("opendbc.car.gm.interface.Params", return_value=SavedPedalSetting(setting, missing_key)):
|
||||
return CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False)
|
||||
|
||||
|
||||
class TestBoltPedalIdentity(unittest.TestCase):
|
||||
def test_pedal_requires_platform_observation_and_saved_opt_in(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate):
|
||||
for setting, observed, missing in ((False, True, False), (True, False, False), (False, True, True)):
|
||||
cp = params(candidate, setting, observed, alpha_long=True, missing_key=missing)
|
||||
self.assertTrue(cp.openpilotLongitudinalControl)
|
||||
self.assertFalse(cp.flags & GMFlags.PEDAL_LONG.value)
|
||||
self.assertFalse(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.PEDAL_LONG.value)
|
||||
|
||||
cp = params(candidate, True, True)
|
||||
self.assertTrue(cp.openpilotLongitudinalControl)
|
||||
self.assertTrue(cp.flags & GMFlags.PEDAL_LONG.value)
|
||||
self.assertTrue(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.PEDAL_LONG.value)
|
||||
self.assertFalse(cp.pcmCruise)
|
||||
|
||||
def test_ordinary_cruise_ownership_is_never_stock_acc(self):
|
||||
for candidate in NO_ACC_BOLT_CAR:
|
||||
for enabled in (False, True):
|
||||
with self.subTest(candidate=candidate, enabled=enabled):
|
||||
cp = params(candidate, enabled, True)
|
||||
self.assertFalse(cp.pcmCruise)
|
||||
if enabled:
|
||||
self.assertTrue(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.NO_ACC.value)
|
||||
self.assertFalse(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.BOLT_ACC_PEDAL.value)
|
||||
else:
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, BOLT_CC_WORDS[candidate][1])
|
||||
self.assertTrue(cp.flags & GMFlags.CC_LONG.value)
|
||||
|
||||
def test_existing_bolt_stock_acc_is_unaffected_by_pedal_setting(self):
|
||||
without = params(CAR.CHEVROLET_BOLT_EUV, False, True)
|
||||
with_setting = params(CAR.CHEVROLET_BOLT_EUV, True, True)
|
||||
self.assertEqual(without.openpilotLongitudinalControl, with_setting.openpilotLongitudinalControl)
|
||||
self.assertEqual(without.pcmCruise, with_setting.pcmCruise)
|
||||
self.assertEqual(without.safetyConfigs[0].safetyParam, with_setting.safetyConfigs[0].safetyParam)
|
||||
self.assertFalse(with_setting.flags & GMFlags.PEDAL_LONG.value)
|
||||
|
||||
def test_2022_pedal_variants_do_not_claim_automatic_fingerprint_identity(self):
|
||||
from opendbc.car.gm.fingerprints import FINGERPRINTS, FW_VERSIONS
|
||||
self.assertNotIn(CAR.CHEVROLET_BOLT_CC_2022_2023, FINGERPRINTS)
|
||||
self.assertNotIn(CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, FINGERPRINTS)
|
||||
self.assertFalse(FW_VERSIONS)
|
||||
|
||||
|
||||
class TestBoltPedalMessages(unittest.TestCase):
|
||||
@staticmethod
|
||||
def sensor(packer, gas, counter, state=0, other_track=None, bad_crc=False):
|
||||
msg = packer.make_can_msg("GAS_SENSOR", 0, {
|
||||
"INTERCEPTOR_GAS": gas,
|
||||
"INTERCEPTOR_GAS2": gas if other_track is None else other_track,
|
||||
"STATE": state, "COUNTER_PEDAL": counter,
|
||||
})
|
||||
data = bytearray(msg[1])
|
||||
data[5] = pedal_crc(data) ^ int(bad_crc)
|
||||
return msg[0], bytes(data), msg[2]
|
||||
|
||||
def test_real_parser_sensor_curve_and_fault_recovery(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate):
|
||||
cp = params(candidate, True, True)
|
||||
state = CarState(cp)
|
||||
parsers = state.get_can_parsers(cp)
|
||||
state.update(parsers)
|
||||
packer = CANPacker(DBC[candidate][Bus.pt])
|
||||
for counter, gas in enumerate((0., 4., 10., 23., 24., 30., 128., 255.), start=1):
|
||||
parsers[Bus.pt].update([(1_000_000_000 + counter * 20_000_000,
|
||||
[self.sensor(packer, gas, counter)])])
|
||||
out = state.update(parsers)
|
||||
self.assertTrue(state.pedal_sensor_healthy, gas)
|
||||
self.assertEqual(out.gasPressed, gas > 23., gas)
|
||||
|
||||
for counter, kwargs in ((9, {"state": 1}), (10, {"other_track": 40.}), (11, {"bad_crc": True})):
|
||||
parsers[Bus.pt].update([(1_000_000_000 + counter * 20_000_000,
|
||||
[self.sensor(packer, 30., counter, **kwargs)])])
|
||||
state.update(parsers)
|
||||
self.assertFalse(state.pedal_sensor_healthy)
|
||||
parsers[Bus.pt].update([(1_240_000_000, [self.sensor(packer, 30., 12)])])
|
||||
state.update(parsers)
|
||||
self.assertTrue(state.pedal_sensor_healthy)
|
||||
parsers[Bus.pt].update([(1_260_000_000, [self.sensor(packer, 30., 12)])])
|
||||
state.update(parsers)
|
||||
self.assertFalse(state.pedal_sensor_healthy)
|
||||
|
||||
def test_controller_pedal_rate_release_and_stock_handoff(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate):
|
||||
cp = params(candidate, True, True)
|
||||
controller = CarController(DBC[candidate], cp)
|
||||
controller.frame = 4
|
||||
controller.last_steer_frame = 4
|
||||
control = structs.CarControl()
|
||||
control.enabled = True
|
||||
control.longActive = True
|
||||
control.actuators.accel = 1.
|
||||
state = structs.CarState()
|
||||
state.vEgo = 12.
|
||||
state.aEgo = 0.
|
||||
state.gearShifter = structs.CarState.GearShifter.low
|
||||
state.cruiseState.available = True
|
||||
cs = SimpleNamespace(out=state.as_reader(), pedal_sensor_healthy=True,
|
||||
pedal_sensor_ts_nanos=950_000_000, stock_acc_status_ts_nanos=950_000_000,
|
||||
cam_lka_steering_cmd_counter=0, loopback_lka_steering_cmd_updated=False,
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0,
|
||||
buttons_counter=0)
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
expected = [0x200, 0x1F5, 0xBD]
|
||||
if candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
|
||||
expected.append(0x315)
|
||||
self.assertEqual([m[0] for m in messages], expected)
|
||||
self.assertGreater(controller.pedal_steady, 0.)
|
||||
first_fraction = controller.pedal_steady
|
||||
|
||||
controller.frame = 8
|
||||
control.actuators.accel = 2.
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual([m[0] for m in messages], expected)
|
||||
self.assertGreater(controller.pedal_steady, first_fraction)
|
||||
self.assertLess(controller.pedal_steady - first_fraction, 0.06)
|
||||
|
||||
controller.frame = 12
|
||||
cs.pedal_sensor_healthy = False
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual(messages[0][1][0:4], b"\x00" * 4)
|
||||
self.assertEqual(messages[1][1], b"\x0c\x0c\x00\x06\x00\x00\x01\x00")
|
||||
self.assertEqual(messages[2][1], b"\x00" * 7)
|
||||
self.assertEqual(controller.pedal_steady, 0.)
|
||||
|
||||
if candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
|
||||
controller.frame = 16
|
||||
cs.pedal_sensor_healthy = True
|
||||
state.cruiseState.enabled = True
|
||||
cs.out = state.as_reader()
|
||||
_, messages = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual(messages[0][1][0:4], b"\x00" * 4)
|
||||
self.assertIn((0x1E1, 2), [(m[0], m[2]) for m in messages])
|
||||
|
||||
def test_driver_gear_and_paddle_ownership(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate):
|
||||
cp = params(candidate, True, True)
|
||||
controller = CarController(DBC[candidate], cp)
|
||||
control = structs.CarControl()
|
||||
control.enabled = True
|
||||
control.longActive = True
|
||||
control.actuators.accel = 1.
|
||||
state = structs.CarState()
|
||||
state.vEgo = 12.
|
||||
state.cruiseState.available = True
|
||||
cs = SimpleNamespace(pedal_sensor_healthy=True, pedal_sensor_ts_nanos=950_000_000,
|
||||
stock_acc_status_ts_nanos=950_000_000, cam_lka_steering_cmd_counter=0,
|
||||
loopback_lka_steering_cmd_updated=False, loopback_lka_steering_cmd_ts_nanos=1_000_000_000,
|
||||
pt_lka_steering_cmd_counter=0, buttons_counter=0)
|
||||
for gear, driver_paddle in ((structs.CarState.GearShifter.park, False),
|
||||
(structs.CarState.GearShifter.reverse, False),
|
||||
(structs.CarState.GearShifter.drive, False),
|
||||
(structs.CarState.GearShifter.manumatic, False),
|
||||
(structs.CarState.GearShifter.low, True)):
|
||||
controller.frame = 4
|
||||
state.gearShifter = gear
|
||||
state.regenBraking = driver_paddle
|
||||
cs.out = state.as_reader()
|
||||
_, commands = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
owned = [msg for msg in commands if msg[0] in (0x200, 0x1F5, 0xBD)]
|
||||
self.assertEqual([msg[0] for msg in owned], [0x200])
|
||||
self.assertEqual(owned[0][1][0:4], b"\x00" * 4)
|
||||
controller.frame = 4
|
||||
state.gearShifter = structs.CarState.GearShifter.low
|
||||
state.regenBraking = False
|
||||
cs.out = state.as_reader()
|
||||
_, commands = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
self.assertEqual([msg[0] for msg in commands if msg[0] in (0x200, 0x1F5, 0xBD)], [0x200, 0x1F5, 0xBD])
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Explicit conventional-cruise interceptor startup and physical command owner."""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, gen_empty_fingerprint, structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.gmcan import pedal_crc
|
||||
from opendbc.car.gm.values import CAR, DBC, ORDINARY_CC_CAR, is_conventional_cc_pedal_profile
|
||||
from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal
|
||||
from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self, enabled):
|
||||
self.enabled = enabled
|
||||
|
||||
def get_bool(self, key):
|
||||
return self.enabled if key == 'GMPedalLongitudinal' else False
|
||||
|
||||
|
||||
def params(identity, *, disabled=False, removed=False, release=False, enabled=True, sensor=True, analog=True):
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0].update({0xF1: 6, 0xBE: 6, 0xC9: 8, 0x3D1: 8, 0x1E1: 7})
|
||||
if not analog:
|
||||
fp[0].pop(0xBE)
|
||||
if sensor:
|
||||
fp[0][0x201] = 6
|
||||
if not removed:
|
||||
fp[2][0x320] = 6
|
||||
with patch('opendbc.car.gm.interface.Params', return_value=Settings(enabled)):
|
||||
cp = CarInterface.get_params(identity, fp, [], False, release, False)
|
||||
prepare_disable_longitudinal(cp, disabled)
|
||||
return cp
|
||||
|
||||
|
||||
def pedal_frames(packer, counter, pressed=False, *, removed=False, bad_crc=False):
|
||||
sensor = packer.make_can_msg('GAS_SENSOR', 0, {
|
||||
'INTERCEPTOR_GAS': 30 if pressed else 0, 'INTERCEPTOR_GAS2': 30 if pressed else 0,
|
||||
'STATE': 0, 'COUNTER_PEDAL': counter % 16,
|
||||
})
|
||||
data = bytearray(sensor[1])
|
||||
data[5] = pedal_crc(data) ^ int(bad_crc)
|
||||
frames = [(sensor[0], bytes(data), sensor[2]),
|
||||
packer.make_can_msg("ASCMLKASteeringCmd", 128, {"RollingCounter": counter % 4}),
|
||||
packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': 0})]
|
||||
if not removed:
|
||||
frames += [packer.make_can_msg('ASCMLKASteeringCmd', 2, {'RollingCounter': counter % 4}),
|
||||
packer.make_can_msg('AEBCmd', 2, {})]
|
||||
return frames
|
||||
|
||||
|
||||
class TestConventionalPedal(unittest.TestCase):
|
||||
def test_explicit_startup_profile_and_disabled_policy(self):
|
||||
from opendbc.car.gm.conventional_pedal import policy_for
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
for removed in (False, True):
|
||||
for release in (False, True):
|
||||
for disabled in (False, True):
|
||||
cp = params(identity, removed=removed, release=release, disabled=disabled)
|
||||
self.assertTrue(is_conventional_cc_pedal_profile(cp))
|
||||
self.assertFalse(cp.pcmCruise)
|
||||
self.assertEqual(cp.openpilotLongitudinalControl, not disabled)
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, (0xC186 if disabled else 0xC180) + int(removed))
|
||||
self.assertEqual(cp.minEnableSpeed, -1.)
|
||||
self.assertEqual(policy_for(cp) is not None, not disabled)
|
||||
if not disabled:
|
||||
self.assertEqual(policy_for(cp).starting_speed, .75 if identity == CAR.CHEVROLET_MALIBU_CC else .5)
|
||||
self.assertEqual(cp.stopAccel, -1.5 if identity == CAR.CHEVROLET_MALIBU_CC else -2.)
|
||||
self.assertFalse(is_conventional_cc_pedal_profile(params(identity, removed=removed, enabled=False)))
|
||||
self.assertFalse(is_conventional_cc_pedal_profile(params(identity, removed=removed, sensor=False)))
|
||||
|
||||
def test_actual_sensor_threshold_and_stock_source_are_independent(self):
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
cp = params(identity)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
for tick in range(24):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
frames = pt_frames(packer, cruise=False, acc_cruise=2, counter=tick % 4)
|
||||
frames += pedal_frames(packer, tick, pressed=tick >= 12)
|
||||
out = ci.update([(now, frames)])
|
||||
self.assertTrue(out.canValid)
|
||||
self.assertFalse(out.cruiseState.enabled)
|
||||
self.assertFalse(out.cruiseState.nonAdaptive)
|
||||
self.assertEqual(out.gasPressed, tick >= 12)
|
||||
self.assertTrue(ci.CS.pedal_sensor_healthy)
|
||||
|
||||
def test_actual_pedal_counter_cancel_and_disabled_takeover(self):
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
for removed in (False, True):
|
||||
for disabled in (False, True):
|
||||
cp = params(identity, removed=removed, disabled=disabled)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
seen_cancel = False
|
||||
for tick in range(40):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
cruise = tick < 25
|
||||
frames = pt_frames(packer, cruise=cruise, counter=tick % 4)
|
||||
frames += pedal_frames(packer, tick, removed=removed)
|
||||
out = ci.update([(now, frames)])
|
||||
self.assertTrue(out.canValid)
|
||||
cc = structs.CarControl(enabled=True, latActive=True, longActive=True)
|
||||
cc.actuators.torque = .1
|
||||
cc.actuators.accel = 1.
|
||||
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid
|
||||
_, messages = ci.apply(cc.as_reader(), now)
|
||||
self.assertFalse(any(message[0] in (0x2CB, 0x315, 0x370, 0x2CD, 0x3D1, 0xBD, 0x1F5) for message in messages))
|
||||
pedal = [message for message in messages if message[0] == 0x200]
|
||||
cancel = [message for message in messages if message[0] == 0x1E1]
|
||||
if disabled:
|
||||
self.assertFalse(pedal or cancel)
|
||||
else:
|
||||
if pedal:
|
||||
self.assertEqual(pedal[0][2], 0)
|
||||
self.assertEqual(pedal[0][1][4] & 0xF, (tick // 4) % 4)
|
||||
if cancel:
|
||||
seen_cancel = True
|
||||
self.assertTrue(cruise)
|
||||
self.assertEqual(cancel[0][2], 0)
|
||||
if any(message[0] == 0x184 for message in messages):
|
||||
ids = [message[0] for message in messages]
|
||||
self.assertLess(ids.index(0x1E1), ids.index(0x184))
|
||||
self.assertEqual(any(message[0] == 0x184 for message in messages), tick % 10 == 0)
|
||||
if not disabled:
|
||||
self.assertTrue(seen_cancel)
|
||||
|
||||
def test_sensor_crc_and_timeout_clear_actual_pedal_output(self):
|
||||
identity = next(iter(ORDINARY_CC_CAR))
|
||||
cp = params(identity, removed=True)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
cc = structs.CarControl(enabled=True, latActive=True, longActive=True)
|
||||
cc.actuators.accel = 1.
|
||||
for tick in range(24):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
ci.update([(now, pt_frames(packer, cruise=False, counter=tick % 4) + pedal_frames(packer, tick, removed=True))])
|
||||
ci.apply(cc.as_reader(), now)
|
||||
self.assertGreater(ci.CC.pedal_steady, 0)
|
||||
ci.update([(now + 10_000_000, pt_frames(packer, cruise=False) + pedal_frames(packer, 24, removed=True, bad_crc=True))])
|
||||
ci.CC.frame = 24
|
||||
ci.apply(cc.as_reader(), now + 10_000_000)
|
||||
self.assertEqual(ci.CC.pedal_steady, 0)
|
||||
|
||||
def test_actual_packed_commands_match_registered_native_owner(self):
|
||||
from opendbc.car.gm.gmcan import create_buttons
|
||||
from opendbc.car.gm.tests.test_bolt_cc import setup, native
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
safety = libsafety_py.libsafety
|
||||
for removed in (False, True):
|
||||
cp = params(CAR.CADILLAC_CT6_CC, removed=removed)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
setup(cp)
|
||||
for tick in range(24):
|
||||
now = 1_000_000_000 + tick * 20_000_000
|
||||
frames = pt_frames(packer, cruise=tick < 12, counter=tick % 4)
|
||||
frames = [message for message in frames if message[0] != 0x1E1]
|
||||
frames += pedal_frames(packer, tick, removed=removed)
|
||||
frames.append(create_buttons(packer, 0, tick % 4, 1))
|
||||
out = ci.update([(now, frames)])
|
||||
self.assertTrue(out.canValid)
|
||||
for message in frames:
|
||||
if message[2] != 128: # Host loopback is not a physical CAN bus.
|
||||
native('rx', message, now // 1000)
|
||||
if tick == 1:
|
||||
native('rx', create_buttons(packer, 0, tick % 4, 2), now // 1000)
|
||||
if tick == 2:
|
||||
native('rx', create_buttons(packer, 0, tick % 4, 1), now // 1000)
|
||||
safety.safety_tick_current_safety_config()
|
||||
self.assertTrue(safety.safety_config_valid())
|
||||
cc = structs.CarControl(enabled=True, latActive=True, longActive=tick >= 3)
|
||||
cc.actuators.accel = 1.
|
||||
cc.actuators.torque = .01
|
||||
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid
|
||||
_, emitted = ci.apply(cc.as_reader(), now)
|
||||
for message in emitted:
|
||||
self.assertTrue(native('tx', message, now // 1000), hex(message[0]))
|
||||
|
||||
def test_malibu_selected_analog_brake_source_and_alternate_threshold(self):
|
||||
from opendbc.car.gm.values import GMFlags
|
||||
for analog in (False, True):
|
||||
for removed in (False, True):
|
||||
cp = params(CAR.CHEVROLET_MALIBU_CC, analog=analog, removed=removed)
|
||||
self.assertTrue(is_conventional_cc_pedal_profile(cp))
|
||||
self.assertEqual(bool(cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG), not analog)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(32):
|
||||
pressed = tick >= 16
|
||||
frames = [m for m in pt_frames(packer, cruise=False, brake=not pressed, counter=tick % 4)
|
||||
if m[0] not in (0xBE, 0xF1)]
|
||||
frames += pedal_frames(packer, tick, removed=removed)
|
||||
frames = [m for m in frames if m[0] != 0xF1]
|
||||
frames += [packer.make_can_msg('EBCMBrakePedalPosition', 0,
|
||||
{'BrakePedalPosition': 21 if pressed else 20})]
|
||||
if analog:
|
||||
frames += [packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 8 if pressed else 7})]
|
||||
out = ci.update([(1_000_000_000 + tick * 10_000_000, frames)])
|
||||
self.assertTrue(out.canValid)
|
||||
self.assertEqual(out.brakePressed, pressed)
|
||||
@@ -0,0 +1,124 @@
|
||||
"""GM pedal state and caller transition boundaries."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.gmcan import create_pedal_command, pedal_crc
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.conventional_pedal import policy_for
|
||||
from opendbc.car.gm.values import CAR, DBC, ORDINARY_CC_CAR
|
||||
from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames
|
||||
from opendbc.car.gm.tests.test_conventional_pedal import params, pedal_frames
|
||||
|
||||
|
||||
class TestConventionalPedalTransitions(unittest.TestCase):
|
||||
def test_actual_decoded_sensor_threshold_and_cruise_standstill(self):
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
for removed in (False, True):
|
||||
for analog in (False, True):
|
||||
cp = params(identity, removed=removed, analog=analog)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
for tick in range(24):
|
||||
level = (22., 23., 24.)[tick // 8]
|
||||
cruise = tick % 2 == 0
|
||||
frames = pt_frames(packer, cruise=cruise, acc_cruise=4, counter=tick % 4)
|
||||
if not analog:
|
||||
frames = [f for f in frames if f[0] != 0xBE]
|
||||
frames += pedal_frames(packer, tick, removed=removed)
|
||||
frames = [f for f in frames if f[0] != 0x201]
|
||||
sensor = packer.make_can_msg('GAS_SENSOR', 0, {'INTERCEPTOR_GAS': level,
|
||||
'INTERCEPTOR_GAS2': level, 'STATE': 0, 'COUNTER_PEDAL': tick % 16})
|
||||
raw = bytearray(sensor[1])
|
||||
raw[-1] = pedal_crc(raw)
|
||||
frames.append((sensor[0], bytes(raw), sensor[2]))
|
||||
out = ci.update([(1_000_000_000 + tick * 10_000_000, frames)])
|
||||
self.assertTrue(out.canValid)
|
||||
self.assertTrue(ci.CS.pedal_sensor_healthy)
|
||||
decoded = ci.can_parsers[Bus.pt].vl['GAS_SENSOR']
|
||||
mean = (decoded['INTERCEPTOR_GAS'] + decoded['INTERCEPTOR_GAS2']) / 2.
|
||||
self.assertEqual(out.gasPressed, mean > 23.)
|
||||
self.assertEqual(mean > 23., level == 24.)
|
||||
self.assertEqual(out.cruiseState.enabled, cruise)
|
||||
self.assertTrue(out.cruiseState.standstill)
|
||||
|
||||
@staticmethod
|
||||
def caller(identity, removed):
|
||||
cp = params(identity, removed=removed)
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[identity][Bus.pt])
|
||||
counter = [0]
|
||||
def step(accel=1.8, *, speed=0., active=True, stopping=False, resume=False, main=True, bad_crc=False, enabled=None):
|
||||
tick = counter[0]
|
||||
counter[0] += 1
|
||||
frames = pt_frames(packer, cruise=True, acc_cruise=4, counter=tick % 4, main=main)
|
||||
frames += pedal_frames(packer, tick, removed=removed, bad_crc=bad_crc)
|
||||
now = 1_000_000_000 + tick * 40_000_000
|
||||
out = ci.update([(now, frames)]).as_reader().as_builder()
|
||||
# Caller boundary fixture preserves actual parser freshness and cruise inputs.
|
||||
out.vEgo = speed
|
||||
out.standstill = speed == 0.
|
||||
ci.CS.out = out.as_reader()
|
||||
cc = structs.CarControl(enabled=active if enabled is None else enabled, latActive=True, longActive=active)
|
||||
cc.actuators.accel = accel
|
||||
cc.actuators.longControlState = (structs.CarControl.Actuators.LongControlState.stopping if stopping else
|
||||
structs.CarControl.Actuators.LongControlState.pid)
|
||||
cc.cruiseControl.resume = resume
|
||||
ci.CC.frame = tick * 4
|
||||
_, messages = ci.apply(cc.as_reader(), now)
|
||||
return next(m for m in messages if m[0] == 0x200), tick % 4
|
||||
for _ in range(24):
|
||||
step()
|
||||
return SimpleNamespace(cp=cp, ci=ci, packer=packer, step=step)
|
||||
|
||||
def test_reached_stored_stop_limits_and_inactive_memory(self):
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
for removed in (False, True):
|
||||
case = self.caller(identity, removed)
|
||||
malibu = identity == CAR.CHEVROLET_MALIBU_CC
|
||||
self.assertEqual((case.cp.stopAccel, case.ci.CC.conventional_pedal_command.start_speed,
|
||||
case.ci.CC.conventional_pedal_command.stop_speed),
|
||||
(-1.5, .75, .75) if malibu else (-2., .5, .5))
|
||||
self.assertEqual(case.ci.CC.conventional_pedal_command.start_speed, policy_for(case.cp).starting_speed)
|
||||
steady, active = case.ci.CC.pedal_steady, case.ci.CC.pedal_active_last
|
||||
emitted, counter = case.step(active=True, enabled=False)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertEqual((case.ci.CC.pedal_steady, case.ci.CC.pedal_active_last), (steady, active))
|
||||
emitted, counter = case.step(active=False)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertEqual((case.ci.CC.pedal_steady, case.ci.CC.pedal_active_last), (steady, active))
|
||||
emitted, counter = case.step(speed=.24, accel=-1., stopping=True)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertEqual(case.ci.CC.apply_brake, 150 if malibu else 200)
|
||||
self.assertEqual((case.ci.CC.pedal_steady, case.ci.CC.pedal_active_last), (steady, active))
|
||||
emitted, counter = case.step(speed=.25, accel=-1., stopping=True)
|
||||
self.assertNotEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertLess(case.ci.CC.pedal_steady, steady)
|
||||
emitted, counter = case.step(speed=.24, accel=1.8, stopping=True, resume=True)
|
||||
self.assertNotEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertGreater(case.ci.CC.pedal_steady, steady)
|
||||
emitted, counter = case.step(speed=.24, accel=1.8)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 18. / 255., counter))
|
||||
emitted, counter = case.step(speed=.6, accel=1.8)
|
||||
if malibu:
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 18. / 255., counter))
|
||||
else:
|
||||
self.assertNotEqual(emitted, create_pedal_command(case.packer, 18. / 255., counter))
|
||||
|
||||
def test_invalid_sensor_recovery_bounds_final_sng_and_veto_zero(self):
|
||||
for identity in ORDINARY_CC_CAR:
|
||||
for removed in (False, True):
|
||||
case = self.caller(identity, removed)
|
||||
emitted, counter = case.step(bad_crc=True)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
self.assertEqual((case.ci.CC.pedal_steady, case.ci.CC.pedal_active_last), (0., True))
|
||||
emitted, counter = case.step()
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, .0229, counter))
|
||||
emitted, counter = case.step(active=False)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
emitted, counter = case.step()
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, .0229, counter))
|
||||
emitted, counter = case.step(active=False, main=False)
|
||||
self.assertEqual(emitted, create_pedal_command(case.packer, 0., counter))
|
||||
@@ -0,0 +1,266 @@
|
||||
#pragma once
|
||||
|
||||
static uint8_t gm_pedal_crc(const CANPacket_t *msg);
|
||||
|
||||
static bool gm_cc_pedal = false;
|
||||
static bool gm_cc_pedal_volt;
|
||||
static bool gm_cc_pedal_volt_f1;
|
||||
static bool gm_cc_pedal_volt_removed;
|
||||
static bool gm_cc_pedal_volt_stock;
|
||||
static bool gm_cc_pedal_volt_rejected;
|
||||
static uint16_t gm_cc_pedal_volt_seen;
|
||||
static uint32_t gm_cc_pedal_volt_us[10] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U};
|
||||
static bool gm_cc_pedal_volt_regen;
|
||||
|
||||
static uint16_t gm_cc_pedal_volt_decode(uint16_t raw) {
|
||||
gm_cc_pedal_volt = false;
|
||||
gm_cc_pedal_volt_f1 = false;
|
||||
gm_cc_pedal_volt_removed = false;
|
||||
gm_cc_pedal_volt_stock = false;
|
||||
gm_cc_pedal_volt_rejected = false;
|
||||
uint16_t canonical = raw;
|
||||
switch (raw) {
|
||||
case 0xE600U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE601U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE602U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE603U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE604U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE605U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE606U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE607U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = false; canonical = 5U; break;
|
||||
case 0xE610U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE611U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE612U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE613U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE614U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE615U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = false; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE616U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = false; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
case 0xE617U: gm_cc_pedal_volt = true; gm_cc_pedal_volt_f1 = true; gm_cc_pedal_volt_removed = true; gm_cc_pedal_volt_stock = true; canonical = 5U; break;
|
||||
default:
|
||||
if ((raw >= 0xE600U) && (raw <= 0xE6FFU)) { gm_cc_pedal_volt_rejected = true; canonical = 5U; }
|
||||
break;
|
||||
}
|
||||
#ifndef ALLOW_DEBUG
|
||||
if (gm_cc_pedal_volt && !gm_cc_pedal_volt_stock) { gm_cc_pedal_volt_rejected = true; }
|
||||
#endif
|
||||
return canonical;
|
||||
}
|
||||
|
||||
static bool gm_cc_pedal_volt_current(void) {
|
||||
bool current = true;
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint32_t limits[10] = {300000U, 100000U, 300000U, 300000U, 300000U, 300000U, 100000U, 300000U, 1000000U, 100000U};
|
||||
for (uint8_t i = 0U; i < 10U; i++) {
|
||||
if (!gm_cc_pedal_volt_stock || ((i != 6U) && (i != 8U))) {
|
||||
current &= ((gm_cc_pedal_volt_seen & (1U << i)) != 0U) && (safety_get_ts_elapsed(now, gm_cc_pedal_volt_us[i]) <= limits[i]);
|
||||
}
|
||||
}
|
||||
return current && !safety_rx_checks_invalid && !relay_malfunction;
|
||||
}
|
||||
// Silverado owns four complete selectors; it shares only the checked pedal codec.
|
||||
static bool gm_cc_pedal_silverado = false;
|
||||
static bool gm_cc_pedal_stock_only = false;
|
||||
static bool gm_cc_pedal_ordinary_stock = false;
|
||||
static uint8_t gm_cc_pedal_silverado_seen = 0U;
|
||||
static uint32_t gm_cc_pedal_silverado_us[8] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U};
|
||||
static bool gm_cc_pedal_main = false;
|
||||
static bool gm_cc_pedal_stock_active = false;
|
||||
static bool gm_cc_pedal_forward = false;
|
||||
static bool gm_cc_pedal_sensor_valid = false;
|
||||
static bool gm_cc_pedal_sensor_gas = false;
|
||||
static bool gm_cc_pedal_sensor_seen = false;
|
||||
static uint8_t gm_cc_pedal_sensor_counter = 0U;
|
||||
static uint32_t gm_cc_pedal_sensor_us = 0U;
|
||||
static uint32_t gm_cc_pedal_main_us = 0U;
|
||||
static uint32_t gm_cc_pedal_stock_us = 0U;
|
||||
static uint32_t gm_cc_pedal_gear_us = 0U;
|
||||
static bool gm_cc_pedal_button_seen = false;
|
||||
static bool gm_cc_pedal_cancel_credit = false;
|
||||
static uint8_t gm_cc_pedal_button_counter = 0U;
|
||||
static uint32_t gm_cc_pedal_button_us = 0U;
|
||||
static bool gm_cc_pedal_cancel_seen = false;
|
||||
static uint32_t gm_cc_pedal_cancel_us = 0U;
|
||||
static bool gm_cc_pedal_tx_seen = false;
|
||||
static uint8_t gm_cc_pedal_tx_counter = 0U;
|
||||
|
||||
static void gm_cc_pedal_reset(void) {
|
||||
gm_cc_pedal_volt_seen = 0U; gm_cc_pedal_volt_regen = false;
|
||||
for (uint8_t i = 0U; i < 10U; i++) { gm_cc_pedal_volt_us[i] = 0U; }
|
||||
gm_cc_pedal_silverado_seen = 0U;
|
||||
for (uint8_t i = 0U; i < 8U; i++) { gm_cc_pedal_silverado_us[i] = 0U; }
|
||||
gm_cc_pedal_main = false;
|
||||
gm_cc_pedal_stock_active = false;
|
||||
gm_cc_pedal_forward = false;
|
||||
gm_cc_pedal_sensor_valid = false;
|
||||
gm_cc_pedal_sensor_gas = false;
|
||||
gm_cc_pedal_sensor_seen = false;
|
||||
gm_cc_pedal_sensor_counter = 0U;
|
||||
gm_cc_pedal_sensor_us = 0U;
|
||||
gm_cc_pedal_main_us = 0U;
|
||||
gm_cc_pedal_stock_us = 0U;
|
||||
gm_cc_pedal_gear_us = 0U;
|
||||
gm_cc_pedal_button_seen = false;
|
||||
gm_cc_pedal_cancel_credit = false;
|
||||
gm_cc_pedal_button_counter = 0U;
|
||||
gm_cc_pedal_button_us = 0U;
|
||||
gm_cc_pedal_cancel_seen = false;
|
||||
gm_cc_pedal_cancel_us = 0U;
|
||||
gm_cc_pedal_tx_seen = false;
|
||||
gm_cc_pedal_tx_counter = 0U;
|
||||
}
|
||||
|
||||
static bool gm_cc_pedal_silverado_current(void) {
|
||||
bool current = gm_cc_pedal_silverado_seen == 0xFFU;
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint32_t limits[8] = {300000U, 300000U, 300000U, 300000U, 100000U, 100000U, 300000U, 100000U};
|
||||
for (uint8_t i = 0U; i < 8U; i++) {
|
||||
current &= safety_get_ts_elapsed(now, gm_cc_pedal_silverado_us[i]) <= limits[i];
|
||||
}
|
||||
return current && gm_cc_pedal_sensor_valid && !safety_rx_checks_invalid && !relay_malfunction;
|
||||
}
|
||||
|
||||
static bool gm_cc_pedal_current(void) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
return (!gm_cc_pedal_volt || gm_cc_pedal_volt_current()) && gm_cc_pedal_sensor_valid && gm_cc_pedal_main && gm_cc_pedal_forward &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_sensor_us) <= 100000U) &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_main_us) <= 300000U) &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_gear_us) <= (gm_cc_pedal_volt ? 1000000U : (gm_cc_pedal_silverado ? 300000U : 100000U))) &&
|
||||
(!gm_cc_pedal_silverado || gm_cc_pedal_silverado_current()) &&
|
||||
!safety_rx_checks_invalid && !relay_malfunction;
|
||||
}
|
||||
|
||||
static void gm_cc_pedal_rx(const CANPacket_t *msg) {
|
||||
if (gm_cc_pedal && (msg->bus == 0U)) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
if (gm_cc_pedal_volt) {
|
||||
const uint16_t source_bits[10] = {1U, 2U, 4U, 8U, 16U, 32U, 64U, 128U, 256U, 512U};
|
||||
const uint32_t addresses[10] = {0x184U, 0x34AU, 0x1E1U, gm_cc_pedal_volt_f1 ? 0xF1U : 0xBEU, 0x1C4U, 0xC9U, 0x201U, 0x3D1U, 0x1F5U, 0xBDU};
|
||||
const uint8_t lengths[10] = {8U, 5U, 7U, 6U, 8U, 8U, 6U, 8U, 8U, 7U};
|
||||
for (uint8_t i = 0U; i < 10U; i++) {
|
||||
if ((msg->addr == addresses[i]) && ((GET_LEN(msg) == lengths[i]) || ((i == 3U) && !gm_cc_pedal_volt_f1 && ((GET_LEN(msg) == 7U) || (GET_LEN(msg) == 8U))))) {
|
||||
gm_cc_pedal_volt_seen |= source_bits[i]; gm_cc_pedal_volt_us[i] = now;
|
||||
}
|
||||
}
|
||||
if ((msg->addr == 0xBDU) && (GET_LEN(msg) == 7U)) { gm_cc_pedal_volt_regen = (msg->data[0] >> 4) != 0U; }
|
||||
}
|
||||
if (gm_cc_pedal_silverado) {
|
||||
const uint32_t addresses[8] = {0x184U, 0x34AU, 0xC9U, 0x3D1U, 0x1E1U, 0x1C4U, 0x1F5U, 0x201U};
|
||||
const uint8_t lengths[8] = {8U, 5U, 8U, 8U, 7U, 8U, 8U, 6U};
|
||||
for (uint8_t i = 0U; i < 8U; i++) {
|
||||
if ((msg->addr == addresses[i]) && (GET_LEN(msg) == lengths[i])) {
|
||||
gm_cc_pedal_silverado_seen |= (uint8_t)(1U << i);
|
||||
gm_cc_pedal_silverado_us[i] = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((msg->addr == 0x201U) && (GET_LEN(msg) == 6U)) {
|
||||
// Incoming tracks are independent 12-bit ADC samples, not synthesized command pairs.
|
||||
const int track1 = (msg->data[0] << 8) | msg->data[1];
|
||||
const int track2 = (msg->data[2] << 8) | msg->data[3];
|
||||
const uint8_t counter = msg->data[4] & 0xFU;
|
||||
gm_cc_pedal_sensor_valid = ((msg->data[4] >> 4) == 0U) && (gm_pedal_crc(msg) == msg->data[5]) &&
|
||||
(!gm_cc_pedal_sensor_seen || (counter != gm_cc_pedal_sensor_counter)) &&
|
||||
(track1 <= 4095) && (track2 <= 4095);
|
||||
gm_cc_pedal_sensor_seen = true;
|
||||
gm_cc_pedal_sensor_counter = counter;
|
||||
gm_cc_pedal_sensor_us = now;
|
||||
if (gm_cc_pedal_volt) {
|
||||
gm_cc_pedal_sensor_gas = ((125677U * (uint32_t)track1) + (251976U * (uint32_t)track2)) > (uint32_t)198510000U;
|
||||
} else {
|
||||
gm_cc_pedal_sensor_gas = ((track1 + track2) / 2) > 595;
|
||||
}
|
||||
if (!gm_cc_pedal_sensor_valid && !gm_cc_pedal_volt_stock) { controls_allowed = false; }
|
||||
} else if ((msg->addr == 0x3D1U) && (GET_LEN(msg) == 8U)) {
|
||||
gm_cc_pedal_stock_active = GET_BIT(msg, 39U);
|
||||
gm_cc_pedal_stock_us = now;
|
||||
cruise_engaged_prev = gm_cc_pedal_stock_active;
|
||||
if (!gm_cc_pedal_stock_active && (!gm_cc_pedal_stock_only || gm_cc_pedal_ordinary_stock)) {
|
||||
gm_cc_pedal_cancel_credit = false;
|
||||
}
|
||||
} else if ((msg->addr == 0xC9U) && (GET_LEN(msg) == 8U)) {
|
||||
gm_cc_pedal_main = GET_BIT(msg, 29U);
|
||||
gm_cc_pedal_main_us = now;
|
||||
if (!gm_cc_pedal_main) { controls_allowed = false; }
|
||||
} else if ((msg->addr == 0x1F5U) && (GET_LEN(msg) == 8U)) {
|
||||
const uint8_t gear = msg->data[3] & 0xFU;
|
||||
gm_cc_pedal_forward = ((gear == 4U) || (gear == 6U)) && !GET_BIT(msg, 41U);
|
||||
gm_cc_pedal_gear_us = now;
|
||||
if (!gm_cc_pedal_forward && !gm_cc_pedal_volt_stock) { controls_allowed = false; }
|
||||
} else if ((msg->addr == 0x1E1U) && (GET_LEN(msg) == 7U)) {
|
||||
const uint8_t counter = msg->data[4] & 0x3U;
|
||||
const uint16_t checksum = 0xFFU + (counter * 0x4EFU);
|
||||
const bool neutral = (msg->data[0] == 0U) && (msg->data[1] == 0U) && (msg->data[2] == 0U) &&
|
||||
(msg->data[3] == 1U) && (msg->data[4] == counter) &&
|
||||
(msg->data[5] == (uint8_t)(0x10U | (checksum >> 8))) && (msg->data[6] == (uint8_t)checksum);
|
||||
const bool timely = safety_get_ts_elapsed(now, gm_cc_pedal_button_us) <= 300000U;
|
||||
if (neutral && (!gm_cc_pedal_button_seen ||
|
||||
(timely && (counter == ((gm_cc_pedal_button_counter + 1U) % 4U))))) {
|
||||
gm_cc_pedal_cancel_credit = true;
|
||||
gm_cc_pedal_button_us = now;
|
||||
} else if (!neutral || !timely || (counter != gm_cc_pedal_button_counter)) {
|
||||
gm_cc_pedal_cancel_credit = false;
|
||||
} else {
|
||||
// A duplicate neutral frame cannot replenish a consumed cancellation slot.
|
||||
}
|
||||
if (!gm_cc_pedal_button_seen || (counter != gm_cc_pedal_button_counter)) {
|
||||
gm_cc_pedal_button_us = now;
|
||||
}
|
||||
gm_cc_pedal_button_seen = true;
|
||||
gm_cc_pedal_button_counter = counter;
|
||||
} else {
|
||||
// Only exact selected PT sources change this owner state.
|
||||
}
|
||||
if (!gm_cc_pedal_volt_stock) { gas_pressed = gm_cc_pedal_sensor_gas; }
|
||||
if (!gm_cc_pedal_main || (!gm_cc_pedal_volt_stock && (!gm_cc_pedal_forward || !gm_cc_pedal_sensor_valid))) { controls_allowed = false; }
|
||||
}
|
||||
}
|
||||
|
||||
static bool gm_cc_pedal_tx(const CANPacket_t *msg) {
|
||||
bool allowed = true;
|
||||
if (msg->addr == 0x200U) {
|
||||
const int track1 = (msg->data[0] << 8) | msg->data[1];
|
||||
const int track2 = (msg->data[2] << 8) | msg->data[3];
|
||||
const int pair_delta = track1 - (2 * track2);
|
||||
const uint8_t counter = msg->data[4] & 0xFU;
|
||||
const bool enabled = GET_BIT(msg, 39U);
|
||||
const bool inactive = !enabled && (track1 == 0) && (track2 == 0);
|
||||
const bool active = enabled && gm_cc_pedal_current() && get_longitudinal_allowed() && !brake_pressed_prev && (!gm_cc_pedal_volt || (!gm_cc_pedal_volt_regen && !gm_cc_pedal_sensor_gas)) &&
|
||||
(track1 >= 604) && (track1 <= 2633) && (track2 >= 304) && (track2 <= 1316) &&
|
||||
(pair_delta >= -16) && (pair_delta <= 16);
|
||||
allowed = !gm_cc_pedal_stock_only && (inactive || active) && ((msg->data[4] & 0x70U) == 0U) && (counter < 4U) &&
|
||||
(gm_pedal_crc(msg) == msg->data[5]) &&
|
||||
(!gm_cc_pedal_tx_seen || (gm_cc_pedal_volt ? (counter == ((gm_cc_pedal_tx_counter + 1U) & 3U)) : (counter != gm_cc_pedal_tx_counter)));
|
||||
if (allowed) {
|
||||
gm_cc_pedal_tx_seen = true;
|
||||
gm_cc_pedal_tx_counter = counter;
|
||||
}
|
||||
} else if (msg->addr == 0x1E1U) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint8_t counter = gm_cc_pedal_stock_only ? gm_cc_pedal_button_counter :
|
||||
(gm_cc_pedal_button_counter + 1U) % 4U;
|
||||
const uint16_t checksum = 0xFFU + (counter * 0x4EFU) - (5U << 4);
|
||||
allowed = gm_cc_pedal_main && ((gm_cc_pedal_stock_only && !gm_cc_pedal_ordinary_stock && !gm_cc_pedal_volt_stock) || gm_cc_pedal_stock_active) &&
|
||||
gm_cc_pedal_cancel_credit && (!gm_cc_pedal_ordinary_stock || !safety_rx_checks_invalid) &&
|
||||
(!gm_cc_pedal_silverado || gm_cc_pedal_silverado_current()) &&
|
||||
(!gm_cc_pedal_volt || gm_cc_pedal_volt_current()) &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_main_us) <= 300000U) &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_stock_us) <= 300000U) &&
|
||||
(safety_get_ts_elapsed(now, gm_cc_pedal_button_us) <= ((gm_cc_pedal_silverado || gm_cc_pedal_volt) ? 100000U : 300000U)) &&
|
||||
(!gm_cc_pedal_cancel_seen || (safety_get_ts_elapsed(now, gm_cc_pedal_cancel_us) > 40000U)) &&
|
||||
(msg->data[0] == 0U) && (msg->data[1] == 0U) && (msg->data[2] == 0U) &&
|
||||
(msg->data[3] == 1U) && (msg->data[4] == counter) &&
|
||||
(msg->data[5] == (uint8_t)(0x60U | (checksum >> 8))) && (msg->data[6] == (uint8_t)checksum);
|
||||
if (allowed) {
|
||||
gm_cc_pedal_cancel_credit = false;
|
||||
gm_cc_pedal_cancel_seen = true;
|
||||
gm_cc_pedal_cancel_us = now;
|
||||
}
|
||||
} else if ((msg->addr == 0x409U) || (msg->addr == 0x40AU)) {
|
||||
for (uint8_t i = 0U; i < 7U; i++) {
|
||||
allowed &= msg->data[i] == 0U;
|
||||
}
|
||||
} else {
|
||||
// Other admitted messages retain the common GM checks.
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.carcontroller import CarController
|
||||
from opendbc.car.gm.carstate import CarState
|
||||
from opendbc.car.gm.gmcan import create_bolt_regen_gear, create_bolt_regen_paddle, create_pedal_command, create_steering_control, pedal_crc
|
||||
from opendbc.car.gm.tests.test_bolt_pedal import params
|
||||
from opendbc.car.gm.values import CAR, DBC, GMSafetyFlags, PEDAL_BOLT_CAR
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.libsafety.libsafety_py import ffi, new_CANPacket
|
||||
|
||||
|
||||
class TestGmBoltPedalSafety(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.packer = CANPacker(DBC[CAR.CHEVROLET_BOLT_CC_2017]["pt"])
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.init_mode(GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017)
|
||||
|
||||
def init_mode(self, variant, pedal=True):
|
||||
param = int(GMSafetyFlags.HW_CAM | GMSafetyFlags.EV | variant)
|
||||
if pedal:
|
||||
param |= int(GMSafetyFlags.PEDAL_LONG | GMSafetyFlags.PADDLE_SCHED)
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, param), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.reset_recorded_can()
|
||||
|
||||
@staticmethod
|
||||
def packet(msg):
|
||||
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
|
||||
|
||||
def sensor(self, counter, state=0, gas=0., pair_error=False, bad_crc=False):
|
||||
msg = self.packer.make_can_msg("GAS_SENSOR", 0, {
|
||||
"INTERCEPTOR_GAS": gas, "INTERCEPTOR_GAS2": gas,
|
||||
"STATE": state, "COUNTER_PEDAL": counter,
|
||||
})
|
||||
data = bytearray(msg[1])
|
||||
if pair_error:
|
||||
data[2] += 8
|
||||
data[5] = pedal_crc(data) ^ int(bad_crc)
|
||||
return self.packet((msg[0], bytes(data), 0))
|
||||
|
||||
def stock(self, name, values):
|
||||
return self.packet(self.packer.make_can_msg(name, 0, values))
|
||||
|
||||
def low_gear(self):
|
||||
return self.stock("ECMPRDNL2", {"PRNDL2": 6, "ManualMode": 0})
|
||||
|
||||
def recorded(self):
|
||||
result = []
|
||||
for index in range(self.safety.get_recorded_can_count()):
|
||||
packet = new_CANPacket()
|
||||
self.assertTrue(self.safety.get_recorded_can(index, packet))
|
||||
result.append((packet.addr, packet.bus, bytes(packet.data[0:7 if packet.addr == 0xBD else 8])))
|
||||
return result
|
||||
|
||||
def test_paired_sensor_fault_checksum_counter_and_timeout(self):
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.sensor(1)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, 1))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, 1))))
|
||||
|
||||
for counter, bad in enumerate((self.sensor(2, state=1), self.sensor(3, pair_error=True),
|
||||
self.sensor(4, bad_crc=True), self.sensor(4)), start=2):
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.safety_rx_hook(bad)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, counter))))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, 0., counter))))
|
||||
|
||||
self.safety.safety_rx_hook(self.sensor(5))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.set_timer(100001)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, 6))))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, 0., 6))))
|
||||
|
||||
def test_sensor_physical_curve_and_driver_override(self):
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
for counter, gas in enumerate((0., 4., 10., 23., 24., 30., 128., 255.), start=1):
|
||||
with self.subTest(gas=gas):
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.sensor(counter, gas=gas)))
|
||||
self.assertEqual(self.safety.get_gas_pressed_prev(), gas > 23.)
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, counter))),
|
||||
gas <= 23.)
|
||||
|
||||
def test_paddle_waits_for_stock_phase_and_expires_without_feed(self):
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.set_controls_allowed(True)
|
||||
paddle = create_bolt_regen_paddle(self.packer, True)
|
||||
gear = create_bolt_regen_gear(self.packer, True, False)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(paddle)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(gear)))
|
||||
self.assertEqual(self.recorded(), [])
|
||||
|
||||
stock_paddle = self.stock("EBCMRegenPaddle", {"RegenPaddle": 0})
|
||||
stock_gear = self.stock("ECMPRDNL2", {"PRNDL2": 6})
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.safety.safety_rx_hook(stock_gear)
|
||||
self.assertEqual([(addr, bus) for addr, bus, _ in self.recorded()], [(0xBD, 0), (0x1F5, 0)])
|
||||
self.assertEqual(self.recorded()[0][2], paddle[1])
|
||||
self.assertEqual(self.recorded()[1][2], gear[1])
|
||||
|
||||
self.safety.reset_recorded_can()
|
||||
self.safety.set_timer(150000)
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.safety.safety_rx_hook(stock_gear)
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.safety.safety_rx_hook(self.sensor(2))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(paddle)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(gear)))
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.safety.safety_rx_hook(stock_gear)
|
||||
self.assertEqual([(addr, bus) for addr, bus, _ in self.recorded()], [(0xBD, 0), (0x1F5, 0)])
|
||||
|
||||
def test_fault_and_driver_source_changes_consume_pending_spoofs(self):
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.set_controls_allowed(True)
|
||||
applied_gear = self.packet(create_bolt_regen_gear(self.packer, True, False))
|
||||
applied_paddle = self.packet(create_bolt_regen_paddle(self.packer, True))
|
||||
stock_paddle = self.stock("EBCMRegenPaddle", {"RegenPaddle": 0})
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_gear))
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_paddle))
|
||||
# A real brake edge clears the pending active request; regaining controls alone cannot replay it.
|
||||
self.safety.safety_rx_hook(self.packet((0xC9, b"\x00" * 5 + b"\x01\x00\x00", 0)))
|
||||
self.safety.safety_rx_hook(self.packet((0xC9, b"\x00" * 8, 0)))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_gear))
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_paddle))
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.assertEqual([addr for addr, _, _ in self.recorded()], [0x1F5, 0xBD])
|
||||
|
||||
for prndl, manual in ((1, 0), (2, 0), (4, 0), (6, 1)):
|
||||
with self.subTest(prndl=prndl, manual=manual):
|
||||
self.safety.reset_recorded_can()
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_gear))
|
||||
self.safety.safety_rx_hook(self.stock("ECMPRDNL2", {"PRNDL2": prndl, "ManualMode": manual}))
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, prndl))))
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.assertEqual(self.recorded(), [])
|
||||
|
||||
self.safety.reset_recorded_can()
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_paddle))
|
||||
self.safety.safety_rx_hook(self.stock("EBCMRegenPaddle", {"RegenPaddle": 2}))
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(applied_paddle))
|
||||
self.safety.safety_rx_hook(stock_paddle)
|
||||
self.assertEqual([addr for addr, _, _ in self.recorded()], [0xBD])
|
||||
|
||||
def test_owned_tx_only_and_stock_acc_sibling(self):
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.stock("ASCMGasRegenCmd", {"GasRegenCmd": 0})))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg("ASCMSteeringButton", 2, {"ACCButtons": 6}))))
|
||||
|
||||
self.init_mode(GMSafetyFlags.BOLT_ACC_PEDAL)
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 1}))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg("ASCMSteeringButton", 2, {"ACCButtons": 6}))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg("ASCMSteeringButton", 2, {"ACCButtons": 2}))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.stock("ASCMGasRegenCmd", {"GasRegenCmd": 0})))
|
||||
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, int(GMSafetyFlags.HW_CAM | GMSafetyFlags.EV)), 0)
|
||||
self.safety.init_tests()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, .2, 1))))
|
||||
|
||||
def test_acc_pedal_friction_requires_exclusive_fresh_owner(self):
|
||||
chassis = CANPacker("gm_global_a_chassis")
|
||||
|
||||
def brake(value, counter, mode=None, bus=0):
|
||||
mode = (1 if value == 0 else 0xA) if mode is None else mode
|
||||
raw = (0x1000 - value) & 0xFFF
|
||||
checksum = (0x10000 - (mode << 12) - raw - counter) & 0xFFFF
|
||||
return self.packet(chassis.make_can_msg("EBCMFrictionBrakeCmd", bus, {
|
||||
"FrictionBrakeCmd": -value, "FrictionBrakeMode": mode,
|
||||
"RollingCounter": counter, "FrictionBrakeChecksum": checksum,
|
||||
}))
|
||||
|
||||
self.init_mode(GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2)
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(0, 0)))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0)
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x2CB), 0)
|
||||
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 0}))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), -1)
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 0)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(brake(0, 0)))
|
||||
self.safety.safety_rx_hook(self.stock("ECMEngineStatus", {"CruiseMainOn": 1}))
|
||||
self.assertTrue(self.safety.safety_tx_hook(brake(100, 1)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 1)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(401, 2)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(brake(0, 2, mode=9)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 3, mode=1)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 3, bus=2)))
|
||||
invalid = bytearray(chassis.make_can_msg("EBCMFrictionBrakeCmd", 0, {
|
||||
"FrictionBrakeCmd": -100, "FrictionBrakeMode": 0xA,
|
||||
"RollingCounter": 3, "FrictionBrakeChecksum": (0x10000 - (0xA << 12) - (0x1000 - 100) - 3) & 0xFFFF,
|
||||
})[1])
|
||||
invalid[3] ^= 1
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet((0x315, bytes(invalid), 0))))
|
||||
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 1}))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 3)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(0, 3, mode=9)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(0, 3)))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0)
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 0}))
|
||||
self.safety.safety_rx_hook(self.stock("ECMEngineStatus", {"CruiseMainOn": 0, "BrakePressed": 1}))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 3)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(brake(0, 3)))
|
||||
self.safety.set_timer(300001)
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(100, 0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(0, 0)))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0)
|
||||
|
||||
self.init_mode(GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2)
|
||||
self.assertFalse(self.safety.safety_tx_hook(brake(0, 0)))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0)
|
||||
|
||||
def test_2017_steering_limit_is_conditional(self):
|
||||
steer = self.packet(create_steering_control(self.packer, 0, 400, 0, True))
|
||||
for variant, pedal, allowed in ((GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017, True, True),
|
||||
(GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017, False, True),
|
||||
(GMSafetyFlags.NO_ACC, True, False)):
|
||||
with self.subTest(variant=variant, pedal=pedal):
|
||||
self.init_mode(variant, pedal=pedal)
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.set_torque_driver(0, 0)
|
||||
self.safety.set_desired_torque_last(400)
|
||||
self.safety.set_rt_torque_last(400)
|
||||
self.assertEqual(self.safety.safety_tx_hook(steer), allowed)
|
||||
|
||||
def test_stock_acc_ownership_requires_fresh_confirmed_disengagement(self):
|
||||
self.init_mode(GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2)
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
|
||||
def active(counter):
|
||||
return self.packet(create_pedal_command(self.packer, .2, counter))
|
||||
|
||||
def disabled(counter):
|
||||
return self.packet(create_pedal_command(self.packer, 0., counter))
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(active(1))) # startup status unknown
|
||||
self.assertTrue(self.safety.safety_tx_hook(disabled(1)))
|
||||
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 0}))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(active(2))) # main state unknown
|
||||
self.safety.safety_rx_hook(self.stock("ECMEngineStatus", {"CruiseMainOn": 1}))
|
||||
self.assertTrue(self.safety.safety_tx_hook(active(2)))
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 1}))
|
||||
self.assertFalse(self.safety.safety_tx_hook(active(3)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_bolt_regen_paddle(self.packer, True))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_bolt_regen_gear(self.packer, True, True))))
|
||||
self.assertTrue(self.safety.safety_tx_hook(disabled(3)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg("ASCMSteeringButton", 2, {"ACCButtons": 6}))))
|
||||
|
||||
self.safety.safety_rx_hook(self.stock("AcceleratorPedal2", {"CruiseState": 0}))
|
||||
self.safety.safety_rx_hook(self.sensor(2))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertTrue(self.safety.safety_tx_hook(active(4)))
|
||||
self.safety.safety_rx_hook(self.stock("ECMEngineStatus", {"CruiseMainOn": 0}))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(active(5)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(disabled(5)))
|
||||
self.safety.safety_rx_hook(self.stock("ECMEngineStatus", {"CruiseMainOn": 1}))
|
||||
self.safety.set_timer(300001)
|
||||
self.safety.safety_rx_hook(self.sensor(3))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(active(6)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(disabled(6)))
|
||||
|
||||
def test_scheduler_rejects_all_noncanonical_payloads(self):
|
||||
for variant in (GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017,
|
||||
GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2):
|
||||
with self.subTest(variant=variant):
|
||||
self.init_mode(variant)
|
||||
self.safety.safety_rx_hook(self.low_gear())
|
||||
self.safety.safety_rx_hook(self.sensor(1))
|
||||
self.safety.set_controls_allowed(True)
|
||||
paddle = create_bolt_regen_paddle(self.packer, True)
|
||||
gear = create_bolt_regen_gear(self.packer, True, bool(variant & GMSafetyFlags.BOLT_GEN2))
|
||||
bad = bytearray(paddle[1])
|
||||
bad[1] = 1
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet((paddle[0], bytes(bad), paddle[2]))))
|
||||
bad = bytearray(paddle[1])
|
||||
bad[0] = 0x30
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet((paddle[0], bytes(bad), paddle[2]))))
|
||||
for index, replacement in ((0, 0x0D), (3, 0x0F), (5, 3), (6, 2)):
|
||||
bad = bytearray(gear[1])
|
||||
bad[index] = replacement
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet((gear[0], bytes(bad), gear[2]))))
|
||||
wrong_generation = create_bolt_regen_gear(self.packer, True, not bool(variant & GMSafetyFlags.BOLT_GEN2))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(wrong_generation)))
|
||||
self.assertEqual(self.recorded(), [])
|
||||
|
||||
def test_real_parser_controller_and_native_safety_four_variants(self):
|
||||
for candidate in PEDAL_BOLT_CAR:
|
||||
with self.subTest(candidate=candidate):
|
||||
cp = params(candidate, True, True)
|
||||
self.assertTrue(cp.openpilotLongitudinalControl)
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyModel, CarParams.SafetyModel.gm)
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm,
|
||||
cp.safetyConfigs[0].safetyParam), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.reset_recorded_can()
|
||||
self.safety.set_timer(950000)
|
||||
packer = CANPacker(DBC[candidate][Bus.pt])
|
||||
car_state = CarState(cp)
|
||||
parsers = car_state.get_can_parsers(cp)
|
||||
car_state.update(parsers)
|
||||
sensor = packer.make_can_msg("GAS_SENSOR", 0, {"INTERCEPTOR_GAS": 0,
|
||||
"INTERCEPTOR_GAS2": 0,
|
||||
"COUNTER_PEDAL": 1, "STATE": 0})
|
||||
sensor_data = bytearray(sensor[1])
|
||||
sensor_data[5] = pedal_crc(sensor_data)
|
||||
source = [(sensor[0], bytes(sensor_data), sensor[2]),
|
||||
packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6, "ManualMode": 0})]
|
||||
if candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL:
|
||||
source.append(packer.make_can_msg("AcceleratorPedal2", 0, {"CruiseState": 0}))
|
||||
source.append(packer.make_can_msg("ECMEngineStatus", 0, {"CruiseMainOn": 1}))
|
||||
parsers[Bus.pt].update([(950_000_000, source)])
|
||||
out = car_state.update(parsers)
|
||||
self.assertTrue(car_state.pedal_sensor_healthy)
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(source[0])))
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(source[1])))
|
||||
if len(source) > 2:
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(source[2])))
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(source[3])))
|
||||
self.assertFalse(out.cruiseState.enabled)
|
||||
self.assertEqual(car_state.stock_acc_status_ts_nanos, 950_000_000)
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertEqual(out.gearShifter, structs.CarState.GearShifter.low)
|
||||
|
||||
controller = CarController(DBC[candidate], cp)
|
||||
controller.frame = 4
|
||||
controller.last_steer_frame = 4
|
||||
control = structs.CarControl()
|
||||
control.enabled = True
|
||||
control.longActive = True
|
||||
control.actuators.accel = 1.
|
||||
out.vEgo = 12.
|
||||
cs = SimpleNamespace(out=out.as_reader(), pedal_sensor_healthy=car_state.pedal_sensor_healthy,
|
||||
pedal_sensor_ts_nanos=car_state.pedal_sensor_ts_nanos,
|
||||
stock_acc_status_ts_nanos=car_state.stock_acc_status_ts_nanos,
|
||||
cam_lka_steering_cmd_counter=0, loopback_lka_steering_cmd_updated=False,
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0,
|
||||
buttons_counter=0)
|
||||
_, commands = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
acc_pedal = candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL
|
||||
self.assertEqual([msg[0] for msg in commands], [0x200, 0x1F5, 0xBD] + ([0x315] if acc_pedal else []))
|
||||
self.safety.set_timer(1_000_000)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(commands[0])))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(commands[1])))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(commands[2])))
|
||||
if acc_pedal:
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(commands[3])))
|
||||
self.assertEqual(self.recorded(), [])
|
||||
self.safety.safety_rx_hook(self.packet(packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6})))
|
||||
self.safety.safety_rx_hook(self.packet(packer.make_can_msg("EBCMRegenPaddle", 0, {"RegenPaddle": 0})))
|
||||
self.assertEqual([item[0] for item in self.recorded()], [0x1F5, 0xBD])
|
||||
|
||||
controller.frame = 8
|
||||
self.safety.reset_recorded_can()
|
||||
_, released = controller.update(control.as_reader(), cs, 1_200_000_000)
|
||||
released = [msg for msg in released if msg[0] in (0x200, 0x1F5, 0xBD)]
|
||||
self.assertEqual([msg[0] for msg in released], [0x200, 0x1F5, 0xBD])
|
||||
self.assertEqual(released[0][1][0:4], b"\x00" * 4)
|
||||
self.assertEqual(released[1][1], b"\x0c\x0c\x00\x06\x00\x00\x01\x00")
|
||||
self.assertEqual(released[2][1], b"\x00" * 7)
|
||||
self.safety.set_timer(1_200_000)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(released[0])))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(released[1])))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(released[2])))
|
||||
self.safety.safety_rx_hook(self.packet(packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6})))
|
||||
self.safety.safety_rx_hook(self.packet(packer.make_can_msg("EBCMRegenPaddle", 0, {"RegenPaddle": 0})))
|
||||
self.assertEqual(self.recorded(), [(0x1F5, 0, released[1][1]), (0xBD, 0, released[2][1])])
|
||||
@@ -0,0 +1,299 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car.gm.gmcan import create_buttons, create_pedal_command, create_steering_control
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def crc(data):
|
||||
value = 255
|
||||
for byte in reversed(data[:5]):
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = ((value << 1) ^ (0xD5 if value & 128 else 0)) & 255
|
||||
return value
|
||||
|
||||
|
||||
class TestGmCcPedal(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.packer = CANPacker('gm_global_a_powertrain_generated')
|
||||
self.init(0xC180)
|
||||
|
||||
def init(self, word):
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, word), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(0)
|
||||
|
||||
@staticmethod
|
||||
def packet(msg):
|
||||
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
|
||||
|
||||
def rx(self, addr, data, bus=0):
|
||||
return self.safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))
|
||||
|
||||
def buttons(self, counter, button=1):
|
||||
return self.packet(create_buttons(self.packer, 0, counter, button))
|
||||
|
||||
def sensor(self, counter, track1=604, track2=304, bad_crc=False):
|
||||
data = bytearray([track1 >> 8, track1 & 255, track2 >> 8, track2 & 255, counter, 0])
|
||||
data[5] = crc(data) ^ int(bad_crc)
|
||||
return data
|
||||
|
||||
def feed(self, counter=0, active=False, main=True, gear=4, brake=False):
|
||||
self.rx(0x184, bytes(8))
|
||||
self.rx(0x34A, bytes(5))
|
||||
self.rx(0xF1, bytes(6))
|
||||
self.rx(0x1C4, bytes(8))
|
||||
c9 = bytearray(8)
|
||||
c9[3] = 32 if main else 0
|
||||
c9[5] = int(brake)
|
||||
self.rx(0xC9, c9)
|
||||
prndl = bytearray(8)
|
||||
prndl[3] = gear
|
||||
self.rx(0x1F5, prndl)
|
||||
self.rx(0x201, self.sensor(counter))
|
||||
stock = bytearray(8)
|
||||
stock[4] = 128 if active else 0
|
||||
self.rx(0x3D1, stock)
|
||||
self.safety.safety_rx_hook(self.buttons(counter % 4))
|
||||
|
||||
def command(self, counter, fraction=.2):
|
||||
return self.packet(create_pedal_command(self.packer, fraction, counter))
|
||||
|
||||
def engage(self):
|
||||
self.safety.safety_rx_hook(self.buttons(0, 2))
|
||||
|
||||
def test_physical_latch_independent_from_stock_and_sensor_drop_recovery(self):
|
||||
for word in (0xC180, 0xC181):
|
||||
self.init(word)
|
||||
self.feed(active=False)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.rx(0x201, self.sensor(3)) # Missed sensor counters do not revoke the independent latch.
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(2)))
|
||||
self.rx(0x3D1, bytes(8))
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(3)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(4)))
|
||||
low = bytearray(8)
|
||||
low[3] = 6
|
||||
self.rx(0x1F5, low)
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
low[3] = 4
|
||||
self.rx(0x1F5, low)
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(1)))
|
||||
|
||||
def test_sensor_crc_override_and_stale_sources(self):
|
||||
self.feed()
|
||||
self.engage()
|
||||
self.rx(0x201, self.sensor(1, bad_crc=True))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0, 0.)))
|
||||
self.rx(0x201, self.sensor(4))
|
||||
self.engage()
|
||||
# Release then press is a genuine physical rearm edge.
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(1)))
|
||||
self.safety.set_timer(100001)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(2)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(2, 0.)))
|
||||
self.rx(0x201, self.sensor(8, track1=900, track2=450))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(3)))
|
||||
|
||||
def test_cancel_has_physical_credit_and_never_acceleration_buttons(self):
|
||||
self.feed(active=True)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(1, 6)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1, 6)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1, 2)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1, 3)))
|
||||
self.safety.set_timer(50000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(2, 6)))
|
||||
self.safety.set_timer(100000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(2, 6)))
|
||||
self.rx(0x3D1, bytes(8))
|
||||
self.safety.safety_rx_hook(self.buttons(2))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(3, 6)))
|
||||
|
||||
def test_main_brake_gear_and_wrong_sensor_provenance(self):
|
||||
for main, gear, brake in ((False, 4, False), (True, 2, False), (True, 0, False), (True, 4, True)):
|
||||
self.init(0xC180)
|
||||
self.feed(main=main, gear=gear, brake=brake)
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.init(0xC180)
|
||||
self.feed()
|
||||
self.engage()
|
||||
self.safety.set_timer(100001)
|
||||
self.rx(0x201, self.sensor(1), bus=2)
|
||||
self.rx(0x201, self.sensor(1)[:-1])
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
|
||||
def test_topology_and_unowned_messages(self):
|
||||
for word in (0xC180, 0xC181):
|
||||
self.init(word)
|
||||
for addr, length in ((0x315, 8), (0x2CB, 5), (0x370, 6), (0x3D1, 8), (0xBD, 7), (0x1F5, 8)):
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes(length))))
|
||||
for addr in (0x409, 0x40A):
|
||||
packet = libsafety_py.make_CANPacket(addr, 0, bytes(7))
|
||||
self.assertEqual(self.safety.safety_tx_hook(packet), word == 0xC181)
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes([1] + [0] * 6))))
|
||||
|
||||
def test_cancel_jitter_skipped_slot_and_idle_recovery(self):
|
||||
self.feed(active=True)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(1, 6)))
|
||||
self.safety.set_timer(110000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(2, 6)))
|
||||
self.safety.set_timer(220000)
|
||||
self.safety.safety_rx_hook(self.buttons(3)) # A dropped physical neutral cannot grant fresh credit.
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0, 6)))
|
||||
self.safety.set_timer(250000)
|
||||
self.safety.safety_rx_hook(self.buttons(0))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(1, 6)))
|
||||
self.safety.set_timer(600000)
|
||||
self.feed(counter=1, active=True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(2, 6)))
|
||||
self.safety.set_timer(650000)
|
||||
self.safety.safety_rx_hook(self.buttons(2))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(3, 6)))
|
||||
|
||||
def test_aol_lateral_never_grants_pedal_longitudinal(self):
|
||||
for word in (0xC180, 0xC181):
|
||||
self.safety.set_alternative_experience(32)
|
||||
self.init(word)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.feed(active=False)
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1)
|
||||
steer = self.packet(create_steering_control(self.packer, 0, 5, 0, True))
|
||||
self.assertTrue(self.safety.safety_tx_hook(steer))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.safety.set_alternative_experience(0)
|
||||
|
||||
def test_recorded_adc_tracks_and_overflow_keep_command_pair_contract(self):
|
||||
for sample in ('053502ba0164', '04e30286048f', '046c024108e3', '0279012a06f6'):
|
||||
with self.subTest(sample=sample):
|
||||
self.init(0xC180)
|
||||
self.feed()
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
data = bytes.fromhex(sample)
|
||||
self.assertEqual(crc(data), data[5])
|
||||
self.assertTrue(self.rx(0x201, data))
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.command(1)), sample == '0279012a06f6')
|
||||
self.init(0xC180)
|
||||
self.feed()
|
||||
self.engage()
|
||||
command = create_pedal_command(self.packer, .2, 1)
|
||||
bad_pair = bytearray(command[1])
|
||||
bad_pair[2:4] = (int.from_bytes(bad_pair[2:4], "big") + 12).to_bytes(2, "big")
|
||||
bad_pair[5] = crc(bad_pair)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet((command[0], bytes(bad_pair), command[2]))))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(command)))
|
||||
for track1, track2 in ((4096, 304), (604, 4096)):
|
||||
self.feed(counter=8 if track1 == 4096 else 9)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.rx(0x201, self.sensor(2 if track1 == 4096 else 3, track1=track1, track2=track2))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
|
||||
def volt_feed(self, word, counter=0, *, active=True, regen=False):
|
||||
self.feed(counter=counter, active=active)
|
||||
if not (word & 1):
|
||||
self.rx(0xBE, bytes(6))
|
||||
self.rx(0xBD, bytes([16 if regen else 0] + [0] * 6))
|
||||
|
||||
def test_volt_cc_exact_records_and_release_denial(self):
|
||||
release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0
|
||||
for index in range(8):
|
||||
word = 0xE600 + index
|
||||
with self.subTest(word=word):
|
||||
self.init(word)
|
||||
self.volt_feed(word)
|
||||
self.engage()
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.command(0)), not release)
|
||||
for address, length in ((0x315, 5), (0x2CB, 8), (0x370, 6), (0x2CD, 8), (0x306, 8)):
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, 0, bytes(length))))
|
||||
for address in (0x409, 0x40A):
|
||||
self.assertEqual(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, 0, bytes(7))),
|
||||
bool(index & 2) and not release)
|
||||
for word in (0xE5FF, 0xE608, 0xE60F, 0xE618, 0xE61F, 0xE6FF):
|
||||
self.init(word)
|
||||
self.feed()
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
|
||||
def test_volt_cc_independent_adc_gas_threshold_and_regen(self):
|
||||
if self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0:
|
||||
self.skipTest("Active Volt CC interceptor profiles are DEBUG-only")
|
||||
for raw1, raw2, gas in ((0, 787, False), (0, 788, True), (604, 486, False), (604, 487, True)):
|
||||
self.init(0xE600)
|
||||
self.volt_feed(0xE600)
|
||||
self.engage()
|
||||
self.assertTrue(self.rx(0x201, self.sensor(1, raw1, raw2)))
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.command(0)), not gas)
|
||||
self.init(0xE601)
|
||||
self.volt_feed(0xE601, regen=True)
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0, 0.)))
|
||||
|
||||
def test_volt_cc_stock_optional_pedal_and_gear_cannot_withdraw_cruise(self):
|
||||
for index in range(8):
|
||||
word = 0xE610 + index
|
||||
self.init(word)
|
||||
# Feed every stock-owned source, deliberately never supplying 201 or gear.
|
||||
for address, length in ((0x184, 8), (0x34A, 5), (0x1C4, 8), (0xBD, 7)):
|
||||
self.rx(address, bytes(length))
|
||||
self.rx(0xF1 if index & 1 else 0xBE, bytes(6))
|
||||
main = bytearray(8)
|
||||
main[3] = 32
|
||||
self.rx(0xC9, main)
|
||||
cruise = bytearray(8)
|
||||
cruise[4] = 128
|
||||
self.rx(0x3D1, cruise)
|
||||
self.safety.safety_rx_hook(self.buttons(0))
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.rx(0x201, self.sensor(1, 4096, 0))
|
||||
self.rx(0x1F5, bytes(8))
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
cancel = create_buttons(self.packer, 2, 1, 6)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(cancel)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(cancel)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1, 6)))
|
||||
|
||||
def test_volt_cc_sequential_command_counter_and_source_expiry(self):
|
||||
if self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0:
|
||||
self.skipTest("Active Volt CC interceptor profiles are DEBUG-only")
|
||||
self.init(0xE600)
|
||||
self.volt_feed(0xE600)
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(3)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(2)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(1)))
|
||||
self.safety.set_timer(100001)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(2)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(2, 0.)))
|
||||
@@ -0,0 +1,204 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car.gm.gmcan import create_buttons, create_pedal_command, create_steering_control
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def crc(data):
|
||||
value = 255
|
||||
for byte in reversed(data[:5]):
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = ((value << 1) ^ (0xD5 if value & 128 else 0)) & 255
|
||||
return value
|
||||
|
||||
|
||||
class TestGmCcPedalDisabled(unittest.TestCase):
|
||||
WORDS = (0xC186, 0xC187)
|
||||
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.packer = CANPacker('gm_global_a_powertrain_generated')
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.init(self.WORDS[0])
|
||||
|
||||
def tearDown(self):
|
||||
self.safety.set_alternative_experience(0)
|
||||
|
||||
def init(self, word):
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, word), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(0)
|
||||
|
||||
@staticmethod
|
||||
def packet(msg):
|
||||
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
|
||||
|
||||
def rx(self, addr, data, bus=0):
|
||||
return self.safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))
|
||||
|
||||
def buttons(self, counter, button=1, bus=0):
|
||||
return self.packet(create_buttons(self.packer, bus, counter, button))
|
||||
|
||||
def sensor(self, counter, track1=604, track2=304, bad_crc=False):
|
||||
data = bytearray([track1 >> 8, track1 & 255, track2 >> 8, track2 & 255, counter, 0])
|
||||
data[5] = crc(data) ^ int(bad_crc)
|
||||
return data
|
||||
|
||||
def sources(self, counter=0, active=True, main=True, brake=False, gear=4, skip=None):
|
||||
c9, stock, prndl = bytearray(8), bytearray(8), bytearray(8)
|
||||
c9[3], c9[5], stock[4], prndl[3] = int(main) << 5, int(brake), int(active) << 7, gear
|
||||
sources = {0x184: bytes(8), 0x34A: bytes(5), 0xF1: bytes(6), 0x1C4: bytes(8),
|
||||
0xC9: c9, 0x1F5: prndl, 0x201: self.sensor(counter), 0x3D1: stock}
|
||||
for addr, data in sources.items():
|
||||
if addr != skip:
|
||||
self.rx(addr, data)
|
||||
if skip != 0x1E1:
|
||||
self.safety.safety_rx_hook(self.buttons(counter % 4))
|
||||
|
||||
def cancel(self, counter, bus=2):
|
||||
return self.buttons(counter, 6, bus)
|
||||
|
||||
def test_observed_camera_cancel_and_no_stock_inactive_exception(self):
|
||||
for word in self.WORDS:
|
||||
for active in (False, True):
|
||||
self.init(word)
|
||||
self.sources(active=active)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.cancel(0)), active)
|
||||
# Switching owners must not leak the disabled bus/counter codec into active words.
|
||||
self.init(0xC181 if word == 0xC187 else 0xC180)
|
||||
self.sources()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0, 0)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(1, 0)))
|
||||
|
||||
def test_main_stock_button_leases(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources(main=False)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.safety.set_timer(300000)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
for missing in (0xC9, 0x3D1, 0x1E1):
|
||||
with self.subTest(word=word, missing=missing):
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.safety.set_timer(300001)
|
||||
self.sources(counter=1, skip=missing)
|
||||
if missing != 0x1E1:
|
||||
self.safety.safety_rx_hook(self.buttons(2))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0 if missing == 0x1E1 else 2)))
|
||||
|
||||
def test_full_template_wrong_bus_counter_and_buttons(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0, 0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
for button in (0, 1, 2, 3, 4, 5, 7):
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0, button, 2)))
|
||||
canonical = create_buttons(self.packer, 2, 0, 6)[1]
|
||||
for bit in range(56):
|
||||
data = bytearray(canonical)
|
||||
data[bit // 8] ^= 1 << (bit % 8)
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1E1, 2, bytes(data))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1E1, 2, canonical[:-1])))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
|
||||
def test_neutral_credit_cadence_and_interleaved_stock(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.safety.set_timer(40000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
self.safety.set_timer(40001)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
self.safety.set_timer(90000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
self.safety.safety_rx_hook(self.buttons(3))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(3)))
|
||||
self.safety.safety_rx_hook(self.buttons(0))
|
||||
self.rx(0x3D1, bytes(8))
|
||||
active = bytearray(8)
|
||||
active[4] = 128
|
||||
self.rx(0x3D1, active)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
|
||||
def test_cancel_while_braking_or_driver_override_and_sensor_fault(self):
|
||||
for word in self.WORDS:
|
||||
for condition in ('brake', 'gas', 'reverse', 'sensor_crc', 'sensor_replay'):
|
||||
with self.subTest(word=word, condition=condition):
|
||||
self.init(word)
|
||||
self.sources(brake=condition == 'brake', gear=2 if condition == 'reverse' else 4)
|
||||
if condition == 'gas':
|
||||
self.rx(0x201, self.sensor(1, 900, 450))
|
||||
elif condition == 'sensor_crc':
|
||||
self.rx(0x201, self.sensor(1, bad_crc=True))
|
||||
elif condition == 'sensor_replay':
|
||||
self.rx(0x201, self.sensor(0))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.safety.set_controls_allowed(True)
|
||||
for fraction in (0., .2):
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, fraction, 0))))
|
||||
|
||||
def test_common_rx_and_relay_health(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.safety.set_relay_malfunction(True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
self.safety.set_relay_malfunction(False)
|
||||
self.safety.set_timer(2000000)
|
||||
self.safety.safety_tick()
|
||||
self.sources(counter=1)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(1)))
|
||||
self.safety.safety_tick()
|
||||
self.safety.safety_rx_hook(self.buttons(2))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(2)))
|
||||
|
||||
def test_camera_acc_status_does_not_replace_pt_cruise(self):
|
||||
for word in self.WORDS:
|
||||
for active in (False, True):
|
||||
self.init(word)
|
||||
self.sources(active=active)
|
||||
status = bytearray(6)
|
||||
status[2] = 0 if active else 128
|
||||
self.rx(0x370, status, 2)
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.cancel(0)), active)
|
||||
self.init(word)
|
||||
self.sources(skip=0x3D1)
|
||||
status[2] = 128
|
||||
self.rx(0x370, status, 2)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
|
||||
def test_no_longitudinal_authority_including_aol(self):
|
||||
for word in self.WORDS:
|
||||
for alternative in (0, 32):
|
||||
self.safety.set_alternative_experience(alternative)
|
||||
self.init(word)
|
||||
self.sources()
|
||||
if alternative:
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1)
|
||||
steer = self.packet(create_steering_control(self.packer, 0, 5, 0, True))
|
||||
self.assertTrue(self.safety.safety_tx_hook(steer))
|
||||
self.safety.set_controls_allowed(True)
|
||||
if alternative:
|
||||
self.safety.aol_set_host_request(3)
|
||||
for fraction in (0., .2, 1.):
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(create_pedal_command(self.packer, fraction, 0))))
|
||||
for addr, length in ((0x409, 7), (0x40A, 7), (0x315, 5), (0x2CB, 8), (0x370, 6), (0x3D1, 8), (0xBD, 7), (0x1F5, 8)):
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes(length))))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.cancel(0)))
|
||||
@@ -0,0 +1,277 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car.gm.gmcan import create_buttons, create_pedal_command, create_steering_control
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def crc(data):
|
||||
value = 255
|
||||
for byte in reversed(data[:5]):
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = ((value << 1) ^ (0xD5 if value & 128 else 0)) & 255
|
||||
return value
|
||||
|
||||
|
||||
class TestGmSilveradoPedal(unittest.TestCase):
|
||||
WORDS = (0xC182, 0xC183, 0xC184, 0xC185)
|
||||
LENGTHS = {0x184: 8, 0x34A: 5, 0xC9: 8, 0x3D1: 8, 0x1E1: 7, 0x1C4: 8, 0x1F5: 8, 0x201: 6}
|
||||
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.packer = CANPacker('gm_global_a_powertrain_generated')
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.init(0xC182)
|
||||
|
||||
def tearDown(self):
|
||||
self.safety.set_alternative_experience(0)
|
||||
|
||||
def init(self, word):
|
||||
self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, word), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(0)
|
||||
|
||||
@staticmethod
|
||||
def packet(msg):
|
||||
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
|
||||
|
||||
def rx(self, addr, data, bus=0):
|
||||
return self.safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))
|
||||
|
||||
def buttons(self, counter, button=1, bus=0):
|
||||
return self.packet(create_buttons(self.packer, bus, counter, button))
|
||||
|
||||
def sensor(self, counter, track1=604, track2=304, state=0, bad_crc=False):
|
||||
data = bytearray([track1 >> 8, track1 & 255, track2 >> 8, track2 & 255, (state << 4) | counter, 0])
|
||||
data[5] = crc(data) ^ int(bad_crc)
|
||||
return data
|
||||
|
||||
def sources(self, counter=0, active=False, main=True, gear=4, brake=False, skip=None, bus=0):
|
||||
c9, stock, prndl = bytearray(8), bytearray(8), bytearray(8)
|
||||
c9[3], c9[5], stock[4], prndl[3] = (32 if main else 0), int(brake), (128 if active else 0), gear
|
||||
values = {0x184: bytes(8), 0x34A: bytes(5), 0xC9: c9, 0x3D1: stock,
|
||||
0x1C4: bytes(8), 0x1F5: prndl, 0x201: self.sensor(counter)}
|
||||
for addr, data in values.items():
|
||||
if addr != skip:
|
||||
self.rx(addr, data, bus=bus)
|
||||
if skip != 0x1E1:
|
||||
self.safety.safety_rx_hook(self.buttons(counter % 4, bus=bus))
|
||||
|
||||
def command(self, counter, fraction=.2, bus=0):
|
||||
msg = create_pedal_command(self.packer, fraction, counter)
|
||||
return libsafety_py.make_CANPacket(msg[0], bus, msg[1])
|
||||
|
||||
def engage(self):
|
||||
self.safety.safety_rx_hook(self.buttons(0, 2))
|
||||
|
||||
def test_active_pedal_latch_and_stock_cancel_pending(self):
|
||||
for word in self.WORDS[:2]:
|
||||
for active in (False, True):
|
||||
self.init(word)
|
||||
self.sources(active=active)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(2)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(4)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(3, bus=2)))
|
||||
|
||||
def test_cancel_bus_counter_and_disabled_stock_state(self):
|
||||
for word in self.WORDS:
|
||||
disabled = word >= 0xC184
|
||||
for active in (False, True):
|
||||
self.init(word)
|
||||
self.sources(counter=2, active=active)
|
||||
counter, bus = (2, 2) if disabled else (3, 0)
|
||||
# An interleaved stock-off update must not erase valid disabled credit.
|
||||
stock = bytearray(8)
|
||||
stock[4] = 128 if active else 0
|
||||
self.rx(0x3D1, stock)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(counter, 6, 2 - bus)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons((counter + 1) % 4, 6, bus)))
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.buttons(counter, 6, bus)), disabled or active)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(counter, 6, bus)))
|
||||
for button in (1, 2, 3):
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(counter, button, bus)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
|
||||
def test_cancel_credit_cadence_replay_and_noncanonical_packet(self):
|
||||
for word in self.WORDS:
|
||||
disabled, bus = word >= 0xC184, 2 if word >= 0xC184 else 0
|
||||
self.init(word)
|
||||
self.sources(active=True)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(0 if disabled else 1, 6, bus)))
|
||||
self.safety.set_timer(40000)
|
||||
self.sources(counter=1, active=True)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1 if disabled else 2, 6, bus)))
|
||||
self.safety.set_timer(40001)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(1 if disabled else 2, 6, bus)))
|
||||
self.safety.set_timer(90000)
|
||||
self.safety.safety_rx_hook(self.buttons(1))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1 if disabled else 2, 6, bus)))
|
||||
self.sources(counter=2, active=True)
|
||||
msg = create_buttons(self.packer, bus, 2 if disabled else 3, 6)
|
||||
for index in range(7):
|
||||
mutated = bytearray(msg[1])
|
||||
mutated[index] ^= 1
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1E1, bus, mutated)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(2 if disabled else 3, 6, bus)))
|
||||
|
||||
def test_every_selected_source_is_required_and_bus_length_bound(self):
|
||||
for word in self.WORDS:
|
||||
for addr, length in self.LENGTHS.items():
|
||||
self.init(word)
|
||||
self.sources(active=True, skip=addr)
|
||||
self.rx(addr, bytes(length), bus=2)
|
||||
self.rx(addr, bytes(length - 1))
|
||||
if word >= 0xC184:
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0, 6, 2)))
|
||||
else:
|
||||
self.safety.set_controls_allowed(True) # Isolate RX freshness from the engagement latch.
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
|
||||
def test_each_source_staleness_and_recovery(self):
|
||||
for word in self.WORDS:
|
||||
for addr in self.LENGTHS:
|
||||
self.init(word)
|
||||
self.sources(active=True)
|
||||
self.safety.set_timer(300001)
|
||||
self.sources(counter=1, active=True, skip=addr)
|
||||
self.safety.set_controls_allowed(True)
|
||||
if word >= 0xC184:
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(1, 6, 2)))
|
||||
else:
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.safety.set_timer(350001)
|
||||
self.sources(counter=2, active=True)
|
||||
self.safety.safety_rx_hook(self.buttons(3))
|
||||
self.safety.set_controls_allowed(True)
|
||||
if word >= 0xC184:
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.buttons(3, 6, 2)))
|
||||
else:
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
|
||||
def test_main_off_and_disabled_never_pedal(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources(active=True, main=False)
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0 if word >= 0xC184 else 1, 6, 2 if word >= 0xC184 else 0)))
|
||||
if word >= 0xC184:
|
||||
self.init(word)
|
||||
self.sources(active=True)
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0, 0.)))
|
||||
|
||||
def test_digital_brake_driver_gas_sensor_integrity_and_forward_gear(self):
|
||||
for word in self.WORDS[:2]:
|
||||
for source in ('brake', 'gas', 'crc', 'pair', 'state', 'replay', 'reverse', 'manual'):
|
||||
self.init(word)
|
||||
self.sources(brake=source == 'brake', gear=2 if source == 'reverse' else 4)
|
||||
if source == 'gas':
|
||||
self.rx(0x201, self.sensor(1, 900, 450))
|
||||
elif source == 'crc':
|
||||
self.rx(0x201, self.sensor(1, bad_crc=True))
|
||||
elif source == 'pair':
|
||||
self.rx(0x201, self.sensor(1, 900, 304))
|
||||
elif source == 'state':
|
||||
self.rx(0x201, self.sensor(1, state=1))
|
||||
elif source == 'replay':
|
||||
self.rx(0x201, self.sensor(0))
|
||||
elif source == 'manual':
|
||||
prndl = bytearray(8)
|
||||
prndl[3], prndl[5] = 4, 2
|
||||
self.rx(0x1F5, prndl)
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.engage()
|
||||
self.rx(0xF1, bytes([0, 255, 0, 0, 0, 0]))
|
||||
self.rx(0xBE, bytes([0, 255, 0, 0, 0, 0]))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0)))
|
||||
low = bytearray(8)
|
||||
low[3] = 6
|
||||
self.rx(0x1F5, low)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(1)))
|
||||
|
||||
def test_malformed_sensor_cannot_refund_disabled_cancel_and_pedal_packet_masks(self):
|
||||
for word in self.WORDS[2:]:
|
||||
for replay in (False, True):
|
||||
self.init(word)
|
||||
self.sources(active=False)
|
||||
self.rx(0x201, self.sensor(0 if replay else 1, bad_crc=not replay))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0, 6, 2)))
|
||||
for word in self.WORDS[:2]:
|
||||
self.init(word)
|
||||
self.sources()
|
||||
self.engage()
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(0, 1.)))
|
||||
msg = create_pedal_command(self.packer, .2, 1)
|
||||
for index in range(6):
|
||||
changed = bytearray(msg[1])
|
||||
changed[index] ^= 1
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x200, 0, changed)))
|
||||
changed = bytearray(msg[1])
|
||||
changed[4] |= 0x10
|
||||
changed[5] = crc(changed)
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x200, 0, changed)))
|
||||
changed[4] &= ~0x10
|
||||
changed[0], changed[1] = 2634 >> 8, 2634 & 255
|
||||
changed[5] = crc(changed)
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x200, 0, changed)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.command(1, 0.)))
|
||||
|
||||
def test_topology_no_unfed_scheduler_or_unowned_speed_packets(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources()
|
||||
for addr, length in ((0x315, 5), (0x2CB, 8), (0x2CD, 5), (0x370, 6), (0x3D1, 8), (0xBD, 7),
|
||||
(0x1F5, 8), (0xA1, 7), (0x306, 8), (0x308, 7), (0x310, 2)):
|
||||
for bus in range(3):
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(length))))
|
||||
for addr in (0x409, 0x40A):
|
||||
self.assertEqual(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes(7))), word == 0xC183)
|
||||
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes([1] + [0] * 6))))
|
||||
for bus, addr in ((0, 0x184), (0, 0x3D1), (2, 0x180), (2, 0x370)):
|
||||
self.assertEqual(self.safety.safety_fwd_hook(bus, addr), -1)
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0)
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x2CB), 0)
|
||||
|
||||
def test_reset_removes_cancel_and_pedal_authority(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.sources(active=True)
|
||||
self.engage()
|
||||
self.init(word)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.buttons(0 if word >= 0xC184 else 1, 6, 2 if word >= 0xC184 else 0)))
|
||||
|
||||
def test_aol_does_not_grant_longitudinal_and_stock_only_stays_denied(self):
|
||||
for word in self.WORDS:
|
||||
self.safety.set_alternative_experience(32)
|
||||
self.init(word)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.sources()
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(create_steering_control(self.packer, 0, 5, 0, True))))
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.engage()
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.command(0)), word < 0xC184)
|
||||
|
||||
def test_alternative_zero_has_no_aol_axis_permission(self):
|
||||
for word in self.WORDS:
|
||||
self.init(word)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.sources()
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.command(0)))
|
||||
Reference in New Issue
Block a user