mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
d8708b65c7
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>
196 lines
9.2 KiB
Python
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
|