Honor Panda fault policy before ECU takeover

This commit is contained in:
firestar5683
2026-10-21 22:50:55 -05:00
parent 4b9de265bf
commit 21c74f72ef
3 changed files with 100 additions and 2 deletions
@@ -0,0 +1,96 @@
"""Actual Card diagnostic gate before ECU mutation, including the recorded G90 IRQ8."""
from types import SimpleNamespace
from typing import cast
from unittest.mock import patch
import pytest
from opendbc.car import structs
from opendbc.car.hyundai.g90_startup import G90Startup, Outcome
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.aol.tests.test_sonata_irq_policy import recorded as sonata_recorded
def recorded(identity='GENESIS_G90'):
cp = structs.CarParams(brand='hyundai', carFingerprint=identity, flags=0x1001000,
openpilotLongitudinalControl=True, pcmCruise=False, alternativeExperience=32,
safetyConfigs=[structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.hyundai, safetyParam=0xc04)])
if identity == 'KIA_EV9':
cp.flags = 2281712129
cp.alternativeExperience = 0
cp.steerControlType = 'angle'
cp.safetyConfigs[0].safetyModel = 'hyundaiCanfd'
cp.safetyConfigs[0].safetyParam = 0x5c95
return cp
def card_for(cp, faults):
card = object.__new__(Car)
panda = SimpleNamespace(safetyModel=structs.CarParams.SafetyModel.elm327, safetyParam=1,
controlsAllowed=False, safetyRxChecksInvalid=False, ignitionLine=True, ignitionCan=False, faults=faults)
class Source:
seen = valid = alive = {'pandaStates': True}
logMonoTime = {'pandaStates': 10_000_000_000}
recv_time = {'pandaStates': 10.}
def __init__(self, panda):
self.messages = {'pandaStates': [panda]}
def __getitem__(self, name):
return self.messages[name]
def update(self, timeout):
pass
if faults is not None:
message = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=10_000_000_000)
message.pandaStates[0] = dict(safetyModel='elm327', safetyParam=1, controlsAllowed=False,
safetyRxChecksInvalid=False, ignitionLine=True, ignitionCan=False, faults=faults)
panda = message.as_reader().pandaStates[0]
card.sm = cast(messaging.SubMaster, Source(panda))
card.params = cast(Params, SimpleNamespace(get_bool=lambda name: False))
card.vehicle_startup = VehicleStartupOwner(SimpleNamespace(cp=cp))
return card
@pytest.mark.parametrize('identity', ['GENESIS_G90', 'KIA_EV9', None])
@pytest.mark.parametrize('faults,allowed', [([], True), (['interruptRateCan2'], False),
(['interruptRateCan1'], False), (['relayMalfunction'], False), (['registerDivergent'], False),
(['interruptRateCan2', 'relayMalfunction'], False), (None, False)])
def test_current_fault_admission_cannot_mutate_an_unexempt_vehicle(identity, faults, allowed):
cp = recorded(identity) if identity else None
card = card_for(cp, faults)
with patch('time.monotonic_ns', return_value=10_000_000_000), patch('time.clock_gettime_ns', return_value=10_000_000_000):
assert card.startup_diagnostic_admission() is allowed
def test_recorded_g90_fault_prevents_ecu_mutation():
cp = recorded()
before = cp.to_dict()
card = card_for(cp, ['interruptRateCan2'])
sends = []
owner = G90Startup(cp, (lambda **kwargs: [], sends.extend))
card.vehicle_startup.owner = owner
with patch('time.monotonic_ns', return_value=10_000_000_000), patch('time.clock_gettime_ns', return_value=10_000_000_000):
assert not card.startup_diagnostic_admission()
fallback = owner.prepare(admission=card.startup_diagnostic_admission)
assert owner.outcome is Outcome.STOCK_UNTOUCHED
assert not owner.session_attempted and not owner.disable_attempted and not sends
assert not fallback.openpilotLongitudinalControl and fallback.pcmCruise
assert cp.to_dict() == before
assert [str(value) for value in card.sm['pandaStates'][0].faults] == ['interruptRateCan2']
@pytest.mark.parametrize('mixed', [False, True])
def test_existing_exact_sonata_diagnostic_classification_is_preserved(mixed):
cp = sonata_recorded()
before = cp.to_dict()
faults = ['interruptRateCan2'] + (['relayMalfunction'] if mixed else [])
card = card_for(cp, faults)
with patch('time.monotonic_ns', return_value=10_000_000_000), patch('time.clock_gettime_ns', return_value=10_000_000_000):
assert card.startup_diagnostic_admission() is (not mixed)
assert cp.to_dict() == before and [str(value) for value in card.sm['pandaStates'][0].faults] == faults
Executable → Regular
+3 -1
View File
@@ -1228,6 +1228,7 @@ class Car:
panda_current=panda_current)
def startup_diagnostic_admission(self):
from openpilot.starpilot.aol.panda_faults import blocking_fault_list
self.sm.update(0)
now, boot = time.monotonic_ns(), time.clock_gettime_ns(time.CLOCK_BOOTTIME)
sm = self.sm
@@ -1247,7 +1248,8 @@ class Car:
0 < stamp <= boot and boot - stamp <= 300_000_000 and
0 < receipt <= now and now - receipt <= 300_000_000 and len(pandas) == count and
all(ps.safetyModel == structs.CarParams.SafetyModel.elm327 and ps.safetyParam == 1 and
not ps.controlsAllowed and not ps.safetyRxChecksInvalid for ps in pandas) and
not ps.controlsAllowed and not ps.safetyRxChecksInvalid and
not blocking_fault_list(getattr(ps, 'faults', None), getattr(owner, 'cp', None)) for ps in pandas) and
all(ps.ignitionLine or ps.ignitionCan for ps in ignition_pandas))
def startup_panda_configured(self, *, inactive_keepalive=False):
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "0f60df516139cd3d3de7afc26989a738ea49e9525f48ce8a4089784a9abd64e0", "schema_version": 1}
{"protected_inventory_sha256": "8795c3f74d210f6cde5b0bca56d488ffe2eee20dd7f56ea29593d66f0a7ee1c3", "schema_version": 1}