mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
1ab5058bc8
Implement conditional driving modes with explicit scene, button and traffic state ownership.
177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
"""Read-only, bounded-refresh owner for saved conditional-mode preferences.
|
|
|
|
The caller supplies monotonic observation time and invokes refresh off the
|
|
control-frame cadence. A current owner revision must be affirmed each sample;
|
|
neither this owner nor the codec writes Params or adopts legacy keys.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
import secrets
|
|
from typing import Any
|
|
|
|
from openpilot.starpilot.conditional_mode.preferences import (
|
|
MAX_DOCUMENT_BYTES, CCMOptions, CEMOptions, ModeSelection, PreferenceError,
|
|
SavedPreferences, decode_preferences, saved_selection_for_drive,
|
|
)
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
|
|
|
|
DOCUMENT_KEY = 'ConditionalModeConfig'
|
|
SAFE_MODE_KEY = 'SafeMode'
|
|
REFRESH_NS = 1_000_000_000
|
|
REVERIFY_NS = REFRESH_NS // 2
|
|
|
|
|
|
class DocumentState(StrEnum):
|
|
ABSENT = 'absent'
|
|
VALID = 'valid'
|
|
INVALID = 'invalid'
|
|
READ_ERROR = 'read_error'
|
|
|
|
|
|
class SafeModeState(StrEnum):
|
|
ABSENT_FALSE = 'absent_false'
|
|
FALSE = 'false'
|
|
TRUE = 'true'
|
|
INVALID = 'invalid'
|
|
READ_ERROR = 'read_error'
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SettingsSnapshot:
|
|
owner_token: str
|
|
revision: int
|
|
observed_mono_ns: int
|
|
verified_mono_ns: int
|
|
document_state: DocumentState
|
|
safe_mode_state: SafeModeState
|
|
document_raw: bytes | None
|
|
safe_mode_raw: bytes | None
|
|
preferences: SavedPreferences | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SettingsVerdict:
|
|
status: str
|
|
revision: int | None
|
|
observed_mono_ns: int | None
|
|
selection: ModeSelection | None
|
|
cem: CEMOptions | None
|
|
ccm: CCMOptions | None
|
|
safe_mode: bool | None
|
|
|
|
|
|
def _read(params: Any, key: str, limit: int) -> tuple[bytes | None, bool]:
|
|
try:
|
|
return read_saved(params, key, limit)
|
|
except (OSError, TypeError, ValueError):
|
|
return b'', False
|
|
|
|
|
|
def _document(raw: bytes | None, readable: bool) -> tuple[DocumentState, SavedPreferences | None]:
|
|
if not readable:
|
|
return DocumentState.READ_ERROR, None
|
|
if raw is None:
|
|
return DocumentState.ABSENT, SavedPreferences()
|
|
try:
|
|
return DocumentState.VALID, decode_preferences(raw)
|
|
except PreferenceError:
|
|
return DocumentState.INVALID, None
|
|
|
|
|
|
def _safe_mode(raw: bytes | None, readable: bool) -> SafeModeState:
|
|
if not readable:
|
|
return SafeModeState.READ_ERROR
|
|
if raw is None:
|
|
return SafeModeState.ABSENT_FALSE
|
|
if raw == b'0':
|
|
return SafeModeState.FALSE
|
|
if raw == b'1':
|
|
return SafeModeState.TRUE
|
|
return SafeModeState.INVALID
|
|
|
|
|
|
class ConditionalSettingsOwner:
|
|
"""One in-memory revision, reread before its one-second authority expires."""
|
|
|
|
def __init__(self, params: Any):
|
|
self.params = params
|
|
self.owner_token = secrets.token_hex(16)
|
|
self.current: SettingsSnapshot | None = None
|
|
self.last_refresh_ns: int | None = None
|
|
|
|
def _unavailable(self, now_mono_ns: int) -> SettingsSnapshot:
|
|
previous = self.current
|
|
snapshot = SettingsSnapshot(
|
|
self.owner_token, 1 if previous is None else previous.revision + 1,
|
|
now_mono_ns, now_mono_ns, DocumentState.READ_ERROR, SafeModeState.READ_ERROR,
|
|
None, None, None,
|
|
)
|
|
self.current = snapshot
|
|
self.last_refresh_ns = now_mono_ns
|
|
return snapshot
|
|
|
|
def refresh(self, now_mono_ns: int) -> SettingsSnapshot:
|
|
"""Perform a bounded disk read when due; errors invalidate prior authority."""
|
|
if type(now_mono_ns) is not int or now_mono_ns <= 0:
|
|
return self._unavailable(0)
|
|
if self.last_refresh_ns is not None and now_mono_ns < self.last_refresh_ns:
|
|
return self._unavailable(now_mono_ns)
|
|
if self.current is not None and self.last_refresh_ns is not None and now_mono_ns - self.last_refresh_ns < REVERIFY_NS:
|
|
return self.current
|
|
|
|
document_raw, document_readable = _read(self.params, DOCUMENT_KEY, MAX_DOCUMENT_BYTES)
|
|
safe_raw, safe_readable = _read(self.params, SAFE_MODE_KEY, 8)
|
|
document_state, preferences = _document(document_raw, document_readable)
|
|
safe_state = _safe_mode(safe_raw, safe_readable)
|
|
previous = self.current
|
|
same_source = bool(previous is not None and previous.document_raw == document_raw and
|
|
previous.safe_mode_raw == safe_raw and previous.document_state is document_state and
|
|
previous.safe_mode_state is safe_state and previous.preferences == preferences)
|
|
snapshot = SettingsSnapshot(
|
|
self.owner_token,
|
|
previous.revision if same_source and previous is not None else (1 if previous is None else previous.revision + 1),
|
|
previous.observed_mono_ns if same_source and previous is not None else now_mono_ns,
|
|
now_mono_ns,
|
|
document_state, safe_state, document_raw, safe_raw, preferences,
|
|
)
|
|
self.current = snapshot
|
|
self.last_refresh_ns = now_mono_ns
|
|
return snapshot
|
|
|
|
def affirm(self, snapshot: SettingsSnapshot | None, *, now_mono_ns: int) -> bool:
|
|
"""Validate the current owner revision without another Params read."""
|
|
current = self.current
|
|
return bool(
|
|
type(snapshot) is SettingsSnapshot and current is not None and
|
|
snapshot.owner_token == self.owner_token and snapshot.revision == current.revision and
|
|
snapshot.observed_mono_ns == current.observed_mono_ns and
|
|
current.document_state in (DocumentState.ABSENT, DocumentState.VALID) and
|
|
current.safe_mode_state in (SafeModeState.ABSENT_FALSE, SafeModeState.FALSE) and
|
|
type(now_mono_ns) is int and current.verified_mono_ns <= now_mono_ns <= current.verified_mono_ns + REFRESH_NS
|
|
)
|
|
|
|
def verdict(self, snapshot: SettingsSnapshot | None, *, now_mono_ns: int, drive_id: int) -> SettingsVerdict:
|
|
"""Supply saved options only while this exact owner revision is live."""
|
|
current = self.current
|
|
safe = (False if current is not None and current.safe_mode_state in (SafeModeState.ABSENT_FALSE, SafeModeState.FALSE)
|
|
else True if current is not None and current.safe_mode_state is SafeModeState.TRUE else None)
|
|
if not self.affirm(snapshot, now_mono_ns=now_mono_ns):
|
|
status = ('read_error' if current is not None and (current.document_state is DocumentState.READ_ERROR or
|
|
current.safe_mode_state is SafeModeState.READ_ERROR) else
|
|
'unavailable_document' if current is not None and current.document_state is DocumentState.INVALID else
|
|
'invalid_safe_mode' if current is not None and current.safe_mode_state is SafeModeState.INVALID else
|
|
'safe_mode' if safe is True else
|
|
'stale_or_foreign_revision')
|
|
return SettingsVerdict(status, current.revision if current is not None else None,
|
|
current.observed_mono_ns if current is not None else None,
|
|
None, None, None, safe)
|
|
assert current is not None and current.preferences is not None
|
|
selection = saved_selection_for_drive(current.preferences, drive_id)
|
|
return SettingsVerdict('ready' if selection is not None else 'invalid_drive', current.revision,
|
|
current.observed_mono_ns, selection, current.preferences.cem,
|
|
current.preferences.ccm, safe)
|