Files
StarPilot/openpilot/starpilot/drive_state/tests/test_hardware_lifecycle.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

280 lines
14 KiB
Python

"""Portable regression checks of the current hardware transition and power branches."""
from typing import Any
import ast
from pathlib import Path
from types import SimpleNamespace as NS
from unittest.mock import Mock, patch
from openpilot.starpilot.drive_state.resolver import Mode
from openpilot.system.hardware.power_monitoring import PowerMonitoring
from openpilot.starpilot.power.offroad_preferences import PowerPolicy
ROOT = Path(__file__).parents[4]
def hardware_body():
tree = ast.parse((ROOT / 'openpilot/system/hardware/hardwared.py').read_text())
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == 'hardware_thread')
return next(node for node in function.body if isinstance(node, ast.While)).body
def executable(nodes):
return compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), 'current-hardware', 'exec')
def test_forced_modes_poll_at_nominal_cadence_without_repeated_edges_and_observe_physical_changes():
from openpilot.starpilot.drive_state.resolver import effective_onroad
body = hardware_body()
start = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'physical_ignition')
end = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'ign_edge')
code = executable(body[start : end + 1])
for mode, started in ((Mode.ONROAD, True), (Mode.OFFROAD, False)):
reads = []
owner = NS(snapshot=lambda mode=mode, reads=reads: reads.append(True) or NS(mode=mode))
values: dict[str, Any] = {
'Mode': Mode,
'drive_mode': mode,
'drive_state': owner,
'physical_ignition_prev': None,
'requested_onroad_prev': None,
'started_ts': 1 if started else None,
'DT_HW': 0.5,
'SERVICE_LIST': {'pandaStates': NS(frequency=10)},
'effective_onroad': effective_onroad,
}
for frame in range(1, 101):
values.update(sm=NS(frame=frame, updated={'pandaStates': True}), pandaStates=[object()], onroad_conditions={'ignition': False, 'not_onroad_cycle': True})
exec(code, values)
assert not values['ign_edge']
assert len(reads) == 20
# Raw changes between scheduled polls are processed once, including timeout loss and recovery.
for frame, ignition, updated, expected in (
(101, True, True, True),
(102, True, False, False),
(103, False, False, True),
(104, False, False, False),
(106, True, True, True),
):
values.update(
sm=NS(frame=frame, updated={'pandaStates': updated}), pandaStates=[object()], onroad_conditions={'ignition': ignition, 'not_onroad_cycle': True}
)
exec(code, values)
assert values['physical_edge'] == expected
assert values['ign_edge'] == expected
def power_branch():
body = hardware_body()
index = next(i for i, node in enumerate(body) if isinstance(node, ast.If) and ast.unparse(node.test) == 'should_start')
return executable(body[index - 1 : index + 1])
def test_physical_shutdown_episode_survives_force_auto_and_clears_only_real_ignition():
values: dict[str, Any] = {
'Mode': Mode,
'off_ts': 100.0,
'physical_off_ts': None,
'forced_power_episode': False,
'started_ts': None,
'started_seen': False,
'startup_blocked_ts': None,
'startup_conditions': {},
'startup_conditions_prev': {},
'cloudlog': Mock(),
}
code = power_branch()
def step(now, mode, ignition, started, edge=False):
values.update(time=NS(monotonic=lambda: now), drive_mode=mode, onroad_conditions={'ignition': ignition}, should_start=started, physical_edge=edge)
exec(code, values)
step(110, Mode.ONROAD, False, True)
assert values['off_ts'] is None and values['physical_off_ts'] == 100 and values['forced_power_episode']
step(120, Mode.OFFROAD, False, False)
step(130, Mode.AUTO, False, False)
assert values['physical_off_ts'] == 100 and values['forced_power_episode']
step(130, Mode.ONROAD, False, True) # Established MONOTONIC clock after suspend; no mode reset.
assert values['physical_off_ts'] == 100
step(140, Mode.ONROAD, True, True, True)
assert values['physical_off_ts'] is None and not values['forced_power_episode']
step(150, Mode.OFFROAD, True, False)
step(300, Mode.OFFROAD, True, False)
assert values['physical_off_ts'] is None and not values['forced_power_episode']
step(301, Mode.OFFROAD, False, False, True)
assert values['physical_off_ts'] == 301
def test_real_shutdown_cutoff_and_timeout_remain_effective_with_physical_clock(monkeypatch):
monitor = object.__new__(PowerMonitoring)
monkeypatch.setattr(monitor, '_power_policy', lambda now: PowerPolicy(), raising=False)
monkeypatch.setattr(monitor, 'params', NS(get_bool=lambda key: False), raising=False)
monitor.car_battery_capacity_uWh = 100000
monitor.car_voltage_mV = 11000
with patch('openpilot.system.hardware.power_monitoring.time.monotonic', return_value=1000):
assert monitor.should_shutdown(False, True, 100, True)
assert not monitor.should_shutdown(True, True, 100, True)
assert not monitor.should_shutdown(False, True, None, True)
monitor.car_voltage_mV = 13000
assert monitor.should_shutdown(False, True, -200000, True)
def test_default_auto_clock_and_explicit_startup_gates_remain_stock():
from openpilot.starpilot.drive_state.resolver import should_start
assert should_start(Mode.AUTO, {'ignition': True, 'device_temp_good': True}, {'terms': True}, already_started=False)
assert not should_start(Mode.AUTO, {'ignition': False, 'device_temp_good': True}, {'terms': True}, already_started=False)
assert not should_start(Mode.AUTO, {'ignition': True, 'device_temp_good': False}, {'terms': True}, already_started=True)
assert not should_start(Mode.AUTO, {'ignition': True, 'device_temp_good': True}, {'terms': False}, already_started=False)
assert should_start(Mode.AUTO, {'ignition': True, 'device_temp_good': True}, {'terms': False}, already_started=True)
clock = Mock(return_value=120)
values: dict[str, Any] = {
'Mode': Mode,
'off_ts': 100,
'physical_off_ts': None,
'forced_power_episode': False,
'started_ts': None,
'started_seen': False,
'startup_blocked_ts': None,
'startup_conditions': {},
'startup_conditions_prev': {},
'cloudlog': Mock(),
'time': NS(monotonic=clock),
'drive_mode': Mode.AUTO,
'onroad_conditions': {'ignition': False},
'should_start': False,
'physical_edge': False,
}
exec(power_branch(), values)
assert values['off_ts'] == 100 and not values['forced_power_episode']
clock.assert_not_called()
def test_forced_offroad_uses_the_ignition_off_process_set(tmp_path):
from contextlib import ExitStack
from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.starpilot.drive_state.resolver import should_start
from openpilot.system.manager.process import ensure_running
from openpilot.system.manager.process_config import managed_processes
params = Params(str(tmp_path / 'params'))
cp = car.CarParams.new_message()
conditions = {'ignition': True, 'device_temp_good': True}
with ExitStack() as stack:
stops = {}
for name, proc in managed_processes.items():
stack.enter_context(patch.object(proc, 'start'))
stops[name] = stack.enter_context(patch.object(proc, 'stop'))
if hasattr(proc, 'recover'):
stack.enter_context(patch.object(proc, 'recover', return_value=True))
started = should_start(Mode.AUTO, conditions, {}, already_started=True)
driving = {p.name for p in ensure_running(managed_processes.values(), started, params, cp)}
assert {'card', 'controlsd', 'selfdrived', 'modeld', 'plannerd'} <= driving
started = should_start(Mode.OFFROAD, conditions, {}, already_started=True)
assert not started
forced = {p.name for p in ensure_running(managed_processes.values(), started, params, cp)}
for name in ('card', 'controlsd', 'selfdrived', 'modeld', 'plannerd', 'radard', 'encoderd', 'loggerd',
'dmonitoringd', 'dmonitoringmodeld', 'calibrationd', 'paramsd', 'lagd', 'torqued'):
assert name not in forced
stops[name].assert_called_with(block=False)
conditions['ignition'] = False
started = should_start(Mode.AUTO, conditions, {}, already_started=False)
ordinary = {p.name for p in ensure_running(managed_processes.values(), started, params, cp)}
assert forced == ordinary
assert {'hardwared', 'ui', 'galaxy', 'pandad'} <= forced
def test_manager_missing_native_key_registry_fails_closed_without_aborting_normal_startup():
from openpilot.common.params import UnknownKeyName
tree = ast.parse((ROOT / 'openpilot/system/manager/manager.py').read_text())
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == 'manager_init')
boundary = next(
node
for node in function.body
if isinstance(node, ast.Try) and any(isinstance(child, ast.Attribute) and child.attr == 'initialize_manager' for child in ast.walk(node))
)
owner = NS(initialize_manager=Mock(side_effect=UnknownKeyName('DriveStateRequest')))
logger = Mock()
values: dict[str, Any] = {'drive_state': owner, 'cloudlog': logger, 'UnknownKeyName': UnknownKeyName}
exec(executable([boundary]), values)
logger.warning.assert_called_once_with('Force drive state is unavailable')
def test_effective_onroad_disables_power_save_before_started_publication_without_changing_auto():
body = hardware_body()
power_index = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'should_pwrsave')
started_index = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'msg.deviceState.started')
send_index = next(i for i, node in enumerate(body) if isinstance(node, ast.Expr) and ast.unparse(node.value) == "pm.send('deviceState', msg)")
assert power_index + 2 < started_index < send_index
code = executable(body[power_index:power_index + 3] + [body[started_index], body[send_index]])
def step(ignition, started, brightness, previous, count=1):
events = []
msg = NS(deviceState=NS(screenBrightnessPercent=brightness, started=False))
values: dict[str, Any] = {
'onroad_conditions': {'ignition': ignition}, 'should_start': started,
'started_ts': 1 if started else None, 'msg': msg, 'pwrsave': previous, 'count': count,
'HARDWARE': NS(set_power_save=lambda enabled: events.append(('power_save', enabled))),
'pm': NS(send=lambda service, message: events.append((service, message.deviceState.started))),
}
exec(code, values)
return values['pwrsave'], events
# Asleep forced Onroad must enable CPUs before manager sees started=True.
assert step(False, True, 0, True) == (False, [('power_save', False), ('deviceState', True)])
# Holding the same state adds no repeated hardware power transition.
assert step(False, True, 0, False) == (False, [('deviceState', True)])
assert step(False, False, 0, False) == (True, [('power_save', True), ('deviceState', False)])
# Physical ignition retains its authority even when forcing the pipeline off.
assert step(True, False, 0, True) == (False, [('power_save', False), ('deviceState', False)])
for ignition, started in ((False, False), (True, False), (True, True)):
for brightness in (0, 0.0009, 0.001, 50):
expected = not ignition and brightness < 1e-3
power_save, events = step(ignition, started, brightness, not expected, count=0)
assert power_save == expected # Exact ordinary Auto equation, including blocked startup.
assert events[0] == ('power_save', expected)
def test_forced_onroad_startup_blocked_uses_nominal_hardware_cadence_and_keeps_edges():
from openpilot.starpilot.drive_state.resolver import effective_onroad, should_start
body = hardware_body()
start = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'physical_ignition')
end = next(i for i, node in enumerate(body) if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == 'ign_edge')
code = executable(body[start:end + 1])
reads = []
values: dict[str, Any] = {
'Mode': Mode, 'drive_mode': Mode.ONROAD,
'drive_state': NS(snapshot=lambda: reads.append(True) or NS(mode=Mode.ONROAD)),
'physical_ignition_prev': None, 'requested_onroad_prev': None,
'started_ts': None, 'DT_HW': 0.5,
'SERVICE_LIST': {'pandaStates': NS(frequency=10)}, 'effective_onroad': effective_onroad,
}
full_frames = []
for frame in range(1, 101):
conditions = {'ignition': False, 'not_onroad_cycle': True, 'device_temp_good': True}
values.update(sm=NS(frame=frame, updated={'pandaStates': True}), pandaStates=[object()], onroad_conditions=conditions)
exec(code, values)
if frame % 5 == 0 or values['ign_edge']:
full_frames.append(frame)
assert not should_start(Mode.ONROAD, conditions, {'terms': False}, already_started=False)
assert full_frames == list(range(5, 101, 5))
assert len(reads) == 20
# Onroad-cycle/thermal eligibility and physical changes still bypass throttle once.
for frame, ignition, ready, expected in (
(101, False, False, True), (102, False, False, False),
(103, False, True, True), (104, False, True, False),
(106, True, True, True), (107, True, True, False),
(108, False, True, True), (109, False, True, False),
):
values.update(sm=NS(frame=frame, updated={'pandaStates': True}), pandaStates=[object()],
onroad_conditions={'ignition': ignition, 'not_onroad_cycle': ready, 'device_temp_good': True})
exec(code, values)
assert values['ign_edge'] == expected
assert not should_start(Mode.ONROAD, values['onroad_conditions'], {'terms': False}, already_started=False)
assert should_start(Mode.ONROAD, values['onroad_conditions'], {'terms': True}, already_started=False)