Files
StarPilot/openpilot/starpilot/galaxy/segment_summary.py
T
firestar5683 80fbb4f6db Feature registration
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>
2026-10-14 20:02:39 -05:00

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()