mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
14241d72c8
Select qualified speed-limit sources, preserve acceptance and override decisions, and publish distinct control and display coordinates. Carry accepted and pending source identity with each displayed decision.
241 lines
10 KiB
Python
241 lines
10 KiB
Python
"""Disabled, source-aware boundary for v2 mapdOut evidence.
|
|
|
|
This module checks wire freshness and ordering. It deliberately never creates a
|
|
VALID or ABSENT SLC observation: a fresh map match is not road qualification.
|
|
"""
|
|
|
|
import math
|
|
from dataclasses import dataclass
|
|
|
|
from openpilot.cereal import log
|
|
from openpilot.starpilot.speed_limits import acceptance as acc
|
|
|
|
|
|
EVENT_MAX_AGE_NS = 250_000_000
|
|
COMPUTED_MAX_AGE_NS = 250_000_000
|
|
GPS_MAX_AGE_NS = {'external': 500_000_000, 'internal': 2_000_000_000}
|
|
MAX_EVENT_BYTES = 2 * 1024 * 1024
|
|
MAX_RETIRED_SESSIONS = 16
|
|
MAX_UINT64 = (1 << 64) - 1
|
|
MAX_INT64 = (1 << 63) - 1
|
|
MAX_FLOAT32 = 3.4028234663852886e38
|
|
MATCHED = frozenset(('matchedLimit', 'matchedNoLimit'))
|
|
LOSSES = frozenset(('noGps', 'noCoverage', 'noMatch'))
|
|
WAY_SELECTIONS = frozenset(('current', 'predicted', 'possible', 'extended'))
|
|
UNKNOWN = acc.Observation(acc.ObservationKind.UNKNOWN)
|
|
STALE = acc.Observation(acc.ObservationKind.STALE)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MapFrame:
|
|
event_valid: bool
|
|
event_mono_ns: int
|
|
version: int
|
|
status: str
|
|
source: str
|
|
gps_mono_ns: int
|
|
computed_mono_ns: int
|
|
generation: int
|
|
producer_session: int
|
|
speed_mps: float
|
|
tile_loaded: bool
|
|
way_id: int
|
|
way_selection: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MapEvidence:
|
|
kind: str
|
|
observation: acc.Observation
|
|
speed_mps: float | None = None # parsed value only; never SLC authority
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MapDiagnostic:
|
|
"""Metadata from the tracker's accepted frame; never a control observation."""
|
|
frame: MapFrame | None
|
|
producer_restarts: int
|
|
source_switches: int
|
|
|
|
|
|
def decode_event(raw: bytes) -> MapFrame:
|
|
"""Copy scalar evidence from an actual serialized host cereal Event."""
|
|
if type(raw) is not bytes or not 0 < len(raw) <= MAX_EVENT_BYTES:
|
|
raise ValueError('invalid mapdOut event size/type')
|
|
try:
|
|
with log.Event.from_bytes(raw) as event:
|
|
if event.which() != 'mapdOut':
|
|
raise ValueError('wrong Event variant')
|
|
road = event.mapdOut
|
|
return MapFrame(bool(event.valid), int(event.logMonoTime), int(road.sampleVersion), str(road.roadStatus),
|
|
str(road.gpsSource), int(road.gpsMonoTime), int(road.computedMonoTime),
|
|
int(road.sourceGeneration), int(road.producerSession), float(road.speedLimit),
|
|
bool(road.tileLoaded), int(road.wayId), str(road.waySelectionType))
|
|
except (ValueError, TypeError) as error:
|
|
raise ValueError('invalid mapdOut event') from error
|
|
except Exception as error: # pycapnp reports malformed segment/pointer errors as KjException
|
|
raise ValueError('invalid mapdOut event') from error
|
|
|
|
|
|
def _uint(value: object, *, positive: bool = False, maximum: int = MAX_UINT64) -> bool:
|
|
return type(value) is int and (value > 0 if positive else value >= 0) and value <= maximum
|
|
|
|
|
|
def _ordering_fields(frame: object) -> bool:
|
|
return (isinstance(frame, MapFrame) and type(frame.event_valid) is bool and type(frame.status) is str and
|
|
type(frame.source) is str and type(frame.tile_loaded) is bool and type(frame.way_selection) is str and
|
|
_uint(frame.event_mono_ns, positive=True) and _uint(frame.generation, positive=True) and
|
|
_uint(frame.producer_session, positive=True))
|
|
|
|
|
|
def _trusted_wire_shape(frame: MapFrame) -> bool:
|
|
if (not _ordering_fields(frame) or not _uint(frame.version, maximum=(1 << 16)-1) or frame.version != 2 or
|
|
frame.status not in MATCHED | LOSSES or frame.source not in (*GPS_MAX_AGE_NS, 'none') or
|
|
frame.way_selection not in WAY_SELECTIONS | {'fail'} or not _uint(frame.gps_mono_ns) or
|
|
not _uint(frame.computed_mono_ns, positive=True) or frame.computed_mono_ns > frame.event_mono_ns or
|
|
type(frame.way_id) is not int or not -MAX_INT64-1 <= frame.way_id <= MAX_INT64 or
|
|
type(frame.speed_mps) is not float or not math.isfinite(frame.speed_mps) or
|
|
abs(frame.speed_mps) > MAX_FLOAT32):
|
|
return False
|
|
if frame.status == 'noGps':
|
|
return ((frame.source == 'none' and frame.gps_mono_ns == 0) or
|
|
(frame.source in GPS_MAX_AGE_NS and 0 < frame.gps_mono_ns <= frame.computed_mono_ns))
|
|
return frame.source in GPS_MAX_AGE_NS and 0 < frame.gps_mono_ns <= frame.computed_mono_ns
|
|
|
|
|
|
def _current_at(frame: MapFrame, now_boot_ns: int) -> bool:
|
|
if not _trusted_wire_shape(frame):
|
|
return False
|
|
if not (0 < frame.computed_mono_ns <= frame.event_mono_ns <= now_boot_ns and
|
|
now_boot_ns - frame.event_mono_ns <= EVENT_MAX_AGE_NS and
|
|
now_boot_ns - frame.computed_mono_ns <= COMPUTED_MAX_AGE_NS):
|
|
return False
|
|
if frame.status in MATCHED or frame.status in ('noCoverage', 'noMatch'):
|
|
if (frame.source not in GPS_MAX_AGE_NS or not 0 < frame.gps_mono_ns <= frame.computed_mono_ns or
|
|
now_boot_ns - frame.gps_mono_ns > GPS_MAX_AGE_NS[frame.source]):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _classify(frame: MapFrame, now_boot_ns: int) -> MapEvidence:
|
|
if not _current_at(frame, now_boot_ns):
|
|
# A structurally malformed fresh message is UNKNOWN; expired evidence is
|
|
# STALE. Neither has a numeric SLC candidate.
|
|
timestamps = (frame.gps_mono_ns, frame.computed_mono_ns, frame.event_mono_ns)
|
|
expired = (all(_uint(value) for value in timestamps) and frame.event_mono_ns <= now_boot_ns and
|
|
(now_boot_ns - frame.event_mono_ns > EVENT_MAX_AGE_NS or
|
|
now_boot_ns - frame.computed_mono_ns > COMPUTED_MAX_AGE_NS or
|
|
(frame.source in GPS_MAX_AGE_NS and frame.gps_mono_ns > 0 and
|
|
now_boot_ns - frame.gps_mono_ns > GPS_MAX_AGE_NS[frame.source])))
|
|
return MapEvidence('stale' if expired else 'unknown', STALE if expired else UNKNOWN)
|
|
if frame.status == 'matchedLimit':
|
|
if (not frame.event_valid or not frame.tile_loaded or frame.way_id <= 0 or
|
|
frame.way_selection not in WAY_SELECTIONS or frame.speed_mps <= 0):
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
return MapEvidence('matched_limit_unqualified', UNKNOWN, float(frame.speed_mps))
|
|
if frame.status == 'matchedNoLimit':
|
|
if (not frame.event_valid or not frame.tile_loaded or frame.way_id <= 0 or
|
|
frame.way_selection not in WAY_SELECTIONS or frame.speed_mps != 0):
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
return MapEvidence('matched_no_limit_unqualified', UNKNOWN)
|
|
if frame.event_valid or frame.speed_mps != 0 or frame.way_id != 0 or frame.tile_loaded:
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
if frame.status == 'noGps' and frame.source == 'none' and frame.gps_mono_ns != 0:
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
return MapEvidence('loss', UNKNOWN)
|
|
|
|
|
|
class MapTracker:
|
|
"""Bounded in-memory ordering state; outputs never authorize map control."""
|
|
|
|
def __init__(self):
|
|
self.reset()
|
|
|
|
def reset(self) -> None:
|
|
self._session: int | None = None
|
|
self._generation = 0
|
|
self._source = 'none'
|
|
self._last_event_ns = 0
|
|
self._last_now_ns = 0
|
|
self._retired: set[int] = set()
|
|
self._blocked = False
|
|
self._current: MapFrame | None = None
|
|
self._producer_restarts = 0
|
|
self._source_switches = 0
|
|
|
|
def diagnostic(self, *, now_boot_ns: int) -> MapDiagnostic:
|
|
# The frame is the same immutable object that passed step() ordering and
|
|
# freshness checks. Recheck age so even a caller between step() calls
|
|
# cannot retain a stopped producer's old diagnostic frame.
|
|
if not _uint(now_boot_ns, positive=True) or now_boot_ns < self._last_now_ns:
|
|
raise ValueError('invalid map diagnostic clock')
|
|
frame = self._current
|
|
if frame is not None and _classify(frame, now_boot_ns).kind not in (
|
|
'matched_limit_unqualified', 'matched_no_limit_unqualified', 'loss'):
|
|
frame = None
|
|
return MapDiagnostic(frame, self._producer_restarts, self._source_switches)
|
|
|
|
def _held(self, now_boot_ns: int) -> MapEvidence:
|
|
if self._current is None:
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
result = _classify(self._current, now_boot_ns)
|
|
if result.kind == 'stale':
|
|
self._current = None
|
|
return result
|
|
|
|
def step(self, frame: MapFrame | None, *, received: bool, now_boot_ns: int,
|
|
transport_alive: bool = True) -> MapEvidence:
|
|
if not _uint(now_boot_ns, positive=True) or type(received) is not bool or type(transport_alive) is not bool:
|
|
raise ValueError('invalid map source clock/transport flags')
|
|
if now_boot_ns < self._last_now_ns:
|
|
self._blocked = True # new drive/replay epoch requires explicit reset()
|
|
self._last_now_ns = now_boot_ns
|
|
if self._blocked:
|
|
self._current = None
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
if not transport_alive:
|
|
self._current = None
|
|
return MapEvidence('stale', STALE)
|
|
if not received:
|
|
return self._held(now_boot_ns)
|
|
if not _ordering_fields(frame):
|
|
self._current = None
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
assert frame is not None
|
|
if frame.producer_session in self._retired:
|
|
return self._held(now_boot_ns)
|
|
if self._session is not None:
|
|
if frame.event_mono_ns <= self._last_event_ns:
|
|
return self._held(now_boot_ns) # old/duplicate packet cannot revoke or renew
|
|
if not _trusted_wire_shape(frame):
|
|
self._current = None # unsupported/malformed wire cannot move trusted ordering
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
if frame.event_mono_ns > now_boot_ns:
|
|
self._current = None # future packet is invalid, but cannot poison the high-water mark
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
if self._session is not None:
|
|
if frame.producer_session == self._session:
|
|
if frame.generation < self._generation or (frame.generation == self._generation and frame.source != self._source):
|
|
self._current = None
|
|
self._blocked = True
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
else:
|
|
if len(self._retired) >= MAX_RETIRED_SESSIONS:
|
|
self._current = None
|
|
self._blocked = True
|
|
return MapEvidence('unknown', UNKNOWN)
|
|
self._retired.add(self._session)
|
|
self._producer_restarts += 1
|
|
self._current = None
|
|
if frame.producer_session != self._session or frame.generation != self._generation:
|
|
self._current = None
|
|
if self._session == frame.producer_session and frame.generation != self._generation:
|
|
self._source_switches += 1
|
|
self._session = frame.producer_session
|
|
self._generation = frame.generation
|
|
self._source = frame.source
|
|
self._last_event_ns = frame.event_mono_ns
|
|
result = _classify(frame, now_boot_ns)
|
|
self._current = frame if result.kind in ('matched_limit_unqualified', 'matched_no_limit_unqualified', 'loss') else None
|
|
return result
|