Files
StarPilot/openpilot/starpilot/conditional_mode/card_settings.py
T
firestar5683 d8708b65c7 CEM
Implement conditional driving modes with explicit scene, button and traffic state ownership.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

134 lines
6.4 KiB
Python

"""Worker-verified settings with one read-only, frame-local Card view.
The live settings worker reads proactively on its existing ~100 ms cadence.
Only conditional/Switchback proposals depend on this view: stale, busy or failed
reads deny those proposals without changing ordinary control or AOL authority.
Replay continues to use the synchronous source-clock owner.
"""
from dataclasses import dataclass
from typing import Any
from openpilot.starpilot.conditional_mode.preferences import CCMOptions, CEMOptions, ModeSelection, SavedPreferences
from openpilot.starpilot.conditional_mode.runtime_settings import (
REVERIFY_NS, ConditionalSettingsOwner, DocumentState, SafeModeState, SettingsSnapshot,
)
@dataclass(frozen=True)
class CardSettingsObservation:
snapshot: SettingsSnapshot | None
status: str
read_started_ns: int
class CardSettingsView(ConditionalSettingsOwner):
"""Main-thread-only view. refresh/affirm never touch the filesystem."""
def __init__(self, owner_token: str):
super().__init__(None)
self.owner_token = owner_token
self.unavailable = self._unavailable(0)
self._selection_cache = None
self._fingerprint_cache = None
def adopt(self, observation: CardSettingsObservation) -> None:
# Latch the whole immutable source once per frame. The worker never writes
# this view; all consumers within that frame see the same revision.
self._selection_cache = None
self._fingerprint_cache = None
snapshot = observation.snapshot
self.current = (snapshot if observation.status == 'verified' and
type(snapshot) is SettingsSnapshot and snapshot.owner_token == self.owner_token
else self.unavailable)
def refresh(self, now_mono_ns: int, *, force: bool = False, wait: bool = False) -> SettingsSnapshot:
assert self.current is not None
return self.current
def affirm(self, snapshot: SettingsSnapshot | None, *, now_mono_ns: int) -> bool:
current = self.current
return bool(current is not None and type(now_mono_ns) is int and
0 <= now_mono_ns - current.verified_mono_ns < REVERIFY_NS and
super().affirm(snapshot, now_mono_ns=now_mono_ns))
@staticmethod
def _preference_fields(snapshot: SettingsSnapshot):
try:
preferences = snapshot.preferences
if (type(preferences) is not SavedPreferences or type(preferences.cem) is not CEMOptions or
type(preferences.ccm) is not CCMOptions):
return None
cem, ccm = preferences.cem, preferences.ccm
# Each successful derivation validated exact immutable scalars. Identity
# checks also notice deliberate object.__setattr__ fixture mutations.
return (preferences, cem, ccm, preferences.version, preferences.mode,
cem.speed_mps, cem.speed_with_lead_mps, cem.signal_speed_mps,
cem.open_road, cem.curves, cem.curves_with_lead, cem.lead, cem.slower_lead,
cem.stopped_lead, cem.stop_lights, cem.model_stop_s, cem.signal_lane_detection,
cem.signal_lane_width_m, cem.persist_manual, ccm.speed_mps, ccm.speed_with_lead_mps,
ccm.set_speed_margin_mps, ccm.lead, ccm.launch_assist, ccm.persist_manual)
except AttributeError:
return None
def _selection_for_drive(self, snapshot: SettingsSnapshot, drive_id: int) -> ModeSelection | None:
# verdict still checks the current owner, revision, SafeMode and clock on
# every call. Only its pure post-affirmation derivation may be reused.
if (type(self) is not CardSettingsView or type(snapshot) is not SettingsSnapshot or
snapshot is not self.current or type(drive_id) is not int or drive_id <= 0):
return super()._selection_for_drive(snapshot, drive_id)
key = self._preference_fields(snapshot)
if key is None:
return super()._selection_for_drive(snapshot, drive_id)
cached = self._selection_cache
if cached is not None and cached[0] == drive_id and all(a is b for a, b in zip(key, cached[1], strict=True)):
return cached[2]
selection = super()._selection_for_drive(snapshot, drive_id)
self._selection_cache = (drive_id, key, selection)
return selection
def fingerprint(self, snapshot: SettingsSnapshot | None) -> str | None:
from openpilot.starpilot.conditional_mode.status import _settings_fingerprint
if (type(self) is not CardSettingsView or type(snapshot) is not SettingsSnapshot or
snapshot is not self.current or type(snapshot.document_state) is not DocumentState or
type(snapshot.safe_mode_state) is not SafeModeState or
snapshot.document_raw is not None and type(snapshot.document_raw) is not bytes or
snapshot.safe_mode_raw is not None and type(snapshot.safe_mode_raw) is not bytes):
return _settings_fingerprint(snapshot)
key = (snapshot.document_state, snapshot.safe_mode_state, snapshot.document_raw, snapshot.safe_mode_raw)
cached = self._fingerprint_cache
if cached is not None and all(a is b for a, b in zip(key, cached[0], strict=True)):
return cached[1]
fingerprint = _settings_fingerprint(snapshot)
self._fingerprint_cache = (key, fingerprint)
return fingerprint
class CardSettingsWorker:
"""One worker-owned reader and an atomically replaced immutable publication."""
def __init__(self, params: Any):
self.reader = ConditionalSettingsOwner(params)
self.view = CardSettingsView(self.reader.owner_token)
self.published = CardSettingsObservation(None, 'startup', 0)
def refresh(self, read_started_ns: int) -> None:
# The caller samples MONOTONIC before I/O. Slow reads never receive a fresh
# completion timestamp. A stalled worker cannot renew the view's authority.
previous = self.reader.current
try:
snapshot = self.reader.refresh(read_started_ns, force=True)
# With force=True only a busy writer lock returns the previous object.
# Do not carry the old owner's one-second busy-lock permission extension.
observation = CardSettingsObservation(None, 'busy', read_started_ns) if snapshot is previous else \
CardSettingsObservation(snapshot, 'verified', read_started_ns)
except Exception:
# Observational settings failure must neither kill the worker nor escape
# into Card. Missing publication remains restrictive until a good read.
observation = CardSettingsObservation(None, 'error', read_started_ns)
self.published = observation
def adopt(self) -> None:
self.view.adopt(self.published)