Preserve G90 diagnostic continuity through controller startup

This commit is contained in:
firestar5683
2026-10-20 21:32:06 -05:00
parent a278aa8fba
commit f5f8a331f1
5 changed files with 285 additions and 3 deletions
@@ -2,10 +2,11 @@
from copy import deepcopy
import time
from opendbc.car import Bus
from opendbc.car import Bus, make_tester_present_msg
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
from opendbc.can.parser import MAX_BAD_COUNTER
from opendbc.car.hyundai.ev9_keeper import EV9Keeper as StartupKeeper
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome, stock_copy as stock_copy
@@ -16,8 +17,55 @@ def required(cp):
class G90Startup(HyundaiECUStartup):
def __init__(self, cp, callbacks):
def __init__(self, cp, callbacks, *, clock=time.monotonic, keeper_factory=StartupKeeper):
super().__init__(cp, callbacks, address=0x7d0, bus=0, label='G90')
self.clock = clock
self.keeper_factory = keeper_factory
self.keeper = None
self.handed_off = False
def prepare(self, *, admission):
result = super().prepare(admission=admission)
if required(self.cp) and self.outcome is Outcome.SENT_UNCONFIRMED:
self.keeper = self.keeper_factory(
lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock)
try:
self.keeper.start()
except Exception:
if self.keeper.thread is not None and self.keeper.thread.ident is None:
self.keeper.event.set()
self.keeper.thread = None
self.close()
raise
return result
def check(self):
super().check()
keeper = getattr(self, 'keeper', None)
if keeper is not None and keeper.abort_reason is not None:
keeper.stop()
self.outcome = Outcome.ABORT_UNCERTAIN
raise RuntimeError('G90 startup diagnostic continuity failed')
def maintain(self, *, configured):
self.check()
def sent(self, frames, *, valid):
keeper = getattr(self, 'keeper', None)
if keeper is None or self.handed_off or not valid or self.closed or not self.published:
return
tester = make_tester_present_msg(self.address, self.bus, suppress_response=True)
if tester not in frames:
return
keeper.stop()
self.check()
self.handed_off = True
def close(self):
keeper = getattr(self, 'keeper', None)
if keeper is not None:
keeper.stop()
super().close()
def _query(self, *args, **kwargs):
return IsoTpParallelQuery(*args, **kwargs)
@@ -0,0 +1,153 @@
from unittest.mock import patch
import pytest
from opendbc.car import gen_empty_fingerprint, make_tester_present_msg
from opendbc.car.hyundai import g90_startup
from opendbc.car.hyundai.ecu_startup import Outcome
from opendbc.car.hyundai.ev9_keeper import EV9Keeper
from opendbc.car.hyundai.g90_startup import G90Startup
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR
class ManualKeeper(EV9Keeper):
def start(self):
self.tick()
def prepared(*, aol=False, send_error=False, keeper_factory=ManualKeeper, holders=None):
now = [8.637416955]
sent = []
cp = CarInterface.get_params(CAR.GENESIS_G90, gen_empty_fingerprint(), [], alpha_long=True, is_release=False, docs=False)
assert cp.openpilotLongitudinalControl
if aol:
cp.safetyConfigs[0].safetyParam |= 0x400
cp.alternativeExperience = 32
def send(frames):
if send_error and make_tester_present_msg(0x7d0, 0, suppress_response=True) in frames:
raise OSError('Actual sender rejected tester')
sent.extend((now[0], frame) for frame in frames)
def query(can_send, can_recv, bus, addresses, requests, responses):
request = requests[0]
class Query:
def get_data(self, timeout):
can_send([(0x7d0, bytes([len(request)]) + request + bytes(7 - len(request)), 0)])
return {} if request == b"\x28\x83\x01" else {(0x7d0, None): b''}
return Query()
owner = G90Startup(cp, (list, send))
if holders is not None:
holders.append(owner)
owner.clock = lambda: now[0]
owner.keeper_factory = keeper_factory
with patch.object(g90_startup, 'IsoTpParallelQuery', side_effect=query), patch.object(g90_startup.time, 'sleep'):
owner.prepare(admission=lambda: True)
return owner, now, sent
@pytest.mark.parametrize('aol', [False, True])
def test_recorded_disable_to_first_controller_interval_has_continuous_tester(aol):
owner, now, sent = prepared(aol=aol)
tester = make_tester_present_msg(0x7d0, 0, suppress_response=True)
try:
assert any(frame == tester for _, frame in sent), 'No tester starts after successful ECU disable'
for second in range(1, 7):
now[0] = 8.637416955 + second
owner.keeper.tick()
now[0] = 15.046147807
times = [stamp for stamp, frame in sent if frame == tester]
assert len(times) == 7
assert max(right - left for left, right in zip(times, times[1:], strict=False)) <= 1.000001
assert now[0] - times[-1] < 1.0
assert owner.outcome is Outcome.SENT_UNCONFIRMED
assert all(frame == tester or frame[1][:3] == b"\x02\x10\x03" or frame[1][:4] == b"\x03\x28\x83\x01" for _, frame in sent)
finally:
owner.published = True
owner.close()
@pytest.mark.parametrize('kind', ['invalid', 'empty', 'wrong_bus', 'wrong_payload', 'unpublished', 'valid'])
def test_only_actual_valid_controller_tester_receipt_ends_continuity(kind):
owner, now, sent = prepared()
try:
tester = make_tester_present_msg(0x7d0, 0, suppress_response=True)
owner.published = kind != 'unpublished'
frames = [tester]
if kind == 'empty':
frames = []
elif kind == 'wrong_bus':
frames = [(tester[0], tester[1], 1)]
elif kind == 'wrong_payload':
frames = [(tester[0], b"\x02\x3e\x00" + bytes(5), tester[2])]
owner.sent(frames, valid=kind != 'invalid')
assert owner.handed_off == (kind == 'valid')
assert owner.keeper.event.is_set() == (kind == 'valid')
count = len(sent)
now[0] += 1.0
owner.keeper.tick()
assert len(sent) == count + (kind != 'valid')
finally:
owner.published = True
owner.close()
@pytest.mark.parametrize('fault', ['deadline', 'clock_reversal', 'sender'])
def test_failed_continuity_cannot_be_acknowledged_or_rearmed(fault):
owner, now, sent = prepared(send_error=fault == 'sender')
owner.published = True
try:
if fault == 'deadline':
now[0] += 60.0
elif fault == 'clock_reversal':
now[0] -= 1.0
owner.keeper.tick()
with pytest.raises(RuntimeError, match='diagnostic continuity failed'):
owner.maintain(configured=True)
assert owner.outcome is Outcome.ABORT_UNCERTAIN
assert not owner.handed_off
count = len(sent)
now[0] += 1.0
assert not owner.keeper.tick()
assert len(sent) == count
finally:
owner.close()
def test_close_stops_tester_before_unpublished_stock_restoration():
owner, now, sent = prepared()
requests = []
def query(*args, **kwargs):
assert owner.keeper.event.is_set()
request = args[4][0]
requests.append(request)
class Query:
def get_data(self, timeout):
return {(0x7d0, None): b''}
return Query()
with patch.object(g90_startup, 'IsoTpParallelQuery', side_effect=query):
owner.close()
assert requests == [b"\x10\x03", b"\x28\x00\x01", b"\x10\x01"]
assert owner.closed and owner.restored
count = len(sent)
now[0] += 1.0
assert not owner.keeper.tick()
assert len(sent) == count
def test_actual_thread_start_failure_restores_stock_and_preserves_original_error():
holders = []
with patch('threading.Thread.start', side_effect=OSError('thread launch failed')):
with pytest.raises(OSError, match='thread launch failed'):
prepared(keeper_factory=EV9Keeper, holders=holders)
owner = holders[0]
assert owner.closed and owner.restored
assert not owner.published
assert owner.keeper.event.is_set()
assert owner.keeper.thread is None
@@ -0,0 +1,80 @@
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.car import Bus, make_tester_present_msg
from opendbc.car.hyundai.tests.test_g90_continuity import prepared
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR, DBC
from opendbc.safety.tests.common import CANPackerSafety
from opendbc.safety.tests.test_hyundai import checksum
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
@pytest.mark.parametrize('address,data', [(0x420, '501420c8b84b6a00'), (0x421, '000000ffe37f2db1')])
@pytest.mark.parametrize('bus', [0, 2])
def test_recorded_returned_stock_scc_keeps_native_relay_protection(address, data, bus):
safety = libsafety_py.libsafety
safety.set_alternative_experience(32)
assert safety.set_safety_hooks(8, 0xC04) == 0
safety.init_tests()
safety.set_alternative_experience(32)
packer = CANPackerSafety(DBC[CAR.GENESIS_G90][Bus.pt])
for name, values, fix in (
('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': 0, 'WHL_SPD_AliveCounter_MSB': 0}, checksum),
('TCS13', {'AliveCounterTCS': 0}, checksum),
('MDPS12', {}, None),
('CLU11', {'CF_Clu_AliveCnt1': 0}, None),
('EMS16', {'CRUISE_LAMP_S': 0, 'AliveCounter': 0}, checksum),
('BCM_PO_11', {'LDA_BTN': 0}, None),
):
assert safety.safety_rx_hook(packer.make_can_msg_safety(name, 0, values, fix_checksum=fix)), name
safety.set_timer(1000)
safety.safety_tick()
assert safety.safety_config_valid()
assert not safety.get_relay_malfunction()
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, bytes.fromhex(data)))
assert bool(safety.get_relay_malfunction()) == (bus == 0)
assert not safety.get_controls_allowed()
assert safety.aol_get_permission_mask() == 0
if bus == 0:
tester = make_tester_present_msg(0x7d0, 0, suppress_response=True)
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(tester[0], tester[2], tester[1]))
@pytest.mark.parametrize('receipt', ['valid', 'invalid', 'exception'])
def test_current_card_publisher_transfers_continuity_only_after_success(receipt):
owner, now, sent = prepared()
owner.published = True
delivered = []
card = Car.__new__(Car)
card.CI = CarInterface(owner.cp)
card.CP = owner.cp
card.vehicle_startup = VehicleStartupOwner(owner)
def send(service, packet):
assert service == 'sendcan'
assert not owner.handed_off
if receipt == 'exception':
raise OSError('Actual publication failed')
delivered.append(packet)
try:
with patch.object(card, 'pm', SimpleNamespace(send=send), create=True), patch.object(card, 'volt_startup_keepalive', return_value=False):
frames = [make_tester_present_msg(0x7d0, 0, suppress_response=True)]
if receipt == 'exception':
with pytest.raises(OSError, match='publication failed'):
card.publish_sendcan(frames)
else:
card.publish_sendcan(frames, valid=receipt == 'valid')
assert bool(delivered) == (receipt != 'exception')
assert owner.handed_off == (receipt == 'valid')
count = len(sent)
now[0] += 1.0
owner.keeper.tick()
assert len(sent) == count + (receipt != 'valid')
finally:
owner.close()
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "c319992bbe9280bf5904b8e8eb6a87d0f2d16d1a5a21482238b9d7e021ad06ff", "schema_version": 1}
{"protected_inventory_sha256": "226a3f00d9020c539ae74d575cbc3fb3525405d370335ae84327401130bce1b7", "schema_version": 1}
+1
View File
@@ -33,6 +33,7 @@ DEBUG_SAFETY_CLASS_TARGETS = {
RELEASE_TARGETS = (
"opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity.py",
"opendbc_repo/opendbc/car/hyundai/tests/test_disable_response.py",
"openpilot/starpilot/aol/tests/test_angle_stop_native_feedback.py",
"openpilot/starpilot/aol/tests/test_angle_stop_fault_guards.py",