mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-20 07:43:48 +08:00
Harden Rivian angle and bridge safety
Keep Rivian angle commands below 4 m/s², exercise the common angle-safety coverage, and prevent generic firmware management from overwriting unverified external Black Pandas. Original safety work based on PR #90 by TonyJOM (Anthony Orta). Co-authored-by: TonyJOM <anthonyorta20@icloud.com>
This commit is contained in:
@@ -14,7 +14,8 @@ from opendbc.car.rivian.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.rivian.interface import CarInterface
|
||||
from opendbc.car.rivian.toi_controller import (TOI_ACK_FRAMES, TOI_MAX_ANGLE_FRAMES,
|
||||
TOI_RECOVERY_TIMEOUT_FRAMES, ToiController, ToiState)
|
||||
from opendbc.car.rivian.values import CAR, FW_QUERY_CONFIG, WMI, ModelLine, ModelYear, RivianFlags, RivianSafetyFlags
|
||||
from opendbc.car.rivian.values import (CAR, FW_QUERY_CONFIG, MAX_ALLOWED_LATERAL_ACCEL, CarControllerParams, WMI,
|
||||
ModelLine, ModelYear, RivianFlags, RivianSafetyFlags)
|
||||
|
||||
|
||||
class TestRivian:
|
||||
@@ -173,6 +174,9 @@ class TestRivian:
|
||||
|
||||
assert not controller.angle_saturated
|
||||
|
||||
def test_angle_command_envelope_is_below_product_lateral_accel_limit(self):
|
||||
assert 0.0 < CarControllerParams.ANGLE_LIMITS.MAX_LATERAL_ACCEL <= MAX_ALLOWED_LATERAL_ACCEL
|
||||
|
||||
def test_angle_saturation_param_is_seeded_and_edge_written(self):
|
||||
writes = []
|
||||
controller = CarController.__new__(CarController)
|
||||
|
||||
@@ -113,6 +113,7 @@ GEAR_MAP = {
|
||||
}
|
||||
|
||||
AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, conservative banked-road allowance for angle safety
|
||||
MAX_ALLOWED_LATERAL_ACCEL = 4.0 # m/s^2; hard product requirement for Rivian angle control
|
||||
|
||||
|
||||
class CarControllerParams:
|
||||
@@ -139,7 +140,10 @@ class CarControllerParams:
|
||||
500,
|
||||
([], []),
|
||||
([], []),
|
||||
MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL),
|
||||
# Keep the vehicle-model command envelope below the product's absolute
|
||||
# lateral-acceleration ceiling, including the road-roll allowance.
|
||||
MAX_LATERAL_ACCEL=min(MAX_ALLOWED_LATERAL_ACCEL,
|
||||
ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL)),
|
||||
MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL),
|
||||
MAX_ANGLE_RATE=2.5,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from opendbc.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness
|
||||
from opendbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from opendbc.car.rivian.values import CarControllerParams
|
||||
from opendbc.car.rivian.values import CarControllerParams, MAX_ALLOWED_LATERAL_ACCEL
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
import opendbc.safety.tests.common as common
|
||||
from opendbc.safety.tests.common import CANPackerSafety
|
||||
@@ -168,8 +168,14 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety
|
||||
return self.packer.make_can_msg_safety("EPAS_AdasStatus", 0, {"EPAS_InternalSas": angle})
|
||||
|
||||
def test_angle_cmd_when_enabled(self):
|
||||
# The VM-based lateral acceleration and jerk limits are exercised below.
|
||||
pass
|
||||
# The VM-based lateral acceleration and jerk limits are exercised by the
|
||||
# dedicated tests below; keep this common-suite hook explicit rather than
|
||||
# silently skipping the enabled-angle coverage.
|
||||
self._check_lateral_accel_limit()
|
||||
self._check_lateral_jerk_limit()
|
||||
|
||||
def test_product_lateral_accel_limit(self):
|
||||
self.assertLessEqual(CarControllerParams.ANGLE_LIMITS.MAX_LATERAL_ACCEL, MAX_ALLOWED_LATERAL_ACCEL)
|
||||
|
||||
def _can_to_deg(self, can_value):
|
||||
return can_value / self.DEG_TO_CAN
|
||||
@@ -180,7 +186,7 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety
|
||||
stored = round(speed_kph_can / 3.6 * 1000)
|
||||
return max(stored / 1000.0 - 1.0, 1.0)
|
||||
|
||||
def test_lateral_accel_limit(self):
|
||||
def _check_lateral_accel_limit(self):
|
||||
for speed in np.linspace(0, 40, 100):
|
||||
speed = max(self._round_speed(speed), 1)
|
||||
for sign in (-1, 1):
|
||||
@@ -198,7 +204,7 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety
|
||||
should_tx = above_can > self.STEER_ANGLE_MAX * self.DEG_TO_CAN
|
||||
self.assertEqual(should_tx, self._tx(self._angle_cmd_msg(above_deg, True)))
|
||||
|
||||
def test_lateral_jerk_limit(self):
|
||||
def _check_lateral_jerk_limit(self):
|
||||
for speed in np.linspace(0, 40, 100):
|
||||
speed = max(self._round_speed(speed), 1)
|
||||
for sign in (-1, 1):
|
||||
|
||||
@@ -77,13 +77,13 @@ def _flash_panda(panda: Panda) -> None:
|
||||
def is_rivian_bridge_panda(panda: Panda, rivian: bool | None = None) -> bool:
|
||||
if panda.is_internal() or panda.get_type() != Panda.HW_TYPE_BLACK:
|
||||
return False
|
||||
if rivian is None:
|
||||
rivian = _is_rivian()
|
||||
if rivian or panda.bootstub:
|
||||
return rivian
|
||||
# A cached Rivian CarParams record is not sufficient evidence to identify a
|
||||
# bridge. An arbitrary external Black Panda may be connected at the same
|
||||
# time, and flashing it would permanently replace its firmware. Only the
|
||||
# signed bridge image can positively identify the hardware.
|
||||
try:
|
||||
expected_signature = Panda.get_signature_from_firmware(FW_PATH)
|
||||
return panda.get_signature() == expected_signature
|
||||
return not panda.bootstub and panda.get_signature() == expected_signature
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -108,12 +108,14 @@ def prepare_rivian_bridge(panda_serials: list[str]) -> set[str]:
|
||||
continue
|
||||
|
||||
bridge_confirmed = is_rivian_bridge_panda(panda, rivian)
|
||||
if not bridge_confirmed:
|
||||
if not bridge_confirmed and not rivian:
|
||||
continue
|
||||
|
||||
bridge_serials.add(serial)
|
||||
if rivian and firmware_available:
|
||||
if bridge_confirmed and rivian and firmware_available:
|
||||
_flash_panda(panda)
|
||||
elif rivian:
|
||||
cloudlog.warning(f"External Black Panda {serial} is not signed with the Rivian bridge firmware; leaving it untouched")
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to prepare Rivian Extreme harness bridge {serial}")
|
||||
finally:
|
||||
|
||||
@@ -46,8 +46,10 @@ def test_outdated_rivian_bridge_uses_bridge_flash_path():
|
||||
panda_class.usb_list.return_value = ["bridge"]
|
||||
panda_class.get_signature_from_firmware.return_value = b"expected"
|
||||
|
||||
# An unknown firmware image cannot be safely identified as the bridge and
|
||||
# must be left to explicit provisioning instead of being overwritten.
|
||||
assert flasher.prepare_rivian_bridge(["internal", "bridge"]) == {"bridge"}
|
||||
flash_panda.assert_called_once_with(panda)
|
||||
flash_panda.assert_not_called()
|
||||
|
||||
|
||||
def test_non_rivian_matching_bridge_is_reserved_without_flashing():
|
||||
@@ -78,3 +80,18 @@ def test_non_rivian_external_black_panda_is_not_misidentified():
|
||||
|
||||
assert flasher.prepare_rivian_bridge(["internal", "external"]) == set()
|
||||
flash_panda.assert_not_called()
|
||||
|
||||
|
||||
def test_bootstub_external_black_panda_is_not_misidentified():
|
||||
panda = _external_black_panda(signature=b"expected", bootstub=True)
|
||||
with patch.object(flasher, "_is_rivian", return_value=True), \
|
||||
patch.object(flasher.os.path, "isfile", return_value=True), \
|
||||
patch.object(flasher, "Panda", wraps=flasher.Panda) as panda_class, \
|
||||
patch.object(flasher, "_flash_panda") as flash_panda:
|
||||
panda_class.return_value = panda
|
||||
panda_class.HW_TYPE_BLACK = b"\x03"
|
||||
panda_class.usb_list.return_value = ["bridge"]
|
||||
panda_class.get_signature_from_firmware.return_value = b"expected"
|
||||
|
||||
assert flasher.prepare_rivian_bridge(["bridge"]) == {"bridge"}
|
||||
flash_panda.assert_not_called()
|
||||
|
||||
@@ -210,9 +210,16 @@ def flash_panda(params_memory):
|
||||
for serial in Panda.list():
|
||||
try:
|
||||
with Panda(serial=serial) as panda:
|
||||
if serial in usb_serials and is_rivian_bridge_panda(panda, rivian):
|
||||
print(f"Skipping Rivian harness bridge {serial}")
|
||||
continue
|
||||
if serial in usb_serials and not panda.is_internal() and panda.get_type() == Panda.HW_TYPE_BLACK:
|
||||
if is_rivian_bridge_panda(panda, rivian):
|
||||
print(f"Skipping Rivian harness bridge {serial}")
|
||||
continue
|
||||
if rivian:
|
||||
# Never let the generic updater overwrite an unverified external
|
||||
# Black Panda while a Rivian configuration is active. Provision a
|
||||
# bridge explicitly before allowing it into the normal updater.
|
||||
print(f"Skipping unverified external Black Panda on Rivian {serial}")
|
||||
continue
|
||||
print(f"Flashing Panda {serial}")
|
||||
flash_fn = None
|
||||
app_fn = panda.get_mcu_type().config.app_fn
|
||||
|
||||
Reference in New Issue
Block a user