Files
StarPilot/openpilot/starpilot/models/tests/test_startup_power.py
T
firestar5683 2b1152adf7 Hyundai
Integrate vehicle-specific steering, longitudinal control, startup, and handoff policies, including Ioniq, EV9, GV70, and non-SCC configurations. Preserve the corresponding native safety and CAN routing contracts.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>

Co-authored-by: Intelli <contact@intelli.software>
2026-10-14 20:02:39 -05:00

163 lines
6.1 KiB
Python

from types import SimpleNamespace
import pytest
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.model_startup import model_power_ready, model_power_ready_bus
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.structs import CarParams
from openpilot.starpilot.models.startup import POWER_STABLE_NS, PowerReadiness, wait_for_chestnut_power
NS = 1_000_000_000
def test_readiness_requires_ready_and_three_seconds_of_current_voltage():
state = PowerReadiness()
for step in range(200):
now = NS + step * NS // 10
assert state.update(now, now, 12_000, step >= 170) is False
now += NS // 10
assert state.update(now, now, 13_200, True)
@pytest.mark.parametrize("bad", ["low", "missing", "stale", "future", "not-ready", "clock", "gap"])
def test_bad_evidence_restarts_stability(bad):
state = PowerReadiness()
for step in range(29):
now = NS + step * NS // 10
assert not state.update(now, now, 12_000, True)
now += NS // 10
sample, voltage, ready = now, 12_000, True
if bad == "low":
voltage = 9_999
elif bad == "missing":
sample = 0
elif bad == "stale":
sample -= NS
elif bad == "future":
sample += 1
elif bad == "not-ready":
ready = False
elif bad == "clock":
now -= 2 * NS
sample = now
elif bad == "gap":
now += NS
sample = now
assert not state.update(now, sample, voltage, ready)
for step in range(1, 30):
stamp = now + step * NS // 10
assert not state.update(stamp, stamp, 12_000, True)
stamp = now + POWER_STABLE_NS + NS // 10
assert state.update(stamp, stamp, 12_000, True)
@pytest.mark.parametrize("lka,offset", [(False, 0), (True, 0), (False, 4), (True, 4)])
def test_vehicle_ready_uses_exact_vehicle_bus_and_payload(lka, offset):
cp = CarParams(carFingerprint=CAR.HYUNDAI_IONIQ_6, brand="hyundai", flags=int(HyundaiFlags.CANFD_LKA_STEER_MSG) if lka else 0)
# Bus offset is derived from the installed safety configuration.
cp.init("safetyConfigs", 2 if offset else 1)
bus = model_power_ready_bus(cp)
assert bus is not None
assert bus == CanBus(cp).ECAN
data = bytearray(32)
data[3] = 0x40
def message(address=0x35, src=bus, dat=data):
return SimpleNamespace(address=address, src=src, dat=bytes(dat))
assert model_power_ready([message()], bus) is True
assert model_power_ready([message(dat=bytes(32))], bus) is False
assert model_power_ready([message(src=bus + 1), message(address=0x36), message(dat=data[:4])], bus) is None
assert model_power_ready([message(), message(dat=bytes(32))], bus) is False
cp.carFingerprint = CAR.KIA_EV6
assert model_power_ready_bus(cp) is None
def test_wait_is_bounded_and_never_opens_gpu_without_power(monkeypatch):
from openpilot.starpilot.models import startup
clock = SimpleNamespace(now=0.)
class Messages:
def __init__(self, services):
self.data = {"pandaStates": [], "deviceState": SimpleNamespace(started=True)}
self.seen = dict.fromkeys(services, False)
self.valid = dict.fromkeys(services, False)
self.updated = dict.fromkeys(services, False)
self.logMonoTime = dict.fromkeys(services, 0)
def __getitem__(self, key): return self.data[key]
def update(self, timeout): clock.now += .1
monkeypatch.setattr(startup.messaging, "SubMaster", Messages)
monkeypatch.setattr(startup.HARDWARE, "get_device_type", lambda: "mici")
monkeypatch.setattr(startup.time, "monotonic", lambda: clock.now)
monkeypatch.setattr(startup.time, "monotonic_ns", lambda: int(clock.now * NS))
with pytest.raises(TimeoutError, match="did not stabilize"):
wait_for_chestnut_power(SimpleNamespace(brand="mock"), timeout=1.)
assert clock.now < 1.2
def test_current_drive_requires_positive_monotonic_evidence():
from openpilot.starpilot.models.startup import current_drive
class SM(dict):
seen = {'deviceState': False}
valid = {'deviceState': True}
logMonoTime = {'deviceState': 20 * NS}
sm = SM(deviceState=SimpleNamespace(started=True, startedMonoTime=10 * NS))
assert not current_drive(sm, 20 * NS)
sm.seen['deviceState'] = True
assert current_drive(sm, 20 * NS)
assert not current_drive(sm, 23 * NS)
assert not current_drive(sm, 19 * NS)
sm['deviceState'].startedMonoTime = 0
assert not current_drive(sm, 20 * NS)
def test_suspend_gap_requires_new_power_stability():
from openpilot.starpilot.models.startup import supply_voltage
state = PowerReadiness()
# Native Panda/CAN stamps use BOOTTIME, which advances while MONOTONIC is suspended.
for tick in range(31):
stamp = (20 * NS) + tick * NS // 10
ready = state.update(stamp, stamp, 12_000, True)
assert ready
assert not state.update(stamp + 60 * NS, stamp, 12_000, True)
assert not state.update(stamp + 60 * NS + NS // 10, stamp + 60 * NS + NS // 10, 12_000, True)
low = SimpleNamespace(pandaType='tres', voltage=3_900)
high = SimpleNamespace(pandaType='tres', voltage=12_000)
assert supply_voltage([low]) == 3_900
assert supply_voltage([low, high]) == 0
def test_paired_clocks_preserve_distinct_domains(monkeypatch):
from openpilot.starpilot.models import startup
monkeypatch.setattr(startup.time, 'CLOCK_BOOTTIME', 7, raising=False)
monkeypatch.setattr(startup.time, 'monotonic_ns', lambda: 20 * NS)
monkeypatch.setattr(startup.time, 'clock_gettime_ns', lambda clock: 80 * NS)
assert startup.paired_clocks_ns() == (20 * NS, 80 * NS)
def test_load_timeout_reports_the_blocked_loader_without_restarting_it(monkeypatch):
import threading
from unittest.mock import Mock
from openpilot.starpilot.models import startup
entered, release = threading.Event(), threading.Event()
def blocked_gpu_load():
entered.set()
release.wait(5)
report = Mock()
monkeypatch.setattr(startup.cloudlog, 'event', report)
loader = threading.Thread(target=blocked_gpu_load)
loader.start()
try:
assert entered.wait(2)
startup.report_load_timeout(loader, 60.)
assert loader.is_alive()
args, kwargs = report.call_args
assert args == ('chestnut.load_timeout',)
assert kwargs['error'] and kwargs['timeout_s'] == 60.
assert 'blocked_gpu_load' in kwargs['loader_stack']
finally:
release.set()
loader.join(2)
assert not loader.is_alive()