Files
dragonpilot/selfdrive/ui/tests/diff/replay.py
Vehicle Researcher 6adb63b915 openpilot v0.11.1 release
date: 2026-06-04T09:49:56
master commit: c0ab3550eca2e9daf197c46b7e4b24aa9637cf2e
2026-06-04 09:50:05 -07:00

149 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import argparse
import coverage
import pyray as rl
from tqdm import tqdm
from typing import Literal
from collections.abc import Callable
from cereal.messaging import PubMaster
from openpilot.common.api import Api
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
from openpilot.system.updated.updated import parse_release_notes
from openpilot.system.version import terms_version, training_version
LayoutVariant = Literal["mici", "tizi"]
FPS = 60
HEADLESS = os.getenv("WINDOWED", "0") != "1"
def setup_state():
params = Params()
params.put("HasAcceptedTerms", terms_version, block=True)
params.put("CompletedTrainingVersion", training_version, block=True)
params.put("DongleId", "test123456789", block=True)
# Combined description for layouts that still use it (BIG home, settings/software)
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30", block=True)
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR), block=True)
# Params for mici home
params.put("Version", "0.10.1", block=True)
params.put("GitBranch", "test-branch", block=True)
params.put("GitCommit", "abc12340ff9131237ba23a1d0fbd8edf9c80e87", block=True)
params.put("GitCommitDate", "'1732924800 2024-11-30 00:00:00 +0000'", block=True)
# Patch Api.get_token to return a static token so the pairing QR code is deterministic across runs
Api.get_token = lambda self, payload_extra=None, expiry_hours=0: "test_token"
def run_replay(variant: LayoutVariant) -> None:
if HEADLESS:
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
os.environ["OFFSCREEN"] = "1" # Run UI without FPS limit (set before importing gui_app)
setup_state()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
from openpilot.selfdrive.ui.ui_state import ui_state, device # Import within OpenpilotPrefix context so param values are setup correctly
from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage
from openpilot.selfdrive.ui.tests.diff.replay_script import build_script
gui_app.init_window("ui diff test", fps=FPS)
# Dynamically import main layout based on variant
if variant == "mici":
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout as MainLayout
else:
from openpilot.selfdrive.ui.layouts.main import MainLayout
main_layout = MainLayout()
# Disable interactive timeout — replay clicks use left_down=False so they never reset the timer,
# and after 30s of real wall-clock time the settings panel would close automatically.
device.set_override_interactive_timeout(99999)
pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"])
script = build_script(pm, main_layout, variant)
script_index = 0
send_fn: Callable | None = None
frame = 0
# Override raylib timing functions to return deterministic values based on frame count instead of real time
rl.get_frame_time = lambda: 1.0 / FPS
rl.get_time = lambda: frame / FPS
# Main loop to replay events and render frames
with tqdm(total=script[-1][0] + 1, desc="Replaying", unit="frame", disable=bool(os.getenv("CI"))) as pbar:
for _ in gui_app.render():
# Handle all events for the current frame
while script_index < len(script) and script[script_index][0] == frame:
_, event = script[script_index]
# Call setup function, if any
if event.setup:
event.setup()
# Send mouse events to the application
if event.mouse_events:
with gui_app._mouse._lock:
gui_app._mouse._events.extend(event.mouse_events)
# Update persistent send function
if event.send_fn is not None:
send_fn = event.send_fn
# Move to next script event
script_index += 1
# Keep sending cereal messages for persistent states (onroad, alerts)
if send_fn:
send_fn()
ui_state.update()
frame += 1
pbar.update(1)
if script_index >= len(script):
break
gui_app.close()
print(f"Total frames: {frame}")
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--big', action='store_true', help='Use big UI layout (tizi/tici) instead of mici layout')
args = parser.parse_args()
variant: LayoutVariant = 'tizi' if args.big else 'mici'
if args.big:
os.environ["BIG"] = "1"
os.environ["RECORD"] = "1"
os.environ["RECORD_QUALITY"] = "0" # Use CRF 0 ("lossless" encode) for deterministic output across different machines
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ.get("RECORD_OUTPUT", f"{variant}_ui_replay.mp4"))
print(f"Running {variant} UI replay...")
with OpenpilotPrefix():
sources = ["openpilot.system.ui"]
if variant == "mici":
sources.append("openpilot.selfdrive.ui.mici")
omit = ["**/*tizi*", "**/*tici*"] # exclude files containing "tizi" or "tici"
else:
sources.extend(["openpilot.selfdrive.ui.layouts", "openpilot.selfdrive.ui.onroad", "openpilot.selfdrive.ui.widgets"])
omit = ["**/*mici*"] # exclude files containing "mici"
cov = coverage.Coverage(source=sources, omit=omit)
with cov.collect():
run_replay(variant)
cov.save()
cov.report()
directory = os.path.join(DIFF_OUT_DIR, f"htmlcov-{variant}")
cov.html_report(directory=directory)
print(f"HTML report: {directory}/index.html")
if __name__ == "__main__":
main()