Files
2026-10-25 17:41:51 -05:00

131 lines
4.5 KiB
Python

"""Read-only Jetlink status for model feedback and Android Auto, polled off the drawing thread."""
from __future__ import annotations
from dataclasses import dataclass
import json
import math
import os
import threading
import time
STALE_NS = 2_500_000_000
# The host's processing time for the last big-model frame (excludes USB); jetlink's runtime record does not carry it
LATENCY_RECORD = "/dev/shm/jetlink/starpilot_latency.json"
LATENCY_FRESH_S = 3.
RUNNING = {'ready': 'ready', 'joining': 'connecting', 'retrying': 'connecting'}
@dataclass(frozen=True)
class JetlinkStatus:
device: str = ''
state: str = 'off' # off, disconnected, connected, connecting, ready, active, unavailable
latency_ms: float | None = None
port: str | None = None # jetlink's port_state: 'connected' (host on the link), 'device' (plugged in), else None
def _write_latency(model, path) -> None:
timings = getattr(getattr(model, "client", None), "last_timings", None) # the big model's client while it drives
total_us = timings[2] if isinstance(timings, (tuple, list)) and len(timings) == 3 else 0
record = {"at": time.monotonic(), "server_ms": total_us / 1000 if type(total_us) in (int, float) and total_us > 0 else None}
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = f"{path}.tmp"
with open(tmp, "w") as f:
json.dump(record, f)
os.replace(tmp, path)
def start_latency_writer(model, path=LATENCY_RECORD, period=1.):
"""modeld, before it goes realtime, so the thread keeps normal priority and the frame loop does no file IO."""
def run():
while True:
try:
_write_latency(model, path)
except Exception:
pass
time.sleep(period)
thread = threading.Thread(target=run, name="jetlink-latency", daemon=True)
thread.start()
return thread
def read_latency(path=LATENCY_RECORD) -> float | None:
try:
with open(path) as f:
record = json.load(f)
ms, at = record.get("server_ms"), record.get("at")
fresh = type(at) in (int, float) and 0 <= time.monotonic() - at < LATENCY_FRESH_S
return float(ms) if fresh and type(ms) in (int, float) and math.isfinite(ms) and ms > 0 else None
except (OSError, ValueError, AttributeError):
return None
def read_status() -> JetlinkStatus:
from openpilot.starpilot.models.jetlink_adapter import hooks, link_on
if not link_on():
return JetlinkStatus() # one file read: nothing of jetlink's loads
status = hooks().status()
if status is None:
return JetlinkStatus(state='unavailable')
if not status.enabled:
return JetlinkStatus() # a Chestnut is fitted
device = 'iPhone' if status.mode == 'ios' else 'Computer'
port = status.port_state if status.port_state in ('connected', 'device') else None
if not status.present:
return JetlinkStatus(device, 'disconnected', port=port)
if status.active:
return JetlinkStatus(device, 'active', read_latency(), port)
if status.reason:
return JetlinkStatus(device, 'unavailable', port=port)
return JetlinkStatus(device, RUNNING.get(status.running, 'ready' if status.ready else 'connected'), port=port)
class JetlinkStatusFeed:
"""Polls once a second only while enabled; snapshot() never does file IO."""
def __init__(self, read=read_status, period=1.0):
self.read, self.period = read, period
self._lock = threading.Lock()
self._stop = threading.Event()
self._thread = None
self._enabled = False
self._status = JetlinkStatus()
self._read_at = 0
def set_enabled(self, enabled):
with self._lock:
self._enabled = enabled
if not enabled:
self._status = JetlinkStatus()
if enabled and self._thread is None:
self._thread = threading.Thread(target=self._run, name='jetlink-status', daemon=True)
self._thread.start()
def poll(self):
with self._lock:
if not self._enabled:
return
try:
status = self.read()
except Exception:
status = JetlinkStatus(state='unavailable')
with self._lock:
if self._enabled:
self._status = status
self._read_at = time.monotonic_ns()
def snapshot(self):
with self._lock:
status, read_at = self._status, self._read_at
if status.state != 'off' and time.monotonic_ns() - read_at > STALE_NS:
return JetlinkStatus(status.device, 'unavailable')
return status
def _run(self):
while not self._stop.is_set():
self.poll()
self._stop.wait(self.period)
def close(self):
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=2)