mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-19 05:53:57 +08:00
151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
"""Keep every button edge without adding a messaging reader or UI update frame."""
|
|
import ast
|
|
import os
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Optional
|
|
|
|
import capnp
|
|
import pytest
|
|
from cereal import log
|
|
from cereal.services import SERVICE_LIST
|
|
|
|
from test_screen_device_runtime import make_device
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
|
|
def packet(timestamp, pressed=None, *, valid=True, button_type='accelCruise'):
|
|
message = log.Event.new_message(logMonoTime=timestamp, valid=valid)
|
|
state = message.init('carState')
|
|
if pressed is not None:
|
|
buttons = state.init('buttonEvents', 1)
|
|
buttons[0].type, buttons[0].pressed = button_type, pressed
|
|
return message
|
|
|
|
|
|
def submaster_fixture():
|
|
sockets = []
|
|
|
|
class Poller:
|
|
def __init__(self):
|
|
self.sockets = []
|
|
|
|
def poll(self, timeout):
|
|
# Native Poller returns new wrapper objects around the same C++ socket.
|
|
return [SocketView(sock) for sock in self.sockets if sock.queue]
|
|
|
|
class SocketView:
|
|
def __init__(self, sock):
|
|
self.sock = sock
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self.sock, name)
|
|
|
|
class Socket:
|
|
def __init__(self, service, poller=None, conflate=False, **kwargs):
|
|
self.service, self.conflate, self.queue = service, conflate, []
|
|
sockets.append(self)
|
|
if poller is not None:
|
|
poller.sockets.append(self)
|
|
|
|
class FrequencyTracker:
|
|
def __init__(self, *args):
|
|
self.times, self.valid = [], True
|
|
|
|
def record_recv_time(self, now):
|
|
self.times.append(now)
|
|
|
|
def receive(sock):
|
|
if not sock.queue:
|
|
return None
|
|
if sock.conflate:
|
|
message, sock.queue[:] = sock.queue[-1], []
|
|
return message
|
|
return sock.queue.pop(0)
|
|
|
|
def drain(sock):
|
|
messages, sock.queue[:] = list(sock.queue), []
|
|
return messages
|
|
|
|
env = dict(List=list, Optional=Optional, Dict=dict, capnp=capnp, log=log, os=os, SERVICE_LIST=SERVICE_LIST,
|
|
time=SimpleNamespace(monotonic=lambda: 100.0), Poller=Poller, sub_sock=Socket,
|
|
FrequencyTracker=FrequencyTracker, recv_one_or_none=receive, drain_sock=drain)
|
|
source = ROOT / 'cereal/messaging/__init__.py'
|
|
nodes = [node for node in ast.parse(source.read_text()).body
|
|
if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in ('SubMaster', 'new_message')]
|
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(source), 'exec'), env)
|
|
return env['SubMaster'], sockets
|
|
|
|
|
|
def test_default_submaster_keeps_existing_conflation():
|
|
cls, sockets = submaster_fixture()
|
|
sm = cls(['carState', 'deviceState'])
|
|
assert len(sockets) == 2 and all(sock.conflate for sock in sockets)
|
|
sockets[0].queue[:] = [packet(1, True), packet(2)]
|
|
sm.update(0)
|
|
assert sm.logMonoTime['carState'] == 2 and sm.frame == 0
|
|
assert not sm['carState'].buttonEvents
|
|
|
|
|
|
@pytest.mark.parametrize('poll', [None, 'deviceState'])
|
|
def test_selected_reader_drains_edges_and_updates_latest_state_once(poll):
|
|
cls, sockets = submaster_fixture()
|
|
sm = cls(['carState', 'deviceState'], poll=poll, drain_services=['carState'])
|
|
assert len(sockets) == 2
|
|
assert sockets[0].conflate is False and sockets[1].conflate is True
|
|
sockets[0].queue[:] = [packet(99_500_000_000, True), packet(99_600_000_000)]
|
|
other = log.Event.new_message(logMonoTime=99_600_000_000, valid=True)
|
|
other.init('deviceState')
|
|
sockets[1].queue[:] = [other]
|
|
sm.update(0)
|
|
assert len(sm.drained['carState']) == 2 and sm.drained['carState'][0].carState.buttonEvents[0].pressed
|
|
assert sm.logMonoTime['carState'] == 99_600_000_000 and not sm['carState'].buttonEvents
|
|
assert sm.frame == 0 and all(sm.updated.values())
|
|
assert sm.freq_tracker['carState'].times == sm.freq_tracker['deviceState'].times == [100.0]
|
|
sm.update(0)
|
|
assert sm.drained['carState'] == [] and not any(sm.updated.values()) and sm.frame == 1
|
|
extended = sm.extend(['modelV2'])
|
|
assert set(extended.drained) == {'carState'}
|
|
assert extended.sock['carState'].conflate is False
|
|
|
|
|
|
@pytest.mark.parametrize('enabled', [False, True])
|
|
@pytest.mark.parametrize('brightness', [0, 101])
|
|
def test_ui_uses_queued_button_press_once_with_optional_wake(enabled, brightness):
|
|
device, state, _ = make_device(StandbyWakeButton=enabled, ScreenBrightnessOnroad=brightness)
|
|
device._last_car_button_frame = 99_000_000_000
|
|
state.sm.drained = {'carState': [packet(99_500_000_000, True), packet(99_600_000_000)]}
|
|
device._update_wakefulness()
|
|
assert device.awake is enabled
|
|
device._interaction_time = 90
|
|
device._update_wakefulness()
|
|
assert not device.awake
|
|
assert state.params_memory.get('StandbyButtonPressTime') is None
|
|
|
|
|
|
def test_ui_rejects_invalid_unknown_release_future_stale_and_reordered_buttons():
|
|
device, state, _ = make_device(StandbyWakeButton=True)
|
|
device._last_car_button_frame = 97_000_000_000
|
|
state.sm.drained = {'carState': [packet(97_500_000_000, True), packet(99_100_000_000, False),
|
|
packet(99_200_000_000, True, valid=False), packet(99_300_000_000, True, button_type='unknown'),
|
|
packet(100_100_000_000, True)]}
|
|
device._update_wakefulness()
|
|
assert not device.awake
|
|
state.sm.drained = {'carState': [packet(99_800_000_000, True)]}
|
|
device._update_wakefulness()
|
|
assert device.awake
|
|
device._interaction_time = 90
|
|
state.sm.drained = {'carState': [packet(99_700_000_000, True)]}
|
|
device._update_wakefulness()
|
|
assert not device.awake
|
|
|
|
|
|
def test_vehicle_button_wake_adds_no_daemon_reader():
|
|
source = (ROOT / 'starpilot/system/wheel_controls/wheel_controlsd.py').read_text()
|
|
assert 'sub_sock(' not in source
|
|
ui = ast.parse((ROOT / 'selfdrive/ui/ui_state.py').read_text())
|
|
calls = [node for node in ast.walk(ui) if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'SubMaster']
|
|
assert len(calls) == 1
|
|
assert any(kw.arg == 'drain_services' and ast.literal_eval(kw.value) == ['carState'] for kw in calls[0].keywords)
|