Repair replay and Galaxy test contracts

This commit is contained in:
firestar5683
2026-10-14 13:05:08 -05:00
parent 6ef0150b0c
commit 3e96c9f4f2
24 changed files with 148 additions and 79 deletions
+2
View File
@@ -41,3 +41,5 @@ openpilot/system/loggerd/loggerd -text
openpilot/starpilot/galaxy/bin/frpc_linux_arm64 -text
openpilot/starpilot/maps/provider/mapd -text
openpilot/selfdrive/test/process_replay/refs/starpilot/*.zst -text
openpilot/tools/replay/replay -text
+27 -1
View File
@@ -1,7 +1,8 @@
import math
from enum import StrEnum, auto
from openpilot.cereal import messaging
from openpilot.cereal import log, messaging
from openpilot.starpilot.selfdrive_loop_timing import timed_params_read
from opendbc.car.structs import car
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.helpers import Pose
@@ -53,3 +54,28 @@ class ExcessiveActuationCheck:
excessive_type = ExcessiveActuationType.LATERAL
return excessive_type
def update_big_model_status(owner) -> bool:
big_active = timed_params_read(owner, "ChestnutActive")
fresh = owner.sm.seen['modelV2'] and owner.sm.alive['modelV2'] and owner.sm.valid['modelV2']
published_big = fresh and owner.sm['modelV2'].big
if published_big and (big_active is True or owner.sm['deviceState'].chestnutPresent):
owner.big_model_chestnut = True
model_unavailable = owner.big_model_active and owner.sm.seen['modelV2'] and not fresh
demoted = owner.big_model_active and fresh and not published_big
chestnut_lost = owner.big_model_active and owner.big_model_chestnut and not owner.sm['deviceState'].chestnutPresent
load_mismatch = big_active is True and fresh and not published_big
big_failed = (big_active is False and not published_big) or load_mismatch or model_unavailable or demoted or chestnut_lost
if big_failed and not owner.big_model_failed:
owner.events.add(log.OnroadEvent.EventName.bigModelFailed)
owner.big_model_failed = big_failed
# A successful load is not yet an executed frame; warmup output can be invalid.
if published_big:
owner.big_model_active = True
control_current = owner.sm.seen['carControl'] and owner.sm.alive['carControl'] and owner.sm.valid['carControl']
axis_active = control_current and (owner.sm['carControl'].latActive or owner.sm['carControl'].longActive)
if not owner.enabled and not axis_active and not model_unavailable:
owner.big_model_active = False
owner.big_model_chestnut = False
return big_failed
+2 -23
View File
@@ -29,7 +29,7 @@ from openpilot.selfdrive.car.car_events import CarEvents
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.selfdrive.selfdrived.events import Events, ET, Alert, Priority as AlertPriority
from openpilot.starpilot.longitudinal.force_stop_alert import HoldAlertState, EVENT_TYPE as FORCE_STOP_HOLD
from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck
from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck, update_big_model_status
from openpilot.system.manager.process_health import driving_process_failures
from openpilot.selfdrive.selfdrived.state import StateMachine
from opendbc.car.gm.aol import native_bootstrap_supported
@@ -268,28 +268,7 @@ class SelfdriveD:
return False
def update_big_model_status(self) -> bool:
big_active = timed_params_read(self, "ChestnutActive")
fresh = self.sm.seen['modelV2'] and self.sm.alive['modelV2'] and self.sm.valid['modelV2']
published_big = fresh and self.sm['modelV2'].big
if published_big and (big_active is True or self.sm['deviceState'].chestnutPresent):
self.big_model_chestnut = True
model_unavailable = self.big_model_active and self.sm.seen['modelV2'] and not fresh
demoted = self.big_model_active and fresh and not published_big
chestnut_lost = self.big_model_active and self.big_model_chestnut and not self.sm['deviceState'].chestnutPresent
load_mismatch = big_active is True and fresh and not published_big
big_failed = (big_active is False and not published_big) or load_mismatch or model_unavailable or demoted or chestnut_lost
if big_failed and not self.big_model_failed:
self.events.add(EventName.bigModelFailed)
self.big_model_failed = big_failed
# A successful load is not yet an executed frame; warmup output can be invalid.
if published_big:
self.big_model_active = True
control_current = self.sm.seen['carControl'] and self.sm.alive['carControl'] and self.sm.valid['carControl']
axis_active = control_current and (self.sm['carControl'].latActive or self.sm['carControl'].longActive)
if not self.enabled and not axis_active and not model_unavailable:
self.big_model_active = False
self.big_model_chestnut = False
return big_failed
return update_big_model_status(self)
def _update_steering_authority(self, CS):
if not monitored_profile(self.CP):
@@ -147,3 +147,12 @@ def non_default_settings(self, settings_recovery):
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
def preview_settings_recovery(payload, settings_recovery, identity):
if (not isinstance(payload, dict) or set(payload) != {'view', 'selected'} or
not isinstance(payload['view'], str) or len(payload['view']) > 64 or
not isinstance(payload['selected'], list) or not 1 <= len(payload['selected']) <= 128 or
any(type(index) is not int for index in payload['selected'])):
raise ValueError('Invalid settings recovery selection')
return settings_recovery().preview(payload['view'], payload['selected'], *identity)
+2 -7
View File
@@ -14,7 +14,7 @@ import threading
import time
from urllib.parse import parse_qs, unquote, urlsplit
from openpilot.starpilot.galaxy.batch_routes import change_drive_state, delete_records, non_default_settings, preview_layout
from openpilot.starpilot.galaxy.batch_routes import change_drive_state, delete_records, non_default_settings, preview_layout, preview_settings_recovery
from openpilot.starpilot.galaxy.access import AccessStatus, default_owner
from openpilot.starpilot.galaxy.auth import LocalSessions
from openpilot.starpilot.galaxy.http_server import (_LocalHTTPHandler, _LocalHTTPServer, allowed_authority,
@@ -2349,12 +2349,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe
result = ToggleBackup(feature_settings(), layout_owner()).restore(
payload, identity, authorized=lambda: self.settings_session() == identity)
elif path == '/api/settings/recovery/preview':
if (not isinstance(payload, dict) or set(payload) != {'view', 'selected'} or
not isinstance(payload['view'], str) or len(payload['view']) > 64 or
not isinstance(payload['selected'], list) or not 1 <= len(payload['selected']) <= 128 or
any(type(index) is not int for index in payload['selected'])):
raise ValueError('Invalid settings recovery selection')
result = settings_recovery().preview(payload['view'], payload['selected'], *identity)
result = preview_settings_recovery(payload, settings_recovery, identity)
elif path == '/api/settings/recovery/confirm':
if (not isinstance(payload, dict) or set(payload) != {'intent', 'confirmed'} or
not isinstance(payload['intent'], str) or len(payload['intent']) > 64 or payload['confirmed'] is not True):
@@ -1,4 +1,5 @@
"""Persistent read connections must never reuse an unread body or partial response."""
from typing import Any
import http.client
from contextlib import closing
@@ -22,7 +23,7 @@ def server(tmp_path):
directory.mkdir(parents=True)
media = directory / 'init.mp4'
media.write_bytes(b'x' * (65536 + 5))
state = {'requests': [], 'chunks': 0, 'revoke_after': None, 'expire_session': False, 'expired': False,
state: dict[str, Any] = {'requests': [], 'chunks': 0, 'revoke_after': None, 'expire_session': False, 'expired': False,
'recording_checks': 0, 'change_recording': False}
def owner(payload):
@@ -29,7 +29,9 @@ assert.equal(requests.length, 4, "an unavailable default cannot be submitted")
feed.stop()
assert.ok(GalaxySettingRow.emits.includes("reset-default"))
const render = compile(GalaxySettingRow.template, { decodeEntities: raw => raw })
const renderTemplate = compile(GalaxySettingRow.template, { decodeEntities: raw => raw })
const rowContext = context => ({ ...context, presentedRow: GalaxySettingRow.computed.presentedRow.call(context) })
const render = (context, cache) => renderTemplate(rowContext(context), cache)
function buttons(node) {
if (!node || typeof node !== "object") return []
return [...(node.type === "button" ? [node] : []), ...(Array.isArray(node.children) ? node.children.flatMap(buttons) : [])]
@@ -68,7 +70,7 @@ assert.equal(dependent.at(-1).data.rows[0].value, "On")
source.stop()
consumer.stop()
assert.equal(GalaxySettingRow.computed.showDefault.call({ row }), true)
assert.equal(GalaxySettingRow.computed.showDefault.call({ row: { ...row, value: 'On' } }), false)
assert.equal(GalaxySettingRow.computed.showDefault.call({ row: { ...row, value: '0.9', defaultValue: '0.90' } }), false)
assert.equal(GalaxySettingRow.computed.showDefault.call(rowContext({ row })), true)
assert.equal(GalaxySettingRow.computed.showDefault.call(rowContext({ row: { ...row, value: 'On' } })), false)
assert.equal(GalaxySettingRow.computed.showDefault.call(rowContext({ row: { ...row, value: '0.9', defaultValue: '0.90' } })), false)
assert.equal(buttons(render({ ...context, showDefault: false }, [])).some(node => node.children === 'Default'), false)
+5 -3
View File
@@ -90,8 +90,8 @@ class ReplayFrameReader:
if SESSION.fullmatch(session_id) is None or profile is not None and profile not in PROFILES:
raise ValueError('Invalid replay frame identity')
self.session_id, self.profile, self.last_key = session_id, profile, None
self.mm = None
self.fd = None
self.mm: mmap.mmap | None = None
self.fd: int | None = None
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC)
try:
info = os.fstat(fd)
@@ -105,7 +105,7 @@ class ReplayFrameReader:
self.fd = fd
def latest(self) -> ReplayFrame | None:
if self.mm is None:
if self.mm is None or self.fd is None:
return None
try:
fcntl.flock(self.fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
@@ -117,6 +117,8 @@ class ReplayFrameReader:
fcntl.flock(self.fd, fcntl.LOCK_UN)
def _latest(self):
if self.mm is None:
return None
for _ in range(3):
seq = SEQ.unpack_from(self.mm, 8)[0]
if not seq or seq & 1:
+5 -2
View File
@@ -6,6 +6,7 @@ import math
import os
from pathlib import Path
import re
from typing import Any
MAX_FRAGMENT_BYTES = 1024 * 1024
MAX_MEDIA_BYTES = 8 * 1024 * 1024
@@ -108,8 +109,8 @@ class MediaStore:
self.session_id, self.width, self.height = session_id, width, height
self.epoch = 0
self.codec = ''
self.init = None
self.fragments = deque()
self.init: dict[str, Any] | None = None
self.fragments: deque[dict[str, Any]] = deque()
self.sequence = 0
def _write(self, name: str, data: bytes):
@@ -141,6 +142,8 @@ class MediaStore:
def publish(self, data: bytes, *, position: float, keyframe: bool):
if not self.fragments and not keyframe:
raise ValueError('Replay media must begin with a keyframe')
if self.init is None:
raise RuntimeError("Replay media epoch has not begun")
self.sequence += 1
item = {'sequence': self.sequence, 'mediaPath': self._write(f'{self.sequence}.m4s', data), 'length': len(data),
'firstPosition': position, 'lastPosition': position, 'keyframe': bool(keyframe)}
+5 -1
View File
@@ -22,6 +22,7 @@ import sys
import threading
import time
import uuid
from typing import cast
from openpilot.starpilot.replay.client import MAX_REQUEST, MAX_RESPONSE, encode, parse, receive, replay_root
from openpilot.tools.replay.display_clock import RECORD
@@ -338,6 +339,8 @@ class ReplayOwner:
if self.status.get('target') != 'web' or request['epoch'] != self.status['epoch'] or not self.status.get('mediaReady'):
raise Rejected('epoch_changed')
from openpilot.starpilot.replay.media import media_descriptor
if self.session_dir is None:
raise Rejected("epoch_changed")
return media_descriptor(self.session_dir, request['epoch'], request['sequence'], count)
def _stop(self, reason):
@@ -389,7 +392,7 @@ class ReplayOwner:
if (type(ranges) is not list or len(ranges) > 512 or
any(type(item) is not list or len(item) != 2 or not all(_finite(number) for number in item) or
not value['start'] <= item[0] <= item[1] <= value['end'] for item in ranges) or
any(before[1] > after[0] for before, after in zip(ranges, ranges[1:], strict=False))):
any(before[1] > after[0] for before, after in zip(cast(list[list[float]], ranges), cast(list[list[float]], ranges)[1:], strict=False))):
raise Rejected('worker_failed')
self.status['seekRanges'] = ranges
warnings = value.get('sourceWarnings', [])
@@ -524,6 +527,7 @@ class ReplayOwner:
with self.lock:
self.child, self.child_birth = child, _process_birth(child.pid)
self._record_session(directory, child)
assert child.stdout is not None and child.stderr is not None and child.stdin is not None
for file in (child.stdout, child.stderr, child.stdin):
os.set_blocking(file.fileno(), False)
stdout = bytearray()
+13 -6
View File
@@ -13,6 +13,7 @@ import subprocess
import sys
import threading
import time
from typing import cast
from openpilot.starpilot.replay.frames import ReplayFramePublisher, SESSION
from openpilot.starpilot.replay.media import MediaMux, MediaStore
@@ -299,7 +300,7 @@ class Compositor:
self.resources.callback(rl.unload_render_texture, self.output)
self.readback = FrameReadback(width * height * (3 if nv12 else 8) // 2, asynchronous=True)
self.resources.callback(self.readback.close)
self.meta = None
self.meta: tuple[int, float, int] | None = None
self.epoch = None
self.last_route_frame = None
self.primary_ready = False
@@ -342,14 +343,15 @@ class Compositor:
updates = original | {name: sm.seen[name] and receipt != self.render_receipts.get(name)
for name, receipt in receipts.items()}
sample = self.ui.replay_sample
updates['extrinsicsCalibration'] = bool(updates['extrinsicsCalibration'] and sample and sample.valid and
updates['extrinsicsCalibration'] = bool(updates['extrinsicsCalibration'] and sample and sample.valid and sample.now_ns is not None and
sm.valid['extrinsicsCalibration'] and
0 < sm.logMonoTime['extrinsicsCalibration'] <= sample.now_ns)
flags = DrawUpdates(updates)
sm.updated = flags
try:
if self.profile == 'compact':
self.camera.render_camera_model_layer(rect, camera_view=state.appearance.camera_view,
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView as CompactRoadView
cast(CompactRoadView, self.camera).render_camera_model_layer(rect, camera_view=state.appearance.camera_view,
reverse_driver=state.reverse_driver_camera,
lead_indicator=state.appearance.show_lead_indicator and state.lead_indicator_source_fresh,
lead_info_mode=state.appearance.lead_info_mode, lead_info_metric=state.appearance.lead_info_metric,
@@ -382,6 +384,8 @@ class Compositor:
VisionStreamType.VISION_STREAM_WIDE_ROAD: 'wideRoadEncodeIdx',
VisionStreamType.VISION_STREAM_CABIN: 'cabinEncodeIdx'}
name = names.get(self.camera.stream_type)
if name is None:
return False
sm, sample = self.ui.sm, self.ui.replay_sample
state = sm[name] if name is not None and sm.seen[name] and sm.valid[name] else None
return bool(sample and sample.valid and state is not None and frame_matches(
@@ -413,6 +417,7 @@ class Compositor:
if position['enabled']:
width, height = widget_size(state.customization, self.profile, key)
positions[side] = Rect(position['x'], position['y'], width, height)
assert saved.on_blinker is not None and saved.on_bsm is not None and saved.invert is not None
status = self.pip.render(rect, saved.mask, signals, enabled=self.cabin, on_blinker=saved.on_blinker,
on_bsm=saved.on_bsm, invert=saved.invert, now=now / 1e9, placements=positions, submit=submit)
if not self.cabin or status not in ('rendered', 'inactive'):
@@ -507,6 +512,8 @@ class Compositor:
from openpilot.starpilot.system.android_auto import gpu_nv12
from openpilot.starpilot.ui.shell import ShellMode
rl = self.rl
if sample.now_ns is None:
return False
self.context.begin_frame(sample.now_ns / 1e9)
self.placement.maintain(now_ns / 1e9)
snapshot = self.adapter.build(ShellMode.ONROAD)
@@ -547,7 +554,7 @@ class Compositor:
try:
pixels = self.readback.finish()
sample = self.ui.replay_clock.sample()
yield (pixels, self.meta) if sample.valid and sample.epoch == self.meta[0] else None
yield (pixels, self.meta) if self.meta is not None and sample.valid and sample.epoch == self.meta[0] else None
finally:
self.readback.release()
@@ -679,12 +686,12 @@ def run(session_dir: Path, route_id: str, data_dir: Path, target: str, profile:
if engine.command({'op': 'seek', 'position': status['position'], 'requestId': status.get('lastRequestId') or '0' * 32}, reprime=True):
primed_clients.update(unprimed)
valid = False
if valid and getattr(core.ui, 'replay_needs_reprime', False):
if valid and status is not None and getattr(core.ui, 'replay_needs_reprime', False):
if engine.command({'op': 'seek', 'position': status['position'], 'requestId': status.get('lastRequestId') or '0' * 32}, reprime=True):
core.ui.replay_needs_reprime = False
valid = False
interval = FRAME_INTERVAL
if valid and not core.readback.pending and now - last_draw >= interval:
if valid and status is not None and sample.now_ns is not None and not core.readback.pending and now - last_draw >= interval:
origin = status.get('routeStartNs')
if type(origin) is not int:
raise RuntimeError('Replay engine did not expose its route clock origin')
@@ -229,6 +229,7 @@ class ReplayCatalogTest(unittest.TestCase):
uploader.listdir_by_creation = lambda root: sorted(os.listdir(root))
path = Path(__file__).resolve().parents[3] / 'system/loggerd/deleter.py'
spec = importlib.util.spec_from_file_location('replay_fixture_deleter', path)
assert spec is not None and spec.loader is not None
deleter = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, {'openpilot.system.loggerd.uploader': uploader}):
spec.loader.exec_module(deleter)
@@ -75,6 +75,7 @@ def test_kernel_lock_prevents_copying_pixels_while_writer_is_active(tmp_path):
assert reader.latest() is None
fcntl.flock(publisher.fd, fcntl.LOCK_UN)
assert reader.latest().data == b'a' * 32
assert reader.fd is not None
fcntl.flock(reader.fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
with pytest.raises(RuntimeError, match='consumer stalled'):
publisher.publish(b'b' * 32, epoch=2, position=1, captured_ns=2)
@@ -93,10 +93,11 @@ def test_descriptor_rejects_hard_linked_metadata(tmp_path):
def test_mux_copy_has_explicit_pts_and_independent_initialization(tmp_path):
root = Path(__file__).parents[4]
library = Path(os.environ.get('SP_REPLAY_TEST_MUX_LIBRARY', root / 'openpilot/tools/replay/libmedia_mux.so'))
ffmpeg = root / '.venv/lib/python3.12/site-packages/ffmpeg/install/bin/ffmpeg'
import ffmpeg as ffmpeg_package
ffmpeg = Path(ffmpeg_package.__file__).resolve().parent / 'install/bin/ffmpeg'
ffprobe = ffmpeg.with_name('ffprobe')
if not library.exists() or not ffmpeg.exists() or not ffprobe.exists():
pytest.skip('Host replay muxer and bundled FFmpeg must be built')
pytest.skip(reason='Host replay muxer and bundled FFmpeg must be built')
source = tmp_path / 'fixture.ts'
subprocess.run([str(ffmpeg), '-nostdin', '-hide_banner', '-loglevel', 'error', '-f', 'rawvideo', '-pixel_format', 'yuv420p',
'-video_size', '32x18', '-framerate', '20', '-i', 'pipe:0',
@@ -355,7 +355,7 @@ def test_real_machine_uses_owner_created_clock_and_cleans_it(tmp_path, monkeypat
binary = Path(BASEDIR) / 'openpilot/tools/replay/replay'
if not binary.is_file():
pytest.skip('Build the isolated replay engine first')
pytest.skip(reason='Build the isolated replay engine first')
routes = tmp_path / 'routes'
routes.mkdir()
segment(routes, 0, [.1, .2, 10], compressed=True)
+11 -11
View File
@@ -50,7 +50,7 @@ def test_camera_matches_recorded_identity_across_distinct_clock_domains():
def test_paused_same_route_clock_never_advances_filters_or_gpu():
core = object.__new__(Compositor)
core.ui = SimpleNamespace(replay_sample=SimpleNamespace(now_ns=100))
vars(core)['ui'] = SimpleNamespace(replay_sample=SimpleNamespace(now_ns=100))
core.last_route_frame = (2, 100)
core.context = Mock()
assert core.draw(2, 0, 9_000_000_000) is False
@@ -65,7 +65,7 @@ def test_recorded_camera_preview_does_not_require_initial_device_state(frame_rea
viewport_width: int = 0
core = object.__new__(Compositor)
core.ui = SimpleNamespace(replay_sample=SimpleNamespace(now_ns=100),
vars(core)['ui'] = SimpleNamespace(replay_sample=SimpleNamespace(now_ns=100),
sm=SimpleNamespace(seen={'extrinsicsCalibration': False}))
core.last_route_frame = None
core.context = core.placement = Mock()
@@ -74,10 +74,10 @@ def test_recorded_camera_preview_does_not_require_initial_device_state(frame_rea
core._primary_matches = Mock(return_value=frame_ready)
core._optional_placeholder_ready = Mock(return_value=False)
core.rl = Mock()
core.gui = SimpleNamespace(_render_prepare=[], _frame=0)
vars(core)['gui'] = SimpleNamespace(_render_prepare=[], _frame=0)
core.content = Mock()
core.scale = 1
core.profile = SimpleNamespace(size=(1600, 800))
vars(core)['profile'] = SimpleNamespace(size=(1600, 800))
core.view = Mock()
core.view.render.side_effect = lambda state: setattr(core, 'primary_ready', state.camera_available)
core.output = None
@@ -242,7 +242,7 @@ def test_epoch_reset_preserves_native_camera_stream_and_eligible_paused_frame(mo
sm.__getitem__ = Mock(return_value=idx)
sm.seen = sm.valid = {'narrowRoadEncodeIdx': True}
sm.logMonoTime = {'narrowRoadEncodeIdx': 9_020_000_000}
core.ui = SimpleNamespace(replay_sample=SimpleNamespace(valid=True, boot_ns=9_050_000_000), sm=sm)
vars(core)['ui'] = SimpleNamespace(replay_sample=SimpleNamespace(valid=True, boot_ns=9_050_000_000), sm=sm)
core.render_receipts = {'modelV2': 1}
core.reset_epoch(3)
assert core.render_receipts == {}
@@ -301,7 +301,7 @@ def test_early_camera_subscriber_is_polled_before_paused_reprime():
# The first receive synchronizes with a publisher created after this client.
client.recv.side_effect = [None, frame]
core = object.__new__(Compositor)
core.ui = Mock(replay_sample=sample, sm=sm)
vars(core)['ui'] = Mock(replay_sample=sample, sm=sm)
core.epoch, core.cabin = 1, False
core.camera = Mock(client=client, frame=None, stream_type=VisionStreamType.VISION_STREAM_NARROW_ROAD)
core.camera._ensure_connection.return_value = True
@@ -335,7 +335,7 @@ def test_camera_reprime_cannot_overwrite_an_unacknowledged_user_action():
import threading
engine = object.__new__(Engine)
engine.write_lock = threading.Lock()
engine.process = SimpleNamespace(stdin=io.StringIO())
vars(engine)['process'] = SimpleNamespace(stdin=io.StringIO())
engine.last_request_id = engine.seek_request = None
request = {'op': 'seek', 'position': 65, 'requestId': 'a' * 32}
assert engine.command(request)
@@ -385,8 +385,8 @@ def test_replay_draw_retains_updates_until_stock_consumers_read_them(monkeypatch
camera.available_streams, camera.frame = {camera.stream_type}, object()
camera.model_renderer = camera._model_renderer = renderer
core = object.__new__(Compositor)
core.ui, core.camera = ui, camera
core.profile = 'compact' if profile == 'mici.onroad' else 'large'
vars(core)['ui'], core.camera = ui, camera
vars(core)['profile'] = 'compact' if profile == 'mici.onroad' else 'large'
core.cabin = core.wide = False
core.render_receipts = {}
core._primary_matches = Mock(return_value=True)
@@ -480,9 +480,9 @@ def test_finished_frame_is_borrowed_until_delivery_finishes(valid, epoch):
pixels = memoryview(bytearray(b'pixels'))
core.readback = Mock(pending=True)
core.readback.finish.return_value = pixels
core.ui = SimpleNamespace(replay_clock=Mock())
vars(core)['ui'] = SimpleNamespace(replay_clock=Mock())
core.ui.replay_clock.sample.return_value = SimpleNamespace(valid=valid, epoch=epoch)
core.meta = 1, 0., 100
vars(core)['meta'] = 1, 0., 100
with pytest.raises(RuntimeError, match='delivery failed'):
with core.finish() as finished:
core.readback.release.assert_not_called()
+10 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import time
from typing import Protocol
import pyray as rl
@@ -26,10 +27,18 @@ def _external_shader(fragment: str) -> str:
return result.replace(yuv, "vec3 rgb = pow(texture(texture0, uv).rgb, vec3(1.0/1.28));")
class PiPStreamSource(Protocol):
@property
def generation(self) -> int: ...
def set_active(self, active: bool) -> None: ...
def poll(self, now: float) -> object | None: ...
def release_retired(self) -> None: ...
class PiPRenderer:
"""One renderer; close it on onroad exit or when the owning shell closes."""
def __init__(self, shape: str, stream: PiPStream | None = None, *, frame_availability=None):
def __init__(self, shape: str, stream: PiPStreamSource | None = None, *, frame_availability=None):
if shape not in ("bubble", "curved"):
raise ValueError("unknown PiP shape")
self.frame_availability = frame_availability if frame_availability is not None else CameraAvailability()
+10 -4
View File
@@ -5,6 +5,7 @@ from html import escape, unescape
import math
import time
import uuid
from typing import cast
import pyray as rl
@@ -133,7 +134,10 @@ class ReplayController:
if type(position) not in (int, float) or not math.isfinite(position):
return False
start, end = self.status.get('start'), self.status.get('end')
if type(start) not in (int, float) or type(end) not in (int, float) or end <= start:
if type(start) not in (int, float) or type(end) not in (int, float):
return False
start, end = cast(float, start), cast(float, end)
if end <= start:
return False
position = max(start, min(end, position))
ranges = self.status.get('seekRanges')
@@ -283,7 +287,7 @@ class ReplayPage(Widget):
self.last_touch = time.monotonic()
self.stopping = False
self._epoch = None
self.stack_widget = self
self.stack_widget: Widget = self
def show_event(self):
super().show_event()
@@ -620,7 +624,8 @@ class ReplayPage(Widget):
start, end = self.controller.status.get('start') or 0, self.controller.status.get('end') or 0
self.seek_preview = start + max(0, min(1, (x - left) / width)) * (end - start)
def _handle_mouse_event(self, event):
def _handle_mouse_event(self, mouse_event):
event = mouse_event
if event.left_released and not rl.check_collision_point_rec(event.pos, self._hit_rect):
self.press = self.seek_preview = None
self.input.cancel()
@@ -682,7 +687,8 @@ class ReplayCompactPage(NavWidget):
def _back_enabled(self):
return self.content.page != 'viewer'
def _handle_mouse_event(self, event):
def _handle_mouse_event(self, mouse_event):
event = mouse_event
was_dismissing = self._playing_dismiss_animation
super()._handle_mouse_event(event)
if event.left_released and not was_dismissing and self._playing_dismiss_animation and self.content.page == 'detail':
@@ -69,7 +69,7 @@ def test_aol_model_geometry_uses_fresh_lateral_display_context(monkeypatch):
return values[key]
source = SubMaster()
source.recv_frame, source.updated, source.valid = sm.recv_frame, sm.updated, sm.valid
ui = NS(sm=source, started_frame=1, status=UIStatus.DISENGAGED)
ui = NS(sm=source, started_frame=1, status=UIStatus.DISENGAGED, display_time_ns=lambda: 1_000_000_000)
with patch.object(native_model, 'ui_state', ui):
renderer._render(rl.Rectangle(0, 0, 476, 240))
renderer._draw_lane_lines.assert_not_called()
@@ -94,7 +94,7 @@ def test_aol_lane_and_path_colors_do_not_use_disengaged_black(monkeypatch):
renderer._rainbow_path.refresh_enabled.return_value = False
renderer._experimental_mode = True
monkeypatch.setattr(renderer, '_exp_gradient', NS(colors=[]), raising=False)
ui = NS(status=UIStatus.DISENGAGED, params=Mock())
ui = NS(status=UIStatus.DISENGAGED, params=Mock(), display_time_ns=lambda: 1_000_000_000)
with patch.object(native_model, 'ui_state', ui), patch.object(native_model, 'draw_polygon') as polygon:
assert renderer._get_ll_color(0.7, True, True).r == 0
renderer._draw_path({'longitudinalPlan': NS(allowThrottle=True)})
@@ -65,23 +65,23 @@ class TestRainbowPath(unittest.TestCase):
def __getitem__(self, key):
return self.values[key]
sm = SubMaster(messages)
ui = NS(status=compact.UIStatus.ENGAGED, started_frame=5)
with patch.object(module, "ui_state", ui), patch.object(module.time, "monotonic_ns", return_value=1_000_000_000), \
ui = NS(status=compact.UIStatus.ENGAGED, started_frame=5, display_time_ns=Mock())
with patch.object(module, "ui_state", ui), patch.object(ui, "display_time_ns", return_value=1_000_000_000), \
patch.object(module, "draw_polygon") as draw:
renderer._draw_path(sm)
self.assertIs(draw.call_args.kwargs["gradient"], renderer._exp_gradient)
renderer.road_style = {"pathMode": "rainbow"}
with patch.object(module, "ui_state", ui), patch.object(module.time, "monotonic_ns", return_value=2_000_000_000), \
with patch.object(module, "ui_state", ui), patch.object(ui, "display_time_ns", return_value=2_000_000_000), \
patch.object(module, "draw_polygon") as draw:
renderer._draw_path(sm)
self.assertEqual(len(draw.call_args.kwargs["gradient"].colors), 12)
sm.recv_frame["carState"] = 4
with patch.object(module, "ui_state", ui), patch.object(module.time, "monotonic_ns", return_value=2_100_000_000), \
with patch.object(module, "ui_state", ui), patch.object(ui, "display_time_ns", return_value=2_100_000_000), \
patch.object(module, "draw_polygon"):
renderer._draw_path(sm)
self.assertEqual(renderer._rainbow_path.phase_deg, 0.0)
sm.recv_frame["carState"] = 5
with patch.object(module, "ui_state", ui), patch.object(module.time, "monotonic_ns", return_value=2_200_000_000), \
with patch.object(module, "ui_state", ui), patch.object(ui, "display_time_ns", return_value=2_200_000_000), \
patch.object(module, "draw_polygon"):
renderer._draw_path(sm)
self.assertGreater(renderer._rainbow_path.phase_deg, 0.0)
@@ -1,4 +1,5 @@
"""Fake transport exercises native ownership, cancellation and passive input."""
from typing import Any
from types import SimpleNamespace as NS
import threading
@@ -19,7 +20,7 @@ ROUTE = {'routeId': '00000001--0123456789', 'displayName': 'Recorded route', 'se
class Backend:
def __init__(self):
self.calls = []
self.status = {'schemaVersion': 1, 'ownerId': 'a' * 32, 'sessionId': None, 'state': 'idle',
self.status: dict[str, Any] = {'schemaVersion': 1, 'ownerId': 'a' * 32, 'sessionId': None, 'state': 'idle',
'routeId': None, 'profile': 'large', 'revision': 0, 'epoch': 0,
'position': 0.0, 'start': 0.0, 'end': 120.0, 'rate': 1.0, 'paused': True,
'frameReady': False, 'errorCode': None, 'owned': False, 'offroad': True}
@@ -644,7 +644,8 @@ class TestRuntimePanelActions(unittest.TestCase):
def test_compact_galaxy_is_on_main_menu_and_device_keeps_native_rows(self):
from openpilot.selfdrive.ui.mici.layouts.settings.device import device_layout as compact_device
from openpilot.starpilot.ui import display_compact
from openpilot.starpilot.ui import display_compact, replay_settings
replay_button = Mock()
display_entry = object()
panel = NS(_scroller=NS(add_widgets=Mock()))
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
@@ -653,14 +654,21 @@ class TestRuntimePanelActions(unittest.TestCase):
object.__setattr__(layout, "star", NS(galaxy_flow=galaxy))
with patch.object(compact_device, "DeviceLayoutMici", return_value=panel) as built, \
patch.object(display_compact.DisplayCompact, "entry_button", return_value=display_entry), \
patch.object(replay_settings, "BigButton", return_value=replay_button) as replay_constructor, \
patch.object(runtime_app.gui_app, "push_widget") as pushed:
layout._open_compact_destination(Destination.DEVICE)
built.assert_called_once_with()
panel._scroller.add_widgets.assert_called_once_with([display_entry])
panel._scroller.add_widgets.assert_called_once_with([display_entry, replay_button])
pushed.assert_called_once_with(panel)
layout._open_compact_destination(Destination.GALAXY)
galaxy.open_compact.assert_called_once_with()
replay_constructor.assert_called_once_with("replay", "recorded driving routes on this device")
replay_button.set_click_callback.assert_called_once()
with patch.object(replay_settings, "open_replay") as open_replay:
replay_button.set_click_callback.call_args.args[0]()
open_replay.assert_called_once_with(layout.star)
def test_compact_toggles_and_software_keep_only_native_rows(self):
from openpilot.selfdrive.ui.mici.layouts.settings import toggles, software
panels = []
@@ -682,6 +690,8 @@ class TestRuntimePanelActions(unittest.TestCase):
self.assertEqual([call.args[0] for call in pushed.call_args_list], panels)
def test_compact_model_visuals_and_pair_use_existing_owners(self):
from openpilot.starpilot.ui import replay_settings
replay_button = Mock()
from openpilot.starpilot.ui import appearance_compact, models_compact, display_compact
from openpilot.selfdrive.ui.mici.layouts.settings.device import device_layout as compact_device
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
@@ -692,6 +702,7 @@ class TestRuntimePanelActions(unittest.TestCase):
patch.object(appearance_compact.AppearanceCompact, "open") as visuals_open, \
patch.object(compact_device, "DeviceLayoutMici", return_value=panel), \
patch.object(display_compact.DisplayCompact, "entry_button", return_value="display entry"), \
patch.object(replay_settings, "BigButton", return_value=replay_button) as replay_constructor, \
patch.object(runtime_app.gui_app, "push_widget") as pushed:
layout._open_compact_destination(Destination.DRIVING_MODEL)
layout._open_compact_destination(Destination.APPEARANCE)
@@ -700,7 +711,13 @@ class TestRuntimePanelActions(unittest.TestCase):
visuals_open.assert_called_once_with()
pushed.assert_called_once_with(panel)
panel.scroll_to_pairing.assert_called_once_with()
panel._scroller.add_widgets.assert_called_once_with(["display entry"])
panel._scroller.add_widgets.assert_called_once_with(["display entry", replay_button])
replay_constructor.assert_called_once_with("replay", "recorded driving routes on this device")
replay_button.set_click_callback.assert_called_once()
with patch.object(replay_settings, "open_replay") as open_replay:
replay_button.set_click_callback.call_args.args[0]()
open_replay.assert_called_once_with(layout.star)
def test_compact_connection_can_be_viewed_while_started_but_not_mutated(self):
with tempfile.TemporaryDirectory() as directory:
@@ -120,7 +120,7 @@ def test_started_presentation_does_not_replace_existing_message_freshness():
def replay_owner(sample):
from openpilot.starpilot.ui.tests.test_runtime_snapshot import SubMasterFake
ui = object.__new__(UIState)
ui.sm = SubMasterFake()
vars(ui)['sm'] = SubMasterFake()
ui.sm.sock = {name: object() for name in ('carControl', 'carState', 'selfdriveState')}
ui.sm.static_freq_services = set(ui.sm.sock)
ui.sm.data = ui.sm.messages
@@ -244,6 +244,7 @@ def test_replay_boot_time_camera_primer_retention_and_freshness():
ui.sm.sock['cabinCameraState'] = object()
ui.sm.static_freq_services.add('cabinCameraState')
ui.sm.put('cabinCameraState', NS(frameId=42))
assert sample.boot_ns is not None
ui.sm.logMonoTime['cabinCameraState'] = sample.boot_ns - 1_000_000
ui.sm.replay_sample = sample
ui._update_replay_alive(sample)
@@ -97,6 +97,7 @@ class Machine:
self.process = subprocess.Popen(self.command, env=self.env, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=self.stderr, text=True)
def reader():
assert self.process.stdout is not None
for line in self.process.stdout:
try:
self.statuses.put(json.loads(line))
@@ -283,6 +284,7 @@ class TestMachine(unittest.TestCase):
for name, levels in (("a", (30, 90, 120)), ("b", (200, 220, 240))):
raw, clip = root / (name + ".yuv"), root / (name + ".ts")
raw.write_bytes(b"".join(bytes([level]) * (32 * 18) + bytes([128]) * (32 * 18 // 2) for level in levels))
assert FFMPEG is not None
subprocess.run([FFMPEG, "-hide_banner", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "yuv420p",
"-s", "32x18", "-r", "20", "-i", str(raw), "-c:v", "libx264", "-qp", "0", "-bf", "0",
"-g", "100", "-tune", "zerolatency", "-f", "mpegts", str(clip)], check=True, timeout=10)