1
0
forked from IQ.Lvbs/IQ.Pilot

IQ.Pilot Prebuilt Release @ ab07000

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit 9f9c9a70cc
3729 changed files with 778697 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import time
from functools import lru_cache
from openpilot.common.api import Api
from openpilot.common.time_helpers import system_time_valid
TOKEN_EXPIRY_HOURS = 2
@lru_cache(maxsize=1)
def _get_token(dongle_id: str, t: int):
if not system_time_valid():
raise RuntimeError("System time is not valid, cannot generate token")
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
def get_token(dongle_id: str):
return _get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60)))

View File

@@ -0,0 +1,98 @@
"""Public, konn3kt-agnostic shim to the private cloud route client.
"""
from __future__ import annotations
UPLOAD_NONE = "none"
UPLOAD_UPLOADING = "uploading"
UPLOAD_UPLOADED = "uploaded"
def _load_cloud():
# Load the private cloud client through the verified runtime loader (signed-manifest / tamper
# protection), the same path the hephaestusd daemons use — never a bare sys.path import, which
# would bypass verification. Absent/failed verification -> cloud features stay off.
try:
from openpilot.system.proprietary_runtime._verified_import import import_verified_module
return import_verified_module("iqpilot_hephaestusd_private",
"iqpilot_private.konn3kt.hephaestus.cloud_routes")
except Exception:
return None
_cloud = _load_cloud()
def cloud_available() -> bool:
return _cloud is not None
def get_dongle_id() -> str | None:
if _cloud is None:
return None
try:
return _cloud.get_dongle_id()
except Exception:
return None
def list_cloud_routes(dongle_id: str) -> list:
if _cloud is None:
return []
try:
return _cloud.list_cloud_routes(dongle_id)
except Exception:
return []
def cloud_route_road_segments(dongle_id: str, fullname: str) -> list:
if _cloud is None:
return []
try:
return _cloud.cloud_route_road_segments(dongle_id, fullname)
except Exception:
return []
def cloud_route_camera_urls(dongle_id: str, fullname: str, camera: str = "road") -> list:
if _cloud is None:
return []
try:
return _cloud.cloud_route_camera_urls(dongle_id, fullname, camera)
except Exception:
return []
def request_mp4_conversion(dongle_id: str, segment_canonical_name: str, camera: str):
if _cloud is None:
return None
try:
return _cloud.request_mp4_conversion(dongle_id, segment_canonical_name, camera)
except Exception:
return None
def get_mp4_conversion(dongle_id: str, segment_canonical_name: str, camera: str):
if _cloud is None:
return None
try:
return _cloud.get_mp4_conversion(dongle_id, segment_canonical_name, camera)
except Exception:
return None
def merge_routes(local_routes: list, cloud_routes: list) -> list:
if _cloud is not None:
try:
return _cloud.merge_routes(local_routes, cloud_routes)
except Exception:
pass
return [_LocalOnly(local) for local in local_routes]
class _LocalOnly:
def __init__(self, local):
self.name = local.name
self.local = local
self.cloud = None
self.is_local = True
self.is_cloud = False
self.upload_state = UPLOAD_NONE

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]

35
selfdrive/ui/lib/motd.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from collections.abc import Iterable
FALLBACK_MOTDS = ("Drive safely. Stay focused.",)
_BUNDLE_NAME = "iqpilot_hephaestusd_private"
_MODULE_NAME = "iqpilot_private.konn3kt.hephaestus.motd"
def _dongle_id() -> str | None:
try:
from openpilot.common.params import Params
return Params().get("DongleId", encoding="utf-8")
except Exception:
return None
def _clean_messages(messages: object) -> list[str]:
if not isinstance(messages, Iterable) or isinstance(messages, (str, bytes)):
return []
return [message.strip() for message in messages if isinstance(message, str) and message.strip()]
def load_motds(dongle_id: str | None = None) -> list[str]:
try:
from openpilot.system.proprietary_runtime._verified_import import import_verified_module
module = import_verified_module(_BUNDLE_NAME, _MODULE_NAME)
messages = module.messages_for_dongle(_dongle_id() if dongle_id is None else dongle_id)
cleaned = _clean_messages(messages)
if cleaned:
return cleaned
except Exception:
pass
return list(FALLBACK_MOTDS)

