mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-29 01:23:58 +08:00
Require fresh Ioniq 6 parser receipts after ECU handoff
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
@@ -18,6 +19,19 @@ from opendbc.car.hyundai.values import CAR
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
|
||||
|
||||
def refresh_host_parser_receipts(card):
|
||||
from collections import deque
|
||||
from opendbc.can.parser import MessageState
|
||||
|
||||
stamp = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
parsers = {}
|
||||
for bus in (1, 2):
|
||||
source = MessageState(address=bus, name=f"source{bus}", size=8, signals=[],
|
||||
timeout_threshold=100_000_000, timestamps=deque([stamp]))
|
||||
parsers[bus] = SimpleNamespace(message_states={bus: source})
|
||||
card.CI.can_parsers = parsers
|
||||
|
||||
|
||||
class FakeIoniq6Bus:
|
||||
def __init__(self, *, source=True, heartbeat_source=True, heartbeat_stays_after_disable=False,
|
||||
bsm_source=True, bsm_stays_after_disable=False, missing_status=None,
|
||||
@@ -561,6 +575,9 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card.ioniq6_host_warmed = False
|
||||
card.ioniq6_long_lost = False
|
||||
card.ioniq6_keepalive = None
|
||||
card.ioniq6_handoff_source_floor_ns = 1
|
||||
card.CI = SimpleNamespace()
|
||||
refresh_host_parser_receipts(card)
|
||||
invalid = SimpleNamespace(canValid=False)
|
||||
card.observe_ioniq6_long_authority([], invalid)
|
||||
self.assertFalse(card.ioniq6_long_lost)
|
||||
@@ -659,6 +676,7 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card.controls_update(cs, cc)
|
||||
card.CI.apply.assert_not_called()
|
||||
cs.canValid = True
|
||||
refresh_host_parser_receipts(card)
|
||||
card.observe_ioniq6_long_authority([], cs)
|
||||
card.controls_update(cs, cc)
|
||||
card.CI.apply.assert_called_once()
|
||||
@@ -887,6 +905,12 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
update=Mock(side_effect=lambda packets: car.CarState(canValid=True, canTimeout=False)),
|
||||
apply=Mock(return_value=(structs.CarControl.Actuators(), [CanData(0x1A0, b'\0' * 8, 1)])),
|
||||
init=Mock())
|
||||
|
||||
def parsed_update(_packets):
|
||||
refresh_host_parser_receipts(card_owner)
|
||||
return car.CarState(canValid=True, canTimeout=False)
|
||||
|
||||
card_owner.CI.update.side_effect = parsed_update
|
||||
card_owner.ioniq6_panda_diagnostic_ready = Mock(return_value=True)
|
||||
card_owner.ioniq6_panda_matches = Mock(side_effect=(False, True, True))
|
||||
packets = iter((
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Recorded parser grace must not complete a new I6 ECU ownership warmup."""
|
||||
from collections import deque
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
|
||||
|
||||
def recorded_owner():
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[0][0x3A5] = 24
|
||||
fp[2].update({0x110: 32, 0x362: 32})
|
||||
fp[1].update({0x130: 16, 0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1BA: 24, 0x1E5: 16, 0x36A: 16})
|
||||
stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False)
|
||||
cp = build_ioniq6_hda2_long_candidate(stock, fp)
|
||||
assert cp is not None
|
||||
cp.safetyConfigs[-1].safetyParam |= 0x0800
|
||||
assert cp.flags == 1059329 and cp.safetyConfigs[-1].safetyParam == 0x8895
|
||||
owner = Car.__new__(Car)
|
||||
owner.CP, owner.CI = cp, CarInterface(cp)
|
||||
owner.ioniq6_long_selected = owner.ioniq6_long_prearmed = True
|
||||
owner.ioniq6_long_lost = owner.ioniq6_host_warmed = False
|
||||
owner.ioniq6_keepalive = SimpleNamespace(stop=Mock())
|
||||
return owner
|
||||
|
||||
|
||||
def test_recorded_c8_actual_parser_grace_waits_for_post_handoff_sources():
|
||||
fixture = json.loads((Path(__file__).parent / "testdata/ioniq6_post_handoff_warmup.json").read_text())
|
||||
owner = recorded_owner()
|
||||
floor = fixture["handoffCompletedNs"]
|
||||
owner.ioniq6_handoff_source_floor_ns = floor
|
||||
now = [0]
|
||||
history = []
|
||||
with patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]):
|
||||
for packet in fixture["packets"]:
|
||||
ns = packet["ns"]
|
||||
if fixture["lastCardSourceBeforeTransactionNs"] < ns <= floor:
|
||||
continue # The real UDS transaction consumed this Card cursor interval.
|
||||
now[0] = ns
|
||||
frames = [(a, bytes.fromhex(data), bus) for a, data, bus in packet["frames"]]
|
||||
state = owner.CI.update([(ns, frames)])
|
||||
if ns <= floor:
|
||||
continue
|
||||
parser_valid = state.canValid
|
||||
owner.observe_ioniq6_long_authority([(ns, frames)], state)
|
||||
history.append((ns, parser_valid, owner.ioniq6_host_warmed, owner.ioniq6_long_lost))
|
||||
assert history[0][1] is True, "Recorded parser initially tolerates stale sources"
|
||||
assert history[0][2] is False, "STALE_PARSER_GRACE_MUST_NOT_FINISH_HANDOFF_WARMUP"
|
||||
assert any(not row[1] for row in history), "Real parser grace eventually expires"
|
||||
assert history[-1][1:3] == (True, True)
|
||||
assert not any(row[3] for row in history)
|
||||
owner.ioniq6_keepalive.stop.assert_not_called()
|
||||
# A genuinely invalid input after completed warmup retains the old refusal.
|
||||
invalid = SimpleNamespace(canValid=False)
|
||||
owner.observe_ioniq6_long_authority([], invalid)
|
||||
assert owner.ioniq6_long_lost and not invalid.canValid
|
||||
owner.ioniq6_keepalive.stop.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fault", ("no_floor", "empty_inventory", "pre_handoff", "future", "expired", "bad_counter", "missing"))
|
||||
def test_warmup_proof_rejects_missing_or_invalid_source_receipts(fault):
|
||||
owner = recorded_owner()
|
||||
owner.ioniq6_handoff_source_floor_ns = 1_000_000_000
|
||||
now = 1_100_000_000
|
||||
required = [source for parser in owner.CI.can_parsers.values()
|
||||
for source in parser.message_states.values() if not source.ignore_alive]
|
||||
assert len(required) >= 6
|
||||
for source in required:
|
||||
source.timestamps = deque([now])
|
||||
source = required[-1]
|
||||
if fault == "no_floor":
|
||||
owner.ioniq6_handoff_source_floor_ns = 0
|
||||
elif fault == "empty_inventory":
|
||||
owner.CI.can_parsers = {}
|
||||
elif fault == "pre_handoff":
|
||||
source.timestamps = deque([owner.ioniq6_handoff_source_floor_ns])
|
||||
elif fault == "future":
|
||||
source.timestamps = deque([now + 1])
|
||||
elif fault == "expired":
|
||||
source.timeout_threshold = 10_000_000
|
||||
source.timestamps = deque([now - 10_000_001])
|
||||
elif fault == "bad_counter":
|
||||
source.counter_fail = 5
|
||||
elif fault == "missing":
|
||||
source.timestamps.clear()
|
||||
with patch("time.clock_gettime_ns", return_value=now):
|
||||
owner.observe_ioniq6_long_authority([], SimpleNamespace(canValid=True))
|
||||
assert not owner.ioniq6_host_warmed and not owner.ioniq6_long_lost
|
||||
|
||||
|
||||
@pytest.mark.parametrize("address,bus", ((0x1A0, 1), (0x1BA, 1), (0x1E5, 1), (0x100, 0)))
|
||||
def test_factory_source_return_still_revokes_during_warmup(address, bus):
|
||||
owner = recorded_owner()
|
||||
owner.ioniq6_handoff_source_floor_ns = 1_000_000_000
|
||||
state = SimpleNamespace(canValid=True)
|
||||
owner.observe_ioniq6_long_authority([(1_100_000_000, [(address, bytes(32), bus)])], state)
|
||||
assert owner.ioniq6_long_lost and not state.canValid
|
||||
owner.ioniq6_keepalive.stop.assert_called_once()
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -228,6 +228,7 @@ class Car:
|
||||
self.ioniq6_last_preflight_at = 0.0
|
||||
self.ioniq6_panda_armed = False
|
||||
self.ioniq6_host_warmed = False
|
||||
self.ioniq6_handoff_source_floor_ns = 0
|
||||
self.ioniq6_long_lost = False
|
||||
self.ioniq6_keepalive = None
|
||||
self.ioniq6_restore_callbacks = None
|
||||
@@ -1243,6 +1244,20 @@ class Car:
|
||||
}
|
||||
self.pm.send('slcCruiseEvent', event)
|
||||
|
||||
def ioniq6_post_handoff_sources_current(self):
|
||||
"""A parser's invalid grace is not proof of post-transaction warmup."""
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
floor = getattr(self, 'ioniq6_handoff_source_floor_ns', 0)
|
||||
if floor <= 0:
|
||||
return False
|
||||
now = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
sources = [source for parser in self.CI.can_parsers.values()
|
||||
for source in parser.message_states.values() if not source.ignore_alive]
|
||||
return bool(sources) and all(
|
||||
source.timestamps and floor < source.timestamps[-1] <= now and
|
||||
now - source.timestamps[-1] <= source.timeout_threshold and source.counter_fail < MAX_BAD_COUNTER
|
||||
for source in sources)
|
||||
|
||||
def observe_ioniq6_long_authority(self, can_list, CS):
|
||||
if getattr(self, 'ioniq6_long_selected', False) and self.ioniq6_long_lost:
|
||||
CS.canValid = False
|
||||
@@ -1252,10 +1267,11 @@ class Car:
|
||||
if any((src == 1 and address in (0x1A0, 0x1BA, 0x1E5)) or (src == 0 and address == 0x100)
|
||||
for _, packet in can_list for address, _, src in packet):
|
||||
self.ioniq6_long_lost = True
|
||||
if CS.canValid:
|
||||
if self.ioniq6_host_warmed:
|
||||
if not CS.canValid:
|
||||
self.ioniq6_long_lost = True
|
||||
elif CS.canValid and self.ioniq6_post_handoff_sources_current():
|
||||
self.ioniq6_host_warmed = True
|
||||
elif self.ioniq6_host_warmed:
|
||||
self.ioniq6_long_lost = True
|
||||
if self.ioniq6_long_lost:
|
||||
CS.canValid = False
|
||||
if self.ioniq6_keepalive is not None:
|
||||
@@ -1629,6 +1645,7 @@ class Car:
|
||||
# The transaction consumed Card's CAN cursor. Rewarm the host parser
|
||||
# from post-handoff independent inputs before its first controller TX.
|
||||
self.ioniq6_host_warmed = False
|
||||
self.ioniq6_handoff_source_floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
self.ioniq6_restore_callbacks = self.can_callbacks
|
||||
elif self.vehicle_startup.owner is None:
|
||||
self.CI.init(self.CP, *self.can_callbacks)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "97d90d29dbe2b0e55aada70d8f7ad969078898f7a390d670ffa65bee1e4d8c0a", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "d97d73289304727972ae534f32902cf5d207ea61af92426673df8a2fd4b6ed60", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user