Files
StarPilot/openpilot/starpilot/conditional_mode/consumer.py
T
2026-10-13 14:21:51 -05:00

128 lines
5.2 KiB
Python

"""Fresh proposal acceptance; selfdrived alone chooses its effective mode."""
from dataclasses import dataclass
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.conditional_mode.projection import CLOCK_PAIR_MAX_SKEW_NS
from openpilot.starpilot.conditional_mode.status import LIFETIME_NS, MODEL_MAX_AGE_NS, ModeObservation, observation
@dataclass(frozen=True)
class ConsumerResult:
experimental: bool
accepted: bool
status: str
class ModeConsumer:
def __init__(self):
self.barrier_ns = 0
self.offset_ns: int | None = None
self.last_now_ns = 0
self.drive_id = 0
self.last: ModeObservation | None = None
self.rejection: str | None = None
def invalidate(self, now_ns: int) -> None:
self.barrier_ns = max(self.barrier_ns, now_ns)
self.last = None
def _reject(self, label: str, fallback: ConsumerResult, invalidate_ns: int | None = None) -> ConsumerResult:
self.rejection = label
if invalidate_ns is not None:
self.invalidate(invalidate_ns)
return fallback
def sample(
self,
state,
*,
now_ns: int,
now_boot_ns: int,
sample_skew_ns: int,
message_ns: int,
receipt_ns: int,
drive_id: int,
model_ns: int,
car_state_ns: int,
authority: bool,
stock_experimental: bool,
choice: ModeChoice,
settings_fingerprint: str | None,
) -> ConsumerResult:
"""A repeated message may be used until its original expiry, never renewed.
The caller checks live native longitudinal authority and independently
affirms the saved settings. The publisher's authority cannot substitute for
either. A restart, clock discontinuity or drive change requires new sources.
"""
fallback = ConsumerResult(stock_experimental if type(stock_experimental) is bool else False, False, 'unavailable')
self.rejection = None
if (
any(type(value) is not int for value in (now_ns, now_boot_ns, sample_skew_ns, message_ns, receipt_ns, drive_id, model_ns, car_state_ns))
or now_ns <= 0
or now_boot_ns <= 0
or now_boot_ns < now_ns - sample_skew_ns // 2
or not 0 <= sample_skew_ns <= CLOCK_PAIR_MAX_SKEW_NS
):
return self._reject('clock_inputs', fallback, self.last_now_ns)
offset = now_boot_ns - now_ns
discontinuity = self.offset_ns is None or abs(offset - self.offset_ns) > CLOCK_PAIR_MAX_SKEW_NS or now_ns < self.last_now_ns or drive_id != self.drive_id
self.last_now_ns = now_ns
self.offset_ns = offset
self.drive_id = drive_id
if discontinuity:
return self._reject('discontinuity', fallback, now_ns)
if authority is not True:
return self._reject('authority', fallback, now_ns)
if (
type(stock_experimental) is not bool
or choice not in (ModeChoice.CEM, ModeChoice.CCM, ModeChoice.STOCK)
or settings_fingerprint is None
or not 0 < drive_id <= now_ns
):
return self._reject('settings', fallback, now_ns)
if not self.barrier_ns < model_ns:
return self._reject('model_barrier', fallback)
if model_ns > now_ns or now_ns - model_ns > MODEL_MAX_AGE_NS:
return self._reject('model_age', fallback, now_ns)
if not self.barrier_ns < car_state_ns:
return self._reject('car_state_barrier', fallback)
if car_state_ns > now_ns or now_ns - car_state_ns > LIFETIME_NS:
return self._reject('car_state_age', fallback, now_ns)
value = observation(state, now_ns)
# Rejections below preserve the last sequence to reject old or altered samples after gaps.
if value is None:
return self._reject('proposal_invalid', fallback)
if (
value.drive_id != drive_id
or value.choice is not choice
or value.fingerprint != settings_fingerprint
or value.override is None
or choice is ModeChoice.STOCK and (value.reason != 'cem_stop' or value.status_code != 8 or value.override is not True)
):
return self._reject('proposal_mismatch', fallback)
if not (self.barrier_ns < message_ns and self.barrier_ns < value.model_ns and self.barrier_ns < value.car_state_ns):
return self._reject('proposal_barrier', fallback)
if (
not message_ns <= value.observed_ns <= now_ns
or value.observed_ns - message_ns > LIFETIME_NS
or not message_ns <= receipt_ns <= now_ns
or now_ns - receipt_ns > LIFETIME_NS
):
return self._reject('proposal_receipt', fallback)
if value.model_ns > model_ns or now_ns - value.model_ns > MODEL_MAX_AGE_NS:
return self._reject('proposal_model_age', fallback)
if value.car_state_ns > car_state_ns or now_ns - value.car_state_ns > LIFETIME_NS:
return self._reject('proposal_car_state_age', fallback)
previous = self.last
if previous is not None:
if value.session != previous.session:
return self._reject('session', fallback, now_ns)
if value.sequence < previous.sequence or value.sequence == previous.sequence and value != previous:
return self._reject('sequence', fallback)
if value.sequence > previous.sequence and (value.observed_ns <= previous.observed_ns or value.model_ns <= previous.model_ns):
return self._reject('sequence_stamps', fallback)
self.last = value
return ConsumerResult(value.override, True, value.status)