View File

@@ -0,0 +1,150 @@
import json
import platform
from pathlib import Path
from openpilot.common.params import Params
_MAPBOX_DEFAULT_HELPER_UNAVAILABLE = False
_GPS_SERVICES = ("gpsLocationExternal", "gpsLocation")
_POSITION_PARAM_KEYS = ("LastGPSPosition", "LastGPSPositionIQLoc")
def _decode_param(value) -> str:
if isinstance(value, bytes):
return value.decode("utf-8", errors="ignore").strip()
if isinstance(value, str):
return value.strip()
return ""
def resolve_mapbox_token(params: Params | None = None) -> str:
global _MAPBOX_DEFAULT_HELPER_UNAVAILABLE
params = params or Params()
token = _decode_param(params.get("MapboxToken"))
if token:
return token
if _MAPBOX_DEFAULT_HELPER_UNAVAILABLE:
return ""
try:
from openpilot.iqpilot.navd.runtime_common import ensure_default_mapbox_token
except Exception:
_MAPBOX_DEFAULT_HELPER_UNAVAILABLE = True
return ""
for args in ((params,), ()):
try:
token = _decode_param(ensure_default_mapbox_token(*args))
except TypeError:
continue
except Exception:
token = ""
if not token:
token = _decode_param(params.get("MapboxToken"))
if token:
return token
return _decode_param(params.get("MapboxToken"))
def has_mapbox_token(params: Params | None = None) -> bool:
return bool(resolve_mapbox_token(params))
def _valid_lat_lon(lat: float, lon: float) -> bool:
return abs(lat) <= 90.0 and abs(lon) <= 180.0 and (abs(lat) > 1e-4 or abs(lon) > 1e-4)
def _float_field(data: dict, *names: str) -> float:
for name in names:
if name in data:
return float(data.get(name) or 0.0)
return 0.0
def _position_from_json(raw) -> tuple[float, float, float, bool]:
text = _decode_param(raw)
if not text:
return 0.0, 0.0, 0.0, False
try:
data = json.loads(text)
if not isinstance(data, dict):
return 0.0, 0.0, 0.0, False
lat = _float_field(data, "latitude", "lat")
lon = _float_field(data, "longitude", "lon", "lng")
if _valid_lat_lon(lat, lon):
return lat, lon, _float_field(data, "bearing", "bearingDeg"), True
except (TypeError, ValueError, json.JSONDecodeError):
pass
return 0.0, 0.0, 0.0, False
def _position_from_msg(msg, lat_name: str = "latitude", lon_name: str = "longitude",
bearing_name: str = "bearingDeg") -> tuple[float, float, float, bool]:
try:
lat = float(getattr(msg, lat_name, 0.0))
lon = float(getattr(msg, lon_name, 0.0))
if _valid_lat_lon(lat, lon):
return lat, lon, float(getattr(msg, bearing_name, 0.0)), True
except Exception:
pass
return 0.0, 0.0, 0.0, False
def _position_from_params(params: Params) -> tuple[float, float, float, bool]:
for key in _POSITION_PARAM_KEYS:
lat, lon, bearing, valid = _position_from_json(params.get(key))
if valid:
return lat, lon, bearing, True
return 0.0, 0.0, 0.0, False
def current_or_last_gps_position(params: Params | None = None) -> tuple[float, float, float, bool]:
# ui_state is imported lazily AND guarded: night-mode init constructs the ui_state singleton,
# which calls in here before the module finishes importing. In that window the import raises
# (partially initialized module) — fall back to the params path (Night Mode passes self.params),
# since there's no live GPS during boot anyway.
try:
from openpilot.selfdrive.ui.ui_state import ui_state
except ImportError:
ui_state = None
if ui_state is not None:
for service in _GPS_SERVICES:
try:
lat, lon, bearing, valid = _position_from_msg(ui_state.sm[service])
if valid:
return lat, lon, bearing, True
except Exception:
pass
try:
lat, lon, bearing, valid = _position_from_msg(
ui_state.sm["iqNavRenderState"],
lat_name="currentLatitude",
lon_name="currentLongitude",
bearing_name="bearingDeg",
)
if valid:
return lat, lon, bearing, True
except Exception:
pass
explicit_params = params is not None
params = params or (ui_state.params if ui_state is not None else Params())
lat, lon, bearing, valid = _position_from_params(params)
if valid:
return lat, lon, bearing, True
if not explicit_params and platform.system() != "Darwin" and Path("/dev/shm/params/d").exists():
try:
lat, lon, bearing, valid = _position_from_params(Params("/dev/shm/params"))
if valid:
return lat, lon, bearing, True
except Exception:
pass
return 0.0, 0.0, 0.0, False

