mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +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>
170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
"""Versioned conditional-mode proposals carried by the sole planner publisher."""
|
|
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
import re
|
|
import secrets
|
|
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.conditional_mode.host import HostProposal
|
|
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
|
from openpilot.starpilot.conditional_mode.runtime_settings import (
|
|
DocumentState,
|
|
SafeModeState,
|
|
SettingsSnapshot,
|
|
)
|
|
|
|
|
|
# Proposals reach selfdrived 30-70 ms after observation; 100 ms expired most before their successor arrived.
|
|
LIFETIME_NS = 150_000_000
|
|
MODEL_MAX_AGE_NS = 150_000_000
|
|
CHOICES = {ModeChoice.STOCK: 'stock', ModeChoice.CEM: 'conditionalExperimental', ModeChoice.CCM: 'conditionalChill'}
|
|
_HEX = re.compile(r'[0-9a-f]{64}\Z')
|
|
|
|
|
|
def settings_fingerprint(snapshot: SettingsSnapshot | None, *, owner=None) -> str | None:
|
|
if owner is not None:
|
|
from openpilot.starpilot.conditional_mode.card_settings import CardSettingsView
|
|
|
|
if type(owner) is CardSettingsView:
|
|
return owner.fingerprint(snapshot)
|
|
return _settings_fingerprint(snapshot)
|
|
|
|
|
|
def _settings_fingerprint(snapshot: SettingsSnapshot | None) -> str | None:
|
|
"""Bind two independent readers to the exact saved document and SafeMode."""
|
|
if (
|
|
snapshot is None
|
|
or snapshot.document_state not in (DocumentState.ABSENT, DocumentState.VALID)
|
|
or snapshot.safe_mode_state not in (SafeModeState.ABSENT_FALSE, SafeModeState.FALSE)
|
|
):
|
|
return None
|
|
digest = hashlib.sha256(b'StarPilot conditional preferences v1\0')
|
|
for raw in (snapshot.document_raw, snapshot.safe_mode_raw):
|
|
# Absence differs from a present empty file and from an explicit default.
|
|
digest.update(b'absent\0' if raw is None else len(raw).to_bytes(4, 'big') + raw)
|
|
return digest.hexdigest()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModeObservation:
|
|
session: str
|
|
sequence: int
|
|
observed_ns: int
|
|
expires_ns: int
|
|
drive_id: int
|
|
model_ns: int
|
|
car_state_ns: int
|
|
settings_revision: int
|
|
fingerprint: str
|
|
choice: ModeChoice
|
|
override: bool | None
|
|
status: str
|
|
reason: str
|
|
status_code: int
|
|
|
|
|
|
class StatusPublisher:
|
|
def __init__(self):
|
|
self.session = secrets.token_hex(16)
|
|
self.sequence = 0
|
|
|
|
def attach(self, event, proposal: HostProposal, snapshot: SettingsSnapshot | None, *, now_ns: int, drive_id: int, model_ns: int, car_state_ns: int):
|
|
"""Attach a proposal without granting or changing native control authority."""
|
|
if event is None:
|
|
event = messaging.new_message('slcState')
|
|
event.valid = True
|
|
event.logMonoTime = now_ns
|
|
self.sequence += 1
|
|
fingerprint = settings_fingerprint(snapshot)
|
|
decision = proposal.decision
|
|
authority = proposal.projected.authority if proposal.projected is not None else None
|
|
permitted = (
|
|
fingerprint is not None
|
|
and proposal.status == 'proposed'
|
|
and decision is not None
|
|
and snapshot is not None
|
|
and proposal.settings_revision == snapshot.revision
|
|
and decision.qualified
|
|
and authority is not None
|
|
and authority.fresh
|
|
and authority.system_long_capable
|
|
and not authority.safe_mode
|
|
and authority.driving_enabled
|
|
and authority.long_active
|
|
and type(proposal.override_experimental) is bool
|
|
and proposal.override_experimental == decision.requested_experimental
|
|
and (proposal.choice in (ModeChoice.CEM, ModeChoice.CCM) or
|
|
proposal.choice is ModeChoice.STOCK and proposal.projected.scene.committed_stop is True and
|
|
decision.reason.value == 'cem_stop' and proposal.override_experimental is True)
|
|
)
|
|
event.slcState.conditionalMode = {
|
|
'version': 1,
|
|
'sessionId': self.session,
|
|
'sequence': self.sequence,
|
|
'observedMonoTime': now_ns,
|
|
'validUntilMonoTime': now_ns + LIFETIME_NS,
|
|
'driveStartMonoTime': drive_id,
|
|
'modelMonoTime': model_ns,
|
|
'carStateMonoTime': car_state_ns,
|
|
'settingsRevision': snapshot.revision if snapshot is not None else 0,
|
|
'settingsFingerprint': fingerprint or '',
|
|
'choice': CHOICES[proposal.choice],
|
|
'hasOverride': permitted,
|
|
'experimental': proposal.override_experimental if permitted else False,
|
|
'status': proposal.status,
|
|
'reason': decision.reason.value if decision is not None else '',
|
|
'statusCode': decision.status_code if decision is not None else 0,
|
|
}
|
|
return event
|
|
|
|
|
|
def observation(state, now_ns: int) -> ModeObservation | None:
|
|
"""Nested freshness is independent of the outer SLC availability flag."""
|
|
try:
|
|
value = state.conditionalMode
|
|
stamp, expiry = int(value.observedMonoTime), int(value.validUntilMonoTime)
|
|
model, car_state, drive = int(value.modelMonoTime), int(value.carStateMonoTime), int(value.driveStartMonoTime)
|
|
session, fingerprint = str(value.sessionId), str(value.settingsFingerprint)
|
|
choice = {wire: choice for choice, wire in CHOICES.items()}[str(value.choice)]
|
|
status, reason = str(value.status), str(value.reason)
|
|
if (
|
|
type(now_ns) is not int
|
|
or value.version != 1
|
|
or not re.fullmatch(r'[0-9a-f]{32}', session)
|
|
or value.sequence <= 0
|
|
or not 0 < stamp <= now_ns <= expiry
|
|
or not 0 < expiry - stamp <= LIFETIME_NS
|
|
or not 0 < drive <= model <= stamp
|
|
or not drive <= car_state <= stamp
|
|
or stamp - model > MODEL_MAX_AGE_NS
|
|
or stamp - car_state > LIFETIME_NS
|
|
or value.settingsRevision <= 0
|
|
or not _HEX.fullmatch(fingerprint)
|
|
or len(status) > 64
|
|
or len(reason) > 64
|
|
or value.statusCode > 255
|
|
or value.hasOverride
|
|
and (status != 'proposed' or choice is ModeChoice.STOCK and
|
|
(reason != 'cem_stop' or value.statusCode != 8 or not value.experimental))
|
|
):
|
|
return None
|
|
return ModeObservation(
|
|
session,
|
|
int(value.sequence),
|
|
stamp,
|
|
expiry,
|
|
drive,
|
|
model,
|
|
car_state,
|
|
int(value.settingsRevision),
|
|
fingerprint,
|
|
choice,
|
|
bool(value.experimental) if value.hasOverride else None,
|
|
status,
|
|
reason,
|
|
int(value.statusCode),
|
|
)
|
|
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
|
|
return None
|