Honda: Keep longitudinal through temporary EPS faults and ignore camera CAN interrupt-rate faults

This commit is contained in:
Danny
2026-10-20 15:41:03 +00:00
committed by firestar5683
6 changed files with 216 additions and 22 deletions
+29 -18
View File
@@ -29,37 +29,48 @@ NIDEC = frozenset((
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11))
NIDEC_WORDS = frozenset((0, 4, 260))
# qualified() runs several times per 100 Hz frame in card and selfdrived, so
# everything that depends only on this module is computed once, and its flag
# tests use plain ints: IntFlag operators cost microseconds each in Python.
_DETECTED = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM | HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID)
_ALLOWED_FLAGS = {str(car): int(car.config.flags) | _DETECTED for car in CLASSIC_BOSCH | NIDEC | RADARLESS}
_BOSCH, _BOSCH_RADARLESS, _BOSCH_ALT_BRAKE = int(HondaFlags.BOSCH), int(HondaFlags.BOSCH_RADARLESS), int(HondaFlags.BOSCH_ALT_BRAKE)
_NIDEC, _NIDEC_ALT_SCM_MESSAGES = int(HondaFlags.NIDEC), int(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
_UNSUPPORTED = int(HondaFlags.BOSCH_CANFD | HondaFlags.BOSCH_ALT_RADAR)
_FIXED = structs.CarParams.FingerprintSource.fixed
_TRANSMISSIONS = (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt)
_HONDA_NIDEC, _HONDA_BOSCH = structs.CarParams.SafetyModel.hondaNidec, structs.CarParams.SafetyModel.hondaBosch
def qualified(cp, *, marked_only=False):
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) != 1 or
cp.fingerprintSource == structs.CarParams.FingerprintSource.fixed or
cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) or
cp.fingerprintSource == _FIXED or cp.transmissionType not in _TRANSMISSIONS or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
safety = cp.safetyConfigs[0]
identity = next((candidate for candidate in CLASSIC_BOSCH | NIDEC | RADARLESS if candidate == cp.carFingerprint), None)
if identity is None:
fingerprint = str(cp.carFingerprint)
allowed = _ALLOWED_FLAGS.get(fingerprint)
if allowed is None:
return False
detected = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM | HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID)
if int(cp.flags) & ~(int(identity.config.flags) | detected):
flags = int(cp.flags)
if flags & ~allowed:
return False
flags = HondaFlags(cp.flags)
if flags & (HondaFlags.BOSCH_CANFD | HondaFlags.BOSCH_ALT_RADAR):
if flags & _UNSUPPORTED:
return False
if cp.carFingerprint in NIDEC and flags & HondaFlags.NIDEC and not flags & HondaFlags.BOSCH:
expected = 4 if flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 0
if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
if fingerprint in NIDEC and flags & _NIDEC and not flags & _BOSCH:
expected = 4 if flags & _NIDEC_ALT_SCM_MESSAGES else 0
if fingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
expected |= 256
return (cp.pcmCruise and cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaNidec and safety.safetyParam == expected)
if cp.carFingerprint in CLASSIC_BOSCH and flags & HondaFlags.BOSCH and not flags & HondaFlags.BOSCH_RADARLESS:
expected = int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE))
safety.safetyModel == _HONDA_NIDEC and safety.safetyParam == expected)
if fingerprint in CLASSIC_BOSCH and flags & _BOSCH and not flags & _BOSCH_RADARLESS:
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
return (cp.pcmCruise and not cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
if cp.carFingerprint in RADARLESS and flags & HondaFlags.BOSCH_RADARLESS:
expected = 8 | int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if fingerprint in RADARLESS and flags & _BOSCH_RADARLESS:
expected = 8 | int(bool(flags & _BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
return False
@@ -94,9 +94,12 @@ static uint8_t honda_stock_aol_request_mask(void) {
static uint8_t honda_stock_aol_permission_mask(void) {
const uint8_t request = honda_stock_aol_request_mask();
uint8_t permission = 0U;
if (acc_main_on && honda_stock_aol_drive &&
((honda_stock_aol_eps == 0U) || (honda_stock_aol_eps == 3U) || (honda_stock_aol_eps == 4U))) {
if (controls_allowed || honda_stock_aol_token) {
// A temporary EPS fault (2/6, e.g. a hard driver override) withdraws lateral only;
// longitudinal stays with controls_allowed as in stock openpilot.
const bool eps_lateral = (honda_stock_aol_eps == 0U) || (honda_stock_aol_eps == 3U) || (honda_stock_aol_eps == 4U);
const bool eps_known = eps_lateral || (honda_stock_aol_eps == 2U) || (honda_stock_aol_eps == 6U);
if (acc_main_on && honda_stock_aol_drive && eps_known) {
if (eps_lateral && (controls_allowed || honda_stock_aol_token)) {
permission = request & 1U;
}
if (controls_allowed) {
@@ -144,6 +144,38 @@ class TestHondaStockAol(unittest.TestCase):
self.assertFalse(self.safety.get_controls_allowed())
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
def test_temporary_eps_during_engagement_keeps_longitudinal(self):
# Hard driver override: EPS reports 2/6 while engaged; only lateral may drop.
for case in CASES:
for eps in (2, 6):
with self.subTest(case=case[0], eps=eps):
self.configure(case)
self.stream()
self.normal_engage()
self.safety.aol_set_host_request(3)
self.assertEqual(self.safety.aol_get_permission_mask(), 3 if self.scoped else 0)
self.stream(eps=eps, cruise=1, request=False)
self.assertTrue(self.safety.get_controls_allowed())
self.assertEqual(self.safety.aol_get_permission_mask(), 2 if self.scoped else 0)
self.stream(cruise=1, request=False)
self.assertEqual(self.safety.aol_get_permission_mask(), 3 if self.scoped else 0)
self.stream(eps=7, cruise=1, request=False)
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
def test_aol_only_temporary_eps_recovers_without_restart(self):
for case in CASES:
with self.subTest(case=case[0]):
self.configure(case)
self.arm()
for eps in (2, 6, 2):
self.stream(eps=eps)
self.assertFalse(self.safety.get_controls_allowed())
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
self.safety.aol_set_host_request(0)
self.stream()
self.assertFalse(self.safety.get_controls_allowed())
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
def test_normal_grant_session_loss_before_first_permission_query(self):
for case in CASES:
for loss in ("heartbeat", "sources"):
+8 -1
View File
@@ -1,4 +1,4 @@
"""Compatibility policy for two recorded GM configurations; native authority stays separate.
"""Compatibility policy for qualified Bosch Honda AOL and two recorded GM configurations; native authority stays separate.
Register divergence remains a protected-register diagnostic, not a benign-fault
claim. Raw fault reports are retained. Every unlisted configuration/bit blocks.
@@ -9,6 +9,13 @@ from opendbc.car.gm.cancel_remap import aol_enabled
def _diagnostic_mask(cp):
try:
if cp.brand == 'honda':
from opendbc.car.honda.stock_aol import qualified
from opendbc.car.honda.values import HondaFlags
# Only the reported CAN2 interrupt-rate diagnostic is exempt. Qualification
# requires a marked, supported, single-panda Bosch AOL configuration.
# Other interrupt rates and mixed faults remain blocking.
return (1 << 3) if int(cp.flags) & int(HondaFlags.BOSCH) and qualified(cp, marked_only=True) else 0
from opendbc.car.gm.aol import qualified_gm
from opendbc.car.gm.values import CAR, is_volt_sdgm_profile, is_bolt_present_no_acc_pedal_profile
if cp.brand != 'gm' or len(cp.safetyConfigs) != 1:
@@ -0,0 +1,116 @@
"""Only reported CAN2 load diagnostics on current qualified Bosch AOL profiles."""
from dataclasses import replace
from types import SimpleNamespace
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.stock_aol import qualified
from opendbc.car.honda.values import CAR
from openpilot.cereal import log
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask, blocking_fault_list
from openpilot.starpilot.aol.runtime import current_native, decide_axes
from openpilot.starpilot.aol.tests.test_panda_inventory import bound_state, sm_for, NOW
from openpilot.starpilot.aol.tests.test_runtime import car_state
PROFILES = ((CAR.HONDA_CIVIC_BOSCH, False), (CAR.HONDA_ACCORD, False),
(CAR.HONDA_CIVIC_2022, False), (CAR.HONDA_CIVIC_2022, True))
def honda(identity=CAR.HONDA_CIVIC_BOSCH, alpha=False):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x1A3] = 8
fingerprint[1][0x1A3] = 8
cp = CarInterface.get_params(identity, fingerprint, [], alpha, False, False)
cp.fingerprintSource = structs.CarParams.FingerprintSource.fw
cp.alternativeExperience = 32
assert qualified(cp, marked_only=True)
return cp
@pytest.mark.parametrize('identity,alpha', PROFILES)
def test_reported_can2_fault_on_qualified_bosch_is_diagnostic(identity, alpha):
cp = honda(identity, alpha)
assert not blocking_fault_mask(1 << 3, cp)
assert not blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp)
@pytest.mark.parametrize('identity,alpha', PROFILES)
@pytest.mark.parametrize('fault,value', tuple(log.PandaState.FaultType.schema.enumerants.items()))
def test_all_other_known_and_mixed_faults_still_block(identity, alpha, fault, value):
cp = honda(identity, alpha)
blocks = fault != 'interruptRateCan2'
assert blocking_fault_mask(1 << value, cp) == blocks
assert blocking_fault_mask((1 << value) | (1 << 3), cp) == blocks
assert blocking_fault_list([value], cp) == blocks
assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2, value], cp) == blocks
@pytest.mark.parametrize('mutation', ('unmarked', 'fixed', 'passive', 'dashcam', 'notcar', 'extra_panda',
'wrong_model', 'wrong_word', 'unknown_flags', 'manual', 'nidec', 'unknown_identity'))
def test_other_honda_configurations_cannot_borrow_exception(mutation):
cp = honda()
if mutation == 'unmarked':
cp.alternativeExperience = 0
elif mutation == 'fixed':
cp.fingerprintSource = 'fixed'
elif mutation == 'passive':
cp.passive = True
elif mutation == 'dashcam':
cp.dashcamOnly = True
elif mutation == 'notcar':
cp.notCar = True
elif mutation == 'extra_panda':
cp.safetyConfigs = [cp.safetyConfigs[0], cp.safetyConfigs[0]]
elif mutation == 'wrong_model':
cp.safetyConfigs[0].safetyModel = 'hondaNidec'
elif mutation == 'wrong_word':
cp.safetyConfigs[0].safetyParam = 16
elif mutation == 'unknown_flags':
cp.flags |= 1 << 31
elif mutation == 'manual':
cp.transmissionType = 'manual'
elif mutation == 'nidec':
cp = honda(CAR.HONDA_CIVIC)
else:
cp.carFingerprint = 'UNKNOWN HONDA'
assert blocking_fault_mask(1 << 3, cp)
assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp)
@pytest.mark.parametrize('mask', (-1, 1 << 27, 1 << 31, 1 << 32))
def test_unknown_or_malformed_faults_are_not_diagnostic(mask):
cp = honda()
assert blocking_fault_mask(mask, cp)
assert blocking_fault_list([31], cp)
@pytest.mark.parametrize('mutation', ('none', 'rx', 'heartbeat', 'relay', 'expired', 'wrong_session', 'denied'))
def test_real_inventory_exception_does_not_create_authority_or_bypass_vetoes(mutation):
cp = honda()
state = bound_state(cp)
slot = replace(state.pandaInventory[0], faults=1 << 3)
if mutation == 'rx':
slot = replace(slot, safetyRxChecksInvalid=True)
elif mutation == 'heartbeat':
slot = replace(slot, heartbeatLost=True)
elif mutation == 'relay':
slot = replace(slot, faults=(1 << 3) | 1)
state = replace(state, pandaInventory=(slot,))
if mutation == 'denied':
state = replace(state, lateralAllowed=False, longitudinalAllowed=False)
native = current_native(sm_for(cp, state), cp,
now_ns=NOW + (200_000_001 if mutation == 'expired' else 0),
axis_session_id='wrong' if mutation == 'wrong_session' else state.axisSessionId)
if mutation in ('rx', 'heartbeat', 'relay', 'expired', 'wrong_session'):
assert native is None
return
assert native == state
decision = decide_axes(standard_lateral=True, standard_longitudinal=False,
intent=SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False),
native=native, car_state=car_state(), initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0.)
assert decision.lateral_active == (mutation == 'none')
assert not decision.longitudinal_active
@@ -176,3 +176,28 @@ def test_gm_saved_profiles_require_existing_longitudinal_owner():
(root / 'CustomPersonalities').write_bytes(b'1')
(root / 'LongitudinalPersonalityProfiles').write_text('{broken')
assert not enabled(params, ascm_params(CAR.CHEVROLET_VOLT, radar=True), 'profile', {})
def test_honda_slc_controls_only_with_openpilot_long():
from types import SimpleNamespace
from openpilot.starpilot.feature_runtime import slc_runtime_settings, slc_transport_capable, vision_control_enabled
def honda(**overrides):
return SimpleNamespace(**{'brand': 'honda', 'carFingerprint': 'HONDA_CIVIC_2022', 'openpilotLongitudinalControl': True,
'pcmCruise': False, 'passive': False, 'dashcamOnly': False, 'notCar': False, **overrides})
with TemporaryDirectory() as temporary:
params = Files(Path(temporary))
(params.root / 'SpeedLimitController').write_bytes(b'1')
(params.root / 'SLCPriority1').write_bytes(b'Dashboard')
(params.root / 'SLCPriority2').write_bytes(b'Map Data')
assert slc_transport_capable(honda(), {})
assert enabled(params, honda(), 'slc', {})
assert slc_runtime_settings(params, honda(), {}).enabled
assert not enabled(params, honda(), 'vision', {})
assert not vision_control_enabled(params, honda())
stock = honda(openpilotLongitudinalControl=False, pcmCruise=True)
assert enabled(params, stock, 'slc', {})
assert not slc_runtime_settings(params, stock, {}).enabled
assert not slc_transport_capable(honda(dashcamOnly=True), {})
assert not enabled(params, honda(dashcamOnly=True), 'slc', {})