View File

@@ -0,0 +1,257 @@
"""Destination search + persistence for the offroad Navigate screen.
Uses Mapbox's Search Box API (the same public Mapbox service the nav map preview already calls) for
POI-and-address autocomplete biased to the device's location — so "Walmart" returns the nearest
Walmart store, not a street named Walmart, and partial addresses complete as you type. Selecting a
result writes NavigationDestination, which navd picks up to build the route.
Home/Work/Recents live in a small JSON file on /data (persistent, no new param key needed).
"""
from __future__ import annotations
import json
import threading
import uuid
from dataclasses import dataclass, asdict
import requests
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.nav_helpers import resolve_mapbox_token, current_or_last_gps_position
SEARCHBOX = "https://api.mapbox.com/search/searchbox/v1"
FAVORITES_PATH = "/data/nav_favorites.json"
MAX_RESULTS = 6
MAX_RECENTS = 8
@dataclass
class SearchResult:
name: str
address: str
mapbox_id: str = ""
distance_m: float | None = None
lat: float | None = None
lon: float | None = None
@property
def has_coords(self) -> bool:
return self.lat is not None and self.lon is not None
class NavSearch:
"""Threaded, debounced Search Box autocomplete. The UI calls search() as the user types and reads
results()/searching each frame; a stale query's results are dropped so only the latest shows."""
def __init__(self):
self._params = Params()
self._session = str(uuid.uuid4())
self._lock = threading.Lock()
self._results: list[SearchResult] = []
self._seq = 0
self._last_query = ""
self._searching = False
def new_session(self) -> None:
# A Search Box "session" groups suggest+retrieve for billing; start one per search visit.
self._session = str(uuid.uuid4())
with self._lock:
self._results = []
self._last_query = ""
def results(self) -> list[SearchResult]:
with self._lock:
return list(self._results)
@property
def searching(self) -> bool:
return self._searching
def search(self, query: str) -> None:
query = query.strip()
if query == self._last_query:
return
self._last_query = query
self._seq += 1
seq = self._seq
if len(query) < 2:
with self._lock:
self._results = []
self._searching = False
return
self._searching = True
threading.Thread(target=self._do_search, args=(query, seq), daemon=True).start()
def _do_search(self, query: str, seq: int) -> None:
try:
token = resolve_mapbox_token(self._params)
lat, lon, _, fix = current_or_last_gps_position(self._params)
params = {"q": query, "access_token": token, "session_token": self._session,
"limit": MAX_RESULTS, "language": "en"}
if fix:
params["proximity"] = f"{lon},{lat}"
resp = requests.get(f"{SEARCHBOX}/suggest", params=params, timeout=8)
resp.raise_for_status()
results = []
for s in resp.json().get("suggestions", []):
mid = s.get("mapbox_id")
addr = s.get("full_address") or s.get("place_formatted") or ""
# Skip brand/category refinement rows (e.g. "Walmart · Brand") — not a single routable place.
if not mid or not addr or s.get("feature_type") in ("category", "brand"):
continue
results.append(SearchResult(name=s.get("name", ""), address=addr, mapbox_id=mid,
distance_m=s.get("distance")))
if seq == self._seq:
with self._lock:
self._results = results
except Exception as e:
cloudlog.event("nav_search.suggest_failed", error=str(e))
if seq == self._seq:
with self._lock:
self._results = []
finally:
if seq == self._seq:
self._searching = False
def retrieve(self, result: SearchResult) -> SearchResult | None:
"""Resolve a suggestion's coordinates (Search Box suggest omits them by design)."""
if result.has_coords:
return result
try:
token = resolve_mapbox_token(self._params)
resp = requests.get(f"{SEARCHBOX}/retrieve/{result.mapbox_id}",
params={"access_token": token, "session_token": self._session}, timeout=8)
resp.raise_for_status()
feats = resp.json().get("features", [])
if not feats:
return None
coords = feats[0]["geometry"]["coordinates"]
props = feats[0].get("properties", {})
result.lon, result.lat = float(coords[0]), float(coords[1])
result.name = props.get("name") or result.name
result.address = props.get("full_address") or result.address
return result
except Exception as e:
cloudlog.event("nav_search.retrieve_failed", error=str(e))
return None
# --- destination + favorites persistence -------------------------------------------------------
def set_destination(lat: float, lon: float, name: str) -> None:
"""Hand a destination to navd (it routes off NavigationDestination). Mirrors hephaestusd's
setNavDestination so a fresh route is always recomputed."""
params = Params()
params.remove("AthenaNavigationRoute")
params.put_bool("NavigationActive", False)
# NavigationDestination is a JSON-typed param: pass the object, not a pre-serialized string.
params.put("NavigationDestination", {"latitude": float(lat), "longitude": float(lon), "name": name or ""})
def cancel_navigation() -> None:
"""Clear the active route/destination so navd stops navigating."""
params = Params()
params.remove("NavigationDestination")
params.remove("AthenaNavigationRoute")
params.put_bool("NavigationActive", False)
def has_active_destination() -> bool:
try:
return bool(Params().get("NavigationDestination"))
except Exception:
return False
def _load_favorites() -> dict:
try:
with open(FAVORITES_PATH) as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _save_favorites(data: dict) -> None:
try:
tmp = FAVORITES_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f)
import os
os.replace(tmp, FAVORITES_PATH)
except Exception as e:
cloudlog.event("nav_search.save_favorites_failed", error=str(e))
def _place_to_result(place: dict | None) -> SearchResult | None:
if not place:
return None
try:
return SearchResult(name=place.get("name", ""), address=place.get("address", ""),
lat=float(place["lat"]), lon=float(place["lon"]))
except Exception:
return None
def get_home() -> SearchResult | None:
return _place_to_result(_load_favorites().get("home"))
def get_work() -> SearchResult | None:
return _place_to_result(_load_favorites().get("work"))
def _place_dict(r: SearchResult) -> dict:
return {"name": r.name, "address": r.address, "lat": r.lat, "lon": r.lon}
def save_home(r: SearchResult) -> None:
data = _load_favorites()
data["home"] = _place_dict(r)
_save_favorites(data)
def save_work(r: SearchResult) -> None:
data = _load_favorites()
data["work"] = _place_dict(r)
_save_favorites(data)
def remove_home() -> None:
data = _load_favorites()
data.pop("home", None)
_save_favorites(data)
def remove_work() -> None:
data = _load_favorites()
data.pop("work", None)
_save_favorites(data)
def remove_recent(r: SearchResult) -> None:
data = _load_favorites()
data["recents"] = [p for p in data.get("recents", [])
if not (abs(p.get("lat", 0) - (r.lat or 0)) < 1e-5 and abs(p.get("lon", 0) - (r.lon or 0)) < 1e-5)]
_save_favorites(data)
def get_recents() -> list[SearchResult]:
out = []
for p in _load_favorites().get("recents", []):
r = _place_to_result(p)
if r is not None:
out.append(r)
return out
def add_recent(r: SearchResult) -> None:
if not r.has_coords:
return
data = _load_favorites()
recents = [p for p in data.get("recents", [])
if not (abs(p.get("lat", 0) - r.lat) < 1e-5 and abs(p.get("lon", 0) - r.lon) < 1e-5)]
recents.insert(0, _place_dict(r))
data["recents"] = recents[:MAX_RECENTS]
_save_favorites(data)

