Files
StarPilot/openpilot/starpilot/conditional_mode/ui_action.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

196 lines
9.2 KiB
Python

"""Explicit UI manual-cycle requests; the planner remains the intent owner."""
from dataclasses import dataclass
import hashlib
import math
import re
import secrets
from openpilot.cereal import messaging
from openpilot.starpilot.conditional_mode.effective_status import EffectiveModeAck, observation as effective_observation
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.conditional_mode.preferences import MAX_DOCUMENT_BYTES
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner, SettingsSnapshot
from openpilot.starpilot.conditional_mode.status import CHOICES, settings_fingerprint
from openpilot.starpilot.saved_source import read_saved
LIFETIME_NS = 100_000_000
SOURCE_MAX_AGE_NS = 250_000_000
_SESSION = re.compile(r'[0-9a-f]{32}\Z')
_FINGERPRINT = re.compile(r'[0-9a-f]{64}\Z')
_CHOICES = {wire: choice for choice, wire in CHOICES.items()}
def fresh_service(sm, name: str, drive_id: int, now_ns: int, max_age_ns: int = SOURCE_MAX_AGE_NS) -> bool:
try:
producer, receipt = sm.logMonoTime[name], sm.recv_time[name]
if type(producer) is not int or type(receipt) not in (int, float) or not math.isfinite(receipt):
return False
receipt_ns = int(receipt * 1e9)
return bool(sm.seen[name] and sm.alive[name] and sm.valid[name] and
drive_id < producer <= now_ns and drive_id < receipt_ns <= now_ns and
now_ns - producer <= max_age_ns and now_ns - receipt_ns <= max_age_ns)
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
return False
def current_authority(sm, cp, drive_id: int, now_ns: int) -> bool:
try:
return bool(type(now_ns) is int and type(drive_id) is int and 0 < drive_id < now_ns and
cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and not cp.notCar and
all(fresh_service(sm, name, drive_id, now_ns) for name in ('carState', 'carControl', 'selfdriveState')) and
fresh_service(sm, 'deviceState', drive_id, now_ns, 1_000_000_000) and
sm['deviceState'].started and sm['deviceState'].startedMonoTime == drive_id and
sm['carState'].canValid and not sm['carState'].canTimeout and
sm['carControl'].enabled and sm['carControl'].longActive and sm['selfdriveState'].enabled)
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
return False
def exact_settings(params, snapshot: SettingsSnapshot) -> bool:
try:
return (read_saved(params, 'ConditionalModeConfig', MAX_DOCUMENT_BYTES) == (snapshot.document_raw, True) and
read_saved(params, 'SafeMode', 8) == (snapshot.safe_mode_raw, True))
except (AttributeError, OSError, TypeError, ValueError):
return False
@dataclass(frozen=True)
class UiActionContext:
drive_id: int
fingerprint: str
choice: ModeChoice
planner_session: str
selfdrive_session: str
manual_code: int
effective_experimental: bool
@property
def token(self) -> str:
fields = (self.drive_id, self.fingerprint, self.choice.value, self.planner_session,
self.selfdrive_session, self.manual_code, self.effective_experimental)
return hashlib.sha256(repr(fields).encode()).hexdigest()
@dataclass(frozen=True)
class UiAction:
session: str
sequence: int
observed_ns: int
expires_ns: int
drive_id: int
fingerprint: str
choice: ModeChoice
planner_session: str
car_state_ns: int
selfdrive_state_ns: int
manual_code: int
effective_experimental: bool
def observation(event, now_ns: int) -> UiAction | None:
try:
if not event.valid or event.which() != 'slcAction' or str(event.slcAction.kind) != 'conditionalModeCycle':
return None
if event.slcAction.sessionId or event.slcAction.sequenceId or event.slcAction.decisionId or event.slcAction.presentationId:
return None
value = event.slcAction.conditionalManual
choice = _CHOICES[str(value.choice)]
observed, expiry, drive = value.observedMonoTime, value.validUntilMonoTime, value.driveStartMonoTime
car_state, selfdrive_state = value.sourceCarStateMonoTime, value.sourceSelfdriveStateMonoTime
session, planner, fingerprint = str(value.sessionId), str(value.plannerSessionId), str(value.settingsFingerprint)
if (type(now_ns) is not int or value.version != 1 or value.sequence <= 0 or
not _SESSION.fullmatch(session) or not _SESSION.fullmatch(planner) or not _FINGERPRINT.fullmatch(fingerprint) or
choice not in (ModeChoice.CEM, ModeChoice.CCM) or value.expectedManualCode not in (0, 1, 2) or
event.logMonoTime != observed or not 0 < drive < observed <= now_ns <= expiry or expiry - observed != LIFETIME_NS or
not drive < car_state <= observed or not drive < selfdrive_state <= observed or
observed - min(car_state, selfdrive_state) > SOURCE_MAX_AGE_NS):
return None
return UiAction(session, value.sequence, observed, expiry, drive, fingerprint, choice, planner,
car_state, selfdrive_state, value.expectedManualCode, value.expectedExperimental)
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
return None
class ConditionalUiActionOwner:
def __init__(self, params):
self.params = params
self.settings = ConditionalSettingsOwner(params)
self.session = secrets.token_hex(16)
self.sequence = 0
self.last_token: str | None = None
self.last_dispatch_ns = 0
self.ack_drive = 0
self.last_ack: EffectiveModeAck | None = None
self.retired_ack_sessions: tuple[str, ...] = ()
self.last_now_ns = 0
def configured_choice(self, *, now_ns: int) -> ModeChoice | None:
snapshot = self.settings.refresh(now_ns)
return snapshot.preferences.mode if self.settings.affirm(snapshot, now_mono_ns=now_ns) and snapshot.preferences is not None else None
def context(self, sm, cp, *, now_ns: int) -> UiActionContext | None:
if type(now_ns) is not int or now_ns < self.last_now_ns:
return None
self.last_now_ns = now_ns
snapshot = self.settings.refresh(now_ns)
fingerprint = settings_fingerprint(snapshot)
if not self.settings.affirm(snapshot, now_mono_ns=now_ns) or snapshot.preferences is None or fingerprint is None:
return None
try:
drive_id = int(sm['deviceState'].startedMonoTime)
if not current_authority(sm, cp, drive_id, now_ns) or not fresh_service(sm, 'starpilotSelfdriveState', drive_id, now_ns):
return None
ack = effective_observation(sm['starpilotSelfdriveState'], now_ns)
if (ack is None or not ack.accepted or ack.drive_id != drive_id or ack.fingerprint != fingerprint or
ack.choice is not snapshot.preferences.mode or ack.planner_session is None or
ack.observed_ns != sm.logMonoTime['starpilotSelfdriveState'] or
not ack.selfdrive_state_ns <= sm.logMonoTime['selfdriveState'] <= now_ns or
ack.effective_experimental is not sm['selfdriveState'].experimentalMode):
return None
if drive_id != self.ack_drive:
self.ack_drive, self.last_ack, self.retired_ack_sessions = drive_id, None, ()
previous = self.last_ack
if ack.session in self.retired_ack_sessions:
return None
if previous is not None:
if ack.session != previous.session:
self.retired_ack_sessions = (*self.retired_ack_sessions[-7:], previous.session)
self.last_ack = ack
return None
if (ack.sequence < previous.sequence or ack.sequence == previous.sequence and ack != previous or
ack.sequence > previous.sequence and ack.observed_ns <= previous.observed_ns):
return None
self.last_ack = ack
return UiActionContext(drive_id, fingerprint, ack.choice, ack.planner_session, ack.session,
ack.status_code if ack.status_code in (1, 2) else 0, ack.effective_experimental)
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
return None
def dispatch(self, expected_context: UiActionContext, sm, cp, publisher, *, now_ns: int) -> bool:
current = self.context(sm, cp, now_ns=now_ns)
snapshot = self.settings.current
if (current is None or current != expected_context or snapshot is None or not exact_settings(self.params, snapshot) or
now_ns <= self.last_dispatch_ns or
current.token == self.last_token and now_ns <= self.last_dispatch_ns + LIFETIME_NS):
return False
self.sequence += 1
event = messaging.new_message('slcAction', valid=True)
event.logMonoTime = now_ns
event.slcAction.kind = 'conditionalModeCycle'
event.slcAction.conditionalManual = {
'version': 1, 'sessionId': self.session, 'sequence': self.sequence,
'observedMonoTime': now_ns, 'validUntilMonoTime': now_ns + LIFETIME_NS,
'driveStartMonoTime': current.drive_id, 'settingsFingerprint': current.fingerprint,
'choice': CHOICES[current.choice], 'plannerSessionId': current.planner_session,
'sourceCarStateMonoTime': sm.logMonoTime['carState'], 'sourceSelfdriveStateMonoTime': sm.logMonoTime['selfdriveState'],
'expectedManualCode': current.manual_code, 'expectedExperimental': current.effective_experimental,
}
try:
publisher.send('slcAction', event)
except (OSError, RuntimeError):
return False
self.last_token, self.last_dispatch_ns = current.token, now_ns
return True