mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
272 lines
13 KiB
Python
272 lines
13 KiB
Python
"""Bounded AOL axis requests and native acknowledgment checks.
|
|
|
|
The requester and the actuator are deliberately separate: a native permission
|
|
cannot be required before the host sends its first desired-axis request.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from opendbc.car.structs import car
|
|
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask
|
|
from openpilot.starpilot.aol.vehicle import native_admission_key, native_matches_cp, native_profile_supported
|
|
from openpilot.starpilot.aol.wire import INTENT_SERVICE, SAFETY_SERVICE, IntentState, decode_intent, decode_safety
|
|
|
|
INTENT_MAX_AGE_NS = 30_000_000
|
|
SAFETY_MAX_AGE_NS = 200_000_000
|
|
AXIS_MAX_AGE_NS = 30_000_000
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AxisDecision:
|
|
desired_lateral: bool = False
|
|
desired_longitudinal: bool = False
|
|
lateral_active: bool = False
|
|
longitudinal_active: bool = False
|
|
native_acknowledged: bool = False
|
|
|
|
@property
|
|
def mode(self) -> str:
|
|
if self.lateral_active and self.longitudinal_active:
|
|
return 'combined'
|
|
if self.lateral_active:
|
|
return 'lateralOnly'
|
|
if self.longitudinal_active:
|
|
return 'longitudinalOnly'
|
|
return 'off'
|
|
|
|
|
|
def current_intent(sm, *, car_state_ns: int, now_ns: int, previous: IntentState | None = None, companion=None, drain=True):
|
|
if companion is not None:
|
|
return companion.current(sm, source_ns=car_state_ns, now_ns=now_ns, drain=drain)
|
|
if not (sm.valid[INTENT_SERVICE] and sm.alive[INTENT_SERVICE] and sm.seen[INTENT_SERVICE]):
|
|
return None
|
|
intent = decode_intent(sm[INTENT_SERVICE])
|
|
if intent is None:
|
|
return None
|
|
# Intent is published before its carState wakeup. If the carState socket
|
|
# times out exactly between those sends, only the already accepted companion
|
|
# for that *same retained carState* may bridge the publication boundary.
|
|
# Revalidate its original lease; never accept a newer intent for older CAN.
|
|
if (intent.carStateLogMonoTime > car_state_ns and previous is not None and
|
|
previous.carStateLogMonoTime == car_state_ns and intent.settingsQualified and
|
|
intent.producerSessionId == previous.producerSessionId and
|
|
int(sm.logMonoTime[INTENT_SERVICE]) == intent.carStateLogMonoTime and
|
|
intent.observedMonoTime <= now_ns <= intent.validUntilMonoTime and
|
|
now_ns - intent.observedMonoTime <= INTENT_MAX_AGE_NS):
|
|
intent = previous
|
|
message_ns = int(previous.carStateLogMonoTime)
|
|
else:
|
|
message_ns = int(sm.logMonoTime[INTENT_SERVICE])
|
|
observed = int(intent.observedMonoTime)
|
|
produced_for = int(intent.carStateLogMonoTime)
|
|
if (not intent.settingsQualified or not intent.producerSessionId or produced_for <= 0 or
|
|
message_ns != produced_for or
|
|
produced_for > car_state_ns or car_state_ns - produced_for > INTENT_MAX_AGE_NS or
|
|
observed > now_ns or now_ns > int(intent.validUntilMonoTime) or
|
|
now_ns - observed > INTENT_MAX_AGE_NS):
|
|
return None
|
|
return intent
|
|
|
|
|
|
def native_inventory_matches_cp(state, CP) -> bool:
|
|
"""Bind the selected serial to the ordered same-read configuration inventory.
|
|
|
|
Historical v1 wires retain single-Panda compatibility only; they carry no
|
|
inventory qualification. Multi-Panda admission requires coherent production v2
|
|
with the exact configuration and one owner.
|
|
The source PandaStates stamp is BOOTTIME provenance, never a monotonic lease.
|
|
"""
|
|
if not state.pandaInventory:
|
|
return state.sourcePandaStatesMonoTime == 0 and len(CP.safetyConfigs) == 1
|
|
try:
|
|
slots = state.pandaInventory
|
|
configs = CP.safetyConfigs
|
|
if state.sourcePandaStatesMonoTime <= 0 or not 1 <= len(slots) <= 2 or len(slots) != len(configs):
|
|
return False
|
|
serials = [slot.hardwareSerial for slot in slots]
|
|
if any(not serial for serial in serials) or len(set(serials)) != len(serials):
|
|
return False
|
|
owners = []
|
|
for index, (slot, config) in enumerate(zip(slots, configs, strict=True)):
|
|
if slot.safetyRxChecksInvalid or slot.heartbeatLost or blocking_fault_mask(slot.faults, CP):
|
|
return False
|
|
if (slot.slotIndex != index or slot.safetyModel != int(config.safetyModel.raw) or
|
|
slot.safetyParam != int(config.safetyParam) or slot.alternativeExperience != int(CP.alternativeExperience)):
|
|
return False
|
|
if str(config.safetyModel) in ('silent', 'noOutput'):
|
|
if slot.safetyParam != 0 or slot.controlsAllowed:
|
|
return False
|
|
else:
|
|
owners.append(slot)
|
|
return bool(len(owners) == 1 and owners[0].hardwareSerial == state.pandaSerial and
|
|
owners[0].safetyModel == state.safetyModel and owners[0].safetyParam == state.safetyParam)
|
|
except (AttributeError, TypeError, ValueError, OverflowError, IndexError):
|
|
return False
|
|
|
|
|
|
def _clear_native_admission(sm):
|
|
try:
|
|
sm._aol_native_admission = None
|
|
except Exception:
|
|
# Custom/slotted owners may not support this optional optimization.
|
|
pass
|
|
|
|
|
|
def _native_admission_matches(sm, CP, state, raw):
|
|
key = None
|
|
try:
|
|
cp_key = native_admission_key(CP) if state.pandaInventory else None
|
|
if cp_key is not None and isinstance(raw, bytes):
|
|
key = (raw, int(sm.logMonoTime[SAFETY_SERVICE]), cp_key)
|
|
if getattr(sm, '_aol_native_admission', None) == key:
|
|
return True
|
|
except Exception:
|
|
# Cache plumbing may never replace or prevent the original validation.
|
|
key = None
|
|
matches = (native_matches_cp(CP, int(state.safetyModel), int(state.safetyParam)) and
|
|
native_inventory_matches_cp(state, CP))
|
|
try:
|
|
# Re-read mutable CP on a miss before binding the successful result.
|
|
sm._aol_native_admission = key if matches and key is not None and native_admission_key(CP) == key[2] else None
|
|
except Exception:
|
|
_clear_native_admission(sm)
|
|
return matches
|
|
|
|
|
|
def current_native(sm, CP, *, now_ns: int, axis_session_id: str | None = None):
|
|
if not (sm.valid[SAFETY_SERVICE] and sm.alive[SAFETY_SERVICE] and sm.seen[SAFETY_SERVICE]):
|
|
_clear_native_admission(sm)
|
|
return None
|
|
raw = sm[SAFETY_SERVICE]
|
|
raw = bytes(raw) if isinstance(raw, (bytes, bytearray, memoryview)) else raw
|
|
state = decode_safety(raw)
|
|
if state is None:
|
|
_clear_native_admission(sm)
|
|
return None
|
|
if (not _native_admission_matches(sm, CP, state, raw) or
|
|
not state.compatible or int(state.protocolVersion) != 1 or not state.pandaSerial or not state.axisSessionId or
|
|
(axis_session_id is not None and str(state.axisSessionId) != axis_session_id) or
|
|
int(sm.logMonoTime[SAFETY_SERVICE]) > now_ns or
|
|
now_ns - int(sm.logMonoTime[SAFETY_SERVICE]) > SAFETY_MAX_AGE_NS or
|
|
int(state.observedMonoTime) > now_ns or now_ns > int(state.validUntilMonoTime)):
|
|
_clear_native_admission(sm)
|
|
return None
|
|
if state.pandaInventory and (int(sm.logMonoTime[SAFETY_SERVICE]) != state.observedMonoTime or
|
|
state.observedMonoTime <= 0 or
|
|
state.validUntilMonoTime - state.observedMonoTime > SAFETY_MAX_AGE_NS):
|
|
_clear_native_admission(sm)
|
|
return None
|
|
return state
|
|
|
|
|
|
def known_reverse_pause(car_state, samples=()) -> bool:
|
|
"""Include the known Neutral transition into and out of Reverse."""
|
|
states = [car_state, *(state for state, _ in samples)]
|
|
gears = car.CarState.GearShifter
|
|
return bool(all(valid for _, valid in samples) and
|
|
any(state.gearShifter in (gears.neutral, gears.reverse) for state in states) and
|
|
all(state.canValid and not state.canTimeout and state.gearShifter in
|
|
(gears.drive, gears.low, gears.sport, gears.brake, gears.neutral, gears.reverse) for state in states))
|
|
|
|
|
|
def decide_axes(*, standard_lateral: bool, standard_longitudinal: bool, intent,
|
|
native, car_state, initialized: bool, model_ready: bool,
|
|
no_entry: bool, immediate_disable: bool, dm_lockout: bool,
|
|
pause_brake_mps: float, lateral_inhibit: bool = False, gear_pause: bool = False) -> AxisDecision:
|
|
if intent is None or not initialized or not car_state.canValid or immediate_disable or no_entry or dm_lockout:
|
|
return AxisDecision()
|
|
gear = car_state.gearShifter
|
|
driving = not gear_pause and gear not in (car.CarState.GearShifter.neutral, car.CarState.GearShifter.park,
|
|
car.CarState.GearShifter.reverse, car.CarState.GearShifter.unknown)
|
|
lateral_safe = (driving and model_ready and not lateral_inhibit and not car_state.steerFaultPermanent and
|
|
not car_state.steerFaultTemporary and
|
|
(pause_brake_mps <= 0 or not car_state.brakePressed or car_state.vEgo >= pause_brake_mps or car_state.standstill))
|
|
desired_lateral = bool(lateral_safe and not intent.pauseLateral and (standard_lateral or intent.allowedLatch))
|
|
desired_longitudinal = bool(driving and standard_longitudinal and not car_state.seatbeltUnlatched and not intent.pauseLongitudinal)
|
|
lateral_ack = native is not None and bool(native.requestedLateral) == desired_lateral
|
|
longitudinal_ack = native is not None and bool(native.requestedLongitudinal) == desired_longitudinal
|
|
return AxisDecision(
|
|
desired_lateral=desired_lateral, desired_longitudinal=desired_longitudinal,
|
|
lateral_active=bool(desired_lateral and lateral_ack and native.lateralAllowed),
|
|
longitudinal_active=bool(desired_longitudinal and longitudinal_ack and native.longitudinalAllowed),
|
|
native_acknowledged=bool(native is not None),
|
|
)
|
|
|
|
|
|
def current_axis(sm, *, now_ns: int):
|
|
if not (sm.valid['aolAxisState'] and sm.alive['aolAxisState'] and sm.seen['aolAxisState']):
|
|
return None
|
|
state = sm['aolAxisState']
|
|
if (not state.qualified or not state.sessionId or
|
|
int(sm.logMonoTime['aolAxisState']) > now_ns or
|
|
now_ns - int(sm.logMonoTime['aolAxisState']) > AXIS_MAX_AGE_NS or
|
|
int(state.observedMonoTime) > now_ns or now_ns > int(state.validUntilMonoTime)):
|
|
return None
|
|
return state
|
|
|
|
|
|
def refresh_expired_lateral_snapshot(sm, *, now_ns: int) -> bool:
|
|
"""Receive queued AOL inputs once; an expired LAT-only proof never gains time."""
|
|
axis = sm['aolAxisState']
|
|
if (sm.seen['aolAxisState'] and sm.valid['aolAxisState'] and sm.alive['aolAxisState'] and
|
|
axis.qualified and axis.nativeAcknowledged and axis.sessionId and
|
|
axis.lateralActive and axis.desiredLateral and
|
|
not axis.longitudinalActive and not axis.desiredLongitudinal and
|
|
0 < sm.logMonoTime['aolAxisState'] <= axis.observedMonoTime <= axis.validUntilMonoTime < now_ns):
|
|
# All normal authority, source, physical and native gates run on the newly
|
|
# received snapshot. An empty receive leaves the expired proof untouched.
|
|
sm.update(0)
|
|
return True
|
|
return False
|
|
|
|
|
|
def monitoring_lateral_engaged(sm, *, now_ns: int) -> bool:
|
|
axis = current_axis(sm, now_ns=now_ns)
|
|
if axis is None or not axis.lateralActive or not axis.nativeAcknowledged:
|
|
return False
|
|
if not (sm.valid[SAFETY_SERVICE] and sm.seen[SAFETY_SERVICE] and sm.alive[SAFETY_SERVICE]):
|
|
return False
|
|
native = decode_safety(sm[SAFETY_SERVICE])
|
|
if native is None:
|
|
return False
|
|
return bool(native.compatible and int(native.protocolVersion) == 1 and
|
|
native.axisSessionId == axis.sessionId and
|
|
native_profile_supported(int(native.safetyModel), int(native.safetyParam)) and
|
|
int(sm.logMonoTime[SAFETY_SERVICE]) <= now_ns and
|
|
now_ns - int(sm.logMonoTime[SAFETY_SERVICE]) <= SAFETY_MAX_AGE_NS and
|
|
int(native.observedMonoTime) <= now_ns <= int(native.validUntilMonoTime) and
|
|
native.requestedLateral and native.lateralAllowed)
|
|
|
|
|
|
def ordinary_lateral_requested(active: bool, CS, CP) -> bool:
|
|
"""The standard lateral output gate, shared by request and actuation owners."""
|
|
standstill = abs(CS.vEgo) <= max(CP.minSteerSpeed, 0.3) or CS.standstill
|
|
return bool(active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and
|
|
(not standstill or CP.steerAtStandstill))
|
|
|
|
|
|
def decide_ordinary_axis(*, requested: bool, native) -> AxisDecision:
|
|
"""Acknowledgment cannot create driver intent or an independent axis."""
|
|
acknowledged = bool(native is not None and native.requestedLateral == requested and
|
|
not native.requestedLongitudinal)
|
|
return AxisDecision(desired_lateral=bool(requested),
|
|
lateral_active=bool(requested and acknowledged and native.lateralAllowed),
|
|
native_acknowledged=acknowledged)
|
|
|
|
|
|
def ordinary_axis_acknowledged(sm, CP, *, now_ns: int, on_validated_native=None) -> bool:
|
|
axis = current_axis(sm, now_ns=now_ns)
|
|
native = current_native(sm, CP, now_ns=now_ns,
|
|
axis_session_id=str(axis.sessionId) if axis is not None else None)
|
|
acknowledged = bool(axis is not None and native is not None and axis.nativeAcknowledged and
|
|
axis.desiredLateral and axis.lateralActive and not axis.desiredLongitudinal and
|
|
native.requestedLateral and not native.requestedLongitudinal and native.lateralAllowed)
|
|
if acknowledged and on_validated_native is not None:
|
|
try:
|
|
on_validated_native(native)
|
|
except Exception:
|
|
pass # Optional evidence observation cannot change actuator admission.
|
|
return acknowledged
|