View File

@@ -0,0 +1,144 @@
from enum import IntEnum
import os
import threading
import time
from pathlib import Path
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.time_helpers import system_time_valid
from openpilot.iqpilot.konn3kt.cloud_client import Konn3ktApi
from openpilot.iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID, get_cached_dongle_id, ensure_dev_pairing_identity
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
class PairState(IntEnum):
UNKNOWN = -2
UNPAIRED = -1
PAIRED = 0
class PrimeState:
FETCH_INTERVAL = 5.0 # seconds between konn3kt pairing checks
API_TIMEOUT = 10.0 # seconds for konn3kt API requests
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
def __init__(self):
self._params = Params()
self._lock = threading.Lock()
# Must be computed at runtime (OPENPILOT_PREFIX can change paths).
# Keep a writable fallback in /tmp in case /persist becomes read-only.
self._konn3kt_state_paths = [
Path(Paths.persist_root()) / "comma" / "konn3kt_prime_type",
Path(Paths.config_root()) / "konn3kt_prime_type",
]
if PC and os.getenv("KONN3KT_DEV_PAIRING") == "1":
try:
ensure_dev_pairing_identity(self._params, force_reset=os.getenv("KONN3KT_DEV_PAIRING_RESET") == "1")
self._write_cached_state(PairState.UNPAIRED)
except Exception as e:
cloudlog.error(f"dev pairing identity setup failed: {e}")
self.pair_state: PairState = self._load_initial_state()
self._running = False
self._thread = None
def _write_cached_state(self, pair_state: PairState) -> None:
payload = str(int(pair_state))
for path in self._konn3kt_state_paths:
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(payload)
return
except OSError:
continue
except Exception:
cloudlog.exception("failed to write konn3kt pairing cache")
return
cloudlog.warning("failed to write konn3kt pairing cache to any path")
def _coerce(self, value: int | None) -> PairState:
if value is None:
return PairState.UNKNOWN
if value >= 0:
return PairState.PAIRED
if value == PairState.UNPAIRED:
return PairState.UNPAIRED
return PairState.UNKNOWN
def _load_initial_state(self) -> PairState:
env_val = os.getenv("PRIME_TYPE")
if env_val is not None:
try:
return self._coerce(int(env_val))
except (ValueError, TypeError):
pass
for path in self._konn3kt_state_paths:
try:
if path.is_file():
return self._coerce(int(path.read_text().strip()))
except Exception:
cloudlog.exception("failed to read konn3kt pairing cache")
return PairState.UNKNOWN
def _refresh_pair_status(self) -> None:
dongle_id = get_cached_dongle_id(self._params, prefer_readonly=True)
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
# the JWT can't be minted until the clock is NTP-synced; at boot skip
# quietly instead of error-spamming every retry
if not system_time_valid():
return
try:
api = Konn3ktApi(dongle_id)
resp = api.get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=api.get_token())
if resp.status_code == 200:
paired = bool(resp.json().get("is_paired", False))
self.set_paired(paired)
elif resp.status_code == 404:
self.set_paired(False)
except Exception as e:
cloudlog.error(f"failed to fetch konn3kt pairing status: {e}")
def set_paired(self, paired: bool) -> None:
new_state = PairState.PAIRED if paired else PairState.UNPAIRED
with self._lock:
if new_state != self.pair_state:
self.pair_state = new_state
self._write_cached_state(new_state)
cloudlog.info(f"konn3kt pairing updated to {new_state}")
def _worker_thread(self) -> None:
from openpilot.selfdrive.ui.ui_state import ui_state, device
while self._running:
if not ui_state.started and device._awake:
self._refresh_pair_status()
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
if not self._running:
break
time.sleep(self.SLEEP_INTERVAL)
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._running = True
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
self._thread.start()
def stop(self) -> None:
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
def is_paired(self) -> bool:
with self._lock:
return self.pair_state > PairState.UNPAIRED
def __del__(self):
self.stop()

