mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
192 lines
8.1 KiB
Python
192 lines
8.1 KiB
Python
"""On-demand metrics from one immutable, closed local full rlog segment."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from contextlib import contextmanager
|
|
import math
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import threading
|
|
import time
|
|
|
|
from openpilot.starpilot.flm.local_logs import (DIR_FLAGS, FILE_FLAGS, LocalLogUnavailable, RLOGS,
|
|
read_closed_rlog)
|
|
from openpilot.starpilot.flm.log_decode import LogDecodeError, decode_segment
|
|
from openpilot.starpilot.galaxy.drive_history import MAX_SEGMENT_ENTRIES, SEGMENT_NAME
|
|
|
|
|
|
MAX_ANALYSIS_SECONDS = 8.0
|
|
MAX_PAIR_SECONDS = 1.0
|
|
MAX_SPEED_MPS = 70.0
|
|
|
|
|
|
class SegmentSummaryUnavailable(Exception):
|
|
pass
|
|
|
|
|
|
class SegmentSummaryChanged(SegmentSummaryUnavailable):
|
|
pass
|
|
|
|
|
|
def _revision(info: os.stat_result) -> tuple[int, int, int, int, int]:
|
|
return info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns
|
|
|
|
|
|
@contextmanager
|
|
def _source_guard(root: Path, segment_name: str):
|
|
matched = SEGMENT_NAME.fullmatch(segment_name) if type(segment_name) is str and len(segment_name) <= 180 else None
|
|
if matched is None or len(matched.group('number')) > 6 or not root.is_absolute() or '..' in root.parts:
|
|
raise ValueError('Invalid local segment selection')
|
|
fds: list[int] = []
|
|
try:
|
|
root_fd = os.open(root, DIR_FLAGS)
|
|
fds.append(root_fd)
|
|
segment_fd = os.open(segment_name, DIR_FLAGS, dir_fd=root_fd)
|
|
fds.append(segment_fd)
|
|
names: list[str] = []
|
|
with os.scandir(segment_fd) as entries:
|
|
for count, entry in enumerate(entries, 1):
|
|
if count > MAX_SEGMENT_ENTRIES or entry.name.endswith('.lock'):
|
|
raise SegmentSummaryChanged('Segment is open or unavailable')
|
|
if entry.name in RLOGS:
|
|
names.append(entry.name)
|
|
if len(names) != 1:
|
|
raise SegmentSummaryUnavailable('One closed full log is required')
|
|
filename = names[0]
|
|
source_fd = os.open(filename, FILE_FLAGS, dir_fd=segment_fd)
|
|
fds.append(source_fd)
|
|
root_identity = os.fstat(root_fd).st_dev, os.fstat(root_fd).st_ino
|
|
segment_identity = os.fstat(segment_fd).st_dev, os.fstat(segment_fd).st_ino
|
|
initial = _revision(os.fstat(source_fd))
|
|
if not stat.S_ISREG(os.fstat(source_fd).st_mode):
|
|
raise SegmentSummaryUnavailable('Full log is not a regular file')
|
|
|
|
def current() -> bool:
|
|
try:
|
|
root_now = os.stat(root, follow_symlinks=False)
|
|
segment_now = os.stat(segment_name, dir_fd=root_fd, follow_symlinks=False)
|
|
source_now = os.stat(filename, dir_fd=segment_fd, follow_symlinks=False)
|
|
if (not stat.S_ISDIR(root_now.st_mode) or not stat.S_ISDIR(segment_now.st_mode) or
|
|
not stat.S_ISREG(source_now.st_mode) or
|
|
(root_now.st_dev, root_now.st_ino) != root_identity or
|
|
(segment_now.st_dev, segment_now.st_ino) != segment_identity or
|
|
_revision(source_now) != initial or _revision(os.fstat(source_fd)) != initial):
|
|
return False
|
|
with os.scandir(segment_fd) as entries:
|
|
return all(count <= MAX_SEGMENT_ENTRIES and not entry.name.endswith('.lock')
|
|
for count, entry in enumerate(entries, 1))
|
|
except OSError:
|
|
return False
|
|
|
|
if not current():
|
|
raise SegmentSummaryChanged('Local recording identity changed')
|
|
yield filename, current
|
|
except OSError as error:
|
|
raise SegmentSummaryUnavailable('Local recording is unavailable') from error
|
|
finally:
|
|
for fd in reversed(fds):
|
|
os.close(fd)
|
|
|
|
|
|
def _project(events, permitted: Callable[[], bool]) -> dict:
|
|
previous_car: tuple[int, float] | None = None
|
|
previous_control: tuple[int, bool, bool] | None = None
|
|
car_high_water = control_high_water = 0
|
|
first_car = last_car = first_control = last_control = None
|
|
distance = lat_seconds = long_seconds = 0.0
|
|
car_pairs = control_pairs = car_gaps = control_gaps = 0
|
|
for event in events:
|
|
if not permitted():
|
|
raise SegmentSummaryUnavailable('Summary is no longer available')
|
|
kind = event.which()
|
|
if kind == 'carState':
|
|
stamp = int(event.logMonoTime)
|
|
speed = float(event.carState.vEgo)
|
|
if not event.valid or stamp <= 0 or not math.isfinite(speed) or not 0 <= speed <= MAX_SPEED_MPS:
|
|
car_high_water = max(car_high_water, stamp)
|
|
previous_car = None
|
|
car_gaps += 1
|
|
continue
|
|
if stamp <= car_high_water:
|
|
previous_car = None
|
|
car_gaps += 1
|
|
continue
|
|
car_high_water = stamp
|
|
if previous_car is not None:
|
|
delta = (stamp - previous_car[0]) / 1e9
|
|
if 0 < delta <= MAX_PAIR_SECONDS:
|
|
distance += (previous_car[1] + speed) * .5 * delta
|
|
car_pairs += 1
|
|
else:
|
|
car_gaps += 1
|
|
first_car = stamp if first_car is None else min(first_car, stamp)
|
|
last_car = stamp if last_car is None else max(last_car, stamp)
|
|
previous_car = stamp, speed
|
|
elif kind == 'carControl':
|
|
stamp = int(event.logMonoTime)
|
|
if not event.valid or stamp <= 0:
|
|
control_high_water = max(control_high_water, stamp)
|
|
previous_control = None
|
|
control_gaps += 1
|
|
continue
|
|
if stamp <= control_high_water:
|
|
previous_control = None
|
|
control_gaps += 1
|
|
continue
|
|
control_high_water = stamp
|
|
lat, long = bool(event.carControl.latActive), bool(event.carControl.longActive)
|
|
first_control = stamp if first_control is None else min(first_control, stamp)
|
|
last_control = stamp if last_control is None else max(last_control, stamp)
|
|
if previous_control is not None:
|
|
delta = (stamp - previous_control[0]) / 1e9
|
|
if 0 < delta <= MAX_PAIR_SECONDS:
|
|
lat_seconds += delta if previous_control[1] else 0.0
|
|
long_seconds += delta if previous_control[2] else 0.0
|
|
control_pairs += 1
|
|
else:
|
|
control_gaps += 1
|
|
previous_control = stamp, lat, long
|
|
return {
|
|
'observedCarSpanSeconds': (last_car - first_car) / 1e9 if first_car is not None and last_car is not None and last_car > first_car else None,
|
|
'estimatedDistanceMeters': round(distance, 2) if car_pairs else None,
|
|
'observedLatActiveSeconds': round(lat_seconds, 2) if control_pairs else None,
|
|
'observedLongActiveSeconds': round(long_seconds, 2) if control_pairs else None,
|
|
'gaps': {'carState': car_gaps, 'carControl': control_gaps},
|
|
'sampleCoverageComplete': bool(car_pairs and control_pairs and not car_gaps and not control_gaps and
|
|
first_control is not None and last_control is not None and
|
|
first_car is not None and last_car is not None and
|
|
first_control <= first_car and last_control >= last_car),
|
|
}
|
|
|
|
|
|
class SegmentSummary:
|
|
def __init__(self, root: Path):
|
|
self.root = root
|
|
self._lock = threading.Lock()
|
|
|
|
def snapshot(self, segment_name: str, *, permitted: Callable[[], bool]) -> dict:
|
|
if not self._lock.acquire(blocking=False):
|
|
raise SegmentSummaryUnavailable('Another segment is being read')
|
|
deadline = time.monotonic() + MAX_ANALYSIS_SECONDS
|
|
def allowed() -> bool:
|
|
return time.monotonic() <= deadline and permitted()
|
|
try:
|
|
if not allowed():
|
|
raise SegmentSummaryUnavailable('Summary is no longer available')
|
|
with _source_guard(self.root, segment_name) as (filename, current):
|
|
closed = read_closed_rlog(self.root, segment_name, permitted=allowed)
|
|
if filename not in RLOGS or closed.codec != RLOGS[filename] or not current():
|
|
raise SegmentSummaryChanged('Local recording identity changed')
|
|
events = decode_segment(closed.compressed, closed.codec, cancelled=lambda: not allowed())
|
|
result = _project(events, allowed)
|
|
if not allowed() or not current():
|
|
raise SegmentSummaryChanged('Local recording identity changed')
|
|
return {'schemaVersion': 1, 'source': 'closed_local_rlog', 'segmentName': segment_name,
|
|
'sourceSha256': closed.sha256, **result}
|
|
except (LocalLogUnavailable, LogDecodeError) as error:
|
|
raise SegmentSummaryUnavailable('Closed full log is unavailable') from error
|
|
finally:
|
|
self._lock.release()
|