1
0
forked from IQ.Lvbs/IQ.Pilot

IQ.Pilot Release Commit @ 0798119

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit b42569dbca
4529 changed files with 1132125 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from openpilot.system.hardware.hw import Paths
_utc_offset_cache: int | None = None
def utc_offset_hours() -> int:
"""Approximate local UTC offset from the device's last GPS longitude (comma devices run in UTC
with no timezone configured). ~1h imprecise (ignores DST/political borders) but auto and close;
cached for the session."""
global _utc_offset_cache
if _utc_offset_cache is not None:
return _utc_offset_cache
offset = 0
try:
from openpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position
lat, lon, _, have_fix = current_or_last_gps_position()
if have_fix:
offset = int(round(lon / 15.0)) # solar offset ≈ standard-time zone
# Crude DST: add an hour in the local warm season (northern springautumn, southern inverse).
month = datetime.now(timezone.utc).month
northern_dst = 3 <= month <= 10
if (lat >= 0 and northern_dst) or (lat < 0 and not northern_dst):
offset += 1
offset = max(-12, min(14, offset))
except Exception:
offset = 0
_utc_offset_cache = offset
return offset
def format_local_time(epoch_seconds: float) -> str:
if epoch_seconds <= 0:
return "Recorded route"
dt = datetime.fromtimestamp(epoch_seconds, timezone(timedelta(hours=utc_offset_hours())))
return f"{dt.strftime('%b')} {dt.day} {dt.strftime('%I:%M %p').lstrip('0').lower()}"
# Dongleless on-device segment directory, e.g. "00000051--3141cf1d76--6".
# The stock tools/lib Route + RE parsers require a 16-hex dongle id + '|' delimiter (cloud naming)
# and reject these, which is why the Routes page was empty. We parse them directly instead.
SEGMENT_DIR_RE = re.compile(r"^(?P<route>[0-9a-f]{8}--[0-9a-z]{10})--(?P<seg>\d+)$")
# Logical camera -> on-disk VIDEO file. Order defines the player's selector order.
# Road uses qcamera.ts (H.264, ~1052x660): small enough to software-decode at hundreds of fps and
# it carries the audio track. Wide/Driver are full-res HEVC (hardware-decoded offroad). The
# streaming decoder handles both containers via ffmpeg's concat demuxer.
CAMERA_FILES: dict[str, str] = {
"road": "qcamera.ts",
"wide": "ecamera.hevc",
"driver": "dcamera.hevc",
}
CAMERA_LABELS: dict[str, str] = {
"road": "Road Cam",
"wide": "Wide Cam",
"driver": "Driver Cam",
}
# The road preview (qcamera.ts) is the only file with an audio track, and the smaller
# cloud-streamable road video. It is not a FrameReader source (TS container).
AUDIO_CAMERA_FILE = "qcamera.ts"
NOMINAL_SEGMENT_SECONDS = 60.0
@dataclass(frozen=True)
class LocalRouteInfo:
name: str
label: str
subtitle: str
segment_count: int
cameras: tuple[str, ...]
modified_at: float
duration_s: float
distance_miles: float | None = None
def _scan_segments(root: Path) -> dict[str, dict[int, Path]]:
"""Group dongleless segment dirs under `root` by route id -> {segment_num: dir}."""
routes: dict[str, dict[int, Path]] = {}
if not root.exists():
return routes
for child in root.iterdir():
try:
if not child.is_dir():
continue
except OSError:
continue
m = SEGMENT_DIR_RE.match(child.name)
if m is None:
continue
routes.setdefault(m.group("route"), {})[int(m.group("seg"))] = child
return routes
def _cameras_present(seg_dir: Path) -> tuple[str, ...]:
present = []
for cam, filename in CAMERA_FILES.items():
try:
if (seg_dir / filename).exists():
present.append(cam)
except OSError:
continue
return tuple(present)
def _format_route_time(ts: float) -> str:
return format_local_time(ts)
def _format_duration(seconds: float) -> str:
s = max(0, int(round(seconds)))
h, rem = divmod(s, 3600)
m, sec = divmod(rem, 60)
if h:
return f"{h}h {m:02d}m"
return f"{m}:{sec:02d}"
def local_route_camera_paths(route_name: str, camera: str = "road", log_root: str | Path | None = None) -> list[str]:
"""Ordered per-segment file paths for one camera of a local route (for FrameReader)."""
root = Path(log_root or Paths.log_root())
segments = _scan_segments(root).get(route_name, {})
filename = CAMERA_FILES.get(camera, CAMERA_FILES["road"])
paths: list[str] = []
for seg_num in sorted(segments):
path = segments[seg_num] / filename
try:
if path.exists():
paths.append(path.as_posix())
except OSError:
continue
return paths
def local_route_audio_paths(route_name: str, log_root: str | Path | None = None) -> list[str]:
"""Ordered per-segment qcamera.ts paths (the only files with an audio track)."""
root = Path(log_root or Paths.log_root())
segments = _scan_segments(root).get(route_name, {})
paths: list[str] = []
for seg_num in sorted(segments):
path = segments[seg_num] / AUDIO_CAMERA_FILE
try:
if path.exists():
paths.append(path.as_posix())
except OSError:
continue
return paths
def local_route_qlog_paths(route_name: str, log_root: str | Path | None = None) -> list[str]:
"""Ordered per-segment qlog paths for a local route."""
root = Path(log_root or Paths.log_root())
segments = _scan_segments(root).get(route_name, {})
paths: list[str] = []
for seg_num in sorted(segments):
for name in ("qlog.zst", "qlog"):
path = segments[seg_num] / name
try:
if path.exists():
paths.append(path.as_posix())
break
except OSError:
continue
return paths
def compute_route_distance_miles(route_name: str, log_root: str | Path | None = None) -> float:
"""Total driven distance (miles) by integrating carState.vEgo over the route's qlogs.
Uses vEgo (not GPS) so it still works on cars with broken GPS. Decimated qlog cadence is
plenty for a distance total. This reads every segment's qlog, so callers should run it off the
UI thread."""
from openpilot.tools.lib.logreader import LogReader
total_m = 0.0
for qlog_path in local_route_qlog_paths(route_name, log_root):
last_t: float | None = None
try:
for msg in LogReader(qlog_path):
if msg.which() != "carState":
continue
t = msg.logMonoTime * 1e-9
v = float(msg.carState.vEgo)
# Guard against segment boundaries / gaps: only integrate contiguous samples.
if last_t is not None and 0.0 < t - last_t < 1.0:
total_m += v * (t - last_t)
last_t = t
except Exception:
continue
return total_m * 0.000621371
def get_local_route(route_name: str, log_root: str | Path | None = None) -> LocalRouteInfo | None:
root = Path(log_root or Paths.log_root())
segments = _scan_segments(root).get(route_name)
if not segments:
return None
return _build_info(route_name, segments)
def _build_info(route_name: str, segments: dict[int, Path]) -> LocalRouteInfo:
seg_nums = sorted(segments)
mtimes = []
for seg_num in seg_nums:
try:
mtimes.append(segments[seg_num].stat().st_mtime)
except OSError:
pass
modified_at = max(mtimes) if mtimes else 0.0
started_at = min(mtimes) if mtimes else 0.0
# Cameras available anywhere in the route (union across segments).
cameras: list[str] = []
for cam in CAMERA_FILES:
if any((segments[s] / CAMERA_FILES[cam]).exists() for s in seg_nums):
cameras.append(cam)
segment_count = len(seg_nums)
duration_s = segment_count * NOMINAL_SEGMENT_SECONDS
cam_names = ", ".join(CAMERA_LABELS[c] for c in cameras) if cameras else "no cameras"
subtitle = f"{_format_duration(duration_s)} · {cam_names}"
return LocalRouteInfo(
name=route_name,
label=_format_route_time(started_at),
subtitle=subtitle,
segment_count=segment_count,
cameras=tuple(cameras),
modified_at=modified_at,
duration_s=duration_s,
)
def list_local_routes(log_root: str | Path | None = None, limit: int = 100) -> list[LocalRouteInfo]:
root = Path(log_root or Paths.log_root())
routes = _scan_segments(root)
infos = [_build_info(name, segs) for name, segs in routes.items()]
infos.sort(key=lambda info: info.modified_at, reverse=True)
return infos[:limit]