36
selfdrive/ui/lib/solar.py Normal file
View File

@@ -0,0 +1,36 @@
import math
from datetime import UTC, datetime
_J2000 = datetime(2000, 1, 1, 12, 0, 0, tzinfo=UTC)
SUNSET_ELEVATION_DEG = -0.833 # standard sunset/sunrise threshold, corrected for atmospheric refraction
def sun_elevation_deg(lat: float, lon: float, dt_utc: datetime | None = None) -> float:
"""Low-precision solar elevation angle (accurate to well under a degree), per the
standard Meeus-derived approximation. lat/lon in degrees (lon positive east)."""
dt_utc = dt_utc or datetime.now(UTC)
n = (dt_utc - _J2000).total_seconds() / 86400.0
mean_lon = math.radians((280.460 + 0.9856474 * n) % 360)
mean_anomaly = math.radians((357.528 + 0.9856003 * n) % 360)
ecliptic_lon = (mean_lon + math.radians(1.915) * math.sin(mean_anomaly)
+ math.radians(0.020) * math.sin(2 * mean_anomaly))
obliquity = math.radians(23.439 - 0.0000004 * n)
declination = math.asin(math.sin(obliquity) * math.sin(ecliptic_lon))
right_ascension = math.atan2(math.cos(obliquity) * math.sin(ecliptic_lon), math.cos(ecliptic_lon))
equation_of_time_deg = math.degrees(mean_lon - right_ascension)
equation_of_time_deg = (equation_of_time_deg + 180) % 360 - 180
utc_hours = dt_utc.hour + dt_utc.minute / 60 + dt_utc.second / 3600
hour_angle = math.radians(15 * (utc_hours - 12) + lon + equation_of_time_deg)
lat_rad = math.radians(lat)
elevation = math.asin(math.sin(lat_rad) * math.sin(declination)
+ math.cos(lat_rad) * math.cos(declination) * math.cos(hour_angle))
return math.degrees(elevation)
def is_after_sunset(lat: float, lon: float, dt_utc: datetime | None = None) -> bool:
return sun_elevation_deg(lat, lon, dt_utc) < SUNSET_ELEVATION_DEG

