mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
215 lines
11 KiB
Python
215 lines
11 KiB
Python
"""Timestamped synthetic CAN/UDS through the actual prepublication caller."""
|
|
import time
|
|
from contextlib import nullcontext
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from opendbc.can.packer import CANPacker
|
|
from opendbc.car import Bus, CanData, gen_empty_fingerprint, structs
|
|
from opendbc.car import car_helpers, disable_ecu as disable_module
|
|
from opendbc.car.hyundai import ecu_startup
|
|
from opendbc.car.hyundai.ecu_startup import Outcome, stock_copy
|
|
from opendbc.car.hyundai.ev6_startup import EV6Startup, eligible
|
|
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
|
from opendbc.car.hyundai.interface import CarInterface
|
|
from opendbc.car.hyundai.ioniq6_handoff import TimestampedCanPacket
|
|
from opendbc.car.hyundai.values import CAR, DBC
|
|
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
|
|
|
|
|
def params(alpha=True, radar=False, car=CAR.KIA_EV6):
|
|
fp = gen_empty_fingerprint()
|
|
fp[2][0x50] = 16
|
|
fp[1][0x1cf] = 8
|
|
if radar:
|
|
if car == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
|
|
fp[0].update(dict.fromkeys(range(0x210, 0x220), 32))
|
|
else:
|
|
fp[1][0x500] = 8
|
|
fw = [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.adas)]
|
|
return CarInterface.get_params(car, fp, fw, alpha, False, False)
|
|
|
|
|
|
class TestEV6Startup(unittest.TestCase):
|
|
@patch.object(time, 'CLOCK_BOOTTIME', getattr(time, 'CLOCK_BOOTTIME', time.CLOCK_MONOTONIC), create=True)
|
|
def exercise(self, mode='sent', *, capture=True, admission=lambda: True, requested=True, wrong_bus=False,
|
|
missing_source=None, fail_ctor=False, warm_fault=None, car=CAR.KIA_EV6, boot_ns=None):
|
|
original = params(car=car)
|
|
holder = VehicleStartupOwner()
|
|
ci_ref = []
|
|
requests = []
|
|
self.last_requests = requests
|
|
packer = CANPacker(DBC[car][Bus.pt])
|
|
counter = 0
|
|
boot_clock = [0 if boot_ns is None else boot_ns]
|
|
def recv(wait_for_one=False):
|
|
nonlocal counter
|
|
counter += 1
|
|
if boot_ns is not None:
|
|
boot_clock[0] += 10_000_000
|
|
if not ci_ref:
|
|
if not capture:
|
|
return []
|
|
data = bytearray(32)
|
|
data[2], data[4] = counter % 256, 7
|
|
data[:2] = hkg_can_fd_checksum(0x51, None, data).to_bytes(2, 'little')
|
|
frames = [CanData(0x51, bytes(data), 1 if wrong_bus else 0)]
|
|
else:
|
|
frames = []
|
|
for parser in ci_ref[0].can_parsers.values():
|
|
for address in parser.addresses:
|
|
name = parser.dbc.addr_to_msg[address].name
|
|
if name != missing_source and name != 'CRUISE_BUTTONS_ALT':
|
|
frame = CanData(*packer.make_can_msg(name, 0 if warm_fault == 'wrongbus' and name == 'SCC_CONTROL' else parser.bus, {}))
|
|
frames.extend([frame] * (7 if warm_fault == 'counter' and name == 'SCC_CONTROL' else 1))
|
|
stamp = 1 if ci_ref and warm_fault == 'stale' else time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
|
return [TimestampedCanPacket(frames, stamp)]
|
|
def query(send, can_recv, bus, addresses, commands, responses, **kwargs):
|
|
command = commands[0]
|
|
self.assertEqual(bus, 1)
|
|
self.assertEqual(addresses, [(0x730, None)])
|
|
self.assertEqual(responses, [{b'\x10\x03': b'\x50\x03', b'\x28\x83\x01': b'', b'\x28\x00\x01': b'\x68\x00', b'\x10\x01': b'\x50\x01'}[command]])
|
|
requests.append(command)
|
|
class Query:
|
|
def get_data(self, timeout):
|
|
send([CanData(0x730, bytes((len(command),))+command+bytes(7-len(command)), 1)])
|
|
if command == b'\x28\x83\x01' and mode not in ('sent', 'sent_restore'):
|
|
raise OSError('simulated partial send')
|
|
if command == b'\x28\x00\x01' and mode not in ('restored', 'sent_restore'):
|
|
return {}
|
|
# Suppressed-positive communication control has no reply. An empty
|
|
# payload inside a reply dictionary is malformed, not silence.
|
|
return {} if command == b'\x28\x83\x01' else {(0x730, None): b''}
|
|
return Query()
|
|
admission_results = []
|
|
def admit():
|
|
result = admission()
|
|
admission_results.append(result)
|
|
return result
|
|
def hook(cp, candidate, fingerprints, firmware):
|
|
return holder.prepare(cp, CarInterface, (recv, lambda frames: None), requested=requested, admission=admit)
|
|
fp = gen_empty_fingerprint()
|
|
fp[2][0x50], fp[1][0x1cf] = 16, 8
|
|
fw = [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.adas)]
|
|
fingerprint = (car, fp, '0'*17, fw, original.fingerprintSource, True)
|
|
clock_context = (patch.object(time, 'clock_gettime_ns', side_effect=lambda clock: boot_clock[0])
|
|
if boot_ns is not None else nullcontext())
|
|
with clock_context, patch.object(car_helpers, 'fingerprint', return_value=fingerprint), \
|
|
patch.object(disable_module, 'IsoTpParallelQuery', side_effect=query), \
|
|
patch.object(ecu_startup, 'IsoTpParallelQuery', side_effect=query):
|
|
if fail_ctor:
|
|
try:
|
|
with patch.object(CarInterface, 'CarController', side_effect=RuntimeError('constructor failed')):
|
|
car_helpers.get_car(recv, lambda frames: None, lambda *args: None, True, False, pre_create_hook=hook)
|
|
finally:
|
|
holder.close()
|
|
ci = car_helpers.get_car(recv, lambda frames: None, lambda *args: None, True, False, pre_create_hook=hook)
|
|
ci_ref.append(ci)
|
|
holder.configure(ci)
|
|
holder.seal_publication()
|
|
self.addCleanup(holder.close)
|
|
if (capture and requested and mode in ('sent', 'sent_restore') and not warm_fault and not wrong_bus and
|
|
admission_results and admission_results[0]):
|
|
self.assertIs(holder.owner.outcome, Outcome.SENT_UNCONFIRMED)
|
|
self.assertTrue(ci.CP.openpilotLongitudinalControl)
|
|
self.assertEqual(ci.CP.safetyConfigs[-1].safetyParam,
|
|
0x2015 if car == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else 0x15)
|
|
return ci, holder, requests
|
|
|
|
def test_sent_prepublication_binds_immutable_template_and_suppressed_protocol(self):
|
|
ci, holder, requests = self.exercise()
|
|
self.assertIs(holder.owner.outcome, Outcome.SENT_UNCONFIRMED)
|
|
self.assertIs(ci.CC.adrv_template, holder.owner.template)
|
|
self.assertTrue(ci.CP.openpilotLongitudinalControl)
|
|
self.assertIn(b'\x28\x83\x01', requests)
|
|
self.assertNotIn(b'\x28\x03\x01', requests)
|
|
ci.CP.safetyConfigs[-1].safetyParam ^= 4
|
|
with self.assertRaisesRegex(RuntimeError, 'prepared'):
|
|
holder.seal_publication()
|
|
|
|
def test_missing_capture_never_requests_uds_and_partial_restore_warms_actual_stock(self):
|
|
ci, holder, requests = self.exercise(capture=False)
|
|
self.assertEqual(requests, [])
|
|
self.assertIs(holder.owner.outcome, Outcome.STOCK_UNTOUCHED)
|
|
self.assertFalse(ci.CP.openpilotLongitudinalControl)
|
|
ci, holder, requests = self.exercise('restored')
|
|
self.assertIs(holder.owner.outcome, Outcome.STOCK_RESTORED)
|
|
self.assertIn(b'\x28\x00\x01', requests)
|
|
self.assertTrue(holder.owner.ready)
|
|
self.assertIsNone(ci.CC.adrv_template)
|
|
|
|
def test_partial_failure_without_restore_admission_cannot_publish(self):
|
|
admitted = iter((True, False))
|
|
with self.assertRaisesRegex(RuntimeError, 'restoration unverified'):
|
|
self.exercise('restored', admission=lambda: next(admitted))
|
|
with self.assertRaisesRegex(RuntimeError, 'restoration unverified'):
|
|
self.exercise('uncertain')
|
|
|
|
def test_stock_copy_exact_and_button_pause_policy_and_explicit_owner_guard(self):
|
|
for radar in (False, True):
|
|
self.assertEqual(stock_copy(params(radar=radar)).to_dict(), params(False, radar).to_dict())
|
|
ci = CarInterface(params())
|
|
buttons = ci.can_parsers[Bus.pt].message_states[0x1cf]
|
|
self.assertGreaterEqual(buttons.timeout_threshold, 500_000_000)
|
|
self.assertNotIn(0x1a0, ci.can_parsers[Bus.pt].addresses)
|
|
stock = CarInterface(params(False))
|
|
self.assertIn(0x1a0, stock.can_parsers[Bus.pt].addresses)
|
|
with self.assertRaisesRegex(RuntimeError, 'matching prepared'):
|
|
VehicleStartupOwner().configure(ci)
|
|
self.assertIsNone(CarInterface.startup_owner(params(), (list, lambda frames: None), requested=False))
|
|
sibling = params()
|
|
sibling.carFingerprint = CAR.HYUNDAI_IONIQ_6
|
|
self.assertFalse(eligible(sibling))
|
|
|
|
def test_missing_stock_source_cannot_publish_and_buttons_can_pause(self):
|
|
with self.assertRaisesRegex(RuntimeError, 'stock CANFD'):
|
|
self.exercise('restored', missing_source='SCC_CONTROL')
|
|
ci, holder, _ = self.exercise('restored')
|
|
parser = ci.can_parsers[Bus.pt]
|
|
# Freeze the real last button receipt while refreshing all other sources.
|
|
packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt])
|
|
stamp = max(source.timestamps[-1] for source in parser.message_states.values() if source.timestamps)
|
|
for i in range(1, 51):
|
|
frames = []
|
|
for source_parser in ci.can_parsers.values():
|
|
for address in source_parser.addresses:
|
|
name = source_parser.dbc.addr_to_msg[address].name
|
|
if name not in ('CRUISE_BUTTONS', 'CRUISE_BUTTONS_ALT'):
|
|
frames.append(CanData(*packer.make_can_msg(name, source_parser.bus, {})))
|
|
ci.update([(stamp+i*10_000_000, frames)])
|
|
self.assertTrue(ci.CS.out.canValid)
|
|
self.assertTrue(holder.owner._sources(ci, stamp+500_000_000, floor=holder.owner.floor_ns))
|
|
|
|
def test_capture_rejects_wrong_bus_crc_repeats_and_old_producers(self):
|
|
for bad in ('bus', 'crc', 'repeat', 'stale'):
|
|
with self.subTest(bad=bad):
|
|
tick = [1_000_000_000]
|
|
counter = [0]
|
|
def recv(tick=tick, counter=counter, bad=bad):
|
|
tick[0] += 100_000_000
|
|
counter[0] += 1
|
|
data = bytearray(32)
|
|
data[2] = 7 if bad == 'repeat' else counter[0]
|
|
data[4] = 1
|
|
data[:2] = hkg_can_fd_checksum(0x51, None, data).to_bytes(2, 'little')
|
|
if bad == 'crc':
|
|
data[0] ^= 1
|
|
return [TimestampedCanPacket([CanData(0x51, bytes(data), 1 if bad == 'bus' else 0)],
|
|
1 if bad == 'stale' else tick[0])]
|
|
owner = EV6Startup(params(), (recv, lambda frames: self.fail('capture must not transmit')))
|
|
with patch.object(time, 'CLOCK_BOOTTIME', getattr(time, 'CLOCK_BOOTTIME', time.CLOCK_MONOTONIC), create=True), \
|
|
patch.object(time, 'clock_gettime_ns', side_effect=lambda clock, tick=tick: tick[0]), \
|
|
patch.object(time, 'monotonic', side_effect=lambda tick=tick: tick[0]/1e9), patch.object(time, 'sleep'):
|
|
self.assertIsNone(owner._capture())
|
|
|
|
def test_unpublished_actual_constructor_failure_restores_transaction(self):
|
|
with self.assertRaisesRegex(RuntimeError, 'constructor failed'):
|
|
self.exercise('sent_restore', fail_ctor=True)
|
|
self.assertIn(b'\x28\x83\x01', self.last_requests)
|
|
self.assertIn(b'\x28\x00\x01', self.last_requests)
|
|
|
|
def test_wrong_bus_stale_and_counter_loss_stock_sources_cannot_publish(self):
|
|
for fault in ('wrongbus', 'stale', 'counter'):
|
|
with self.subTest(fault=fault), self.assertRaisesRegex(RuntimeError, 'stock CANFD'):
|
|
self.exercise('restored', warm_fault=fault)
|