Files
StarPilot/selfdrive/ui/tests/diff/replay.py
T
David 03a4f7ef9a ui: add big (tizi) replay (#37198)
* init: tizi_replay.py from pr 37123

* separate coverage folder

* ui replay: adjust HOLD constant, fix coverage, use separate folder for coverage

* openpilot prefix

* fix directory

* fix ui_state

* fix settings click pos

* remove

* attempt merge replay files

* remove

* todo

* fix recording

* spacing

* simplify

* comment

* refactor hold

* refactor: remove layout definitions from VARIANTS and import conditionally in run_replay

* refactor:  remove VARIANTS config

* add argparser with --big flag and improve coverage sources

* refactor

* lowercase

* refactor: combine scripts

* add types

* refactor: move imports for gui_app and ui_state to improve coverage and organization

* update

* update script

* comment

* fix headless

* todo

* fix: get_time and get_frame_time determinism

* todo

* remove file accidently commited

* fix: improve inject_click and handle_event for deterministic event timestamps

* comment

* simplify add

* refactor script building

* fix mici clicks

* pass in pm

* fix wifi state

* refactor clicks

* more refactor

* click cancel instead of remove overlay

* setup_send_fn

* add setup fn

* dummy update

* change

* remove todo

* rename fn to frame_fn

* refactor

* fix workflow

* rename raylib ui preview to old

* rename mici workflow

* fix diff videos

* ignore sub html and mp4 files

* rename for diff

* rename for diff again (mici)

* use ScriptEvent instead of DummyEvent, and move mouse events directly to it; rename hold to wait

* fix: only import MouseEvent for type hint to fix coverage

* adjust settings button click

* clarify

* move ScriptEvent to replay_script

* add handle_event function

* remove passing in setup function, and refactor click events

* clean

* formatting

* refactor

* no import

* comment

* refactor

* refactor setup functions to replay_setup

* refactor

* add ReplayContext

* refactor

* move more setup functions

* refactor and simplify

* refactor

* refactor: add Script class

* refactor: enhance Script event handling and add wait functionality

* refactor

* remove setup_and_click

* use script.setup instead

* comments

* rename wait_frames to wait_after

* add comments

* revert workflows

* revert rename

* move arg parsing to main

* remove quotes

* add type

* return types

* type

* VariantType

* rename to LayoutVariant

* clarify

* switch

* todo

* Revert "fix diff videos"

This reverts commit 7a6e45a409cb7e6d7a330317639fcee74ef8bd31.

* add todos

* add more coverage

* wait 2 frames by default

* add comment

* comment

* switch

* fix space

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove extra

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove unnecessary blank line in ReplayContext class

* simplify

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2026-02-15 20:03:30 -08:00

130 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import argparse
import coverage
import pyray as rl
from typing import Literal
from collections.abc import Callable
from cereal.messaging import PubMaster
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.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)
params.put("CompletedTrainingVersion", training_version)
params.put("DongleId", "test123456789")
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
def run_replay(variant: LayoutVariant) -> None:
from openpilot.selfdrive.ui.ui_state import ui_state # 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
setup_state()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
if HEADLESS:
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
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()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
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
for should_render 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()
if should_render:
main_layout.render()
frame += 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_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()