View File

@@ -0,0 +1,55 @@
import time
import threading
import subprocess
# Shared, throttled current-Wi-Fi-SSID lookup for the status bars (home pill + onroad sidebar).
# The SSID isn't in deviceState, so we shell out to NetworkManager on a background thread and cache
# the result; the render thread only ever reads the cached string.
_ssid = ""
_last_fetch = 0.0
_fetching = False
_lock = threading.Lock()
REFRESH_SECONDS = 10.0
def _read_ssid() -> str:
try:
out = subprocess.run(["nmcli", "-t", "-f", "active,ssid", "dev", "wifi"],
capture_output=True, text=True, timeout=4).stdout
for line in out.splitlines():
if line.startswith("yes:"):
name = line.split(":", 1)[1].strip()
if name:
return name
except Exception:
pass
try:
name = subprocess.run(["iwgetid", "-r"], capture_output=True, text=True, timeout=4).stdout.strip()
if name:
return name
except Exception:
pass
return ""
def _fetch() -> None:
global _ssid, _last_fetch, _fetching
name = _read_ssid()
with _lock:
_ssid = name
_last_fetch = time.monotonic()
_fetching = False
def current_ssid(on_wifi: bool) -> str:
"""Return the connected SSID (or "" if unknown / not on Wi-Fi). Kicks a throttled background
refresh; never blocks the caller."""
global _fetching
if not on_wifi:
return ""
with _lock:
if not _fetching and time.monotonic() - _last_fetch > REFRESH_SECONDS:
_fetching = True
threading.Thread(target=_fetch, daemon=True).start()
return _ssid