mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
718a2c3ddd
Discard a preempted clock pair and try up to three fresh pairs while retaining the 1 ms accepted span and all existing freshness/resume gates. Yield after receive-only decoder construction so normal UI collection refreshes before full physical admission, Offroad request or input. Recorded d5 clock failure; reproduced timing regressions; 250 focused native cases, lint/type checks and independent source/evidence review. No vehicle commands, device deployment or physical cure claim.
268 lines
9.8 KiB
Python
268 lines
9.8 KiB
Python
"""Native game and version gesture using the supported parked lifecycle owner."""
|
|
|
|
from collections.abc import Callable
|
|
import math
|
|
import time
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.common.hardware import HARDWARE
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
|
from openpilot.starpilot.ui.spacepilot.guard import ParkGate, Witness, read_offroad_witness, read_witness, sample_clocks
|
|
from openpilot.starpilot.ui.spacepilot.session import GameSession, Stage
|
|
from openpilot.starpilot.ui.spacepilot.ignition import IgnitionReturn
|
|
from openpilot.starpilot.drive_state.owner import DriveStateOwner
|
|
from openpilot.starpilot.storage import starpilot_storage_root
|
|
from openpilot.starpilot.ui.spacepilot.render import EXIT, SpaceRenderer, canvas_rect
|
|
from openpilot.starpilot.favorites.actions import INCREASE_SPEED, DECREASE_SPEED
|
|
from openpilot.starpilot.ui.spacepilot.state import FiveTaps, Flight
|
|
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
|
|
from openpilot.system.ui.widgets import Widget
|
|
|
|
|
|
# This observer outlives page exit, but never a UI process restart. It owns only
|
|
# the exact game request and no game/socket/display resources.
|
|
_ignition_return: IgnitionReturn | None = None
|
|
# Covered widgets remain open until hide_event finishes; top-widget identity is
|
|
# insufficient to prove cleanup before a mode request.
|
|
_open_games: set['SpacePilotPage'] = set()
|
|
|
|
|
|
def poll_game_mode() -> None:
|
|
"""Called before rendering, including while the ordinary display is asleep."""
|
|
global _ignition_return
|
|
pending = _ignition_return
|
|
if pending is None:
|
|
return
|
|
try:
|
|
if pending.observe():
|
|
for page in tuple(_open_games):
|
|
page._exit(unsafe=True)
|
|
pending.release(lambda: not _open_games)
|
|
except Exception:
|
|
cloudlog.exception('SpacePilot ignition return ended')
|
|
pending.finished = True
|
|
if pending.finished:
|
|
_ignition_return = None
|
|
|
|
|
|
def live_witness() -> Witness | None:
|
|
try:
|
|
after, boot = sample_clocks()
|
|
native_device = HARDWARE.get_device_type() in ('mici', 'tici', 'tizi')
|
|
if ui_state.started:
|
|
return read_witness(ui_state, after, boot, c4=native_device)
|
|
return read_offroad_witness(ui_state, after, boot, native_device=native_device, preparing=True)
|
|
except (AttributeError, OSError, ValueError):
|
|
return None
|
|
|
|
|
|
class SpacePilotPage(Widget):
|
|
def __init__(self, gate: ParkGate, on_unsafe: Callable[[], None]):
|
|
super().__init__()
|
|
self.gate = ParkGate()
|
|
self.gate.since, self.gate.previous, self.gate.ready = gate.since, gate.previous, gate.ready
|
|
self.session = GameSession(ui_state, DriveStateOwner(ui_state.params, starpilot_storage_root() / 'drive-state'),
|
|
native_device=HARDWARE.get_device_type() in ('mici', 'tici', 'tizi'))
|
|
self.on_unsafe = on_unsafe
|
|
self.flight = Flight(time.monotonic_ns())
|
|
self.renderer = SpaceRenderer()
|
|
self.controller_after_ns = time.monotonic_ns()
|
|
self.controller_last_ns = 0
|
|
self._return_registered = False
|
|
self.last_tick = 0.
|
|
self.closed = True
|
|
self.touch_held = self.exit_pressed = False
|
|
|
|
def show_event(self) -> None:
|
|
super().show_event()
|
|
self.closed = False
|
|
_open_games.add(self)
|
|
witness = live_witness()
|
|
self.gate.update(witness)
|
|
# A fresh Park sample can start preparation even if opening the page took
|
|
# long enough to reset the UI dwell. GameSession earns its own full dwell
|
|
# before requesting Offroad and again before accepting game input.
|
|
if witness is None:
|
|
self.session.close('Entry evidence changed')
|
|
self.last_tick = time.monotonic()
|
|
self.touch_held = self.exit_pressed = False
|
|
gui_app.add_nav_stack_tick(self._tick)
|
|
|
|
def hide_event(self) -> None:
|
|
self.closed = True
|
|
gui_app.remove_nav_stack_tick(self._tick)
|
|
self.session.close()
|
|
self.flight.close()
|
|
self.gate.reset()
|
|
self.touch_held = self.exit_pressed = False
|
|
super().hide_event()
|
|
_open_games.discard(self)
|
|
|
|
def _safe(self) -> bool:
|
|
return bool(not self.closed and gui_app.get_active_widget() is self and self.session.playable())
|
|
|
|
def _tick(self) -> None:
|
|
global _ignition_return
|
|
if self.closed:
|
|
return
|
|
if gui_app.get_active_widget() is not self:
|
|
self._exit(unsafe=True)
|
|
return
|
|
fire = self.session.step()
|
|
if not self._return_registered and self.session.ignition_return is not None:
|
|
_ignition_return = self.session.ignition_return
|
|
self._return_registered = True
|
|
if self.session.stage == Stage.CLOSED:
|
|
cloudlog.info('SpacePilot ended: %s', self.session.reason)
|
|
self._exit(unsafe=True)
|
|
return
|
|
# Renew only from this active, successfully checked page. No global timeout
|
|
# override survives exit, a failed render, stopped ticks or UI restart.
|
|
if not ui_state.started:
|
|
device._reset_interactive_timeout()
|
|
now = time.monotonic()
|
|
if fire:
|
|
self.flight.act(now)
|
|
if self.session.stage == Stage.PLAYING:
|
|
self.flight.update(now - self.last_tick)
|
|
self.last_tick = now
|
|
if self.session.stage != Stage.PLAYING:
|
|
self.controller_after_ns = time.monotonic_ns()
|
|
|
|
def _exit(self, *, unsafe: bool) -> None:
|
|
if self.closed:
|
|
return
|
|
self.hide_event()
|
|
gui_app.mouse_events.clear()
|
|
if unsafe:
|
|
self.on_unsafe()
|
|
elif gui_app.get_active_widget() is self:
|
|
gui_app.pop_widget()
|
|
|
|
def capture_controller(self, key: str | None, timestamp_ns: int) -> None:
|
|
# The shared input owner validates identity, press/release, duplicate and age.
|
|
# Consume every action while this page owns the batch, including during exit.
|
|
if (key not in (INCREASE_SPEED, DECREASE_SPEED) or timestamp_ns <= self.controller_after_ns or
|
|
timestamp_ns - self.controller_last_ns < 120_000_000):
|
|
return
|
|
if not self._safe():
|
|
if self.session.stage == Stage.PLAYING:
|
|
self._exit(unsafe=True)
|
|
return
|
|
self.controller_last_ns = timestamp_ns
|
|
self.flight.act(time.monotonic())
|
|
|
|
def _in_exit(self, pos: MousePos) -> bool:
|
|
x, y, w, h = EXIT
|
|
rect = canvas_rect(self.rect)
|
|
return bool(rl.check_collision_point_rec(pos, rl.Rectangle(rect.x + x * rect.width / 1072,
|
|
rect.y + y * rect.height / 480, w * rect.width / 1072, h * rect.height / 480)))
|
|
|
|
def _handle_mouse_press(self, pos: MousePos) -> None:
|
|
if self.touch_held:
|
|
return
|
|
self.touch_held = True
|
|
self.exit_pressed = self._in_exit(pos)
|
|
if self.session.stage != Stage.PLAYING:
|
|
return
|
|
if not self._safe():
|
|
self._exit(unsafe=True)
|
|
return
|
|
if not self.exit_pressed:
|
|
self.flight.act(time.monotonic())
|
|
|
|
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
|
|
event = mouse_event
|
|
if event.left_released:
|
|
self.touch_held = False
|
|
if self.exit_pressed and self._in_exit(event.pos):
|
|
self._exit(unsafe=False)
|
|
self.exit_pressed = False
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
if self.closed or gui_app.get_active_widget() is not self:
|
|
self._exit(unsafe=True)
|
|
return
|
|
try:
|
|
if self.session.stage == Stage.PLAYING:
|
|
if not self._safe():
|
|
self._exit(unsafe=True)
|
|
return
|
|
self.renderer.render(self.flight, rect, wheel_ready=bool(self.session.wheel.armed), wheel_available=self.session.vehicle is not None)
|
|
else:
|
|
self.renderer.preparing(rect, stopping=self.session.stage == Stage.STOPPING)
|
|
except Exception:
|
|
cloudlog.exception('SpacePilot render failed')
|
|
self._exit(unsafe=True)
|
|
|
|
|
|
class VersionButton(BigButton):
|
|
"""Hidden gesture lives on the version number, using native touch cancellation."""
|
|
def __init__(self, on_unsafe: Callable[[], None]):
|
|
super().__init__('version', ui_state.params.get('Version') or 'N/A')
|
|
self.on_unsafe = on_unsafe
|
|
self.taps = FiveTaps()
|
|
self.gate = ParkGate()
|
|
self.press: MouseEvent | None = None
|
|
self.completed: float | None = None
|
|
|
|
def show_event(self) -> None:
|
|
self.taps.reset()
|
|
self.gate.reset()
|
|
self.press = None
|
|
self.completed = None
|
|
self.set_value(ui_state.params.get('Version') or 'N/A')
|
|
super().show_event()
|
|
|
|
def hide_event(self) -> None:
|
|
self.taps.reset()
|
|
self.gate.reset()
|
|
self.press = None
|
|
self.completed = None
|
|
super().hide_event()
|
|
|
|
def _update_state(self) -> None:
|
|
super()._update_state()
|
|
witness = live_witness()
|
|
self.gate.update(witness)
|
|
if not self.enabled or witness is None:
|
|
self.taps.reset()
|
|
|
|
def _process_mouse_events(self) -> None:
|
|
if not self._touch_valid():
|
|
self.taps.reset()
|
|
self.press = None
|
|
self.completed = None
|
|
for event in gui_app.mouse_events:
|
|
if event.slot == 0 and event.left_pressed and not rl.check_collision_point_rec(event.pos, self._sub_label.rect):
|
|
self.taps.reset()
|
|
super()._process_mouse_events()
|
|
|
|
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
|
|
event = mouse_event
|
|
if event.left_pressed:
|
|
self.press = event if rl.check_collision_point_rec(event.pos, self._sub_label.rect) else None
|
|
self.completed = None
|
|
elif self.press is not None:
|
|
if (not 0 <= event.t - self.press.t <= .45 or
|
|
math.hypot(event.pos.x - self.press.pos.x, event.pos.y - self.press.pos.y) > 12 or not self._touch_valid()):
|
|
self.press = None
|
|
self.taps.reset()
|
|
elif event.left_released:
|
|
self.completed = event.t if rl.check_collision_point_rec(event.pos, self._sub_label.rect) else None
|
|
self.press = None
|
|
|
|
def _handle_mouse_release(self, pos: MousePos) -> None:
|
|
witness = live_witness()
|
|
self.gate.update(witness)
|
|
completed, self.completed = self.completed, None
|
|
if completed is None or witness is None:
|
|
self.taps.reset()
|
|
return
|
|
if self.taps.tap(completed):
|
|
gui_app.mouse_events.clear()
|
|
gui_app.push_widget(SpacePilotPage(self.gate, self.on_unsafe))
|