mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +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.
124 lines
5.6 KiB
Python
124 lines
5.6 KiB
Python
"""Release only this UI session's game request after a continuous physical key-off."""
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
from openpilot.starpilot.parked_evidence import PANDA_TTL_NS, RESUME_SKEW_NS
|
|
from openpilot.starpilot.ui.spacepilot.guard import DWELL_NS, FAST_AGE_NS, SLOW_AGE_NS, read_offroad_witness, sample_clocks
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IgnitionSample:
|
|
mono: int
|
|
boot: int
|
|
stamps: tuple[int, int]
|
|
topology: tuple[tuple[str, str], ...]
|
|
uptimes: tuple[int, ...]
|
|
pandad_pid: int
|
|
ignition: bool
|
|
|
|
|
|
def sample_ignition(ui, *, native_device: bool) -> IgnitionSample | None:
|
|
try:
|
|
mono, boot = sample_clocks()
|
|
if (not native_device or ui.projection_read_only or
|
|
ui.replay_clock is not None or getattr(ui.sm, 'replay_sample', None) is not None):
|
|
return None
|
|
sm = ui.sm
|
|
stamps = []
|
|
for name, now, ttl in (('pandaStates', boot, PANDA_TTL_NS), ('managerState', mono, SLOW_AGE_NS)):
|
|
stamp, received = int(sm.logMonoTime[name]), float(sm.recv_time[name])
|
|
if (not sm.valid[name] or not sm.alive[name] or sm.recv_frame[name] <= 0 or stamp <= 0 or
|
|
not 0 <= now - stamp <= ttl or not math.isfinite(received) or
|
|
not 0 <= mono - int(received * 1e9) <= ttl):
|
|
return None
|
|
stamps.append(stamp)
|
|
processes = tuple(sm['managerState'].processes)
|
|
names = [p.name for p in processes]
|
|
pandad = [p for p in processes if p.name == 'pandad']
|
|
if len(names) != len(set(names)) or len(pandad) != 1 or not pandad[0].running or pandad[0].pid <= 0:
|
|
return None
|
|
pandas = tuple(sm['pandaStates'])
|
|
if not pandas:
|
|
return None
|
|
for p in pandas:
|
|
if (str(p.pandaType) == 'unknown' or str(p.harnessStatus) == 'notConnected' or p.uptime < 0 or
|
|
p.controlsAllowed or p.heartbeatLost or p.safetyRxChecksInvalid or
|
|
str(p.faultStatus) != 'none' or len(p.faults)):
|
|
return None
|
|
return IgnitionSample(mono, boot, (stamps[0], stamps[1]),
|
|
tuple((str(p.pandaType), str(p.harnessStatus)) for p in pandas),
|
|
tuple(int(p.uptime) for p in pandas), int(pandad[0].pid),
|
|
any(p.ignitionLine or p.ignitionCan for p in pandas))
|
|
except (AttributeError, KeyError, OSError, RuntimeError, TypeError, ValueError, OverflowError):
|
|
return None
|
|
|
|
|
|
class IgnitionReturn:
|
|
"""No persisted ownership: restarting UI cannot adopt someone else's Offroad.
|
|
|
|
A loss of continuity permanently disarms this return, even if data reconnects.
|
|
Ordinary game exit keeps observing, but holds no reader, widget or wake token.
|
|
"""
|
|
def __init__(self, ui, owner, revision: str, *, native_device: bool):
|
|
self.ui, self.owner, self.revision, self.native_device = ui, owner, revision, native_device
|
|
self.previous = sample_ignition(ui, native_device=native_device)
|
|
self.finished = self.previous is None or not self.previous.ignition
|
|
self.off_since: int | None = None
|
|
self.off_stamp: int | None = None
|
|
|
|
def _owned(self) -> bool:
|
|
state = self.owner.snapshot()
|
|
return bool(state.available and str(state.mode) == 'offroad' and state.revision == self.revision)
|
|
|
|
def _continuous(self, current: IgnitionSample | None) -> bool:
|
|
old = self.previous
|
|
return bool(current is not None and old is not None and
|
|
0 <= current.mono - old.mono <= FAST_AGE_NS and
|
|
abs((current.boot - current.mono) - (old.boot - old.mono)) <= RESUME_SKEW_NS and
|
|
current.topology == old.topology and current.pandad_pid == old.pandad_pid and
|
|
all(new >= previous for new, previous in zip(current.stamps, old.stamps, strict=True)) and
|
|
all(previous <= new <= previous + 1 for new, previous in zip(current.uptimes, old.uptimes, strict=True)))
|
|
|
|
def observe(self) -> bool:
|
|
"""Return true only after fresh, advancing all-off reports and quiet noOutput."""
|
|
if self.finished:
|
|
return False
|
|
try:
|
|
current = sample_ignition(self.ui, native_device=self.native_device)
|
|
if not self._owned() or not self._continuous(current):
|
|
self.finished = True
|
|
return False
|
|
assert current is not None
|
|
if self.previous is not None and current.ignition != self.previous.ignition and current.stamps[0] <= self.previous.stamps[0]:
|
|
self.finished = True
|
|
return False
|
|
self.previous = current
|
|
if current.ignition:
|
|
self.off_since = self.off_stamp = None
|
|
return False
|
|
# Require actual service shutdown too, without using old Park/CAN data
|
|
# after ignition off. An unknown/faulted offroad witness cannot release.
|
|
if read_offroad_witness(self.ui, current.mono, current.boot, native_device=self.native_device) is None:
|
|
self.finished = True
|
|
return False
|
|
if self.off_since is None:
|
|
self.off_since, self.off_stamp = current.mono, current.stamps[0]
|
|
return self.off_stamp is not None and current.mono - self.off_since >= DWELL_NS and current.stamps[0] > self.off_stamp
|
|
except (AttributeError, KeyError, OSError, RuntimeError, TypeError, ValueError):
|
|
self.finished = True
|
|
return False
|
|
|
|
def release(self, game_closed) -> None:
|
|
"""Normal owner transaction rechecks physical evidence and closed game."""
|
|
if self.finished:
|
|
return
|
|
def authorized():
|
|
return bool(game_closed() and self.observe())
|
|
try:
|
|
if authorized():
|
|
self.owner.request('auto', expected_revision=self.revision, authorized=authorized, override_allowed=lambda: False)
|
|
finally:
|
|
# One bounded transaction; a busy/rejected owner is never retried blindly.
|
|
self.finished = True
|