From 40abb31b4da400f147089f0a56ad8c0e18f99cb1 Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 19 Oct 2026 22:16:19 -0700 Subject: [PATCH] Honda: Keep longitudinal through temporary EPS faults and ignore camera CAN interrupt-rate faults --- opendbc_repo/opendbc/car/honda/stock_aol.py | 47 ++++++++++++------- .../opendbc/safety/modes/honda_stock_aol.h | 9 ++-- .../safety/tests/test_honda_stock_aol.py | 18 +++++++ openpilot/starpilot/aol/panda_faults.py | 13 +++-- .../aol/tests/test_honda_panda_faults.py | 16 +++++++ .../starpilot/tests/test_feature_runtime.py | 25 ++++++++++ 6 files changed, 102 insertions(+), 26 deletions(-) create mode 100644 openpilot/starpilot/aol/tests/test_honda_panda_faults.py diff --git a/opendbc_repo/opendbc/car/honda/stock_aol.py b/opendbc_repo/opendbc/car/honda/stock_aol.py index cc7e115941..68984e2aab 100644 --- a/opendbc_repo/opendbc/car/honda/stock_aol.py +++ b/opendbc_repo/opendbc/car/honda/stock_aol.py @@ -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 diff --git a/opendbc_repo/opendbc/safety/modes/honda_stock_aol.h b/opendbc_repo/opendbc/safety/modes/honda_stock_aol.h index 4d30596952..bc0f99b371 100644 --- a/opendbc_repo/opendbc/safety/modes/honda_stock_aol.h +++ b/opendbc_repo/opendbc/safety/modes/honda_stock_aol.h @@ -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) { diff --git a/opendbc_repo/opendbc/safety/tests/test_honda_stock_aol.py b/opendbc_repo/opendbc/safety/tests/test_honda_stock_aol.py index 967f3e50ac..c8eab86776 100644 --- a/opendbc_repo/opendbc/safety/tests/test_honda_stock_aol.py +++ b/opendbc_repo/opendbc/safety/tests/test_honda_stock_aol.py @@ -144,6 +144,24 @@ 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_normal_grant_session_loss_before_first_permission_query(self): for case in CASES: for loss in ("heartbeat", "sources"): diff --git a/openpilot/starpilot/aol/panda_faults.py b/openpilot/starpilot/aol/panda_faults.py index 049a01b16d..05186425f7 100644 --- a/openpilot/starpilot/aol/panda_faults.py +++ b/openpilot/starpilot/aol/panda_faults.py @@ -1,4 +1,4 @@ -"""Compatibility policy for two recorded GM configurations; native authority stays separate. +"""Compatibility policy for Honda 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. @@ -6,9 +6,15 @@ claim. Raw fault reports are retained. Every unlisted configuration/bit blocks. from openpilot.cereal import log from opendbc.car.gm.cancel_remap import aol_enabled +_FAULT_BITS = log.PandaState.FaultType.schema.enumerants +# Honda Bosch camera CAN latches interrupt-rate faults every drive; they report bus load, not safety. +_HONDA_INTERRUPT_RATE_MASK = sum(1 << value for name, value in _FAULT_BITS.items() if name.startswith('interruptRate')) + def _diagnostic_mask(cp): try: + if cp.brand == 'honda': + return _HONDA_INTERRUPT_RATE_MASK 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: @@ -44,9 +50,6 @@ def blocking_fault_mask(mask, cp): def blocking_fault_list(faults, cp): try: allowed = _diagnostic_mask(cp) - named = ((log.PandaState.FaultType.interruptRateCan1, 1 << 2), - (log.PandaState.FaultType.interruptRateCan2, 1 << 3), - (log.PandaState.FaultType.registerDivergent, 1 << 18)) - return any(not any(fault == name and allowed & bit for name, bit in named) for fault in faults) + return any(not any(fault == value and allowed & (1 << value) for value in _FAULT_BITS.values()) for fault in faults) except (TypeError, ValueError, OverflowError): return True diff --git a/openpilot/starpilot/aol/tests/test_honda_panda_faults.py b/openpilot/starpilot/aol/tests/test_honda_panda_faults.py new file mode 100644 index 0000000000..7c7691d78f --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_honda_panda_faults.py @@ -0,0 +1,16 @@ +from types import SimpleNamespace + +import pytest +from openpilot.cereal import log +from openpilot.starpilot.aol.panda_faults import blocking_fault_mask, blocking_fault_list + + +@pytest.mark.parametrize('fault, blocks', [('interruptRateCan2', False), ('interruptRateUsb', False), + ('relayMalfunction', True), ('registerDivergent', True), + ('heartbeatLoopWatchdog', True)]) +def test_honda_latched_interrupt_rate_faults_do_not_block(fault, blocks): + cp = SimpleNamespace(brand='honda') + value = log.PandaState.FaultType.schema.enumerants[fault] + assert blocking_fault_mask(1 << value, cp) == blocks + assert blocking_fault_list([value], cp) == blocks + assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2, value], cp) == blocks diff --git a/openpilot/starpilot/tests/test_feature_runtime.py b/openpilot/starpilot/tests/test_feature_runtime.py index 7f4b229ac5..43753fa329 100644 --- a/openpilot/starpilot/tests/test_feature_runtime.py +++ b/openpilot/starpilot/tests/test_feature_runtime.py @@ -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', {})