import math import pytest from iqdbc.can import CANPacker, CANParser from iqdbc.car import Bus, gen_empty_fingerprint, structs from iqdbc.car.hyundai.carstate import CarState, EV_MODE_STATUS_TIMEOUT_NS, _get_ev_mode_state from iqdbc.car.hyundai.interface import CarInterface from iqdbc.car.hyundai.values import CANFD_HYBRID_STATUS_ADDR, CANFD_HYBRID_STATUS_DLC, CAR, DBC, EV_MODE_ACTIVE_VALUES, \ EV_MODE_STATUS_ADDR, EV_MODE_STATUS_DLC, EV_MODE_STATUS_MSG, EV_MODE_STATUS_SIGNAL, \ HyundaiExtFlags, HyundaiFlags from openpilot.common.params import Params def get_params(candidate, *, hybrid=True, hybrid_bus=0, hybrid_status=None, hybrid_status_bus=0, hybrid_status_dlc=CANFD_HYBRID_STATUS_DLC, status_bus=0, status_dlc=EV_MODE_STATUS_DLC): Params().put_int("HyundaiCameraSCC", 1) Params().put_int("CanfdHDA2", 1) fingerprint = gen_empty_fingerprint() if hybrid: fingerprint[hybrid_bus][0x105] = 32 if hybrid_status is None: hybrid_status = hybrid if hybrid_status: fingerprint[hybrid_status_bus][CANFD_HYBRID_STATUS_ADDR] = hybrid_status_dlc if status_bus is not None: fingerprint[status_bus][EV_MODE_STATUS_ADDR] = status_dlc return CarInterface.get_params(candidate, fingerprint, [], False, False, False) @pytest.mark.parametrize(("candidate", "hybrid", "status_bus", "status_dlc", "expected"), ( (CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 0, 32, True), (CAR.HYUNDAI_SANTAFE_MX5_HEV, False, 0, 32, False), (CAR.HYUNDAI_SANTAFE_MX5_HEV, True, None, 32, False), (CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 1, 32, False), (CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 0, 16, False), (CAR.KIA_SORENTO_4TH_GEN, False, None, 32, False), # Shared ICE/HEV/PHEV candidates rely on the observed ECAN capability frames, not their model name. (CAR.HYUNDAI_TUCSON_4TH_GEN, True, 0, 32, True), (CAR.HYUNDAI_TUCSON_4TH_GEN, False, 0, 32, False), (CAR.KIA_SORENTO_HEV_4TH_GEN, True, 0, 32, True), (CAR.HYUNDAI_KONA_HEV_2ND_GEN, True, 0, 32, True), (CAR.HYUNDAI_KONA_HEV_2ND_GEN, False, 0, 32, False), (CAR.HYUNDAI_ELANTRA_HEV_2021, True, 0, 32, False), )) def test_ev_mode_capability(candidate, hybrid, status_bus, status_dlc, expected): CP = get_params(candidate, hybrid=hybrid, status_bus=status_bus, status_dlc=status_dlc) assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) == expected @pytest.mark.parametrize(("hybrid_status_bus", "hybrid_status_dlc"), ( (1, CANFD_HYBRID_STATUS_DLC), (0, 16), )) def test_ev_mode_capability_requires_ecan_hybrid_status_dlc32(hybrid_status_bus, hybrid_status_dlc): CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV, hybrid_status_bus=hybrid_status_bus, hybrid_status_dlc=hybrid_status_dlc) assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) def test_0x105_and_ev_status_without_0xfa_do_not_enable_ev_mode(): CP = get_params(CAR.HYUNDAI_TUCSON_4TH_GEN, hybrid=True, hybrid_status=False) assert bool(CP.flags & HyundaiFlags.HYBRID) assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) def test_ev_mode_display_capability_does_not_change_hybrid_safety_classification(): CP = get_params(CAR.HYUNDAI_TUCSON_4TH_GEN, hybrid=False, hybrid_status=True) assert not bool(CP.flags & HyundaiFlags.HYBRID) assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) def test_ev_mode_capability_uses_the_detected_ecan_offset(): CP = get_params(CAR.KIA_SORENTO_HEV_4TH_GEN, hybrid_bus=4, hybrid_status_bus=4, status_bus=4) assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) def test_ev_mode_parser_registration_is_capability_gated(): supported = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV) unsupported = get_params(CAR.KIA_SORENTO_4TH_GEN, hybrid=False, status_bus=1, status_dlc=16) supported_parser = CarState.get_can_parsers_canfd(None, supported)[Bus.pt] unsupported_parser = CarState.get_can_parsers_canfd(None, unsupported)[Bus.pt] assert EV_MODE_STATUS_ADDR in supported_parser.addresses assert supported_parser.message_states[EV_MODE_STATUS_ADDR].ignore_alive assert supported_parser.message_states[EV_MODE_STATUS_ADDR].ignore_counter assert EV_MODE_STATUS_ADDR not in unsupported_parser.addresses def test_sorento_ice_corner_radar_status_does_not_enable_ev_mode(): Params().put_int("HyundaiCameraSCC", 1) Params().put_int("CanfdHDA2", 1) fingerprint = gen_empty_fingerprint() fingerprint[1][0x230] = 16 # Actual Sorento ICE ACAN corner-radar status frame. CP = CarInterface.get_params(CAR.KIA_SORENTO_4TH_GEN, fingerprint, [], False, False, False) parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt] assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) assert EV_MODE_STATUS_ADDR not in parser.addresses def test_ev_mode_state_requires_a_fresh_dlc32_frame(): CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV) parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt] packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) assert _get_ev_mode_state(parser) == (False, False) timestamp = 1_000_000_000 wrong_bus_short_status = (EV_MODE_STATUS_ADDR, b"\x00" * 16, 1) parser.update([timestamp, [wrong_bus_short_status]]) assert _get_ev_mode_state(parser) == (False, False) ev_active = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": 1, EV_MODE_STATUS_SIGNAL: 6}) parser.update([timestamp + 100_000_000, [ev_active]]) assert _get_ev_mode_state(parser) == (True, True) ev_inactive = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": 2, EV_MODE_STATUS_SIGNAL: 3}) parser.update([timestamp + 200_000_000, [ev_inactive]]) assert _get_ev_mode_state(parser) == (False, True) parser.dat[EV_MODE_STATUS_ADDR] = b"\x00" * 16 assert _get_ev_mode_state(parser) == (False, False) parser.dat[EV_MODE_STATUS_ADDR] = ev_inactive[1] parser.update([timestamp + 200_000_000 + EV_MODE_STATUS_TIMEOUT_NS + 1, []]) assert not parser.bus_timeout assert _get_ev_mode_state(parser) == (False, False) @pytest.mark.parametrize("mode", range(16)) def test_ev_mode_enum_mapping(mode): CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV) parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt] packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) msg = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": mode, EV_MODE_STATUS_SIGNAL: mode}) parser.update([1_000_000_000, [msg]]) assert int(parser.vl[EV_MODE_STATUS_MSG][EV_MODE_STATUS_SIGNAL]) == mode assert _get_ev_mode_state(parser) == (mode in EV_MODE_ACTIVE_VALUES, True) @pytest.mark.parametrize(("payload", "expected_mode", "expected_active"), ( ("758139048402000000000000000000000000d3009805001c24000000c05dc80f", 1, True), ("4d466f048402000000000000000000000000bf007408001c24000000c05dc80f", 2, True), ("32206e048402000000000000000000000000c9007418001c24000000c05dc80f", 6, True), # Route 455 mode 3 was a false positive with the old single-bit 0x230 interpretation. ("4f935e0484020000000000000000000000004400640d001c10000000c05dc40f", 3, False), ("066558048402000000000000000000000000c4003020001c24000000c05dc80f", 8, False), ("059683048402000000000000000000000000a1008425001c24000000c05dc80f", 9, False), ("011305048402000000000000000000000000d600a829001c24000000c05dc80f", 10, False), )) def test_ev_mode_dbc_decodes_real_mx5_frames(payload, expected_mode, expected_active): parser = CANParser("hyundai_canfd_generated", [(EV_MODE_STATUS_MSG, math.nan)], 0) updated = parser.update([1_000_000_000, [(EV_MODE_STATUS_ADDR, bytes.fromhex(payload), 0)]]) assert updated == {EV_MODE_STATUS_ADDR} assert int(parser.vl[EV_MODE_STATUS_MSG][EV_MODE_STATUS_SIGNAL]) == expected_mode assert _get_ev_mode_state(parser) == (expected_active, True) def test_ev_mode_rejects_checksum_corruption(): CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV) parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt] payload = bytearray.fromhex("32206e048402000000000000000000000000c9007418001c24000000c05dc80f") payload[-1] ^= 1 parser.update([1_000_000_000, [(EV_MODE_STATUS_ADDR, bytes(payload), parser.bus)]]) assert EV_MODE_STATUS_ADDR not in parser.dat assert _get_ev_mode_state(parser) == (False, False) def test_ev_mode_fields_default_invalid(): state = structs.CarState() assert not state.evModeActive assert not state.evModeValid def test_ev_mode_parser_is_optional_for_can_validity(): CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV) parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt] state = parser.message_states[EV_MODE_STATUS_ADDR] assert state.ignore_alive assert state.ignore_counter