mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 04:53:47 +08:00
a82935f326
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services. 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: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: Shane Smiskol <shane@smiskol.com> 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: pharmacomaniac <blittle65@gmail.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
222 lines
11 KiB
Python
222 lines
11 KiB
Python
"""Development-only Settings capture and Home navigation preview.
|
|
|
|
Other leaf panels and compact scrolling are unavailable. This entry point never starts
|
|
device services or changes settings. Development controls sit outside the device
|
|
canvas; exported framebuffers retain only the protected presentation.
|
|
"""
|
|
|
|
import argparse
|
|
from dataclasses import asdict
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import time
|
|
|
|
import pyray as rl
|
|
import raylib
|
|
|
|
from openpilot.starpilot.ui.device import DeviceView
|
|
from openpilot.starpilot.ui.device_state import DeviceInput
|
|
from openpilot.starpilot.ui.home import HomeView
|
|
from openpilot.starpilot.ui.home_state import HomeInput
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts, Profile
|
|
from openpilot.starpilot.ui.preview_home import reference_state
|
|
from openpilot.starpilot.ui.settings import SettingsView
|
|
from openpilot.starpilot.ui.settings_state import Destination, PreviewRouter, SettingsAction, SettingsActionKind, SettingsInput, SettingsState
|
|
from openpilot.starpilot.ui.software import SoftwareView
|
|
from openpilot.starpilot.ui.software_state import SoftwareInput
|
|
from openpilot.starpilot.ui.toggles import TogglesView
|
|
from openpilot.starpilot.ui.toggles_state import TogglesInput
|
|
|
|
|
|
def reference_settings_state() -> SettingsState:
|
|
# An explicit captured entry position, not an implementation of animation.
|
|
return SettingsState(compact_y=-17.667003631591797, nav_bar_y=4.752651691436768, nav_bar_alpha=0.062367421150705246)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--profile", choices=list(Profile), required=True)
|
|
parser.add_argument("--font-directory", type=Path, required=True)
|
|
parser.add_argument("--asset-directory", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--snapshot", choices=("reference", "settled"), default="reference")
|
|
parser.add_argument("--interactive", action="store_true", help="Show development chrome, Home/Settings/large leaf routing and inert requests")
|
|
args = parser.parse_args()
|
|
if args.output.exists():
|
|
raise ValueError("Refusing to overwrite existing Settings evidence")
|
|
args.output.mkdir(parents=True)
|
|
profile = Profile(args.profile)
|
|
width, height = profile.size
|
|
canvas_width = min(width, 960)
|
|
canvas_height = round(height * canvas_width / width)
|
|
flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT | rl.ConfigFlags.FLAG_WINDOW_HIGHDPI
|
|
if not args.interactive:
|
|
flags |= rl.ConfigFlags.FLAG_WINDOW_HIDDEN
|
|
rl.set_config_flags(flags)
|
|
rl.init_window(canvas_width, canvas_height + (80 if args.interactive else 0), "Development preview — Settings entry only")
|
|
if not rl.is_window_ready():
|
|
raise RuntimeError("Unable to create native Settings graphics context")
|
|
target = rl.load_render_texture(width, height)
|
|
try:
|
|
if not target.id or not target.texture.id:
|
|
raise RuntimeError("Unable to create Settings framebuffer")
|
|
with BitmapFonts(profile, args.font_directory) as fonts:
|
|
settings = SettingsView(fonts, args.asset_directory)
|
|
home = HomeView(fonts, args.asset_directory)
|
|
try:
|
|
settings.prepare()
|
|
state = reference_settings_state() if args.snapshot == "reference" else SettingsState()
|
|
timings = []
|
|
for frame in range(18):
|
|
begin = time.perf_counter()
|
|
rl.begin_texture_mode(target)
|
|
rl.clear_background(rl.BLACK)
|
|
settings.render(state)
|
|
rl.end_texture_mode()
|
|
pixels = rl.load_image_from_texture(target.texture)
|
|
try:
|
|
if pixels.data == rl.ffi.NULL:
|
|
raise RuntimeError("Unable to read Settings framebuffer")
|
|
if frame >= 6:
|
|
timings.append((time.perf_counter() - begin) * 1000)
|
|
if frame == 17:
|
|
rl.image_format(pixels, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)
|
|
rl.image_flip_vertical(pixels)
|
|
if not rl.export_image(pixels, str(args.output / f"{profile}.png")):
|
|
raise RuntimeError("Unable to export Settings framebuffer")
|
|
rgba_hash = hashlib.sha256(bytes(rl.ffi.buffer(pixels.data, width * height * 4))).hexdigest()
|
|
finally:
|
|
if pixels.data != rl.ffi.NULL:
|
|
rl.unload_image(pixels)
|
|
report = {"profile": profile, "dimensions": [width, height], "snapshot": args.snapshot, "development_only": True,
|
|
"state": asdict(state), "rgba_sha256": rgba_hash, "submission_readback_ms": timings,
|
|
"native_raylib_sha256": hashlib.sha256(Path(raylib._cffi.__file__).read_bytes()).hexdigest(),
|
|
"scope": "static entry presentation; no compact motion, leaf panels, device services or performance qualification"}
|
|
(args.output / f"{profile}.json").write_text(json.dumps(report, indent=2) + "\n")
|
|
if args.interactive:
|
|
_interactive(profile, settings, home, target, canvas_width, canvas_height)
|
|
finally:
|
|
home.close()
|
|
settings.close()
|
|
finally:
|
|
if target.id:
|
|
rl.unload_render_texture(target)
|
|
rl.close_window()
|
|
|
|
|
|
class SettingsPreviewInput:
|
|
"""Dispatch the interactive preview's actual Home, rail and leaf input."""
|
|
|
|
def __init__(self, profile: Profile, router: PreviewRouter):
|
|
self.router = router
|
|
self.home_state = reference_state()
|
|
self.settings_input = SettingsInput(profile, self._route_settings, lambda: router.selected)
|
|
self.device_input = DeviceInput(router.device_action)
|
|
self.software_input = SoftwareInput(router.software_action)
|
|
self.toggles_input = TogglesInput(router.toggle_action)
|
|
self.home_input = HomeInput(profile, router.home_action)
|
|
|
|
def _route_settings(self, action: SettingsAction) -> None:
|
|
self.settings_input.cancel()
|
|
self.device_input.cancel()
|
|
self.software_input.cancel()
|
|
self.toggles_input.cancel()
|
|
self.router.settings_action(action)
|
|
|
|
def step(self, x: float, y: float, now: float, *, pressed: bool = False, down: bool = False,
|
|
released: bool = False, back_pressed: bool = False, footer_pressed: bool = False) -> None:
|
|
router = self.router
|
|
if back_pressed or footer_pressed:
|
|
self._route_settings(SettingsAction(SettingsActionKind.CLOSE))
|
|
self.home_input.cancel()
|
|
elif router.in_settings:
|
|
if pressed:
|
|
self.settings_input.press(x, y, router.settings)
|
|
if router.selected == Destination.DEVICE:
|
|
self.device_input.press(x, y, router.device)
|
|
elif router.selected == Destination.SOFTWARE:
|
|
self.software_input.press(x, y, router.software)
|
|
elif router.selected == Destination.TOGGLES:
|
|
self.toggles_input.press(x, y, router.toggles)
|
|
if down:
|
|
self.settings_input.move(x, y, router.settings)
|
|
if router.selected == Destination.DEVICE:
|
|
self.device_input.move(x, y, router.device)
|
|
elif router.selected == Destination.SOFTWARE:
|
|
self.software_input.move(x, y, router.software)
|
|
elif router.selected == Destination.TOGGLES:
|
|
self.toggles_input.move(x, y, router.toggles)
|
|
if released:
|
|
selected_at_release = router.selected
|
|
self.settings_input.release(x, y, router.settings)
|
|
if router.in_settings and router.selected == selected_at_release:
|
|
if selected_at_release == Destination.DEVICE:
|
|
self.device_input.release(x, y, router.device)
|
|
elif selected_at_release == Destination.SOFTWARE:
|
|
self.software_input.release(x, y, router.software)
|
|
elif selected_at_release == Destination.TOGGLES:
|
|
self.toggles_input.release(x, y, router.toggles)
|
|
else:
|
|
if pressed:
|
|
self.home_input.press(x, y, now)
|
|
if down:
|
|
self.home_input.move(x, y, now)
|
|
self.home_input.tick(now, self.home_state)
|
|
if released:
|
|
self.home_input.release(x, y, now, self.home_state)
|
|
|
|
|
|
def _interactive(profile, settings, home, target, canvas_width, canvas_height):
|
|
router = PreviewRouter(profile)
|
|
inputs = SettingsPreviewInput(profile, router)
|
|
device = DeviceView(settings.fonts) if profile == Profile.LARGE else None
|
|
software = SoftwareView(settings.fonts) if profile == Profile.LARGE else None
|
|
toggles = TogglesView(settings.fonts, settings.assets.directory) if profile == Profile.LARGE else None
|
|
rl.set_exit_key(0)
|
|
rl.set_target_fps(60)
|
|
print(router.notice, flush=True)
|
|
while not rl.window_should_close():
|
|
position = rl.get_mouse_position()
|
|
x, y = position.x * profile.size[0] / canvas_width, position.y * profile.size[1] / canvas_height
|
|
now = time.monotonic()
|
|
# Desktop development sampler only; this is not device Widget event routing.
|
|
pressed = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) # noqa: TID251
|
|
released = rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) # noqa: TID251
|
|
back_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE) or rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE)
|
|
inputs.step(x, y, now, pressed=pressed, down=rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT),
|
|
released=released, back_pressed=back_pressed, footer_pressed=pressed and position.y >= canvas_height)
|
|
rl.begin_texture_mode(target)
|
|
rl.clear_background(rl.BLACK)
|
|
if router.in_settings:
|
|
if router.selected == Destination.DEVICE and device is not None:
|
|
device.render(router.device)
|
|
settings.render_rail(router.settings, selected=Destination.DEVICE)
|
|
elif router.selected == Destination.SOFTWARE and software is not None:
|
|
software.render(router.software)
|
|
settings.render_rail(router.settings, selected=Destination.SOFTWARE)
|
|
elif router.selected == Destination.TOGGLES and toggles is not None:
|
|
toggles.render(router.toggles)
|
|
settings.render_rail(router.settings, selected=Destination.TOGGLES)
|
|
else:
|
|
settings.render(router.settings)
|
|
else:
|
|
home.render(inputs.home_state)
|
|
rl.end_texture_mode()
|
|
rl.begin_drawing()
|
|
rl.clear_background(rl.Color(24, 24, 28, 255))
|
|
rl.draw_texture_pro(target.texture, rl.Rectangle(0, 0, profile.size[0], -profile.size[1]),
|
|
rl.Rectangle(0, 0, canvas_width, canvas_height), rl.Vector2(0, 0), 0, rl.WHITE)
|
|
font = rl.get_font_default()
|
|
rl.draw_text_ex(font, "DEVELOPMENT PREVIEW - click footer / Esc to go back", rl.Vector2(8, canvas_height + 8), 16, 1, rl.YELLOW)
|
|
rl.draw_text_ex(font, router.notice, rl.Vector2(8, canvas_height + 34), 14, 1, rl.WHITE)
|
|
rl.draw_text_ex(font, "Large Device, Toggles and Software requests are inert. Other leaves unavailable; compact cards are static.",
|
|
rl.Vector2(8, canvas_height + 58), 12, 1, rl.GRAY)
|
|
rl.end_drawing()
|
|
if toggles is not None:
|
|
toggles.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|