mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
8571e16f68
Register vehicle AOL adapters and runtime dispatch after the vehicle factories and native contracts exist. Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
204 lines
8.0 KiB
Python
204 lines
8.0 KiB
Python
"""Strict, bounded AOL payloads in the reserved Event Data envelopes.
|
|
|
|
The outer Event remains Data at ordinals 124/125. The inner payload is flat
|
|
Cap'n Proto with its own version and kind; no caller consumes a raw reader.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import struct
|
|
|
|
import capnp
|
|
from openpilot.cereal import custom
|
|
|
|
SAFETY_SERVICE = 'aolSafetyWire'
|
|
INTENT_SERVICE = 'aolIntentWire'
|
|
SAFETY_KIND = 1
|
|
INTENT_KIND = 2
|
|
WIRE_VERSION = 1
|
|
MAX_WIRE_BYTES = 512
|
|
MAX_WIRE_WORDS = (MAX_WIRE_BYTES - 8) // 8
|
|
MAX_ID_BYTES = 96
|
|
SAFETY_INVENTORY_VERSION = 2
|
|
MAX_PANDA_SLOTS = 2
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PandaSlot:
|
|
slotIndex: int
|
|
hardwareSerial: str
|
|
safetyModel: int
|
|
safetyParam: int
|
|
alternativeExperience: int
|
|
controlsAllowed: bool = False
|
|
safetyRxChecksInvalid: bool = False
|
|
heartbeatLost: bool = False
|
|
faults: int = 0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SafetyState:
|
|
protocolVersion: int
|
|
compatible: bool
|
|
observedMonoTime: int
|
|
validUntilMonoTime: int
|
|
safetyModel: int
|
|
safetyParam: int
|
|
lateralAllowed: bool
|
|
longitudinalAllowed: bool
|
|
requestedLateral: bool
|
|
requestedLongitudinal: bool
|
|
pandaSerial: str
|
|
axisSessionId: str
|
|
sourcePandaStatesMonoTime: int = 0
|
|
pandaInventory: tuple[PandaSlot, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IntentState:
|
|
producerSessionId: str
|
|
sequence: int
|
|
carStateLogMonoTime: int
|
|
observedMonoTime: int
|
|
validUntilMonoTime: int
|
|
allowedLatch: bool
|
|
pauseLateral: bool
|
|
pauseLongitudinal: bool
|
|
settingsQualified: bool
|
|
lateralArmed: bool = False
|
|
optionalSetRelease: bool = False
|
|
|
|
|
|
def _bounded_id(value: str) -> str:
|
|
if not isinstance(value, str) or len(value.encode('utf-8')) > MAX_ID_BYTES:
|
|
raise ValueError('AOL wire identifier is invalid')
|
|
return value
|
|
|
|
|
|
def _payload(raw_value, schema, kind: int, materialize, versions=(WIRE_VERSION,)):
|
|
if not isinstance(raw_value, (bytes, bytearray, memoryview)):
|
|
return None
|
|
byte_count = raw_value.nbytes if isinstance(raw_value, memoryview) else len(raw_value)
|
|
if not 16 <= byte_count <= MAX_WIRE_BYTES or byte_count % 8:
|
|
return None
|
|
try:
|
|
raw = bytes(raw_value)
|
|
if len(raw) != byte_count:
|
|
return None
|
|
segment_count_minus_one, segment_words = struct.unpack_from('<II', raw)
|
|
if segment_count_minus_one != 0 or segment_words > MAX_WIRE_WORDS or len(raw) != 8 + 8 * segment_words:
|
|
return None
|
|
with schema.from_bytes(raw, traversal_limit_in_words=MAX_WIRE_WORDS, nesting_limit=4) as decoded:
|
|
if int(decoded.kind) != kind or int(decoded.version) not in versions:
|
|
return None
|
|
return materialize(decoded)
|
|
except (ValueError, OverflowError, RuntimeError, UnicodeDecodeError, capnp.KjException):
|
|
return None
|
|
|
|
|
|
def _memoized(decode):
|
|
# Consumers poll at 100 Hz while wires change at 10-100 Hz; results are frozen, so reuse the last exact-bytes decode.
|
|
last = [None, None]
|
|
|
|
def wrapper(raw):
|
|
key = bytes(raw) if isinstance(raw, (bytes, bytearray, memoryview)) else None
|
|
if key is None:
|
|
return decode(raw)
|
|
if last[0] != key:
|
|
last[0], last[1] = key, decode(key)
|
|
return last[1]
|
|
wrapper.__wrapped__ = decode
|
|
return wrapper
|
|
|
|
|
|
def _read_safety(value) -> SafetyState | None:
|
|
try:
|
|
source_ns = int(value.sourcePandaStatesMonoTime)
|
|
inventory = tuple(PandaSlot(int(slot.slotIndex), _bounded_id(str(slot.hardwareSerial)),
|
|
int(slot.safetyModel), int(slot.safetyParam), int(slot.alternativeExperience),
|
|
bool(slot.controlsAllowed), bool(slot.safetyRxChecksInvalid),
|
|
bool(slot.heartbeatLost), int(slot.faults))
|
|
for slot in value.pandaInventory)
|
|
if (int(value.version) == WIRE_VERSION and (source_ns or inventory) or
|
|
int(value.version) == SAFETY_INVENTORY_VERSION and
|
|
(source_ns <= 0 or not 1 <= len(inventory) <= MAX_PANDA_SLOTS)):
|
|
return None
|
|
result = SafetyState(int(value.protocolVersion), bool(value.compatible), int(value.observedMonoTime),
|
|
int(value.validUntilMonoTime), int(value.safetyModel), int(value.safetyParam),
|
|
bool(value.lateralAllowed), bool(value.longitudinalAllowed), bool(value.requestedLateral),
|
|
bool(value.requestedLongitudinal), _bounded_id(str(value.pandaSerial)),
|
|
_bounded_id(str(value.axisSessionId)), source_ns, inventory)
|
|
if result.validUntilMonoTime < result.observedMonoTime:
|
|
return None
|
|
return result
|
|
except (KeyError, ValueError, OverflowError, RuntimeError, capnp.KjException):
|
|
return None
|
|
|
|
|
|
def _read_intent(value) -> IntentState | None:
|
|
try:
|
|
result = IntentState(_bounded_id(str(value.producerSessionId)), int(value.sequence),
|
|
int(value.carStateLogMonoTime), int(value.observedMonoTime), int(value.validUntilMonoTime),
|
|
bool(value.allowedLatch), bool(value.pauseLateral), bool(value.pauseLongitudinal),
|
|
bool(value.settingsQualified), bool(value.lateralArmed), bool(value.optionalSetRelease))
|
|
if result.validUntilMonoTime < result.observedMonoTime:
|
|
return None
|
|
return result
|
|
except (KeyError, ValueError, OverflowError, RuntimeError, capnp.KjException):
|
|
return None
|
|
|
|
|
|
@_memoized
|
|
def decode_safety(raw: bytes) -> SafetyState | None:
|
|
return _payload(raw, custom.AolAxisState.SafetyWire, SAFETY_KIND, _read_safety,
|
|
(WIRE_VERSION, SAFETY_INVENTORY_VERSION))
|
|
|
|
|
|
@_memoized
|
|
def decode_intent(raw: bytes) -> IntentState | None:
|
|
return _payload(raw, custom.AolAxisState.IntentWire, INTENT_KIND, _read_intent)
|
|
|
|
|
|
def encode_intent(value: IntentState) -> bytes:
|
|
_bounded_id(value.producerSessionId)
|
|
message = custom.AolAxisState.IntentWire.new_message(
|
|
kind=INTENT_KIND, version=WIRE_VERSION, producerSessionId=value.producerSessionId,
|
|
sequence=value.sequence, carStateLogMonoTime=value.carStateLogMonoTime,
|
|
observedMonoTime=value.observedMonoTime, validUntilMonoTime=value.validUntilMonoTime,
|
|
allowedLatch=value.allowedLatch, pauseLateral=value.pauseLateral,
|
|
pauseLongitudinal=value.pauseLongitudinal, settingsQualified=value.settingsQualified,
|
|
lateralArmed=value.lateralArmed, optionalSetRelease=value.optionalSetRelease)
|
|
data = message.to_bytes()
|
|
if len(data) > MAX_WIRE_BYTES:
|
|
raise ValueError('AOL intent payload exceeds wire bound')
|
|
return data
|
|
|
|
|
|
def encode_safety(value: SafetyState) -> bytes:
|
|
_bounded_id(value.pandaSerial)
|
|
_bounded_id(value.axisSessionId)
|
|
inventory = value.pandaInventory
|
|
inventory_version = bool(inventory or value.sourcePandaStatesMonoTime)
|
|
if inventory_version and (value.sourcePandaStatesMonoTime <= 0 or not 1 <= len(inventory) <= MAX_PANDA_SLOTS):
|
|
raise ValueError('AOL Panda inventory is invalid')
|
|
for slot in inventory:
|
|
_bounded_id(slot.hardwareSerial)
|
|
message = custom.AolAxisState.SafetyWire.new_message(
|
|
kind=SAFETY_KIND, version=SAFETY_INVENTORY_VERSION if inventory_version else WIRE_VERSION,
|
|
protocolVersion=value.protocolVersion,
|
|
compatible=value.compatible, observedMonoTime=value.observedMonoTime,
|
|
validUntilMonoTime=value.validUntilMonoTime, safetyModel=value.safetyModel,
|
|
safetyParam=value.safetyParam, lateralAllowed=value.lateralAllowed,
|
|
longitudinalAllowed=value.longitudinalAllowed, requestedLateral=value.requestedLateral,
|
|
requestedLongitudinal=value.requestedLongitudinal, pandaSerial=value.pandaSerial,
|
|
axisSessionId=value.axisSessionId, sourcePandaStatesMonoTime=value.sourcePandaStatesMonoTime,
|
|
pandaInventory=[{'slotIndex': slot.slotIndex, 'hardwareSerial': slot.hardwareSerial, 'safetyModel': slot.safetyModel,
|
|
'safetyParam': slot.safetyParam, 'alternativeExperience': slot.alternativeExperience,
|
|
'controlsAllowed': slot.controlsAllowed, 'safetyRxChecksInvalid': slot.safetyRxChecksInvalid,
|
|
'heartbeatLost': slot.heartbeatLost, 'faults': slot.faults} for slot in inventory])
|
|
data = message.to_bytes()
|
|
if len(data) > MAX_WIRE_BYTES:
|
|
raise ValueError('AOL safety payload exceeds wire bound')
|
|
return data
|