EV6 mismatch

This commit is contained in:
firestar5683
2026-10-16 12:56:52 -05:00
parent 89430a70da
commit 4fa668d81a
42 changed files with 267 additions and 11 deletions
@@ -20,6 +20,8 @@ from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled
from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy
from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons
from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock_aol
from opendbc.car.hyundai.ev6_aol import qualified as qualified_ev6_aol
from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy
from opendbc.car.hyundai.gv70_longitudinal import is_gv70, scc_request, suppress_stock_cancel
from opendbc.car.hyundai.g90_lead import G90LeadState, eligible as g90_lead_eligible
@@ -135,7 +137,8 @@ class CarController(CarControllerBase):
ioniq6_status = (self.ioniq6_longitudinal is not None and
int(CP.safetyConfigs[-1].safetyParam) in (0x8015, 0x8095, 0x8815, 0x8895))
self.ioniq6_bsm_enabled = ioniq6_status
self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled else None
self.ev6_aol_status = CP.carFingerprint == CAR.KIA_EV6 and (qualified_canfd_stock_aol(CP) or qualified_ev6_aol(CP))
self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled or self.ev6_aol_status else None
self.ioniq6_bsm_counter = 0
self.ioniq6_bsm_last_can_ns = 0
self._ray_pedal = ray_pedal_enabled(CP)
@@ -456,7 +459,8 @@ class CarController(CarControllerBase):
)
else:
steering_status_active = CC.enabled
if self.ioniq6_longitudinal is not None and self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895):
if self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895)):
# AOL may steer while ordinary cruise engagement remains off.
steering_status_active |= CC.latActive
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN,
+22 -6
View File
@@ -1326,21 +1326,32 @@ class Car:
self.CP.carFingerprint == GM_CAR.CHEVROLET_VOLT_ASCM and self.CP.pcmCruise and
not self.CP.openpilotLongitudinalControl and gm_lane_centering_supported(self.CP)))
def ev6_startup_keepalive(self):
from opendbc.car.hyundai.ev6_startup import EV6Startup, required
from opendbc.car.hyundai.ecu_startup import Outcome
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
return (isinstance(owner, EV6Startup) and owner.outcome is Outcome.SENT_UNCONFIRMED and
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
required(self.CP))
def controls_update(self, CS: car.CarState, CC: car.CarControl, *, initialize_only=False):
"""control update loop, driven by carControl"""
if initialize_only:
ev6_startup = self.ev6_startup_keepalive()
now_ns = time.monotonic_ns()
source_ns = int(self.sm.logMonoTime['carControl'])
receipt_ns = int(self.sm.recv_time['carControl'] * 1e9)
# Disabled torque/accel are commands; inactive curvature is only telemetry.
if (not self.aol_replay or not native_bootstrap_supported(self.CP) or self.vehicle_startup.owner is not None or
if (not (ev6_startup or (self.aol_replay and native_bootstrap_supported(self.CP) and self.vehicle_startup.owner is None)) or
not CS.canValid or CS.canTimeout or
not (self.sm.seen['carControl'] and self.sm.valid['carControl'] and self.sm.alive['carControl']) or
not (0 < source_ns <= now_ns and now_ns - source_ns <= 150_000_000 and
0 < receipt_ns <= now_ns and now_ns - receipt_ns <= 150_000_000) or
CC.enabled or CC.latActive or CC.longActive or CC.actuators.torque != 0.0 or CC.actuators.accel != 0.0):
return
if ev6_startup and not self.vehicle_startup.owner.prepared_for(self.CP):
return
if getattr(self, 'volt_cc_selected', False):
if not self.volt_cc_control_current():
@@ -1407,11 +1418,16 @@ class Car:
self.ci_initialized = True
if initialize_only:
if (isinstance(self.CI, GMInterface) and self.volt_startup_keepalive() and
if ev6_startup and any(ps.faults for ps in self.sm['pandaStates']):
return
if (((isinstance(self.CI, GMInterface) and self.volt_startup_keepalive()) or ev6_startup) and
self.startup_panda_configured(inactive_keepalive=True)):
inactive = car.CarControl.new_message().as_reader()
self.last_actuators_output, can_sends = self.CI.apply(
inactive, time.clock_gettime_ns(time.CLOCK_BOOTTIME), startup_keepalive=True)
now_boot_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
if ev6_startup:
self.last_actuators_output, can_sends = self.CI.apply(inactive, now_boot_ns)
else:
self.last_actuators_output, can_sends = self.CI.apply(inactive, now_boot_ns, startup_keepalive=True)
self.publish_sendcan(can_sends, valid=CS.canValid)
return
@@ -1484,9 +1500,9 @@ class Car:
self.sm.seen['onroadEvents'])
if not self.CP.passive and initialized:
self.controls_update(CS, self.sm['carControl'])
elif ((not self.ci_initialized or self.volt_startup_keepalive()) and self.sm.seen['onroadEvents'] and
elif ((not self.ci_initialized or self.volt_startup_keepalive() or self.ev6_startup_keepalive()) and self.sm.seen['onroadEvents'] and
any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and
self.aol_replay and native_bootstrap_supported(self.CP)):
((self.aol_replay and native_bootstrap_supported(self.CP)) or self.ev6_startup_keepalive())):
self.controls_update(CS, self.sm['carControl'], initialize_only=True)
radar_can = self._deferred_radar_can
@@ -0,0 +1,214 @@
"""EV6 stock cruise retains separate steering intent and truthful dash status."""
import os
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.car import Bus, structs
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR, DBC
from opendbc.can import CANPacker, CANParser
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
from openpilot.starpilot.aol.tests import test_canfd_stock_profiles as stock
from openpilot.starpilot.tests import test_ev6_startup as startup
def packets(ci, tick, *, main=0, lkas=0):
result = []
for parser in ci.can_parsers.values():
packer = CANPacker(parser.dbc.name)
for address in sorted(parser.addresses):
name = parser.dbc.addr_to_msg[address].name
values = {} if name in ('FR_CMR_02_100ms', 'DOORS_SEATBELTS', 'BLINKERS') else {'COUNTER': tick % 16}
if name == ci.CS.cruise_btns_msg_canfd:
values.update(ADAPTIVE_CRUISE_MAIN_BTN=main, LDA_BTN=lkas)
elif name == 'SCC_CONTROL':
values.update(MainMode_ACC=1, ACCMode=0)
elif name == ci.CS.gear_msg_canfd:
values['GEAR'] = 5
elif name == 'DOORS_SEATBELTS':
values['DRIVER_SEATBELT'] = 1
result.append(packer.make_can_msg(name, parser.bus, values))
return result
@pytest.mark.parametrize('main_action', [0, AOL_TOGGLE])
def test_actual_card_stock_lkas_then_main_respects_saved_binding(main_action):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
saved = Params()
for key in ('AlwaysOnLateral', 'OpenpilotEnabledToggle'):
saved.put_bool(key, True, block=True)
saved.put('MainCruiseButtonControl', main_action, block=True)
saved.put('LKASButtonControl', AOL_TOGGLE, block=True)
cp = stock.params(CAR.KIA_EV6)
def factory(*args, pre_create_hook, **kwargs):
selected = pre_create_hook(cp.as_reader().as_builder(), cp.carFingerprint, {}, [])
return CarInterface(selected)
with patch.object(card_module.messaging, 'recv_one_retry', return_value=SimpleNamespace(can=[1])), \
patch.object(card_module, 'get_car', side_effect=factory):
card = card_module.Car()
card.ci_initialized = True
card.publish_sendcan = lambda *args, **kwargs: None
card.CI.update([]) # Register lazy subscriptions before supplying the warmup batch.
try:
for tick, (main, lkas) in enumerate([(0, 0)] * 20 + [(0, 1), (0, 0), (1, 0), (0, 0)]):
now = 10_000_000_000 + tick * 10_000_000
batch = messaging.log_from_bytes(card_module.can_list_to_can_capnp(packets(card.CI, tick, main=main, lkas=lkas))).as_builder()
batch.logMonoTime = now
with patch.object(card_module, 'REPLAY', False), \
patch.object(card_module.messaging, 'drain_sock_raw', return_value=[batch.to_bytes()]), \
patch.object(card_module.time, 'monotonic_ns', return_value=now), \
patch.object(card_module.time, 'monotonic', return_value=now / 1e9), patch.object(card.sm, 'update'):
card.step()
assert card.CS_prev.canValid and not card.CS_prev.canTimeout
assert card.CS_prev.cruiseState.available
if tick == 21:
assert card.aol_card_intent.allowed_latch
assert card.aol_card_intent.allowed_latch == (main_action == 0)
assert not card.last_actuators_output.torqueOutputCan # No native permission was provided.
finally:
card.vehicle_startup.close()
@pytest.mark.parametrize('alpha', [False, True])
def test_actual_controller_dash_follows_aol_without_changing_actuation(alpha):
if alpha:
ci, _, _ = startup.TestEV6Startup().exercise(mode='sent_restore')
ci.CP.alternativeExperience = 32
else:
ci = CarInterface(stock.params(CAR.KIA_EV6))
ci.CP.safetyConfigs[-1].safetyParam |= 0x800
parser = CANParser(DBC[CAR.KIA_EV6][Bus.pt], [('LKAS', 0)], ci.CC.CAN.ACAN)
for tick in range(120):
now = 10_000_000_000 + tick * 10_000_000
ci.update([(now, packets(ci, tick))])
command = structs.CarControl(enabled=40 <= tick < 80, latActive=20 <= tick < 60)
_, frames = ci.apply(command.as_reader(), now)
steering = [frame for frame in frames if frame[0] == 0x50 and frame[2] == ci.CC.CAN.ACAN]
assert len(steering) == 1
parser.update([(now, steering)])
values = parser.vl['LKAS']
assert values['LKA_SysIndReq'] == (2 if command.enabled or command.latActive else 1)
assert values['ActToiSta'] == command.latActive
assert values['StrTqReqVal'] == 0
cluster = [frame for frame in frames if frame[0] == 0x1E0]
assert len(cluster) == int(alpha and tick % 5 == 0)
@pytest.mark.parametrize('variation', ['normal', 'aol_off', 'active', 'stale', 'native_mismatch', 'native_fault', 'invalid_can', 'changed_cp'])
def test_actual_card_replaces_disabled_adrv_during_selfdrive_initialization(variation):
from opendbc.car.hyundai.radar_interface import RadarInterface
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', variation != 'aol_off', block=True)
# The fixture normally seals for its own assertions; Card owns the real seal here.
with patch.object(VehicleStartupOwner, 'seal_publication'):
ci, holder, _ = startup.TestEV6Startup().exercise(mode='sent_restore')
card = card_module.Car(CI=ci, RI=RadarInterface(ci.CP), startup_owner=holder.owner)
assert card.ev6_startup_keepalive()
holder.owner.published = False
assert not card.ev6_startup_keepalive()
holder.owner.published = True
holder.owner.ci = None
assert not card.ev6_startup_keepalive()
holder.owner.ci = ci
card.CI.update([])
emitted = []
card.publish_sendcan = lambda frames, valid=True: emitted.extend((card.sm.frame, frame, valid) for frame in frames)
try:
for tick in range(620):
now = 10_000_000_000 + tick * 10_000_000
batch = messaging.log_from_bytes(card_module.can_list_to_can_capnp(packets(card.CI, tick))).as_builder()
batch.logMonoTime = now
control = messaging.new_message('carControl', valid=True, logMonoTime=now - (200_000_000 if variation == 'stale' else 0))
if variation == 'active':
control.carControl.enabled = control.carControl.latActive = True
control.carControl.actuators.torque = .1
events = messaging.new_message('onroadEvents', 1, valid=True, logMonoTime=now)
events.onroadEvents[0].name = 'selfdriveInitializing'
events.onroadEvents[0].noEntry = True
pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now)
panda = pandas.pandaStates[0]
config = card.CP.safetyConfigs[0]
panda.safetyModel = config.safetyModel if tick >= 5 else 'elm327'
panda.safetyParam = (config.safetyParam ^ int(variation == 'native_mismatch')) if tick >= 5 else 1
panda.alternativeExperience = card.CP.alternativeExperience if tick >= 5 else 0
panda.ignitionLine = True
if variation == 'changed_cp':
card.CP.mass += 1
if variation == 'native_fault':
panda.faults = ['relayMalfunction']
card.sm.update_msgs(now / 1e9, [control.as_reader(), events.as_reader(), pandas.as_reader()])
with patch.object(card_module, 'REPLAY', False), \
patch.object(card_module.messaging, 'drain_sock_raw', return_value=[batch.to_bytes()]), \
patch.object(card_module.time, 'monotonic_ns', return_value=now), \
patch.object(card_module.time, 'monotonic', return_value=now / 1e9), \
patch.object(card_module.time, 'clock_gettime_ns', return_value=now), patch.object(card.sm, 'update'):
if variation == 'invalid_can':
with patch.object(card.CI, 'update', wraps=card.CI.update) as parse:
def invalid(*args, **kwargs):
state = parse._mock_wraps(*args, **kwargs)
state.canValid = False
return state
parse.side_effect = invalid
card.step()
else:
card.step()
assert card.CS_prev.canValid == (variation != 'invalid_can') and not card.CS_prev.canTimeout
if variation not in ('normal', 'aol_off'):
assert not emitted
return
assert card.ci_initialized and saved.get_bool('ControlsReady')
assert emitted and all(valid for _, _, valid in emitted)
assert min(frame for frame, _, _ in emitted) <= 6
scc = [(frame, msg) for frame, msg, _ in emitted if msg[0] == 0x1A0]
testers = [frame for frame, msg, _ in emitted if msg[0] == 0x730]
assert len(scc) >= 300 and len(testers) >= 6
assert max(b - a for a, b in zip(testers, testers[1:], strict=False)) <= 100
parser = CANParser(DBC[CAR.KIA_EV6][Bus.pt], [('SCC_CONTROL', 0)], card.CI.CC.CAN.ECAN)
for frame, msg in scc:
parser.update([(10_000_000_000 + frame * 10_000_000, [msg])])
values = parser.vl['SCC_CONTROL']
assert (values['ACCMode'], values['aReqRaw'], values['aReqValue'], values['StopReq']) == (0, 0., 0., 0)
assert card.last_actuators_output.torqueOutputCan == 0
finally:
card.vehicle_startup.close()
def test_stock_held_main_rebinding_and_cancel_keep_physical_edges():
from dataclasses import replace
from openpilot.starpilot.aol.intent import AolSettings
from openpilot.starpilot.car.hyundai.aol import create_intent
owner = create_intent(stock.params(CAR.KIA_EV6), AolSettings(True, 0., AOL_TOGGLE, 0, (0, 0, 0), (0, 0, 0)))
cs = structs.CarState(canValid=True, gearShifter='drive')
cs.cruiseState.available = True
def update(events, now):
cs.buttonEvents = [structs.CarState.ButtonEvent(type=kind, pressed=pressed) for kind, pressed in events]
owner.update(cs, fault_active=False, now_ns=now)
update([], 1)
update([('lkas', True)], 2)
update([('lkas', False)], 3)
update([('mainCruise', True)], 4)
assert owner.allowed_latch
owner.settings = replace(owner.settings, main_action=AOL_TOGGLE)
update([('mainCruise', True)], 5)
assert owner.allowed_latch # A new mapping cannot turn an already held level into another press.
update([('mainCruise', False)], 6)
update([('mainCruise', True)], 7)
assert not owner.allowed_latch
owner.settings = replace(owner.settings, main_action=0)
update([('mainCruise', False)], 8)
update([('lkas', True)], 9)
assert owner.allowed_latch
update([('cancel', True)], 10)
assert not owner.allowed_latch
+3
View File
@@ -257,6 +257,9 @@ def native_latch_rejected(CP, native) -> bool:
def create_intent(CP, settings):
from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified
if CP.carFingerprint == HyundaiCAR.KIA_EV6 and qualified_canfd_stock(CP):
from openpilot.starpilot.car.hyundai.ev6_intent import StockEv6CardIntent
return StockEv6CardIntent(settings)
if ev6_qualified(CP):
from openpilot.starpilot.car.hyundai.ev6_intent import Ev6CardIntent
return Ev6CardIntent(settings)
@@ -4,6 +4,25 @@ from opendbc.car.hyundai.ev6_aol import Ev6PhysicalAuthorization
from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE, ButtonType
class StockEv6CardIntent(AolCardIntent):
def __init__(self, settings):
super().__init__(settings, explicit_latch=True)
def update(self, CS, *, consumed_buttons=frozenset(), fault_active=None, now_ns=0, native_rejection_ns=0,
standard_enabled=False):
if self.settings.main_action == 0 and self.settings.lkas_action == AOL_TOGGLE:
# An unassigned cruise MAIN owns stock ACC, not the separate LKAS latch.
for event in CS.buttonEvents:
if event.type == ButtonType.mainCruise:
self._main_held = bool(event.pressed)
ordinary = CS.as_reader().as_builder()
ordinary.buttonEvents = [event for event in CS.buttonEvents if event.type != ButtonType.mainCruise]
else:
ordinary = CS
super().update(ordinary, consumed_buttons=consumed_buttons, fault_active=fault_active, now_ns=now_ns,
native_rejection_ns=native_rejection_ns, standard_enabled=standard_enabled)
class Ev6CardIntent(AolCardIntent):
observe_stock_engagement = True
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
#pragma once
extern const uint8_t gitversion[29];
const uint8_t gitversion[29] = "DEV-9de940e9-P92eb5651-DEBUG";
const uint8_t gitversion[29] = "DEV-89430a70-P92eb5651-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-9de940e9-P92eb5651-DEBUG
DEV-89430a70-P92eb5651-DEBUG
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "6df82300319bbfdea3da54bf32ce2c3a255aecbe4020755fdec9a69cf359674c", "schema_version": 1}
{"protected_inventory_sha256": "9f8785c8ab618682316768289a489c024a5d2cbfc62772aedc2c2b96334a16bc", "schema_version": 1}