diff --git a/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py index a256ea9ac2..d727e9098b 100644 --- a/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py +++ b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py @@ -72,12 +72,6 @@ def native_accepts(cp, model, param): def independent_buttons_qualified(cp, *, marked_only=True): - if cp.carFingerprint == CAR.HYUNDAI_SONATA: - expected_word = 0xC04 - elif cp.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID: - expected_word = 0xC06 - else: - return False - return bool(qualified(cp, marked_only=marked_only) and + return bool(cp.carFingerprint in CLASSIC_LONG_IDS and qualified(cp, marked_only=marked_only) and cp.alternativeExperience in ((AOL_EXPERIENCE,) if marked_only else (0, AOL_EXPERIENCE)) and - aol_word(cp) == expected_word) + aol_word(cp) in (0xC04, 0xC05, 0xC06, 0xC44, 0xC45, 0xC46)) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_classic_family_cancel_caller.py b/opendbc_repo/opendbc/car/hyundai/tests/test_classic_family_cancel_caller.py new file mode 100644 index 0000000000..ff8c2e9e80 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_classic_family_cancel_caller.py @@ -0,0 +1,499 @@ +from opendbc.car.hyundai.classic_long_aol import CLASSIC_LONG_IDS +from opendbc.car.hyundai.values import HyundaiFlags +import ast +import inspect +import textwrap +import pytest +import importlib +import os +from types import SimpleNamespace +from unittest.mock import patch + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.classic_long_aol import qualified as qualified_classic +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum +from openpilot.cereal import messaging +from openpilot.common.params import Params +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.selfdrive.car import card as card_module +from openpilot.starpilot.aol.intent import AOL_TOGGLE, read_settings +from openpilot.starpilot.aol.runtime import current_native +from openpilot.starpilot.aol.tests.test_classic_long_profiles import params as classic_params +from openpilot.starpilot.aol.tests import test_ioniq_availability as caller_fixture +from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety +from openpilot.starpilot.car.hyundai.classic_long_intent import ClassicLongCardIntent + +disable_ecu_module = importlib.import_module('opendbc.car.disable_ecu') + + +def classic_frames(ci, tick, *, main=0, lkas=0, button=0, engaged=False): + + assert qualified_classic(ci.CP, marked_only=True) + packer = CANPacker('hyundai_can_generated') + + gas_name = 'E_EMS11' if ci.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV) else 'EMS16' + hz = {gas_name:100, 'ELECT_GEAR':20, 'WHL_SPD11':100, 'TCS13':100, 'MDPS12':50, 'CLU11':50, + 'BCM_PO_11':50, 'LKAS11':100} + scheduled = {(packer.dbc.name_to_msg[name].address, 0): name for name in hz if name != 'LKAS11'} + for parser in ci.can_parsers.values(): + for address in parser.addresses: + scheduled[(address, parser.bus)] = parser.dbc.addr_to_msg[address].name + drive_gear = next(code for code, label in ci.CS.shifter_values.items() if ci.CS.parse_gear_shifter(label) == structs.CarState.GearShifter.drive) + frames = [] + for (address,bus),name in sorted(scheduled.items()): + parser = next((p for p in ci.can_parsers.values() if p.bus == bus), None) + declared = parser.message_states[address].frequency if parser is not None and address in parser.addresses else 0 + frequency = hz.get(name, min(declared,100) if 0 < declared <= 100 else 100) + period = max(1,round(100/frequency)) + if tick % period: + continue + counter = tick // period + values = {} + if name == 'CLU11': + values.update(CF_Clu_AliveCnt1=counter % 16, CF_Clu_CruiseSwState=button, CF_Clu_CruiseSwMain=main) + elif name == 'EMS16': + values.update(AliveCounter=counter % 4, CF_Ems_AclAct=0) + elif name == 'E_EMS11': + values['CR_Vcu_AccPedDep_Pos']=0 + elif name == 'ELECT_GEAR': + values['Elect_Gear_Shifter']=drive_gear + elif name == 'WHL_SPD11': + values.update(WHL_SPD_AliveCounter_LSB=counter % 4, WHL_SPD_AliveCounter_MSB=(counter//4) % 4, + WHL_SPD_FL=72,WHL_SPD_FR=72,WHL_SPD_RL=72,WHL_SPD_RR=72) + elif name == 'TCS13': + values.update(AliveCounterTCS=counter % 8, ACCEnable=0, ACC_REQ=int(engaged), DriverOverride=0) + elif name == 'MDPS12': + values.update(CR_Mdps_StrColTq=0, CR_Mdps_OutTq=0) + elif name == 'BCM_PO_11': + values['LDA_BTN']=lkas + elif name == 'CGW1': + values['CF_Gway_DrvSeatBeltSw']=1 + elif name == 'LVR12': + values['CF_Lvr_Gear']=drive_gear + elif name == 'TCU12': + values['CUR_GR'] = drive_gear + elif name == 'SAS11': + values['SAS_Angle']=1. + elif name == 'CLU15': + values.update(CF_Clu_VehicleSpeed=72, CF_Clu_Gear=drive_gear) + frame = packer.make_can_msg(name,bus,values) + if name in ('EMS16','WHL_SPD11','TCS13'): + address, data, bus = checksum(frame) + frame = (address, bytes(data), bus) + frames.append(frame) + assert all(address not in (0x420,0x421) for address,_,_ in frames), 'Disabled SCC source must not be fabricated' + assert all(address!=0x340 or bus==2 for address,_,bus in frames), 'Never echo outgoing LKAS on protected RX bus' + return frames + + +def _invalid_controls_emits_no_commands(received): + if 'sendcan' not in received: + return True + envelope = received['sendcan'] + return envelope.which() == 'sendcan' and not envelope.valid and len(envelope.sendcan) == 0 + + +def family_cases(): + identities = [] + for identity in sorted(CLASSIC_LONG_IDS): + if qualified_classic(classic_params(identity, lda=True)): + identities.append(identity) + assert len(identities) == 32 + return identities + + +@pytest.mark.parametrize('identity', family_cases()) +def test_actual_classic_family_cancel_and_manual_withdrawal(identity, monkeypatch): + withdrawal = 'toggle-off' + expect_retention = True + shared_long, incidental = True, 'cancel' + native = libsafety_py.libsafety + case = caller_fixture.IoniqAvailabilityTests(methodName='runTest') + card = None + with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '0', 'AOL_REPLAY_RUNTIME': '1'}): + saved = Params() + saved.put_bool('OpenpilotEnabledToggle', True, block=True) + saved.put_bool('AlwaysOnLateral', True, block=True) + saved.put('LKASButtonControl', AOL_TOGGLE, block=True) + saved.put('MainCruiseButtonControl', 0, block=True) + now = [10_000_000_000] + for key in ('CancelButtonControl', 'LongCancelButtonControl', 'VeryLongCancelButtonControl'): + saved.put(key, 0, block=True) + try: + saved.put_bool('AlphaLongitudinalEnabled', True, block=True) + saved.put_bool('IsReleaseBranch', False, block=True) + cp = classic_params(identity, lda=True) + assert qualified_classic(cp) and not cp.pcmCruise and cp.openpilotLongitudinalControl + assert cp.lateralTuning.which() == 'torque' + expects_owner = identity == CAR.GENESIS_G90 + assert CarInterface.startup_required(cp) == expects_owner + from opendbc.car import car_helpers, gen_empty_fingerprint + from opendbc.car.hyundai import ecu_startup + from opendbc.car.hyundai.tests.test_g90_continuity import ManualKeeper + diagnostic_requests = [] + startup_owners = [] + native.set_alternative_experience(0) + assert native.set_safety_hooks(structs.CarParams.SafetyModel.elm327, 1) == 0 + native.init_tests() + startup_factory = CarInterface.startup_owner + + def traced_factory(prepared_cp, callbacks, *, requested): + recv, send = callbacks + + def diagnostic_send(frames): + for address, data, bus in frames: + assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)) + if saved.get('CarParams') is None: + assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0 + return send(frames) + owner = startup_factory(prepared_cp, (recv, diagnostic_send), requested=requested) + if owner is None: + assert not expects_owner + return None + assert expects_owner + owner._query = startup_query + owner.clock = lambda: now[0] / 1e9 + owner.keeper_factory = ManualKeeper + startup_owners.append(owner) + return owner + + def startup_query(sender, receiver, bus, addresses, requests, responses): + request = requests[0] + assert bus == 0 and addresses == [(0x7d0, None)] + assert request in (b'\x10\x03', b'\x28\x83\x01') + assert saved.get('CarParams') is None + diagnostic_requests.append(request) + + def receive(timeout): + sender([(0x7d0, bytes([len(request)]) + request + bytes(7 - len(request)), bus)]) + return {(0x7d0, None): b''} if request == b'\x10\x03' else {} + return SimpleNamespace(get_data=receive) + + fp = gen_empty_fingerprint() + fp[0][0x391] = 8 + identified = (identity, fp, '0' * 17, [], cp.fingerprintSource, True) + initial_can = messaging.new_message('can', 1, valid=True, logMonoTime=now[0]) + initial_can.can[0].address, initial_can.can[0].src, initial_can.can[0].dat = 0x391, 0, bytes(8) + with patch.object(car_helpers, 'fingerprint', return_value=identified), \ + patch.object(card_module.messaging, 'recv_one_retry', return_value=initial_can.as_reader()), \ + patch.object(CarInterface, 'startup_owner', side_effect=traced_factory), \ + patch.object(card_module.Car, 'startup_diagnostic_admission', return_value=True), \ + patch.object(ecu_startup, 'IsoTpParallelQuery', side_effect=startup_query), \ + patch('time.clock_gettime_ns', side_effect=lambda clock: now[0]), \ + patch('time.monotonic_ns', side_effect=lambda: now[0]), patch('time.sleep'): + card = card_module.Car() + if expects_owner: + assert len(startup_owners) == 1 and card.vehicle_startup.owner is startup_owners[0] + assert card.vehicle_startup.owner.published and card.vehicle_startup.owner.prepared_for(card.CP) + else: + assert not startup_owners and card.vehicle_startup.owner is None + assert diagnostic_requests == ([b'\x10\x03', b'\x28\x83\x01'] if expects_owner else []) + assert qualified_classic(card.CP, marked_only=True) + expected_word = 0xC04 + expected_word |= 2 if card.CP.flags & HyundaiFlags.HYBRID else (1 if card.CP.flags & HyundaiFlags.EV else 0) + expected_word |= 0x40 if card.CP.flags & HyundaiFlags.ALT_LIMITS else 0 + assert card.CP.safetyConfigs[0].safetyParam == expected_word and card.CP.alternativeExperience == 32 + assert isinstance(card.aol_card_intent, ClassicLongCardIntent) + if not expect_retention: + assert not card.aol_card_intent.independent_cruise_buttons, 'Published classic policy changed; reclassify baseline' + assert not card.ci_initialized and not saved.get_bool('ControlsReady') + card.slc_producer_session = 'authority-card' + card.CI.update([]) + sd, controls, drive = case._authority_callers(False, cp=card.CP.as_reader().as_builder(), genuine_events=True, + initial_panda_config=(int(structs.CarParams.SafetyModel.elm327), 1, 0, + bool(native.get_controls_allowed()), bool(native.safety_test_toyota_rx_summary_invalid()))) + + sd.aol_settings = read_settings(saved) + assert sd.aol_settings.lkas_action == AOL_TOGGLE and sd.aol_settings.main_action == 0 + assert sd.CP.to_dict() == card.CP.to_dict() == controls.CP.to_dict() + assert len(card.CP.safetyConfigs) == 1 and bool(card.CP.openpilotLongitudinalControl) == shared_long + config = card.CP.safetyConfigs[0] + model, word = int(config.safetyModel.raw), int(config.safetyParam) + configured = [False] + dispatch_tree = ast.parse(textwrap.dedent(inspect.getsource(card_module.Car.step))).body[0].body + assignments = [node for node in dispatch_tree if isinstance(node, ast.Assign) and + any(isinstance(t, ast.Name) and t.id == 'initialized' for t in node.targets)] + decisions = [node for node in dispatch_tree if isinstance(node, ast.If) and ast.unparse(node.test) == 'not self.CP.passive and initialized'] + assert len(assignments) == len(decisions) == 1 + dispatch = compile(ast.Module(body=assignments + decisions, type_ignores=[]), 'actual-Card-step-control-dispatch', 'exec') + queued, received, previous_sd = [], {}, {} + tick = 0 + next_panda_tick = [1] + patch_scope = monkeypatch.context() + local = patch_scope.__enter__() + try: + + def diagnostic_query(sender, receiver, bus, addresses, requests, responses): + assert bus == 0 and addresses == [(0x7d0, None)] + assert len(requests) == 1 + request = requests[0] + assert request in (b'\x10\x03', b'\x28\x83\x01') + diagnostic_requests.append(request) + + def receive(timeout): + sender([(0x7d0, bytes([len(request)]) + request + bytes(7-len(request)), bus)]) + return {(0x7d0, None): b'\x50\x03'} if request == b'\x10\x03' else {} + return SimpleNamespace(get_data=receive) + original_receive, original_send = card.can_callbacks + + def checked_diagnostic_send(frames): + for address, data, bus in frames: + assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)) + return original_send(frames) + card.can_callbacks = (original_receive, checked_diagnostic_send) + local.setattr(disable_ecu_module, 'IsoTpParallelQuery', diagnostic_query) + local.setattr(card_module, 'REPLAY', False) + local.setattr(card_module.time, 'monotonic_ns', lambda: now[0]) + local.setattr(card_module.time, 'clock_gettime_ns', lambda clock: now[0]) + local.setattr(card_module.messaging, 'drain_sock_raw', lambda *args, **kwargs: list(queued)) + local.setattr(card.sm, 'update', lambda *args: None) + create_message = messaging.new_message + + def clocked_message(service=None, size=None, **kwargs): + + kwargs.setdefault('logMonoTime', now[0]) + return create_message(service, size, **kwargs) + local.setattr(messaging, 'new_message', clocked_message) + send = card.pm.send + + def capture(service, event): + raw = event if isinstance(event, bytes) else event.to_bytes() + received[service] = messaging.log_from_bytes(raw).as_builder() + return send(service, raw) + local.setattr(card.pm, 'send', capture) + + def step(*, main=0, lkas=0, button=0, engaged=False, withdraw_request=False): + nonlocal tick + tick += 1 + now[0] = source_ns = 10_000_000_000 + tick * 10_000_000 + prior_axis = previous_sd.get('aolAxisState') + prior_state = previous_sd.get('selfdriveState') + request = 0 if prior_axis is None else ( + int(prior_axis.aolAxisState.desiredLateral) | int(prior_axis.aolAxisState.desiredLongitudinal) << 1) + if withdraw_request: + request = 0 + heartbeat = bool(request or (prior_state is not None and prior_state.selfdriveState.enabled)) + if saved.get_bool('ControlsReady') and not configured[0]: + native.set_alternative_experience(card.CP.alternativeExperience) + assert native.set_safety_hooks(model, word) == 0 + native.init_tests() + native.set_alternative_experience(card.CP.alternativeExperience) + configured[0] = True + native.set_timer(source_ns // 1000) + if expects_owner: + card.vehicle_startup.owner.keeper.tick() + native.set_aol_test_heartbeat(heartbeat) + native.aol_set_host_request(request) + frames = classic_frames(card.CI, tick, main=main, lkas=lkas, button=button, engaged=engaged) + for address, data, bus in frames: + assert native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)), (hex(address), bus) + if tick % 100 == 0: + native.safety_tick() + actual_request = native.aol_get_request_mask() + mask = native.aol_get_permission_mask() + + rx_invalid = bool(native.safety_test_toyota_rx_summary_invalid()) + relay_fault = bool(native.get_relay_malfunction()) + assert not relay_fault + batch = messaging.log_from_bytes(card_module.can_list_to_can_capnp(frames)).as_builder() + batch.logMonoTime = source_ns + queued[:] = [batch.to_bytes()] + device = messaging.new_message('deviceState', valid=True, logMonoTime=source_ns) + device.deviceState.started, device.deviceState.startedMonoTime = True, 9_000_000_000 + pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=source_ns) + panda = pandas.pandaStates[0] + panda.safetyModel = config.safetyModel if configured[0] else structs.CarParams.SafetyModel.elm327 + panda.safetyParam = word if configured[0] else 1 + panda.alternativeExperience = card.CP.alternativeExperience if configured[0] else 0 + panda.controlsAllowed = native.get_controls_allowed() + panda.safetyRxChecksInvalid = rx_invalid + panda.ignitionLine = True + actual_model = model if configured[0] else int(structs.CarParams.SafetyModel.elm327) + actual_word = word if configured[0] else 1 + actual_experience = card.CP.alternativeExperience if configured[0] else 0 + slot = PandaSlot(0, 'authority-panda', actual_model, actual_word, actual_experience, + native.get_controls_allowed(), safetyRxChecksInvalid=rx_invalid) + wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=source_ns) + wire.aolSafetyWire = encode_safety(SafetyState(1, configured[0], source_ns, source_ns + 30_000_000, + actual_model, actual_word, bool(mask & 1), bool(mask & 2), bool(actual_request & 1), bool(actual_request & 2), + 'authority-panda', card.aol_axis_session(), source_ns, (slot,))) + messages = [device.as_reader(), wire.as_reader()] + if tick >= next_panda_tick[0]: + messages.append(pandas.as_reader()) + next_panda_tick[0] = tick + 10 + messages += [previous_sd[name] for name in ('selfdriveState', 'aolAxisState', 'onroadEvents') if name in previous_sd] + if 'onroadEvents' not in previous_sd: + messages.append(messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=source_ns).as_reader()) + card.sm.update_msgs(source_ns / 1e9, messages) + state, _ = card.state_update() + if state.canValid: + assert state.cruiseState.available + for service in ('carState', 'carOutput', 'aolIntentWire', 'starpilotCarState'): + received.pop(service, None) + card.state_publish(state, None) + state_envelope = received['carState'] + output_envelope = received['carOutput'] + assert state_envelope.logMonoTime == source_ns and state_envelope.valid == state.canValid + assert output_envelope.logMonoTime == source_ns + + axis, host, command = drive(tick, intent_message=received['aolIntentWire'], + car_state_message=state_envelope, car_output_message=output_envelope, + companion=False, authority_message=received.get('starpilotCarState'), + allowed=bool(mask & 1), long_allowed=bool(mask & 2), native_request_lat=bool(actual_request & 1), + native_request_long=bool(actual_request & 2), panda_controls_allowed=native.get_controls_allowed(), + panda_fault=rx_invalid, relay_fault=relay_fault, panda_state_message=pandas, safety_message=wire) + if sd.conditional_car_state_valid: + assert sd.aol_car_state_log_ns == state_envelope.logMonoTime + assert controls.sm.logMonoTime['carState'] == state_envelope.logMonoTime + assert controls.sm.valid['carState'] == state_envelope.valid + assert controls.sm['carState'].to_dict() == state_envelope.carState.to_dict() + assert controls.sm.logMonoTime['carOutput'] == output_envelope.logMonoTime + assert controls.sm.valid['carOutput'] == output_envelope.valid + assert controls.sm['carOutput'].to_dict() == output_envelope.carOutput.to_dict() + assert bytes(controls.sm['aolSafetyWire']) == bytes(wire.aolSafetyWire) + for consumer in (unit for unit in (card, sd, controls) if 'pandaStates' in unit.sm.services): + observed = consumer.sm['pandaStates'][0] + assert int(observed.safetyModel.raw) in (int(structs.CarParams.SafetyModel.elm327), model) + if int(observed.safetyModel.raw) == int(structs.CarParams.SafetyModel.elm327): + assert observed.safetyParam == 1 and observed.alternativeExperience == 0 + else: + assert configured[0] and observed.safetyParam == word and observed.alternativeExperience == card.CP.alternativeExperience + previous_sd.update(sd.pm.messages) + validated = current_native(sd.sm, sd.CP, now_ns=source_ns, axis_session_id=sd.aol_session_id) + if validated is not None: + assert configured[0] and not rx_invalid + assert validated.lateralAllowed == bool(mask & 1) + assert validated.requestedLateral == bool(actual_request & 1) + else: + assert not axis.lateralActive and not axis.longitudinalActive + assert not command.latActive and not command.longActive + if not configured[0]: + assert validated is None + + now[0] += 1 + card.sm.update_msgs(now[0] / 1e9, [controls.pm.messages['carControl']]) + received.pop('sendcan', None) + exec(dispatch, dict(vars(card_module), self=card, CS=state)) + steering = [] + if state.canValid and card.ci_initialized and not configured[0] and 'sendcan' in received: + assert not command.enabled and not command.latActive and not command.longActive + assert command.actuators.torque == 0 and command.actuators.accel == 0 + for frame in received['sendcan'].sendcan: + accepted = bool(native.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src, frame.dat))) + assert accepted == (frame.address == 0x7d0 and len(frame.dat) == 8) + if state.canValid and card.ci_initialized and configured[0] and 'sendcan' in received and received['sendcan'].valid: + steering = [frame for frame in received['sendcan'].sendcan + if frame.address == 0x340 and frame.src == 0] + assert steering + for frame in received['sendcan'].sendcan: + assert native.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src, frame.dat)) + if 'sendcan' in received and not received['sendcan'].valid: + assert len(received['sendcan'].sendcan) == 0 + assert not command.enabled and not command.latActive and not command.longActive + assert command.actuators.torque == 0 and command.actuators.accel == 0 + return state, axis, host, command, steering + + for _ in range(120): + step() + assert card.ci_initialized and configured[0] and saved.get_bool('ControlsReady'), 'ORDINARY_CLASSIC_NATIVE_BOOTSTRAP_STALLED' + if expects_owner: + assert card.vehicle_startup.owner.handed_off + assert not card.aol_card_intent.allowed_latch + assert native.safety_config_valid() + for _ in range(2): + step(button=2) + for _ in range(20): + state, axis, host, command, _ = step(engaged=True) + assert state.cruiseState.enabled and host.enabled and native.get_controls_allowed() + assert axis.lateralActive and command.latActive + if shared_long: + assert axis.longitudinalActive and command.longActive + + assert card.aol_card_intent.allowed_latch, 'FULL_ENGAGEMENT_DID_NOT_KEEP_AOL' + assert card.aol_card_intent._last_latch_edge_ns > 0 + edge = card.aol_card_intent._last_latch_edge_ns + assert diagnostic_requests == [b'\x10\x03', b'\x28\x83\x01'] + assert card.ci_initialized and saved.get_bool('ControlsReady') + if not expect_retention: + for _ in range(2): + step(button=4) + for _ in range(5): + state, axis, host, command, _ = step() + assert not card.aol_card_intent.allowed_latch + assert not axis.lateralActive and not command.latActive + assert not axis.longitudinalActive and not command.longActive and not host.enabled + assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0 + return + cancel_observed = False + for _cancel_index in range(2): + state, axis, host, command, steering = step(main=int(incidental == 'main'), button=4 if incidental == 'cancel' else 0) + assert card.aol_card_intent.allowed_latch, ( + 'FAMILY_CANCEL_INTENT_DROPPED', identity, word, native.get_controls_allowed(), native.aol_get_permission_mask()) + assert native.aol_get_permission_mask() & 1, 'HKG_NATIVE_LATERAL_DROPPED' + assert axis.lateralActive and command.latActive, 'HKG_JOINED_LATERAL_DROPPED' + if any(event.type == structs.CarState.ButtonEvent.Type.cancel and event.pressed for event in state.buttonEvents): + cancel_observed = True + assert not axis.longitudinalActive and not command.longActive and not host.enabled + assert cancel_observed + nonzero = False + for _ in range(20): + state, axis, host, command, steering = step() + assert card.aol_card_intent.allowed_latch, ( + 'FAMILY_CANCEL_INTENT_DROPPED', identity, word, native.get_controls_allowed(), native.aol_get_permission_mask()) + assert card.aol_card_intent._last_latch_edge_ns == edge, 'HKG_INCIDENTAL_EDGE_RENEWED' + assert native.aol_get_permission_mask() & 1, 'HKG_NATIVE_LATERAL_DROPPED' + assert axis.lateralActive and command.latActive, 'HKG_JOINED_LATERAL_DROPPED' + assert not state.cruiseState.enabled and not host.enabled and not native.get_controls_allowed() + assert not axis.longitudinalActive and not command.longActive + nonzero |= abs(command.actuators.torque) > 0 + assert steering + if shared_long: + + for _ in range(2): + state, axis, host, command, _ = step(main=1) + assert card.aol_card_intent.allowed_latch and axis.lateralActive and command.latActive + assert not host.enabled and not command.longActive and not native.get_controls_allowed() + for _ in range(20): + state, axis, host, command, _ = step() + assert card.aol_card_intent.allowed_latch and axis.lateralActive and command.latActive + assert card.aol_card_intent._last_latch_edge_ns == edge + assert not host.enabled and not axis.longitudinalActive and not command.longActive + assert not native.get_controls_allowed() + assert nonzero, 'Actual Controls did not produce nonzero correction for measured steering error' + invalid = controls.pm.messages['carControl'].as_builder() + invalid.valid = False + now[0] += 1 + card.sm.update_msgs(now[0] / 1e9, [invalid.as_reader()]) + received.pop('sendcan', None) + card.controls_update(state, card.sm['carControl']) + + assert _invalid_controls_emits_no_commands(received), 'Invalid Controls packet produced valid or nonempty Card sendcan' + assert _invalid_controls_emits_no_commands({}) + empty = messaging.new_message('sendcan', 0, valid=False) + assert _invalid_controls_emits_no_commands({'sendcan': empty.as_reader()}) + for valid, frame_count in ((True, 0), (False, 1), (True, 1)): + rejected = messaging.new_message('sendcan', frame_count, valid=valid) + assert not _invalid_controls_emits_no_commands({'sendcan': rejected.as_reader()}) + if withdrawal == 'toggle-off': + for _ in range(2): + step(lkas=1) + for _ in range(3): + state, axis, host, command, _ = step(withdraw_request=withdrawal == 'native-request-off') + assert not axis.lateralActive and not command.latActive + assert not axis.longitudinalActive and not command.longActive and not host.enabled + if withdrawal == 'toggle-off': + assert not card.aol_card_intent.allowed_latch + assert native.aol_get_permission_mask() == 0 + finally: + patch_scope.__exit__(None, None, None) + finally: + native.init_tests() + native.set_alternative_experience(0) + native.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0) + case.doCleanups() + if card is not None: + card.vehicle_startup.close() diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_cancel_joined.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_cancel_joined.py index 0863ee8206..2238f8f79a 100644 --- a/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_cancel_joined.py +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_cancel_joined.py @@ -12,7 +12,7 @@ class TestClassicCancelDebug: class TestClassicCancelReleaseDenied: - @pytest.mark.parametrize('word', [0xC04, 0xC06]) + @pytest.mark.parametrize('word', [0xC04, 0xC05, 0xC06, 0xC44, 0xC45, 0xC46]) def test_release_does_not_admit_classic_longitudinal_or_independent_lateral(self, word): safety = libsafety_py.libsafety safety.init_tests() diff --git a/openpilot/starpilot/aol/tests/test_classic_family_button_scope.py b/openpilot/starpilot/aol/tests/test_classic_family_button_scope.py new file mode 100644 index 0000000000..0e1c179df9 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_classic_family_button_scope.py @@ -0,0 +1,33 @@ +import pytest + +from opendbc.car.hyundai.classic_long_aol import CLASSIC_LONG_IDS, aol_word, independent_buttons_qualified +from openpilot.starpilot.aol.intent import AolSettings +from openpilot.starpilot.aol.tests.test_classic_long_profiles import params +from openpilot.starpilot.car.hyundai.classic_long_intent import ClassicLongCardIntent + + +@pytest.mark.parametrize('identity', sorted(CLASSIC_LONG_IDS, key=str)) +@pytest.mark.parametrize('lda', [False, True]) +@pytest.mark.parametrize('release', [False, True]) +def test_each_actual_factory_preserves_marker_lda_and_release_boundary(identity, lda, release): + cp = params(identity, lda=lda, release=release) + before = cp.to_dict() + assert independent_buttons_qualified(cp, marked_only=False) == (lda and not release) + assert not independent_buttons_qualified(cp) + assert cp.to_dict() == before + cp.safetyConfigs[0].safetyParam = aol_word(cp) + cp.alternativeExperience = 32 + before = cp.to_dict() + assert independent_buttons_qualified(cp) == (lda and not release) + assert cp.to_dict() == before + + +@pytest.mark.parametrize('identity', sorted(CLASSIC_LONG_IDS, key=str)) +@pytest.mark.parametrize('lkas,main', [(9, 0), (0, 0), (0, 9), (9, 9)]) +def test_each_family_profile_keeps_exact_saved_mapping(identity, lkas, main): + cp = params(identity, lda=True) + before = cp.to_dict() + settings = AolSettings(True, 0., lkas, main, (0, 0, 0), (0, 0, 0)) + owner = ClassicLongCardIntent(cp, settings) + assert owner.independent_cruise_buttons == (lkas == 9 and main == 0) + assert owner.settings == settings and cp.to_dict() == before diff --git a/openpilot/starpilot/aol/tests/test_hkg_independent_cancel_contract.py b/openpilot/starpilot/aol/tests/test_hkg_independent_cancel_contract.py index f4a55d644d..60f0b91dfc 100644 --- a/openpilot/starpilot/aol/tests/test_hkg_independent_cancel_contract.py +++ b/openpilot/starpilot/aol/tests/test_hkg_independent_cancel_contract.py @@ -37,13 +37,13 @@ def test_stock_lkas_owned_aol_survives_unassigned_cruise_gesture(identity, alter assert not owner.allowed_latch -def test_classic_long_keeps_published_policy_until_full_join_is_qualified(): +def test_classic_long_qualified_profile_keeps_lkas_owned_cancel_intent(): from openpilot.starpilot.aol.tests.test_classic_long_profiles import params as classic_params from opendbc.car.hyundai.classic_long_aol import qualified cp = classic_params(lda=True) assert qualified(cp) owner = create_intent(cp, AolSettings(True, 0., AOL_TOGGLE, 0, (0, 0, 0), (0, 0, 0))) - assert not owner.independent_cruise_buttons + assert owner.independent_cruise_buttons cs = structs.CarState(canValid=True, gearShifter='drive') cs.cruiseState.available = True owner.update(cs, now_ns=1) @@ -54,7 +54,7 @@ def test_classic_long_keeps_published_policy_until_full_join_is_qualified(): owner.update(cs, now_ns=3) cs.buttonEvents = [structs.CarState.ButtonEvent(type='cancel', pressed=True)] owner.update(cs, now_ns=4) - assert not owner.allowed_latch + assert owner.allowed_latch @pytest.mark.parametrize('identity', sorted(STOCK_EV_CARS, key=str)) diff --git a/openpilot/starpilot/aol/tests/test_ioniq_availability.py b/openpilot/starpilot/aol/tests/test_ioniq_availability.py index 752edb9de9..feb20918c5 100644 --- a/openpilot/starpilot/aol/tests/test_ioniq_availability.py +++ b/openpilot/starpilot/aol/tests/test_ioniq_availability.py @@ -52,7 +52,7 @@ def assert_fresh_ordinary_recovery(sd, axis, command, source): class IoniqAvailabilityTests(unittest.TestCase): - def _authority_callers(self, full_op, *, cp=None, intent_companion=None, genuine_events=False): + def _authority_callers(self, full_op, *, cp=None, intent_companion=None, genuine_events=False, initial_panda_config=None): self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False)) from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params from opendbc.car.gm.values import CAR @@ -154,6 +154,7 @@ class IoniqAvailabilityTests(unittest.TestCase): # that unseen state, then feed only the preceding actual published command. previous_control_message = [None] due = {} + controls_panda_due = [base] def feed_event_sources(stamp, companions=(), *, car_output_valid=True): from openpilot.cereal.services import SERVICE_LIST names = ['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'extrinsicsCalibration', @@ -167,7 +168,7 @@ class IoniqAvailabilityTests(unittest.TestCase): # Ignored authority wires remain fresh on every 100 Hz source tick. period = round(1e9 / min(frequency, 100)) if frequency > 0 else 0 if (stamp < due.get(name, 0) and name not in ('aolIntentWire', 'aolSafetyWire', 'starpilotCarState') and - not (name == 'pandaStates' and name in supplied and + not (name == 'pandaStates' and initial_panda_config is None and name in supplied and any(p.safetyRxChecksInvalid or p.faults for p in supplied[name].pandaStates))): continue due[name] = stamp + period @@ -180,8 +181,13 @@ class IoniqAvailabilityTests(unittest.TestCase): value.started, value.startedMonoTime, value.freeSpacePercent = True, base - 1_000_000_000, 50. elif name == 'pandaStates': for panda, config in zip(value, cp.safetyConfigs, strict=True): - panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam - panda.alternativeExperience, panda.controlsAllowed = cp.alternativeExperience, full_op + if initial_panda_config is None: + panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam + panda.alternativeExperience, panda.controlsAllowed = cp.alternativeExperience, full_op + else: + model, word, experience, controls_allowed, rx_invalid = initial_panda_config + panda.safetyModel, panda.safetyParam, panda.alternativeExperience = model, word, experience + panda.controlsAllowed, panda.safetyRxChecksInvalid = controls_allowed, rx_invalid elif name == 'extrinsicsCalibration': value.calStatus, value.rpyCalib = 'calibrated', [0., 0., 0.] elif name == 'deviceMotion': @@ -208,7 +214,7 @@ class IoniqAvailabilityTests(unittest.TestCase): long_allowed=True, pause_long=False, state_stamp=None, intent_message=None, offset_ns=0, native_request_lat=True, native_request_long=None, native_valid=True, panda_fault=False, panda_controls_allowed=None, critical_event=None, calibrated=True, car_output_valid=True, relay_fault=False, - car_state_message=None, car_output_message=None, authority_message=None): + car_state_message=None, car_output_message=None, authority_message=None, panda_state_message=None, safety_message=None): now = base + tick * 10_000_000 + offset_ns clock.return_value = now if seconds is not None: @@ -248,6 +254,8 @@ class IoniqAvailabilityTests(unittest.TestCase): panda.safetyRxChecksInvalid = panda_fault if relay_fault: panda.faults = ['relayMalfunction'] + if panda_state_message is not None: + pandas = panda_state_message frames.append(pandas.as_reader()) if car_output_message is not None: frames.append(car_output_message.as_reader()) @@ -268,6 +276,8 @@ class IoniqAvailabilityTests(unittest.TestCase): cp.alternativeExperience, panda_controls_allowed, panda_fault, False, 1 if relay_fault else 0) for index, config in enumerate(cp.safetyConfigs)) if genuine_events else ())) + if safety_message is not None: + native = safety_message if authority_message is not None: frames.append(authority_message.as_reader()) elif companion: @@ -304,6 +314,11 @@ class IoniqAvailabilityTests(unittest.TestCase): if car_output_message is not None: control_frames = [sample for sample in control_frames if sample.which() != 'carOutput'] control_frames.append(car_output_message.as_reader()) + if panda_state_message is not None and 'pandaStates' in controls.sm.services: + control_frames = [sample for sample in control_frames if sample.which() != 'pandaStates'] + if now >= controls_panda_due[0]: + control_frames.append(panda_state_message.as_reader()) + controls_panda_due[0] = now + 100_000_000 replaced = ('carState', 'aolSafetyWire', 'modelV2', 'aolAxisState', 'selfdriveState') controls.sm.update_msgs(now / 1e9, [message for message in control_frames if message.which() not in replaced] + [control_state.as_reader(), native.as_reader(), model.as_reader(), diff --git a/openpilot/starpilot/aol/tests/test_native_inventory_control_cadence.py b/openpilot/starpilot/aol/tests/test_native_inventory_control_cadence.py new file mode 100644 index 0000000000..8f030a5a83 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_native_inventory_control_cadence.py @@ -0,0 +1,164 @@ +from dataclasses import replace +from types import SimpleNamespace + +import pytest + +from openpilot.cereal import messaging +from openpilot.starpilot.aol.runtime import current_native +from openpilot.starpilot.aol.transport_pause import _newer_combined_intent_native, healthy_transport +from openpilot.starpilot.aol.tests.test_panda_inventory import NOW, bound_state, configured_cp, sm_for +from openpilot.starpilot.aol.tests.test_runtime import car_state + + +@pytest.mark.parametrize('case', ['older', 'equal', 'newer', 'legacy', 'slot_controls', 'native_long', 'native_lat', + 'request_long', 'request_lat', 'session', 'expired', 'future', 'lease', 'native_rx', 'native_heartbeat', 'native_fault', + 'native_identity', 'native_experience', 'native_invalid', 'native_lost', 'panda_rx', 'panda_heartbeat', 'panda_fault', + 'panda_identity', 'panda_experience', 'panda_invalid', 'panda_lost', 'panda_unseen', 'axis_lat', 'axis_long', + 'axis_request_lat', 'axis_request_long', 'axis_ack', 'brake', 'gas', 'acc', 'can', 'timeout', 'steer', 'cruise']) +def test_newer_native_retains_only_complete_current_combined_intent(case): + cp = configured_cp() + native = bound_state(cp) + source = native.sourcePandaStatesMonoTime + timestamp = source - 50_000_000 if case not in ('equal', 'newer') else source + int(case == 'newer') + cs = car_state() + cs.cruiseState.enabled = True + axis = SimpleNamespace(sessionId=native.axisSessionId, nativeAcknowledged=True, + desiredLateral=True, lateralActive=True, desiredLongitudinal=True, longitudinalActive=True) + native_changes = {'native_long': {'longitudinalAllowed': False}, 'native_lat': {'lateralAllowed': False}, + 'request_long': {'requestedLongitudinal': False}, 'request_lat': {'requestedLateral': False}, + 'future': {'observedMonoTime': NOW + 1}, 'lease': {'validUntilMonoTime': NOW + 200_000_001}} + if case in native_changes: + native = replace(native, **native_changes[case]) + slot_changes = {'slot_controls': {'controlsAllowed': False}, 'native_rx': {'safetyRxChecksInvalid': True}, + 'native_heartbeat': {'heartbeatLost': True}, 'native_fault': {'faults': 1}, + 'native_identity': {'safetyParam': native.safetyParam ^ 1}, 'native_experience': {'alternativeExperience': cp.alternativeExperience ^ 1}} + if case in slot_changes: + native = replace(native, pandaInventory=(replace(native.pandaInventory[0], **slot_changes[case]),)) + if case == 'legacy': + native = replace(native, pandaInventory=(), sourcePandaStatesMonoTime=0) + timestamp = NOW - 50_000_000 + sm = sm_for(cp, native, panda_stamp=timestamp) + update = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=timestamp) + update.pandaStates[0] = sm['pandaStates'][0] + p = update.pandaStates[0] + p.controlsAllowed = False + panda_changes = {'panda_rx': ('safetyRxChecksInvalid', True), 'panda_heartbeat': ('heartbeatLost', True), + 'panda_fault': ('faults', ['relayMalfunction']), 'panda_identity': ('safetyParam', 0), + 'panda_experience': ('alternativeExperience', cp.alternativeExperience ^ 1)} + if case in panda_changes: + setattr(p, *panda_changes[case]) + sm.update_msgs(NOW / 1e9, [update.as_reader()]) + if case in ('panda_invalid', 'panda_lost', 'panda_unseen'): + getattr(sm, {'panda_invalid': 'valid', 'panda_lost': 'alive', 'panda_unseen': 'seen'}[case])['pandaStates'] = False + if case in ('native_invalid', 'native_lost'): + getattr(sm, 'valid' if case == 'native_invalid' else 'alive')['aolSafetyWire'] = False + axis_changes = {'session': ('sessionId', 'foreign'), 'axis_lat': ('lateralActive', False), + 'axis_long': ('longitudinalActive', False), 'axis_request_lat': ('desiredLateral', False), + 'axis_request_long': ('desiredLongitudinal', False), 'axis_ack': ('nativeAcknowledged', False)} + if case in axis_changes: + setattr(axis, *axis_changes[case]) + cs_changes = {'brake': ('brakePressed', True), 'gas': ('gasPressed', True), 'acc': ('accFaulted', True), + 'can': ('canValid', False), 'timeout': ('canTimeout', True), 'steer': ('steerFaultPermanent', True)} + if case in cs_changes: + setattr(cs, *cs_changes[case]) + if case == 'cruise': + cs.cruiseState.enabled = False + now = NOW + 200_000_001 if case == 'expired' else NOW + assert healthy_transport(sm, cp, cs, axis.sessionId, now) == (case == 'request_long') + before = (native, tuple(sm.logMonoTime.items()), tuple(sm.recv_time.items())) + result = _newer_combined_intent_native(sm, cp, cs, axis, now) + assert (result is not None) == (case == 'older') + assert (native, tuple(sm.logMonoTime.items()), tuple(sm.recv_time.items())) == before + if result is not None: + assert result == current_native(sm, cp, now_ns=now, axis_session_id=axis.sessionId) + assert result.observedMonoTime == NOW and result.validUntilMonoTime == NOW + 200_000_000 + + +@pytest.mark.parametrize('case', ['older', 'equal', 'newer', 'fault', 'native_identity', 'expired', 'source_lost']) +def test_actual_gm_sd_controls_and_card_keep_current_denials(case): + from unittest.mock import patch + from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params + from opendbc.car.gm.values import CAR + from openpilot.common.prefix import OpenpilotPrefix + from openpilot.selfdrive.car.card import Car + from openpilot.starpilot.aol.intent import AolProcessFaultContext, AolSettings + from openpilot.starpilot.aol.tests.test_ioniq_availability import IoniqAvailabilityTests + from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback + from openpilot.starpilot.aol.vehicle import create_intent, policy_for + from openpilot.starpilot.aol.wire import IntentState, encode_intent, encode_safety + + fixture = IoniqAvailabilityTests(methodName='runTest') + try: + with OpenpilotPrefix(): + cp = ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True, radar=True) + sd, controls, drive = fixture._authority_callers(True, cp=cp) + assert sd.CP.brand == 'gm' and sd.CP.openpilotLongitudinalControl + card = Car.__new__(Car) + card.CP, card.slc_producer_session = sd.CP, 'authority-card' + services = ['aolAxisState', 'aolSafetyWire', 'pandaStates', 'onroadEvents'] + card.sm = messaging.SubMaster(services, ignore_avg_freq=services) + card.aol_process_fault_context = AolProcessFaultContext() + card.aol_transport_feedback = TransportPauseFeedback(card.CP) + settings = AolSettings(True, 0., 9, 0, (0, 0, 0), (0, 0, 0)) + card.aol_card_intent = create_intent(card.CP, settings, policy_for(card.CP)) + assert card.aol_card_intent is not None + cs = car_state() + cs.cruiseState.available = cs.cruiseState.enabled = True + card.aol_card_intent.update(cs, fault_active=False, now_ns=NOW - 1, standard_enabled=True) + assert card.aol_card_intent.allowed_latch + for tick in range(3): + now = NOW + tick * 10_000_000 + state = bound_state(sd.CP, now=now) + pandas = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True, + logMonoTime=state.sourcePandaStatesMonoTime) + for panda, config in zip(pandas.pandaStates, cp.safetyConfigs, strict=True): + panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam + panda.alternativeExperience, panda.controlsAllowed = cp.alternativeExperience, True + if tick == 2: + pandas.logMonoTime = state.sourcePandaStatesMonoTime + (1 if case == 'newer' else 0 if case == 'equal' else -50_000_000) + pandas.pandaStates[-1].controlsAllowed = False + if case == 'fault': + pandas.pandaStates[-1].faults = ['relayMalfunction'] + if case == 'source_lost': + pandas.valid = False + if case == 'native_identity': + state = replace(state, pandaInventory=(replace(state.pandaInventory[0], safetyParam=0),)) + if case == 'expired': + state = replace(state, observedMonoTime=now - 200_000_001, validUntilMonoTime=now - 1) + wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=state.observedMonoTime) + wire.aolSafetyWire = encode_safety(state) + vehicle = messaging.new_message('carState', valid=True, logMonoTime=now) + vehicle.carState = cs + intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now) + intent.aolIntentWire = encode_intent(IntentState('authority-card', tick + 1, now, now, + now + 200_000_000, True, False, False, True, True)) + raw_companions = [intent.as_reader()] + authority = messaging.new_message('starpilotCarState', valid=True, logMonoTime=now) + authority.starpilotCarState.sourceCarStateMonoTime = now + authority.starpilotCarState.lateralAuthorityUnavailable = False + steering_companions = [authority.as_reader()] + optional_receive = messaging.recv_one_or_none + def receive(sock, raw_companions=raw_companions, steering_companions=steering_companions, optional_receive=optional_receive): + if sock is sd.aol_intent_companion.sock: + return raw_companions.pop(0) if raw_companions else None + if sock is sd.steering_companion.sock: + return steering_companions.pop(0) if steering_companions else None + return optional_receive(sock) + with patch.object(messaging, 'recv_one_or_none', side_effect=receive): + axis, _, command = drive(tick, panda_state_message=pandas, safety_message=wire, + car_state_message=vehicle, intent_message=intent, authority_message=authority) + assert not raw_companions and not steering_companions + if case in ('native_identity', 'expired') and tick == 2: + assert not axis.lateralActive and not axis.longitudinalActive and not command.latActive and not command.longActive + else: + assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive + frames = [pandas.as_reader(), wire.as_reader(), sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']] + card.sm.update_msgs(now / 1e9, frames) + fault = card.aol_disarming_fault(cs, int(card.sm.logMonoTime['onroadEvents']), now) + card.aol_card_intent.update(cs, fault_active=fault, now_ns=now, standard_enabled=True) + assert fault == (tick == 2 and case != 'older') + assert card.aol_card_intent.allowed_latch == (tick != 2 or case == 'older') + if tick == 2 and case == 'older': + assert not healthy_transport(card.sm, cp, cs, state.axisSessionId, now) + finally: + fixture.doCleanups() diff --git a/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py b/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py index bdcaa4a15b..254a3fba09 100644 --- a/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py +++ b/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py @@ -42,7 +42,7 @@ def test_hybrid_added_scope_preserves_existing_profile_vetoes(defect): elif defect == 'passive': cp.passive = True elif defect == 'fingerprint': - cp.carFingerprint = CAR.HYUNDAI_IONIQ + cp.carFingerprint = CAR.HYUNDAI_IONIQ_5 elif defect == 'identity_flags': cp.flags = int(cp.flags) & ~int(HyundaiFlags.HYBRID) before = cp.to_dict() diff --git a/openpilot/starpilot/aol/transport_pause.py b/openpilot/starpilot/aol/transport_pause.py index e1766fc2e0..73387a41d8 100644 --- a/openpilot/starpilot/aol/transport_pause.py +++ b/openpilot/starpilot/aol/transport_pause.py @@ -64,6 +64,21 @@ def _healthy_transport_native(sm, cp, cs, session, now_ns, native=None, *, requi return None +def _newer_combined_intent_native(sm, cp, cs, axis, now_ns): + if (not axis.nativeAcknowledged or not axis.desiredLateral or not axis.lateralActive or + not axis.desiredLongitudinal or not axis.longitudinalActive or not cs.cruiseState.enabled or + cs.brakePressed or cs.gasPressed or cs.accFaulted): + return None + native = _healthy_transport_native(sm, cp, cs, str(axis.sessionId), now_ns, require_longitudinal=False) + if (native is None or not native.pandaInventory or not native.requestedLateral or not native.lateralAllowed or + not native.requestedLongitudinal or not native.longitudinalAllowed or + not 0 < int(sm.logMonoTime['pandaStates']) < native.sourcePandaStatesMonoTime or + any(not slot.controlsAllowed for slot, config in zip(native.pandaInventory, cp.safetyConfigs, strict=True) + if str(config.safetyModel) not in ('silent', 'noOutput'))): + return None + return native + + def independent_lateral_health(sm, cp, cs, session, now_ns): """Structural health for retaining LAT intent; never grant either output axis.""" native = _healthy_transport_native(sm, cp, cs, session, now_ns, require_longitudinal=False) @@ -1138,6 +1153,8 @@ class TransportPauseFeedback: 0 < axis.sourceCarStateMonoTime <= stamp and stamp - axis.sourceCarStateMonoTime <= AXIS_MAX_AGE_NS) native = _healthy_transport_native(sm, cp, cs, str(axis.sessionId), now_ns) + if native is None and latched and current and continuity and baseline: + native = _newer_combined_intent_native(sm, cp, cs, axis, now_ns) lateral_native = (self._native_long_withdrawal_lateral_current(sm, cp, cs, axis, now_ns, latched=latched, producer=intent_producer_session) if native is None and current and continuity and baseline else None) diff --git a/prebuilt.json b/prebuilt.json index b72844ffdc..406b907c6d 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "c8754462000a4920ab6c6a8eae9399d642aa4037780642999a646ba6be576591", "schema_version": 1} +{"protected_inventory_sha256": "e826736ec828dcc70a217d8f7a60c5aab6fe2613d23f7db75ba464ce7eb18873", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 8d491af615..4581138685 100755 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -33,6 +33,9 @@ DEBUG_SAFETY_CLASS_TARGETS = { RELEASE_TARGETS = ( + "openpilot/starpilot/aol/tests/test_native_inventory_control_cadence.py", + "openpilot/starpilot/aol/tests/test_classic_family_button_scope.py", + "openpilot/starpilot/aol/tests/test_hkg_independent_cancel_contract.py", "opendbc_repo/opendbc/car/hyundai/tests/test_restore_session_settle.py", "openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py", "opendbc_repo/opendbc/car/hyundai/tests/test_sibling_continuity.py",