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..108a972c6b 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,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"): diff --git a/openpilot/starpilot/aol/panda_faults.py b/openpilot/starpilot/aol/panda_faults.py index 049a01b16d..f5539bdf0c 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 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: 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..9c3be454c2 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_honda_panda_faults.py @@ -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 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', {})