mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
80fbb4f6db
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>
192 lines
11 KiB
Python
192 lines
11 KiB
Python
import ast
|
|
from pathlib import Path
|
|
import subprocess
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, Mock, call
|
|
|
|
import pytest
|
|
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1] / "hardware.py"
|
|
|
|
|
|
@pytest.mark.parametrize("device,usb", [("tici", True), ("tizi", False), ("mici", False)])
|
|
def test_internal_panda_transport_irq_affinity(device, usb):
|
|
tree = ast.parse(SOURCE.read_text())
|
|
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "HardwareComma")
|
|
method = next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == "initialize_hardware")
|
|
start = next(i for i, node in enumerate(method.body) if isinstance(node, ast.Expr) and
|
|
ast.unparse(node.value) == "affine_irq(3, 'spi_geni')")
|
|
affinity = Mock()
|
|
code = compile(ast.fix_missing_locations(ast.Module(body=method.body[start:start + 2], type_ignores=[])), str(SOURCE), "exec")
|
|
exec(code, {"self": SimpleNamespace(get_device_type=lambda: device), "affine_irq": affinity})
|
|
expected = [(3, "spi_geni")]
|
|
if usb:
|
|
expected += [(3, "xhci-hcd:usb3"), (3, "xhci-hcd:usb1")]
|
|
assert [call.args for call in affinity.call_args_list] == expected
|
|
|
|
|
|
@pytest.mark.parametrize('model,right_volume,eq_enabled', [('tici', 0x1c, 1), ('tizi', 0x17, 0)])
|
|
def test_amplifier_profile_registers_and_shutdown_order(model, right_volume, eq_enabled):
|
|
from collections import namedtuple
|
|
|
|
path = SOURCE.with_name('amplifier.py')
|
|
tree = ast.parse(path.read_text())
|
|
selected: list[ast.stmt] = [node for node in tree.body if isinstance(node, (ast.Assign, ast.FunctionDef, ast.ClassDef))]
|
|
namespace: dict = {'namedtuple': namedtuple}
|
|
exec(compile(ast.fix_missing_locations(ast.Module(body=selected, type_ignores=[])), str(path), 'exec'), namespace)
|
|
amp = namespace['Amplifier'].__new__(namespace['Amplifier'])
|
|
amp.set_configs = Mock(return_value=True)
|
|
assert amp.initialize_configuration(model)
|
|
configs = amp.set_configs.call_args.args[0]
|
|
assert configs[0].register == configs[-1].register == 0x51
|
|
assert configs[0].value == 0 and configs[-1].value == 1
|
|
assert next(config.value for config in configs if config.register == 0x3e) == right_volume
|
|
eq = [config.value for config in configs if config.register == 0x49 and config.offset == 1]
|
|
assert eq[-1] == eq_enabled
|
|
assert sum(config.register >= 0x84 for config in configs) == (50 if model == 'tici' else 0)
|
|
|
|
|
|
def test_hardware_startup_and_wake_pass_actual_amplifier_model():
|
|
tree = ast.parse(SOURCE.read_text())
|
|
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'HardwareComma')
|
|
for name in ('initialize_hardware', 'set_power_save'):
|
|
method = next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == name)
|
|
calls = [node for node in ast.walk(method) if isinstance(node, ast.Call) and
|
|
ast.unparse(node.func) == 'self.amplifier.initialize_configuration']
|
|
assert len(calls) == 1
|
|
assert ast.unparse(calls[0].args[0]) == 'self.get_device_type()'
|
|
|
|
|
|
@pytest.mark.parametrize('model', ['tici', 'tizi'])
|
|
@pytest.mark.parametrize('path_name', ['initialize_hardware', 'set_power_save'])
|
|
def test_startup_and_wake_write_model_registers(model, path_name):
|
|
from collections import namedtuple
|
|
|
|
registers = dict.fromkeys(range(256), 0xa5)
|
|
writes = []
|
|
|
|
class Bus:
|
|
def __init__(self, bus):
|
|
assert bus == 0
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
return False
|
|
|
|
def read_byte_data(self, address, register, force):
|
|
assert address == 0x10 and force
|
|
return registers[register]
|
|
|
|
def write_byte_data(self, address, register, value, force):
|
|
assert address == 0x10 and force
|
|
registers[register] = value
|
|
writes.append((register, value))
|
|
|
|
amp_path = SOURCE.with_name('amplifier.py')
|
|
tree = ast.parse(amp_path.read_text())
|
|
selected: list[ast.stmt] = [node for node in tree.body if isinstance(node, (ast.Assign, ast.FunctionDef, ast.ClassDef))]
|
|
namespace: dict = {'namedtuple': namedtuple, 'SMBus': Bus}
|
|
exec(compile(ast.fix_missing_locations(ast.Module(body=selected, type_ignores=[])), str(amp_path), 'exec'), namespace)
|
|
amp = namespace['Amplifier']()
|
|
tree = ast.parse(SOURCE.read_text())
|
|
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'HardwareComma')
|
|
method = next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == path_name)
|
|
code = compile(ast.fix_missing_locations(ast.Module(body=[method.body[0]], type_ignores=[])), str(SOURCE), 'exec')
|
|
exec(code, {'self': SimpleNamespace(amplifier=amp, get_device_type=lambda: model), 'powersave_enabled': False})
|
|
expected_registers = dict.fromkeys(range(256), 0xa5)
|
|
configs = [amp._get_shutdown_config(True), *namespace['BASE_CONFIG'], *namespace['CONFIGS'][model], amp._get_shutdown_config(False)]
|
|
if path_name == 'set_power_save':
|
|
configs.insert(0, amp._get_shutdown_config(False))
|
|
expected = []
|
|
for config in configs:
|
|
value = (expected_registers[config.register] & ~config.mask) | ((config.value << config.offset) & config.mask)
|
|
expected_registers[config.register] = value
|
|
expected.append((config.register, value))
|
|
assert writes == expected
|
|
assert registers == expected_registers
|
|
assert registers[0x3e] & 0x1f == (0x1c if model == 'tici' else 0x17)
|
|
assert registers[0x49] & 2 == (2 if model == 'tici' else 0)
|
|
assert len([register for register, _ in writes if register >= 0x84]) == (50 if model == 'tici' else 0)
|
|
|
|
|
|
@pytest.mark.parametrize('device,reset_hold,recover_hold', [('tici', 1, 0.5), ('tizi', 0.01, 0.01), ('mici', 0.01, 0.01)])
|
|
def test_internal_panda_reset_and_recovery_timing(device, reset_hold, recover_hold):
|
|
tree = ast.parse(SOURCE.read_text())
|
|
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'HardwareComma')
|
|
methods: list[ast.stmt] = [node for node in cls.body if isinstance(node, ast.FunctionDef) and
|
|
node.name in {'reset_internal_panda', 'recover_internal_panda'}]
|
|
events = Mock()
|
|
namespace: dict = {'gpio_init': Mock(), 'gpio_set': events.gpio, 'time': SimpleNamespace(sleep=events.sleep),
|
|
'GPIO': SimpleNamespace(STM_RST_N='reset', STM_BOOT0='boot0')}
|
|
exec(compile(ast.Module(body=methods, type_ignores=[]), str(SOURCE), 'exec'), namespace)
|
|
hardware = SimpleNamespace(get_device_type=lambda: device)
|
|
namespace['reset_internal_panda'](hardware)
|
|
assert events.mock_calls == [call.gpio('reset', True), call.gpio('boot0', False), call.sleep(reset_hold), call.gpio('reset', False)]
|
|
events.reset_mock()
|
|
namespace['recover_internal_panda'](hardware)
|
|
assert events.mock_calls == [call.gpio('reset', True), call.gpio('boot0', True), call.sleep(recover_hold),
|
|
call.gpio('reset', False), call.sleep(recover_hold), call.gpio('boot0', False)]
|
|
|
|
|
|
@pytest.mark.parametrize('device', ['tici', 'tizi', 'mici'])
|
|
@pytest.mark.parametrize('failure', [None, 'missing', 'usb', 'protocol', 'multiple', 'dfu', 'wait', 'initial_wait', 'heartbeat'])
|
|
def test_pandad_enumeration_and_recovery(device, failure):
|
|
pandad_path = SOURCE.parents[3] / 'selfdrive/pandad/pandad.py'
|
|
tree = ast.parse(pandad_path.read_text())
|
|
functions: list[ast.stmt] = [node for node in tree.body if isinstance(node, ast.FunctionDef) and
|
|
node.name in {'wait_for_internal_panda', 'main'}]
|
|
events = Mock()
|
|
first_discovery = {'missing': [], 'usb': OSError(), 'multiple': ['one', 'two'], 'wait': []}.get(failure, ['serial'])
|
|
initial_ready = device != 'tici' or failure not in ('wait', 'initial_wait')
|
|
initial_discovery = ['serial'] if failure == 'heartbeat' else []
|
|
events.list.side_effect = [*([initial_discovery] if initial_ready else []), first_discovery,
|
|
*([] if failure == 'multiple' else [['serial']]), KeyboardInterrupt()]
|
|
events.health.return_value = {'heartbeat_lost': True}
|
|
panda = MagicMock(list=events.list)
|
|
panda.return_value.__enter__.return_value = SimpleNamespace(health=events.health, is_internal=lambda: True)
|
|
events.dfu_list.return_value = ['dfu'] if failure == 'dfu' else []
|
|
events.udev.return_value = SimpleNamespace(returncode=1 if failure == 'wait' else 0)
|
|
if failure == 'initial_wait':
|
|
events.udev.side_effect = [SimpleNamespace(returncode=1), SimpleNamespace(returncode=0)]
|
|
dfu = Mock(return_value=SimpleNamespace(recover=events.dfu_recover), list=events.dfu_list)
|
|
flash = Mock(side_effect=[RuntimeError(), None] if failure == 'protocol' else None)
|
|
process = Mock()
|
|
process.wait.side_effect = KeyboardInterrupt()
|
|
launch = Mock(return_value=process)
|
|
namespace: dict = {'Panda': panda, 'PandaDFU': dfu,
|
|
'HARDWARE': SimpleNamespace(get_device_type=lambda: device, reset_internal_panda=events.reset,
|
|
recover_internal_panda=events.recover),
|
|
'time': SimpleNamespace(sleep=events.sleep), 'cloudlog': Mock(), 'signal': Mock(),
|
|
'usb1': SimpleNamespace(USBErrorNoDevice=OSError, USBErrorPipe=BrokenPipeError), 'PandaProtocolMismatch': RuntimeError,
|
|
'Params': Mock(return_value=SimpleNamespace(put_bool=events.put_bool)),
|
|
'flash_panda': flash, 'subprocess': SimpleNamespace(Popen=launch, run=events.udev, DEVNULL=subprocess.DEVNULL), 'BASEDIR': 'base',
|
|
'os': SimpleNamespace(environ={}, path=SimpleNamespace(join=lambda *parts: '/'.join(parts)))}
|
|
exec(compile(ast.Module(body=functions, type_ignores=[]), str(pandad_path), 'exec'), namespace)
|
|
with pytest.raises(KeyboardInterrupt):
|
|
namespace['main']()
|
|
|
|
wait = [call.udev(['udevadm', 'wait', '--timeout=15', '/sys/bus/usb/devices/1-1.2'], check=False, stderr=None)] if device == 'tici' else []
|
|
expected = [call.udev(['udevadm', 'wait', '--timeout=0', '/sys/bus/usb/devices/1-1.2'], check=False,
|
|
stderr=subprocess.DEVNULL)] if device == 'tici' else []
|
|
if initial_ready:
|
|
expected += [call.list()] # Initial health inspection.
|
|
if failure == 'heartbeat':
|
|
expected += [call.health(), call.put_bool('PandaHeartbeatLost', True, block=True)]
|
|
for reset in [call.reset()] if failure in (None, 'dfu', 'initial_wait', 'heartbeat') else [call.reset(), call.recover()]:
|
|
expected += [reset, *wait, call.dfu_list()]
|
|
if failure == 'dfu':
|
|
expected += [call.dfu_recover(), call.sleep(1), *wait]
|
|
expected += [call.list()]
|
|
assert events.mock_calls == expected
|
|
if failure == 'dfu':
|
|
dfu.assert_called_once_with('dfu')
|
|
if failure == 'multiple':
|
|
flash.assert_not_called()
|
|
launch.assert_not_called()
|
|
else:
|
|
assert flash.call_args_list == [call('serial')] * (2 if failure == 'protocol' else 1)
|
|
launch.assert_called_once_with(['./pandad', 'serial'], cwd='base/openpilot/selfdrive/pandad')
|