From e672bd30c1cccf06408641b0a0223388f09cbf8f Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:33:22 -0500 Subject: [PATCH] Support SecOC Yaris AOL with stock and alpha longitudinal --- opendbc_repo/opendbc/car/toyota/interface.py | 3 + opendbc_repo/opendbc/car/toyota/values.py | 1 + opendbc_repo/opendbc/safety/modes/toyota.h | 4 +- .../opendbc/safety/modes/toyota_aol.h | 6 +- .../safety/tests/test_toyota_secoc_aol.py | 73 ++++++++++++++++--- .../starpilot/aol/tests/test_toyota_secoc.py | 69 +++++++++++++----- .../aol/tests/test_toyota_secoc_joined.py | 50 +++++++++---- openpilot/starpilot/car/toyota/aol.py | 15 +++- 8 files changed, 166 insertions(+), 55 deletions(-) diff --git a/opendbc_repo/opendbc/car/toyota/interface.py b/opendbc_repo/opendbc/car/toyota/interface.py index 2f192b95d8..14ec11e0a9 100644 --- a/opendbc_repo/opendbc/car/toyota/interface.py +++ b/opendbc_repo/opendbc/car/toyota/interface.py @@ -159,6 +159,9 @@ class CarInterface(CarInterfaceBase): ret.openpilotLongitudinalControl = ((bool(ret.flags & ToyotaFlags.TSS2) and not (ret.flags & ToyotaFlags.RADAR_ACC)) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) or (radar_filter and alpha_long)) + if candidate == CAR.TOYOTA_YARIS and ret.flags & ToyotaFlags.DISABLE_RADAR: + ret.safetyConfigs[0].safetyParam |= int(ToyotaSafetyFlags.SECOC_RADAR_LONG) + from opendbc.car.toyota import matrix_filter manual_matrix_filter = any(matrix_filter.hardware_supported(candidate, fingerprint, car_fw, length=size) for size in (4, 8)) if candidate in (CAR.TOYOTA_MATRIX_RETROFIT, CAR.TOYOTA_PRIUS_RETROFIT): diff --git a/opendbc_repo/opendbc/car/toyota/values.py b/opendbc_repo/opendbc/car/toyota/values.py index 405213cd96..ebf745f637 100644 --- a/opendbc_repo/opendbc/car/toyota/values.py +++ b/opendbc_repo/opendbc/car/toyota/values.py @@ -57,6 +57,7 @@ class ToyotaSafetyFlags(IntFlag): LTA = (4 << 8) SECOC = (8 << 8) LONG_FILTER = (16 << 8) + SECOC_RADAR_LONG = SECOC | LONG_FILTER # Exact radar-disable SecOC profile; separates its keepalive permission. GAS_INTERCEPTOR = (32 << 8) ALT_CRUISE = (64 << 8) # Exact Lexus IS DSU MAIN profile RAV4H_FILTER_PROFILE = (128 << 8) # Exact smartDSU owner; bit14 remains reserved for ALT_CRUISE diff --git a/opendbc_repo/opendbc/safety/modes/toyota.h b/opendbc_repo/opendbc/safety/modes/toyota.h index 72e477e8be..2753533b81 100644 --- a/opendbc_repo/opendbc/safety/modes/toyota.h +++ b/opendbc_repo/opendbc/safety/modes/toyota.h @@ -681,14 +681,14 @@ static safety_config toyota_init(uint16_t param) { const uint32_t TOYOTA_PARAM_STOCK_LONGITUDINAL = 2UL << TOYOTA_PARAM_OFFSET; const uint32_t TOYOTA_PARAM_LTA = 4UL << TOYOTA_PARAM_OFFSET; -#ifdef ALLOW_DEBUG const uint32_t TOYOTA_PARAM_SECOC = 8UL << TOYOTA_PARAM_OFFSET; +#ifdef ALLOW_DEBUG toyota_secoc = GET_FLAG(base_param, TOYOTA_PARAM_SECOC); #endif toyota_alt_brake = GET_FLAG(base_param, TOYOTA_PARAM_ALT_BRAKE); toyota_stock_longitudinal = GET_FLAG(base_param, TOYOTA_PARAM_STOCK_LONGITUDINAL); - toyota_radar_diagnostics_allowed = !toyota_dsu_bypass && !toyota_stock_longitudinal && !toyota_secoc && (base_param != 0x1049U) && (base_param != 0x1449U) && !toyota_eps8_sdsu && !toyota_legacy_sdsu && !toyota_legacy_advert_param(base_param) && !toyota_camera_2aa_param(base_param) && !toyota_prius_filter_param(base_param); + toyota_radar_diagnostics_allowed = !toyota_dsu_bypass && !toyota_stock_longitudinal && (!GET_FLAG(base_param, TOYOTA_PARAM_SECOC) || (toyota_secoc && (base_param == 6217U))) && (base_param != 0x1049U) && (base_param != 0x1449U) && !toyota_eps8_sdsu && !toyota_legacy_sdsu && !toyota_legacy_advert_param(base_param) && !toyota_camera_2aa_param(base_param) && !toyota_prius_filter_param(base_param); toyota_lta = !toyota_legacy_advert_param(base_param) && GET_FLAG(base_param, TOYOTA_PARAM_LTA); toyota_dbc_eps_torque_factor = base_param & TOYOTA_EPS_FACTOR; diff --git a/opendbc_repo/opendbc/safety/modes/toyota_aol.h b/opendbc_repo/opendbc/safety/modes/toyota_aol.h index de72898202..bb84766828 100644 --- a/opendbc_repo/opendbc/safety/modes/toyota_aol.h +++ b/opendbc_repo/opendbc/safety/modes/toyota_aol.h @@ -217,13 +217,13 @@ static void toyota_aol_configure(uint16_t param) { toyota_pedal = (param == 0x2049U) || (param == 0x2249U) || (param == 0x2449U) || (param == 0x2649U); toyota_aol_rav4h_filter = (param == 0x9049U) || (param == 0x9249U); toyota_sdsu_status = toyota_aol_rav4h_filter || toyota_eps8_sdsu || toyota_legacy_sdsu || toyota_prius_sdsu_param(param); - toyota_aol_stock = (toyota_dsu_bypass && ((param & 0x200U) != 0U)) || (param == 0x2649U) || (param == 585U) || (param == 856U) || (param == 4681U) || (param == 0x9249U) || (param == 0x2249U) || (param == 0x424DU) || (param == 0x649U) || (param == 0x1649U) || (param == 0x8249U) || (param == 0x8649U) || ((toyota_legacy_sdsu || toyota_legacy_advert_param(param) || toyota_camera_2aa_param(param) || toyota_prius_filter_param(param)) && ((param & 0x200U) != 0U)); #ifdef ALLOW_DEBUG - toyota_aol_secoc = (param == 2121U); + toyota_aol_secoc = (param == 2121U) || (param == 2633U) || (param == 6217U); #else toyota_aol_secoc = false; #endif - toyota_aol_enabled = (toyota_dsu_bypass && aol_requested) || ((toyota_eps8_sdsu || toyota_legacy_sdsu || toyota_legacy_advert_param(param) || toyota_camera_2aa_param(param) || toyota_prius_filter_param(param)) && aol_requested) || (toyota_aol_lta && aol_requested) || (toyota_aol_stock && aol_requested) || + toyota_aol_stock = (toyota_dsu_bypass && ((param & 0x200U) != 0U)) || (param == 0x2649U) || (param == 585U) || (param == 856U) || (param == 4681U) || (param == 0x9249U) || (param == 0x2249U) || (param == 0x424DU) || (param == 0x649U) || (param == 0x1649U) || (param == 0x8249U) || (param == 0x8649U) || ((toyota_legacy_sdsu || toyota_legacy_advert_param(param) || toyota_camera_2aa_param(param) || toyota_prius_filter_param(param)) && ((param & 0x200U) != 0U)) || (toyota_aol_secoc && (param == 2633U)); + toyota_aol_enabled = (toyota_dsu_bypass && aol_requested) || ((toyota_eps8_sdsu || toyota_legacy_sdsu || toyota_legacy_advert_param(param) || toyota_camera_2aa_param(param) || toyota_prius_filter_param(param)) && aol_requested) || (toyota_aol_lta && aol_requested) || (toyota_aol_stock && (!toyota_aol_secoc || (alternative_experience == 32)) && aol_requested) || #ifdef ALLOW_DEBUG (toyota_aol_secoc && (alternative_experience == 32)) || #endif diff --git a/opendbc_repo/opendbc/safety/tests/test_toyota_secoc_aol.py b/opendbc_repo/opendbc/safety/tests/test_toyota_secoc_aol.py index 5767f9006f..2c1e85332a 100644 --- a/opendbc_repo/opendbc/safety/tests/test_toyota_secoc_aol.py +++ b/opendbc_repo/opendbc/safety/tests/test_toyota_secoc_aol.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Always On Lateral on SecOC TSS2 Toyotas (2021+ Sienna, safety param 2121 = EPS scale 73 | SECOC). +"""Always On Lateral on SecOC TSS2 Toyotas: camera2121, Yaris stock2633 and radar takeover6217. They have no 0x3BC GEAR_PACKET: the gear is read from 0x127 GEAR_PACKET_HYBRID (P 0, R 1, N 2, D 3, B 4; checksummed, ~60 Hz). Everything else is the TSS2 rule: main switch, doors, EPS state, and for longitudinal also cruise and belt. @@ -25,6 +25,9 @@ def checksum(address, data): class TestToyotaSecocAol(unittest.TestCase): + WORD = WORD + BOTH_PERMISSION = 3 + def setUp(self): self.safety = libsafety_py.libsafety @@ -32,7 +35,8 @@ class TestToyotaSecocAol(unittest.TestCase): self.safety.set_alternative_experience(0) self.safety.set_safety_hooks(CarParams.SafetyModel.noOutput, 0) - def init(self, word=WORD, ae=32): + def init(self, word=None, ae=32): + word = self.WORD if word is None else word self.omit_addresses = set() self.safety.init_tests() self.safety.set_alternative_experience(ae) @@ -74,7 +78,7 @@ class TestToyotaSecocAol(unittest.TestCase): self.physical(gear=g) self.assertEqual(self.safety.aol_get_permission_mask(), 1) self.physical(gear=g, cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) def test_park_reverse_neutral_deny(self): for g in (P, R, N, 5, 15): @@ -151,7 +155,7 @@ class TestToyotaSecocAol(unittest.TestCase): for changes in ({"belt": False}, {"brake": True}, {"gas": True}): self.init() self.physical(cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) self.physical(cruise=True, **changes) self.assertEqual(self.safety.aol_get_permission_mask(), 1, changes) @@ -172,11 +176,11 @@ class TestToyotaSecocAol(unittest.TestCase): def test_lta_stays_blocked(self): self.init() self.physical(gear=D, cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x131, 0, bytes([8]) + bytes(7)))) def test_exact_admission(self): - for word, ae in ((WORD, 0), (WORD, 160), (WORD, 288), (WORD, 33), (WORD | 512, 32), (2120, 32), (2122, 32)): + for word, ae in ((self.WORD, 0), (self.WORD, 160), (self.WORD, 288), (self.WORD, 33), (self.WORD ^ 1, 32), (self.WORD ^ 2, 32)): self.init(word, ae) self.physical() self.assertEqual(self.safety.aol_get_request_mask(), 0, (word, ae)) @@ -220,7 +224,7 @@ class TestToyotaSecocAol(unittest.TestCase): def test_host_heartbeat_request_and_required_rx_health_are_independent(self): self.init() self.physical(cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) self.safety.set_aol_test_heartbeat(False) self.assertEqual(self.safety.aol_get_permission_mask(), 0) self.safety.set_aol_test_heartbeat(True) @@ -241,17 +245,17 @@ class TestToyotaSecocAol(unittest.TestCase): self.physical(cruise=True) self.safety.set_timer(1_000_000 + elapsed) self.physical(cruise=True, request=False) - self.assertEqual(self.safety.aol_get_permission_mask(), 3 if allowed else 0) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION if allowed else 0) def test_optional_source_corruption_revokes_until_fresh_recovery(self): for address in (0x1D3, 0x127, 0x620, 0x262): self.init() self.physical(cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) self.safety.safety_rx_hook(libsafety_py.make_CANPacket(address, 0, bytes(7))) self.assertEqual(self.safety.aol_get_permission_mask(), 0) self.physical(cruise=True) - self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertEqual(self.safety.aol_get_permission_mask(), self.BOTH_PERMISSION) if address != 0x620: self.safety.safety_rx_hook(libsafety_py.make_CANPacket(address, 0, bytes(8))) self.assertEqual(self.safety.aol_get_permission_mask(), 0) @@ -265,12 +269,59 @@ class TestToyotaSecocAol(unittest.TestCase): self.init() self.physical(cruise=True) packet = libsafety_py.make_CANPacket(0x183, 0, (accel & 0xffff).to_bytes(2, 'big') + bytes(6)) - self.assertEqual(bool(self.safety.safety_tx_hook(packet)), allowed) + self.assertEqual(bool(self.safety.safety_tx_hook(packet)), allowed and self.BOTH_PERMISSION == 3) self.init() self.physical(cruise=False) self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x183, 0, bytes([0, 1]) + bytes(6)))) self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x343, 0, bytes([0, 1]) + bytes(6)))) +class TestYarisStockSecocAol(TestToyotaSecocAol): + WORD = 2633 + BOTH_PERMISSION = 1 + + def test_stock_never_requests_long_or_blocks_stock_accel_forwarding(self): + self.init() + self.physical(cruise=True) + self.assertEqual(self.safety.aol_get_request_mask(), 1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for address in (0x183, 0x343): + self.assertEqual(self.safety.safety_fwd_hook(2, address), 0) + for value in (0, 100, -100): + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket( + 0x183, 0, (value & 0xffff).to_bytes(2, 'big') + bytes(6)))) + + +class TestYarisAlphaSecocAol(TestToyotaSecocAol): + WORD = 6217 + + def test_release_refuses_secoc_aol_and_radar_keepalive(self): + release = libsafety_py.ffi.dlopen(libsafety_py._build_libsafety(release=True)) + native = TestToyotaSecocAol(methodName='runTest') + native.safety = release + for word in (2121, 2633, 6217): + native.init(word) + native.physical(cruise=True) + self.assertEqual(release.aol_get_request_mask(), 0) + self.assertEqual(release.aol_get_permission_mask(), 0) + self.assertFalse(release.safety_tx_hook(libsafety_py.make_CANPacket( + 0x750, 0, bytes.fromhex('0f023e0000000000')))) + native.tearDown() + + def test_radar_keepalive_is_exact_and_profile_scoped(self): + for word in (2121, 2633, 6217, 6216, 6218): + self.init(word) + for bus in (0, 1, 2): + for payload in (bytes.fromhex('0f023e0000000000'), bytes.fromhex('0f03280101000000'), + bytes.fromhex('6d023e0000000000'), bytes.fromhex('0f023e0000000001')): + packet = libsafety_py.make_CANPacket(0x750, bus, payload) + self.assertEqual(bool(self.safety.safety_tx_hook(packet)), + word == 6217 and bus == 0 and payload == bytes.fromhex('0f023e0000000000')) + self.init() + self.physical(cruise=True) + for address in (0x183, 0x343): + self.assertEqual(self.safety.safety_fwd_hook(2, address), -1) + + if __name__ == "__main__": unittest.main() diff --git a/openpilot/starpilot/aol/tests/test_toyota_secoc.py b/openpilot/starpilot/aol/tests/test_toyota_secoc.py index 5da3ea9a6c..26cf08ad7e 100644 --- a/openpilot/starpilot/aol/tests/test_toyota_secoc.py +++ b/openpilot/starpilot/aol/tests/test_toyota_secoc.py @@ -1,4 +1,4 @@ -"""Always On Lateral on the SecOC Sienna (TOYOTA_SIENNA_4TH_GEN, safety param 2121).""" +"""SecOC AOL factories, key admission, and hybrid gear policy including radar Yaris.""" import unittest from opendbc.car import gen_empty_fingerprint, structs @@ -11,11 +11,11 @@ from openpilot.starpilot.longitudinal.toyota_output_policy import production_ena TOYOTA = int(structs.CarParams.SafetyModel.toyota) -def secoc_params(vehicle, key=True): +def secoc_params(vehicle, key=True, *, alpha=False): fp = gen_empty_fingerprint() fp[0][0x3F6] = 8 cp = CarInterface.get_params(vehicle, fp, [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.hybrid)], - False, False, False) + alpha, False, False) cp.secOcKeyAvailable = key return cp @@ -108,7 +108,7 @@ TEST_KEY = '11' * 16 @contextmanager -def actual_card(raw_key=TEST_KEY, *, simulation=True, vehicle=CAR.TOYOTA_SIENNA_4TH_GEN): +def actual_card(raw_key=TEST_KEY, *, simulation=True, vehicle=CAR.TOYOTA_SIENNA_4TH_GEN, alpha=False): with OpenpilotPrefix(), mock.patch.dict(os.environ, {'SIMULATION': '1' if simulation else '0', 'AOL_REPLAY_RUNTIME': '0', 'TOYOTA_LONG_OUTPUT_REPLAY_RUNTIME': '0'}): params = Params() @@ -118,7 +118,7 @@ def actual_card(raw_key=TEST_KEY, *, simulation=True, vehicle=CAR.TOYOTA_SIENNA_ params.put('LKASButtonControl', 8, block=True) if raw_key is not None: params.put('SecOCKey', raw_key, block=True) - cp = secoc_params(vehicle, key=False) + cp = secoc_params(vehicle, key=False, alpha=alpha) interface = CarInterface(cp) with mock.patch('openpilot.selfdrive.car.card.open', side_effect=FileNotFoundError, create=True): @@ -130,7 +130,7 @@ def actual_card(raw_key=TEST_KEY, *, simulation=True, vehicle=CAR.TOYOTA_SIENNA_ del selected -def parsed_state(interface, *, gear=3, main=True, door=False, belt=True, eps=1, lda=0, now_ns=1_000_000_000, ticks=8): +def parsed_state(interface, *, gear=3, main=True, door=False, belt=True, eps=1, lda=0, now_ns=1_000_000_000, ticks=8, cruise=False): interface.update([]) packer = getattr(interface, '_secoc_fixture_packer', None) if packer is None: @@ -138,7 +138,7 @@ def parsed_state(interface, *, gear=3, main=True, door=False, belt=True, eps=1, interface._secoc_fixture_packer = packer tracked = {bus: tuple(parser.vl) for bus, parser in interface.can_parsers.items()} values: dict[str, dict[str, float]] = { - 'PCM_CRUISE': {'CRUISE_ACTIVE': 0}, 'PCM_CRUISE_2': {'MAIN_ON': main}, + 'PCM_CRUISE': {'CRUISE_ACTIVE': cruise}, 'PCM_CRUISE_2': {'MAIN_ON': main}, 'GEAR_PACKET_HYBRID': {'GEAR': gear}, 'EPS_STATUS': {'LKA_STATE': eps}, 'BODY_CONTROL_STATE': {'DOOR_OPEN_FL': door, 'SEATBELT_DRIVER_UNLATCHED': not belt}, 'WHEEL_SPEEDS': {f'WHEEL_SPEED_{wheel}': 72 for wheel in ('FR', 'FL', 'RR', 'RL')}, @@ -162,22 +162,24 @@ def parsed_state(interface, *, gear=3, main=True, door=False, belt=True, eps=1, return state, frames -@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME)) +@pytest.mark.parametrize('vehicle,alpha', ((CAR.TOYOTA_SIENNA_4TH_GEN, False), (CAR.TOYOTA_RAV4_PRIME, False), + (CAR.TOYOTA_YARIS, False), (CAR.TOYOTA_YARIS, True))) @pytest.mark.parametrize('key', (None, '', 'not-a-key', '11' * 15, TEST_KEY)) -def test_actual_card_decodes_key_before_aol_admission_and_publishes_exact_profile(key, vehicle): - with actual_card(key, vehicle=vehicle) as (selected, saved): +def test_actual_card_decodes_key_before_aol_admission_and_publishes_exact_profile(key, vehicle, alpha): + with actual_card(key, vehicle=vehicle, alpha=alpha) as (selected, saved): admitted = decode_key(key) is not None assert selected.CP.secOcKeyAvailable == admitted assert selected.aol_qualified == admitted assert selected.CP.alternativeExperience == (32 if admitted else 0) - assert selected.CP.safetyConfigs[0].safetyParam == 2121 - assert selected.CP.secOcRequired and selected.CP.openpilotLongitudinalControl + word = (6217 if alpha else 2633) if vehicle == CAR.TOYOTA_YARIS else 2121 + assert selected.CP.safetyConfigs[0].safetyParam == word + assert selected.CP.secOcRequired and selected.CP.openpilotLongitudinalControl == (vehicle != CAR.TOYOTA_YARIS or alpha) assert policy_for(selected.CP).auxiliary_lkas_supported assert saved.get('LKASButtonControl') == 8 if admitted: assert selected.aol_card_intent.settings.lkas_action == selected.aol_card_intent.settings.main_action == 0 assert selected.CI.CS.secoc_key == selected.CI.CC.secoc_key == bytes.fromhex(TEST_KEY) - assert native_matches_cp(selected.CP, TOYOTA, 2121) + assert native_matches_cp(selected.CP, TOYOTA, word) assert production_enabled(selected.CP) == (vehicle == CAR.TOYOTA_SIENNA_4TH_GEN) else: assert selected.aol_card_intent is None @@ -199,10 +201,11 @@ def test_actual_card_decodes_key_before_aol_admission_and_publishes_exact_profil del controls -@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME)) +@pytest.mark.parametrize('vehicle,alpha', ((CAR.TOYOTA_SIENNA_4TH_GEN, False), (CAR.TOYOTA_RAV4_PRIME, False), + (CAR.TOYOTA_YARIS, False), (CAR.TOYOTA_YARIS, True))) @pytest.mark.parametrize('gear,expected', ((0, 'park'), (1, 'reverse'), (2, 'neutral'), (3, 'drive'), (4, 'brake'))) -def test_actual_secoc_factory_parser_uses_hybrid_gear_without_legacy_gear(gear, expected, vehicle): - cp = secoc_params(vehicle) +def test_actual_secoc_factory_parser_uses_hybrid_gear_without_legacy_gear(gear, expected, vehicle, alpha): + cp = secoc_params(vehicle, alpha=alpha) interface = CarInterface(cp) state, frames = parsed_state(interface, gear=gear) assert state.canValid and not state.canTimeout @@ -220,7 +223,7 @@ def test_actual_secoc_factory_parser_uses_hybrid_gear_without_legacy_gear(gear, assert str(after_legacy.gearShifter) == expected -@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_YARIS, CAR.TOYOTA_RAV4_TSS2_2023)) +@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_RAV4_TSS2_2023,)) def test_other_secoc_and_angle_factories_never_join_sienna_aol(vehicle): cp = CarInterface.get_params(vehicle, gen_empty_fingerprint(), [], False, False, False) cp.secOcKeyAvailable = True @@ -316,12 +319,38 @@ def test_secoc_fixture_advances_real_counter_and_duplicate_counter_invalidates_c assert not state.canValid and not state.canTimeout -@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME)) +@pytest.mark.parametrize('vehicle,alpha', ((CAR.TOYOTA_SIENNA_4TH_GEN, False), (CAR.TOYOTA_RAV4_PRIME, False), + (CAR.TOYOTA_YARIS, False), (CAR.TOYOTA_YARIS, True))) @pytest.mark.parametrize('gear,drivable', ((0, False), (1, False), (2, False), (3, True), (4, True))) -def test_secoc_parsed_gear_matches_common_entry_policy(vehicle, gear, drivable): +def test_secoc_parsed_gear_matches_common_entry_policy(vehicle, alpha, gear, drivable): from openpilot.selfdrive.car.car_events import CarEvents from openpilot.cereal import log - cp = secoc_params(vehicle) + cp = secoc_params(vehicle, alpha=alpha) current, _ = parsed_state(CarInterface(cp), gear=gear) events = CarEvents(cp).create_common_events(current, current) assert (log.OnroadEvent.EventName.wrongGear not in events.names) == drivable + + +def test_every_secoc_factory_is_accounted_for(): + assert {vehicle for vehicle in CAR if vehicle.config.flags & ToyotaFlags.SECOC} == {CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME, CAR.TOYOTA_YARIS} + + +@pytest.mark.parametrize('alpha', (False, True)) +@pytest.mark.parametrize('mutation', ('missing_radar', 'wrong_disable', 'wrong_long', 'wrong_word', 'no_key', 'release')) +def test_yaris_secoc_refuses_inconsistent_configuration(alpha, mutation): + cp = secoc_params(CAR.TOYOTA_YARIS, alpha=alpha) + assert qualified(cp) + if mutation == 'missing_radar': + cp.flags &= ~int(ToyotaFlags.RADAR_ACC) + elif mutation == 'wrong_disable': + cp.flags ^= int(ToyotaFlags.DISABLE_RADAR) + elif mutation == 'wrong_long': + cp.openpilotLongitudinalControl = not cp.openpilotLongitudinalControl + elif mutation == 'wrong_word': + cp.safetyConfigs[0].safetyParam = 2121 + elif mutation == 'no_key': + cp.secOcKeyAvailable = False + else: + cp.dashcamOnly = True + assert not qualified(cp) + assert not policy_for(cp).intent_supported diff --git a/openpilot/starpilot/aol/tests/test_toyota_secoc_joined.py b/openpilot/starpilot/aol/tests/test_toyota_secoc_joined.py index 9501f419d1..fff27927a2 100644 --- a/openpilot/starpilot/aol/tests/test_toyota_secoc_joined.py +++ b/openpilot/starpilot/aol/tests/test_toyota_secoc_joined.py @@ -27,18 +27,21 @@ class CapturePublisher: self.messages[service] = messaging.log_from_bytes(raw) -@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME)) -def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle): +@pytest.mark.parametrize('vehicle,alpha,ordinary', ((CAR.TOYOTA_SIENNA_4TH_GEN, False, False), + (CAR.TOYOTA_RAV4_PRIME, False, False), (CAR.TOYOTA_YARIS, False, False), + (CAR.TOYOTA_YARIS, True, False), (CAR.TOYOTA_YARIS, False, True), (CAR.TOYOTA_YARIS, True, True))) +def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle, alpha, ordinary): case = IoniqAvailabilityTests(methodName='runTest') - with actual_card(simulation=False, vehicle=vehicle) as (selected, _), ExitStack() as cleanup: + with actual_card(simulation=False, vehicle=vehicle, alpha=alpha) as (selected, _), ExitStack() as cleanup: cleanup.callback(case.doCleanups) sd, controls, step = case._authority_callers( - False, cp=selected.CP.as_reader().as_builder(), genuine_events=True) + ordinary, cp=selected.CP.as_reader().as_builder(), genuine_events=True) assert not selected.sm.simulation and not sd.sm.simulation and not controls.sm.simulation capture = CapturePublisher() cleanup.enter_context(mock.patch.object(selected, 'pm', capture)) cleanup.enter_context(mock.patch('openpilot.selfdrive.car.card.REPLAY', False)) transition = cleanup.enter_context(mock.patch.object(sd.state_machine, 'update', wraps=sd.state_machine.update)) + radar_startup = cleanup.enter_context(mock.patch('opendbc.car.toyota.interface.disable_ecu')) apply = cleanup.enter_context(mock.patch.object(selected.CI, 'apply', wraps=selected.CI.apply)) original_update_msgs = controls.sm.update_msgs def model_target(timestamp, messages): @@ -57,13 +60,14 @@ def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle): safety = libsafety_py.libsafety safety.init_tests() safety.set_alternative_experience(32) - safety.set_safety_hooks(structs.CarParams.SafetyModel.toyota, 2121) + word = selected.CP.safetyConfigs[0].safetyParam + safety.set_safety_hooks(structs.CarParams.SafetyModel.toyota, word) def reset_native(): safety.set_alternative_experience(0) safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0) cleanup.callback(reset_native) base = 10_000_000_000 - warm, _ = parsed_state(selected.CI, now_ns=base - 80_000_000) + warm, _ = parsed_state(selected.CI, now_ns=base - 80_000_000, cruise=ordinary) assert warm.canValid and not warm.canTimeout original_message = messaging.new_message driving_torques = [] @@ -78,16 +82,18 @@ def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle): with mock.patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \ mock.patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=clocked_message): - physical, frames = parsed_state(selected.CI, gear=gear, now_ns=now, ticks=1) + physical, frames = parsed_state(selected.CI, gear=gear, now_ns=now, ticks=1, cruise=ordinary) assert physical.canValid and not physical.canTimeout safety.set_timer(now // 1000) safety.set_aol_test_heartbeat(True) for address, payload, bus in frames: assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, payload)) - safety.aol_set_host_request(1) + want_long = ordinary and selected.CP.openpilotLongitudinalControl + wanted = 3 if want_long else 1 + safety.aol_set_host_request(wanted) request = safety.aol_get_request_mask() permission = safety.aol_get_permission_mask() - assert request == 1 and permission == (1 if gear in (3, 4) else 0) + assert request == wanted and permission == (wanted if gear in (3, 4) else 0) selected.aol_card_intent.update(physical.as_reader(), now_ns=now) selected.state_publish(physical, None) car_source = capture.messages['carState'] @@ -97,21 +103,22 @@ def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle): before = transition.call_count axis, _, command = step( tick, car_state_message=car_source.as_builder(), intent_message=intent_source.as_builder(), - allowed=bool(permission & 1), long_allowed=False, - native_request_lat=bool(request & 1), native_request_long=False, - panda_controls_allowed=False) + allowed=bool(permission & 1), long_allowed=bool(permission & 2), + native_request_lat=bool(request & 1), native_request_long=bool(request & 2), + panda_controls_allowed=bool(safety.get_controls_allowed())) assert transition.call_count == before + 1 assert axis.sourceCarStateMonoTime == car_source.logMonoTime == now assert axis.observedMonoTime == now and axis.validUntilMonoTime == now + 30_000_000 assert axis.intentProducerSessionId == selected.slc_producer_session - assert command.latActive == (gear in (3, 4)) and not command.longActive + assert command.latActive == (gear in (3, 4)) + assert command.longActive == (want_long and gear in (3, 4)) native = decode_safety(sd.sm['aolSafetyWire']) assert native is not None and native.observedMonoTime == now - assert native.safetyParam == 2121 and native.axisSessionId == axis.sessionId + assert native.safetyParam == word and native.axisSessionId == axis.sessionId assert native.requestedLateral == bool(request & 1) assert native.lateralAllowed == bool(permission & 1) assert len(native.pandaInventory) == 1 - assert native.pandaInventory[0].safetyParam == 2121 + assert native.pandaInventory[0].safetyParam == word assert native.pandaInventory[0].alternativeExperience == 32 control_source = controls.pm.messages['carControl'] assert control_source.logMonoTime == now @@ -132,10 +139,23 @@ def test_fresh_secoc_source_permission_sd_controls_card_sendcan_join(vehicle): assert bool(steer[1][0] & 1) == command.latActive assert steer == add_mac(bytes.fromhex(TEST_KEY), 1, 1, counter, (steer[0], steer[1][:4], steer[2])) assert safety.safety_tx_hook(libsafety_py.make_CANPacket(steer[0], steer[2], steer[1])) + if alpha: + keepalive = [packet for packet in sent.sendcan if packet.address == 0x750] + for packet in keepalive: + assert bytes(packet.dat) == bytes.fromhex('0f023e0000000000') + assert safety.safety_tx_hook(libsafety_py.make_CANPacket(packet.address, packet.src, packet.dat)) + accel_frames = [packet for packet in sent.sendcan if packet.address == 0x183] + if not selected.CP.openpilotLongitudinalControl: + assert not accel_frames + for packet in accel_frames: + assert safety.safety_tx_hook(libsafety_py.make_CANPacket(packet.address, packet.src, packet.dat)) torque = int.from_bytes(steer[1][1:3], 'big', signed=True) if gear in (3, 4): driving_torques.append(torque) assert abs(torque) <= min(1500, 15 * (tick + 1)) else: assert not axis.lateralActive and torque == 0 + assert radar_startup.call_count == int(alpha) + if alpha: + assert radar_startup.call_args.kwargs == {'bus': 0, 'addr': 0x750, 'sub_addr': 0xf, 'com_cont_req': bytes.fromhex('280101')} assert any(torque != 0 for torque in driving_torques), 'Fresh joined Controls must produce actual steering torque' diff --git a/openpilot/starpilot/car/toyota/aol.py b/openpilot/starpilot/car/toyota/aol.py index 1c5ba7fa8a..7b7a4c9bac 100644 --- a/openpilot/starpilot/car/toyota/aol.py +++ b/openpilot/starpilot/car/toyota/aol.py @@ -17,14 +17,14 @@ AOL_WORDS = frozenset(( 0x8042, 0x8242, 0xC042, 0xC242, 0xD049, 0xD249, 0xC049, 0xC249, 0x9449, 0x9549, 0x9558, 0x9564, 0x9649, 0x9749, 0x9758, 0x9764, 0xD44D, 0xD549, 0xD64D, 0xD749, - 73, 585, 856, 4169, 4681, 2121, 0x9049, 0x9249, + 73, 585, 856, 4169, 4681, 2121, 2633, 6217, 0x9049, 0x9249, 0xB049, 0xB249, 0x2449, 0x2649, 0x2049, 0x2249, 0x424D, 0x449, 0x649, 0x1449, 0x1649, 0x8049, 0x8249, 0x8449, 0x8649, 0x9158, 0x9358, 0x9149, 0x9349, 0x9164, 0x9364, 0xd149, 0xd349, 0xd04d, 0xd24d, )) -SECOC_TORQUE_CARS = frozenset((CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME)) +SECOC_TORQUE_CARS = frozenset((CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_PRIME, CAR.TOYOTA_YARIS)) RADAR_TORQUE_CARS = frozenset((CAR.TOYOTA_CHR_TSS2, CAR.TOYOTA_RAV4_TSS2_2022)) CAMERA_TORQUE_CARS = frozenset(( CAR.TOYOTA_ALPHARD_TSS2, CAR.TOYOTA_AVALON_TSS2, CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_COROLLA_TSS2, @@ -35,18 +35,25 @@ CAMERA_TORQUE_CARS = frozenset(( def secoc_qualified(cp, *, marked_only=False): + radar = cp is not None and cp.carFingerprint == CAR.TOYOTA_YARIS required = ToyotaFlags.TSS2 | ToyotaFlags.NO_DSU | ToyotaFlags.SECOC | ToyotaFlags.RAISED_ACCEL_LIMIT + if radar: + required |= ToyotaFlags.RADAR_ACC + if cp.openpilotLongitudinalControl: + required |= ToyotaFlags.DISABLE_RADAR allowed = required | ToyotaFlags.HYBRID | ToyotaFlags.HAS_BSM if (cp is None or cp.brand != 'toyota' or cp.carFingerprint not in SECOC_TORQUE_CARS or - cp.notCar or cp.passive or cp.dashcamOnly or not cp.pcmCruise or not cp.openpilotLongitudinalControl or + cp.notCar or cp.passive or cp.dashcamOnly or not cp.pcmCruise or (not cp.openpilotLongitudinalControl and not radar) or not cp.secOcRequired or not cp.secOcKeyAvailable or cp.steerControlType != structs.CarParams.SteerControlType.torque or len(cp.safetyConfigs) != 1 or cp.alternativeExperience not in ((32,) if marked_only else (0, 32)) or cp.flags & required != required or int(cp.flags) & ~int(allowed)): return False config = cp.safetyConfigs[0] + mode = (ToyotaSafetyFlags.SECOC_RADAR_LONG if cp.openpilotLongitudinalControl else + ToyotaSafetyFlags.SECOC | ToyotaSafetyFlags.STOCK_LONGITUDINAL) if radar else ToyotaSafetyFlags.SECOC return (config.safetyModel == structs.CarParams.SafetyModel.toyota and - config.safetyParam == EPS_SCALE[cp.carFingerprint] | int(ToyotaSafetyFlags.SECOC)) + config.safetyParam == EPS_SCALE[cp.carFingerprint] | int(mode)) def lexus_is_qualified(cp, *, marked_only=False):