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.
170 lines
7.4 KiB
Python
170 lines
7.4 KiB
Python
"""Parked game lifecycle: observe, request supported Offroad, then verify shutdown.
|
|
|
|
The game never starts or stops a managed process itself. A successful onroad
|
|
entry leaves the existing DriveStateOwner in Offroad even after game exit; only
|
|
a later explicit drive-state selection or verified physical key-off releases it.
|
|
"""
|
|
|
|
from enum import StrEnum
|
|
import time
|
|
|
|
from openpilot.starpilot.parked_vehicle import ParkedVehicleReader
|
|
from openpilot.starpilot.ui.spacepilot.guard import ParkGate, read_offroad_witness, read_witness, sample_clocks
|
|
from openpilot.starpilot.ui.spacepilot.state import WheelInput
|
|
from openpilot.starpilot.ui.spacepilot.ignition import IgnitionReturn
|
|
|
|
TRANSITION_NS = 8_000_000_000
|
|
|
|
|
|
class Stage(StrEnum):
|
|
VERIFYING = 'verifying'
|
|
STOPPING = 'stopping'
|
|
PLAYING = 'playing'
|
|
CLOSED = 'closed'
|
|
|
|
|
|
class GameSession:
|
|
def __init__(self, ui, drive_owner, *, native_device: bool, reader_factory=ParkedVehicleReader):
|
|
self.ui, self.drive_owner, self.native_device = ui, drive_owner, native_device
|
|
self.stage = Stage.VERIFYING
|
|
self.started_ns = time.monotonic_ns()
|
|
self.requested_offroad = False
|
|
self.ignition_return: IgnitionReturn | None = None
|
|
self.vehicle = None
|
|
self.wheel = WheelInput()
|
|
self.gate = ParkGate()
|
|
self.reason = ''
|
|
self._reader_factory = reader_factory
|
|
self._was_ignition = None
|
|
self._request_revision = None
|
|
self._clock_offset = None
|
|
|
|
def _clocks(self):
|
|
mono, boot = sample_clocks()
|
|
offset = boot - mono
|
|
if self._clock_offset is not None and abs(offset - self._clock_offset) > 5_000_000:
|
|
raise ValueError('Device resumed; re-enter SpacePilot')
|
|
self._clock_offset = offset
|
|
return mono, boot
|
|
|
|
def _ignition(self):
|
|
return any(p.ignitionLine or p.ignitionCan for p in self.ui.sm['pandaStates'])
|
|
|
|
def _onroad_witness(self):
|
|
mono, boot = self._clocks()
|
|
return read_witness(self.ui, mono, boot, c4=self.native_device)
|
|
|
|
def step(self) -> bool:
|
|
"""Return one debounced boost only after actual process/noOutput admission."""
|
|
if self.stage == Stage.CLOSED:
|
|
return False
|
|
try:
|
|
mono, boot = self._clocks()
|
|
ignition = self._ignition()
|
|
if self._was_ignition is not None and ignition != self._was_ignition:
|
|
# Never reuse a parser or old admission across a physical ignition cycle.
|
|
raise ValueError('Vehicle ignition changed')
|
|
self._was_ignition = ignition
|
|
if self.stage != Stage.PLAYING and mono - self.started_ns > TRANSITION_NS:
|
|
raise ValueError('Driving services did not finish entering parked game mode')
|
|
|
|
if self.vehicle is None and ignition:
|
|
sm = self.ui.sm
|
|
if (not sm.valid['carParams'] or sm.recv_frame['carParams'] <= self.ui.started_frame or
|
|
not 0 < int(sm.logMonoTime['carParams']) <= mono):
|
|
raise ValueError('Current vehicle identity is unavailable')
|
|
self.vehicle = self._reader_factory(sm['carParams'])
|
|
# Cold imports/parser construction can outlast this frame's UI sample.
|
|
# Let the normal collector refresh next frame before any admission,
|
|
# request or input; all original dwell and freshness checks still run.
|
|
return False
|
|
samples = self.vehicle.poll() if self.vehicle is not None else ()
|
|
mono, boot = self._clocks()
|
|
vehicle_ready = bool(self.vehicle is not None and self.vehicle.ready(mono, boot))
|
|
|
|
if self.stage in (Stage.STOPPING, Stage.PLAYING) and ignition and not vehicle_ready:
|
|
raise ValueError('Physical Park or fresh CAN was lost')
|
|
|
|
if self.stage == Stage.VERIFYING and self.ui.started:
|
|
witness = self._onroad_witness()
|
|
if witness is None:
|
|
raise ValueError('Park and disengage before entering SpacePilot')
|
|
if not self.gate.update(witness) or not vehicle_ready:
|
|
return False
|
|
state = self.drive_owner.snapshot()
|
|
if not state.available:
|
|
raise ValueError('Drive-state owner is unavailable')
|
|
def parked_now():
|
|
now, boot_now = self._clocks()
|
|
return bool(self.stage == Stage.VERIFYING and self._onroad_witness() is not None and
|
|
self.vehicle is not None and self.vehicle.ready(now, boot_now))
|
|
result = self.drive_owner.request('offroad', expected_revision=state.revision,
|
|
authorized=parked_now, override_allowed=parked_now)
|
|
if str(result.mode) != 'offroad' or not result.available:
|
|
raise ValueError('Offroad request was not accepted')
|
|
self._request_revision = result.revision
|
|
self.requested_offroad = True
|
|
self.ignition_return = IgnitionReturn(self.ui, self.drive_owner, result.revision, native_device=self.native_device)
|
|
self.stage = Stage.STOPPING
|
|
self.started_ns = mono
|
|
self.gate.reset()
|
|
self.wheel = WheelInput()
|
|
return False
|
|
|
|
if self.requested_offroad:
|
|
state = self.drive_owner.snapshot()
|
|
if not state.available or str(state.mode) != 'offroad' or state.revision != self._request_revision:
|
|
raise ValueError('Drive mode changed; SpacePilot has ended')
|
|
if self.stage == Stage.STOPPING and self.ui.started:
|
|
# No game/input until all normal services have actually stopped. Keep
|
|
# the original fresh physical admission while they are still onroad.
|
|
if self._onroad_witness() is None or not vehicle_ready:
|
|
raise ValueError('Physical parked evidence changed during shutdown')
|
|
return False
|
|
|
|
witness = read_offroad_witness(self.ui, mono, boot, native_device=self.native_device, vehicle=self.vehicle)
|
|
ready = self.gate.update(witness)
|
|
if self.stage == Stage.PLAYING and not ready:
|
|
raise ValueError('Parked game evidence is no longer trustworthy')
|
|
if not ready:
|
|
return False
|
|
if self.stage != Stage.PLAYING:
|
|
self.stage = Stage.PLAYING
|
|
# Pre-admission presses/releases cannot arm a wheel action.
|
|
self.wheel = WheelInput(self.vehicle.sample.source_boot_ns if self.vehicle is not None else 0)
|
|
return False
|
|
fire = False
|
|
for sample in samples:
|
|
fire = self.wheel.update(sample.source_boot_ns, sample.buttons, mono / 1e9) or fire
|
|
return fire
|
|
except (AttributeError, KeyError, OSError, RuntimeError, TypeError, ValueError, ImportError) as exc:
|
|
self.close(str(exc))
|
|
return False
|
|
|
|
def playable(self) -> bool:
|
|
"""Touch/controller/render must revalidate, never rely on last frame alone."""
|
|
if self.stage != Stage.PLAYING:
|
|
return False
|
|
try:
|
|
mono, boot = self._clocks()
|
|
if self._ignition() != self._was_ignition:
|
|
return False
|
|
if self.requested_offroad:
|
|
state = self.drive_owner.snapshot()
|
|
if not state.available or str(state.mode) != 'offroad' or state.revision != self._request_revision:
|
|
return False
|
|
return self.gate.update(read_offroad_witness(self.ui, mono, boot, native_device=self.native_device, vehicle=self.vehicle))
|
|
except (AttributeError, KeyError, OSError, RuntimeError, TypeError, ValueError):
|
|
return False
|
|
|
|
def close(self, reason: str = '') -> None:
|
|
self.reason = reason
|
|
self.stage = Stage.CLOSED
|
|
if self.vehicle is not None:
|
|
self.vehicle.close()
|
|
self.vehicle = None
|
|
self.gate.reset()
|
|
self.wheel = WheelInput()
|
|
# Deliberately no request(Auto/Onroad): closing/crashing must not restart
|
|
# driving services or restore engagement processing behind a game button.
|