Add the setup ladder: try handshakes in order, remember the one that worked

One module now decides how to answer a car's handshake, and it is also the
learned profile: the strategy that last worked is the one tried first.

The first pass is today's behavior. Later rungs are knobs that already
existed: the other version reply (the flip the dongle work added, now owned
here), the phone-style reply with both bands and with 2.4 GHz only, a longer
wait for a car that keeps saying not ready, and, for adapters only, the
handshake variations.

A failure advances the ladder only when the car answered the version
exchange, then went silent or refused the Wi-Fi setup, before any video was
acknowledged in the session. A sleeping car, this comma's own Bluetooth
trouble and anything after video (a car shutting off) never move it. Three
passes per boot at most, resuming at the first untried rung on the next
ignition; a learned strategy is dropped after two handshake failures; a
session that streams for a minute makes its strategy the learned one.

State is a small per-car file beside the config (setup_state.json, newest 16
cars, damaged or unreadable means a fresh start, never blocks a connection).
The status exposes the ladder for the setup screen, and reports carry what
was tried, how each rung ended and how long the car took to be ready.
This commit is contained in:
dirwin31
2026-11-01 14:12:37 -08:00
committed by Danny
parent 2af7bdd1dd
commit 57900f3d73
7 changed files with 556 additions and 10 deletions
@@ -266,11 +266,14 @@ class WirelessBootstrap:
def __init__(self, sock, log: Callable[..., None], *, device_serial: str = "starpilot",
version_status: int = STATUS_SUCCESS, stage_timeout: float = 20.0, start_request_delay: float = 5.0,
initial_kick_delay: float = INITIAL_KICK_SECONDS, join_ping_interval: float = JOIN_PING_INTERVAL,
experiment: str = "", modern_reply: bool = False):
experiment: str = "", modern_reply: bool = False, patience: int = REFUSAL_PATIENCE,
channel_override: int | None = None):
self.sock = sock
self.experiment = experiment if valid_experiment(experiment) else ""
self.lifetime_id = uuid.uuid4().hex
self.modern_reply = modern_reply
self.patience = patience # stage timeouts a car that keeps saying "not ready" gets (setup ladder)
self.channel_override = channel_override # Wi-Fi band to answer with inside the phone-style reply; None keeps the car's own
self.version_major = 0
self.car_started = False
self.flags = EXPERIMENTS[self.experiment][1] if self.experiment else set()
@@ -337,7 +340,7 @@ class WirelessBootstrap:
self.version_major = major
if major < MODERN_MAJOR or not self.modern_reply:
return field(1, major) + field(2, minor) + field(3, self.device_serial) + field(4, self.version_status)
selected = channel_type(request)
selected = self.channel_override or channel_type(request)
session = field(1, self.device_serial) + field(2, self.lifetime_id)
reply = (field(1, major) + field(2, minor) + field(3, self.device_serial) + field(4, MODERN_VERSION_SUCCESS) +
field(5, selected) + field(6, session))
@@ -376,7 +379,7 @@ class WirelessBootstrap:
# exchange if the car has not started it; newer head units (2025 Honda) wait for it.
start_request_at: float | None = None
deadline = time.monotonic() + self.stage_timeout
latest_deadline = time.monotonic() + REFUSAL_PATIENCE * self.stage_timeout
latest_deadline = time.monotonic() + self.patience * self.stage_timeout
initial_kick_at: float | None = time.monotonic() + self.initial_kick_delay
hint_ready_at: float | None = None
while endpoint is None:
@@ -202,6 +202,22 @@ def session_setup(events: list[dict]) -> dict:
) if value}
def session_ladder(events: list[dict]) -> list[dict]:
"""The handshake strategies tried in a session, in order, and how each ended (advanced, wrapped, exhausted, counted,
dropped, or worked). Kept for working pairings too: it is what shows which strategy a head unit likes."""
tried: list[dict] = []
for event in events:
name = event["event"]
if name == "ladder_strategy":
tried.append({"rung": str(event.get("rung", ""))[:24], "learned": bool(event.get("learned")),
"at_s": elapsed(events[0], event)})
elif name == "ladder_failed" and tried:
tried[-1]["outcome"] = str(event.get("outcome", ""))[:16]
elif name == "ladder_worked" and tried:
tried[-1].update(outcome="worked", streamed_s=event.get("streamed_s"), t_ready_s=event.get("t_ready_s"))
return [{key: value for key, value in item.items() if value is not None} for item in tried[-12:]]
def session_strategy(events: list[dict], setup: dict) -> dict:
"""What a streaming session ran with; the raw material for "this head unit usually likes this profile"."""
if not any(event["event"] == "video_acknowledged" for event in events):
@@ -248,6 +264,7 @@ def session_record(path: Path) -> dict:
# True when the comma's address went to the car in the pairing request; None when the car never opened that channel.
"phone_address_sent": bool(opened.get("phone_address")) if opened is not None else None,
"strategy": session_strategy(events, setup),
"ladder": session_ladder(events),
"software": {key: str(software.get(key) or "unknown") for key in ("commit", "branch")} if isinstance(software, dict) else {},
"settings": settings if isinstance(settings, dict) else {},
"seq": _session_number(path),
@@ -464,6 +481,7 @@ def drive_event(sessions: list[dict]) -> dict:
label = _head_unit_label(head_unit)
setup = next((s["setup"] for s in reversed(sessions) if s.get("setup")), {})
sent = next((s["phone_address_sent"] for s in reversed(sessions) if s.get("phone_address_sent") is not None), None)
ladder = next((s["ladder"][-1] for s in reversed(sessions) if s.get("ladder")), {})
failures = [failure_detail(failure) for failure in drive_failures(sessions)]
failed = [session for session in sessions if session["seq"] in {failure["session"] for failure in failures}]
heading = "failures" if failures else "sessions"
@@ -499,6 +517,7 @@ def drive_event(sessions: list[dict]) -> dict:
**({"aa.adapter_model": setup["profile"]} if setup.get("profile") else {}),
**({"aa.profile_source": setup["profile_source"]} if setup.get("profile_source") else {}),
**({"aa.phone_address_sent": "yes" if sent else "no"} if sent is not None else {}),
**({"aa.ladder_rung": ladder["rung"], "aa.ladder_learned": "yes" if ladder.get("learned") else "no"} if ladder else {}),
**({"aa.attention": ",".join(reasons)[:190]} if reasons else {}),
**({"aa.cause": latest["cause"], "aa.stage": latest["stage"], "aa.error_kind": latest["error_kind"]} if latest else {}),
**({"aa.reach_class": latest["reach"]} if latest and latest.get("reach") else {}),
@@ -672,6 +672,7 @@ class ProjectionSession(Session):
self.config_ack_slack = 0 # codec-config messages a head unit may acknowledge like frames
self.epoch_acked = 0
self.video_confirmed = False
self.video_confirmed_at = 0.0
self.input_channel: int | None = None
self.max_window = self.MAX_WINDOW # raised by the supervisor for adapters that need the car's full window
self.allow_hevc = False # the owner's Allow H.265 video setting; used only when the car offers no H.264 mode
@@ -950,6 +951,7 @@ class ProjectionSession(Session):
# bound on acknowledged frames without changing flow-control credit.
if not self.video_confirmed and self.epoch_acked > self.config_ack_slack:
self.video_confirmed = True
self.video_confirmed_at = time.monotonic()
self.event("video_acknowledged", session=self.session_id, ack_ms=round(self.max_ack_seconds * 1000))
# ANDROID AUTO VIDEO PERFORMANCE GUARDRAIL: Receiver ACK flow control bounds in-flight video; extra buffering increases latency.
@@ -0,0 +1,257 @@
"""Which wireless handshake to try for a car, in what order, and which one last worked.
The ladder is the one place that decides this. It owns the flip between the standard and phone-style version reply that
the dongle work added, and it is also the "learned profile": the rung that last worked is simply the strategy used first
the next time.
One rule decides everything: **a failure advances the ladder only when the car answered the version exchange and no video
was acknowledged yet in this session.** A sleeping car, this comma's own Bluetooth trouble, a car that shuts off after the
drive has streamed, anything that is not the handshake being refused: none of these say anything about which strategy the
car prefers, so none of them move it. Without that rule every ignition with an asleep car would walk the whole ladder and
a car that works fine would end up running the dongle experiments.
The first pass for a car is exactly what ran before (rung ``standard``); the later rungs are knobs that already existed.
"""
from __future__ import annotations
import json
import os
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from openpilot.starpilot.system.starport.bootstrap import CHANNEL_24GHZ, CHANNEL_DUAL, MODERN_MAJOR, REFUSAL_PATIENCE
from openpilot.starpilot.system.starport.identity import DATA_DIR
STATE_PATH = DATA_DIR / "setup_state.json"
STATE_VERSION = 1
MAX_CARS = 16
MAX_PASSES = 3 # full runs through the ladder per boot (one boot is one ignition cycle); not yet confirmed against real cars
DROP_AFTER = 2 # failures at the remembered strategy before it is forgotten and the ladder starts over
CLEAN_SECONDS = 60.0 # a session that streamed this long without ending in a handshake failure counts as working
LONG_PATIENCE = 2 * REFUSAL_PATIENCE
RUNGS = ("standard", "other_reply", "modern_dual", "modern_24ghz", "patient", "experiments")
LABELS = {
"standard": "Standard handshake",
"other_reply": "The other version reply",
"modern_dual": "Phone-style reply, both Wi-Fi bands",
"modern_24ghz": "Phone-style reply, 2.4 GHz",
"patient": "Waiting longer for the car",
"experiments": "Adapter handshake variations",
}
MODERN_RUNGS = ("other_reply", "modern_dual", "modern_24ghz") # only mean something to a car that speaks protocol 4 or newer
@dataclass(frozen=True)
class Strategy:
rung: str
modern_reply: bool
channel_type: int | None = None # forces the Wi-Fi band reply inside the phone-style reply; None keeps the car's own preference
patience: int = REFUSAL_PATIENCE
experiments: bool = False
def as_dict(self) -> dict:
return {"rung": self.rung, "modern_reply": self.modern_reply, "channel_type": self.channel_type,
"patience": self.patience, "experiments": self.experiments}
@property
def label(self) -> str:
return LABELS.get(self.rung, self.rung)
def resolve(rung: str, base_modern: bool) -> Strategy:
"""The concrete strategy for a rung, relative to the car's remembered reply style (today's behavior)."""
if rung == "other_reply":
return Strategy(rung, not base_modern)
if rung == "modern_dual":
return Strategy(rung, True, CHANNEL_DUAL)
if rung == "modern_24ghz":
return Strategy(rung, True, CHANNEL_24GHZ)
if rung == "patient":
return Strategy(rung, base_modern, patience=LONG_PATIENCE)
if rung == "experiments":
return Strategy(rung, base_modern, experiments=True)
return Strategy("standard", base_modern)
def applicable(rung: str, version_major: int, adapter: bool) -> bool:
if rung in MODERN_RUNGS:
return version_major >= MODERN_MAJOR
if rung == "experiments":
return adapter
return True
def _boot_id() -> str:
try:
return Path("/proc/sys/kernel/random/boot_id").read_text().strip()
except OSError:
return ""
def _valid_strategy(value) -> Strategy | None:
if not isinstance(value, dict) or value.get("rung") not in RUNGS:
return None
channel, patience = value.get("channel_type"), value.get("patience")
if type(value.get("modern_reply")) is not bool or type(value.get("experiments")) is not bool:
return None
if channel not in (None, CHANNEL_24GHZ, CHANNEL_DUAL, 1) or type(patience) is not int or not 1 <= patience <= 12:
return None
return Strategy(value["rung"], value["modern_reply"], channel, patience, value["experiments"])
def _fresh() -> dict:
return {"rung": "standard", "results": {}, "passes": 0, "boot": "", "working": None, "working_failures": 0,
"t_ready": [], "successes": 0, "updated": 0.0}
def _clean(entry) -> dict:
"""A saved entry, or a fresh one when anything about it is not what this module writes."""
fresh = _fresh()
if not isinstance(entry, dict):
return fresh
try:
rung = entry.get("rung")
results = entry.get("results")
if rung in RUNGS:
fresh["rung"] = rung
if isinstance(results, dict):
fresh["results"] = {key: value for key, value in results.items() if key in RUNGS and value in ("failed", "skipped", "worked")}
for key in ("passes", "working_failures", "successes"):
if type(entry.get(key)) is int and 0 <= entry[key] <= 10_000:
fresh[key] = entry[key]
fresh["boot"] = entry["boot"][:64] if isinstance(entry.get("boot"), str) else ""
working = _valid_strategy(entry.get("working"))
fresh["working"] = working.as_dict() if working else None
fresh["t_ready"] = [round(float(value), 1) for value in entry.get("t_ready", []) if isinstance(value, (int, float)) and 0 <= value <= 3600][-8:]
fresh["updated"] = float(entry["updated"]) if isinstance(entry.get("updated"), (int, float)) else 0.0
except (TypeError, ValueError, KeyError):
return _fresh()
return fresh
class SetupLadder:
def __init__(self, path: Path | None = None, boot_id=_boot_id, clock=time.time):
self.path = path or STATE_PATH
self.boot_id, self.clock = boot_id, clock
self.cars: dict[str, dict] = {}
self.load()
def load(self) -> None:
try:
data = json.loads(self.path.read_text())
cars = data["cars"] if isinstance(data, dict) and data.get("version") == STATE_VERSION and isinstance(data.get("cars"), dict) else {}
except (OSError, ValueError, KeyError, TypeError, UnicodeError):
cars = {}
self.cars = {str(address).upper(): _clean(entry) for address, entry in list(cars.items())[:MAX_CARS]}
def save(self) -> bool:
ordered = dict(sorted(self.cars.items(), key=lambda item: item[1]["updated"], reverse=True)[:MAX_CARS])
self.cars = ordered
try:
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(dir=self.path.parent, prefix=".setup-state-")
try:
with os.fdopen(fd, "w") as handle:
json.dump({"version": STATE_VERSION, "cars": ordered}, handle, separators=(",", ":"))
handle.flush()
os.fsync(handle.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, self.path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
except OSError:
return False # progress is a convenience; a read-only disk must never stop a connection
return True
def _entry(self, address: str) -> dict:
entry = self.cars.setdefault(address.upper(), _fresh())
boot = self.boot_id()
if entry["boot"] != boot: # a new ignition cycle gets its full passes again and resumes at the first untried rung
entry["boot"], entry["passes"] = boot, 0
return entry
def forget(self, address: str) -> bool:
"""A car's pairing was deleted: drop what was learned about it."""
removed = self.cars.pop(address.upper(), None) is not None
if removed:
self.save()
return removed
def exhausted(self, address: str) -> bool:
entry = self.cars.get(address.upper())
return bool(entry) and entry["boot"] == self.boot_id() and entry["passes"] >= MAX_PASSES and not entry["working"]
def strategy(self, address: str, base_modern: bool) -> Strategy:
"""What to try now: the strategy that worked, else the current rung (standard once the passes are used up)."""
entry = self._entry(address)
working = _valid_strategy(entry["working"])
if working:
return working
if entry["passes"] >= MAX_PASSES:
return resolve("standard", base_modern)
return resolve(entry["rung"], base_modern)
def failed(self, address: str, strategy: Strategy, *, ladder_failure: bool, video_seen: bool, version_major: int,
adapter: bool) -> str | None:
"""Record a failed attempt; returns what changed ("advanced", "wrapped", "exhausted", "counted", "dropped") or None.
Only a handshake the car answered, before any video this session, counts: see the module docstring."""
if not ladder_failure or video_seen:
return None
entry = self._entry(address)
entry["updated"] = self.clock()
working = _valid_strategy(entry["working"])
if working and strategy.rung == working.rung:
entry["working_failures"] += 1
if entry["working_failures"] < DROP_AFTER:
self.save()
return "counted"
entry.update(working=None, working_failures=0, rung="standard", results={})
self.save()
return "dropped"
if entry["passes"] >= MAX_PASSES:
return None
entry["results"][strategy.rung] = "failed"
start = RUNGS.index(strategy.rung) if strategy.rung in RUNGS else 0
outcome = "wrapped"
for rung in RUNGS[start + 1:]:
if applicable(rung, version_major, adapter):
entry["rung"], outcome = rung, "advanced"
break
entry["results"][rung] = "skipped"
if outcome == "wrapped":
entry["passes"] += 1
entry["rung"], entry["results"] = "standard", {}
if entry["passes"] >= MAX_PASSES:
outcome = "exhausted"
self.save()
return outcome
def worked(self, address: str, strategy: Strategy, t_ready_s: float | None = None) -> None:
entry = self._entry(address)
entry.update(working=strategy.as_dict(), working_failures=0, rung=strategy.rung, results={strategy.rung: "worked"},
passes=0, successes=entry["successes"] + 1, updated=self.clock())
if t_ready_s is not None and 0 <= t_ready_s <= 3600:
entry["t_ready"] = (entry["t_ready"] + [round(float(t_ready_s), 1)])[-8:]
self.save()
def snapshot(self, address: str, adapter: bool = False) -> dict:
"""What the setup screen and the reports show: every rung, its state, and whether this car has a learned strategy."""
entry = self.cars.get(address.upper()) if address else None
entry = entry or _fresh()
working = _valid_strategy(entry["working"])
current = working.rung if working else entry["rung"]
passes = entry["passes"] if entry["boot"] == self.boot_id() else 0 # passes belong to one ignition cycle
steps = []
for rung in RUNGS:
if rung == "experiments" and not adapter:
continue
state = entry["results"].get(rung) or ("current" if rung == current else "waiting")
steps.append({"id": rung, "label": LABELS[rung], "state": state})
return {"steps": steps, "current": current, "learned": bool(working), "passes": passes, "max_passes": MAX_PASSES,
"exhausted": passes >= MAX_PASSES and not working, "successes": entry["successes"],
"t_ready_s": entry["t_ready"][-1] if entry["t_ready"] else None}
@@ -33,9 +33,9 @@ from pathlib import Path
from typing import Any
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.starpilot.system.starport import car_profiles, device_health, identity as identity_store, reachability, system_slice
from openpilot.starpilot.system.starport import car_profiles, device_health, identity as identity_store, new_setup, reachability, setup_ladder, system_slice
from openpilot.starpilot.system.starport.auto_connect import AutoConnectPolicy
from openpilot.starpilot.system.starport.bootstrap import MODERN_MAJOR, NAMES, BootstrapError, WirelessBootstrap
from openpilot.starpilot.system.starport.bootstrap import MODERN_MAJOR, NAMES, BootstrapError, BootstrapTimeout, WirelessBootstrap
from openpilot.starpilot.system.starport.bugsink_report import PairingAttempt
from openpilot.starpilot.system.starport.frame_source import (DEFAULT_PATH as DEFAULT_FRAME_PATH, FLAG_ASYNC_READBACK, FLAG_NV12,
FORMAT_NV12, FrameRequest)
@@ -374,6 +374,10 @@ class Supervisor:
self._car_seen_at = -CAR_LINK_HOLD
self._hfp_link = threading.Event() # set when the chosen car opens the hands-free link
self._modern_reply: dict[str, bool] = {} # car address -> answer its v4+ version request like a current phone
self._ladder = setup_ladder.SetupLadder(identity_store.CONFIG_PATH.with_name("setup_state.json")) # which handshake to try for each car, and which one last worked
self._strategy: setup_ladder.Strategy | None = None # the one this attempt runs
self._video_seen = False # video was acknowledged in this session: later failures say nothing about the handshake
self._run_started = 0.0
self.auto = AutoConnectPolicy()
self._companion_retry_at = 0.0
self.log = EventLog()
@@ -418,6 +422,8 @@ class Supervisor:
status["dedicated_wifi_interface"] = bool(self.config["dedicated_wifi_interface"])
status["allow_hevc"] = bool(self.config["allow_hevc"])
status["car_candidates"] = list(getattr(self, "_candidates", []))[:8] # paired Android Auto devices to choose between
status["new_setup"] = new_setup.enabled()
status["setup_ladder"] = self._ladder.snapshot(self.config["receiver_address"], self.car_type() == "adapter")
status["auto_paused"] = self.auto.suppressed
status["recent"] = list(self.log.recent)[-8:]
return status
@@ -534,6 +540,7 @@ class Supervisor:
return False # nothing of this device was kept
self.config.update(rfcomm_cache=cache, modern_reply_cache=replies)
self._modern_reply.pop(address, None)
self._ladder.forget(address)
if chosen:
if self._bluez is not None:
self._bluez.release()
@@ -1190,6 +1197,7 @@ class Supervisor:
connection = self.config["connection"]
wireless = connection == "wireless"
receiver = {"wired": "usb", "headunitpad": "headunitpad", "openheadunit": "openheadunit"}.get(connection, self.config["receiver_name"])
self._video_seen, self._strategy, self._run_started = False, None, time.monotonic()
from openpilot.starpilot.system.starport.bugsink_transport import device_metadata
software = device_metadata()
profile, profile_source = car_profiles.resolve(self.config)
@@ -1285,6 +1293,7 @@ class Supervisor:
config = self.config
address = config["receiver_address"]
ident = identity_store.load_identity()
self._strategy = None
self._stage("connecting_bluetooth", config["receiver_name"])
bluez = self._phone()
@@ -1318,21 +1327,35 @@ class Supervisor:
self.log("rfcomm_channel", channel=channel, source=source)
boot = None
adapter = self.car_type() == "adapter"
# The ladder decides how to answer this car: what worked before, else the first rung (today's behavior), moving on
# only when a car that answered the handshake keeps refusing it before any video.
base_modern = self._modern_reply.get(address, config.get("modern_reply_cache", {}).get(address, False))
strategy = self._strategy = self._ladder.strategy(address, base_modern)
learned = self._ladder.snapshot(address, adapter)["learned"]
self.log("ladder_strategy", rung=strategy.rung, label=strategy.label, learned=learned, modern_reply=strategy.modern_reply,
channel_type=strategy.channel_type, patience=strategy.patience, experiments=strategy.experiments)
try:
self._stage("rfcomm", f"channel {channel}")
rfcomm = self._track(bt_sockets.connect_rfcomm(address, channel))
boot = WirelessBootstrap(rfcomm, self._bootstrap_log, device_serial=config["device_name"],
version_status=int(config.get("version_status", 0)),
experiment="all" if car_profiles.tuning(config)["bootstrap_experiments"] else "",
modern_reply=self._modern_reply.get(address, config.get("modern_reply_cache", {}).get(address, False)))
experiment="all" if car_profiles.tuning(config)["bootstrap_experiments"] or strategy.experiments else "",
modern_reply=strategy.modern_reply, patience=strategy.patience, channel_override=strategy.channel_type)
self._set(state="wifi_start")
result = boot.run(lambda credentials: lease.acquire(credentials, cancelled=boot.join_is_cancelled), cancelled=self._stop.is_set)
except Exception as error:
if _stray_link(error):
raise StrayLinkError(f"Bluetooth link to the car is stuck: {error}") from error # says nothing about the cached channel
if boot is not None and boot.silent_after_modern_version(error):
self._modern_reply[address] = not boot.modern_reply # try the other version reply next
self.log("bootstrap_reply_switched", modern=self._modern_reply[address])
# A car that answered the version exchange and then went silent or refused the Wi-Fi setup says something about the
# handshake; every other failure (asleep, local Bluetooth trouble, after video) says nothing and leaves the ladder alone.
ladder_failure = (boot is not None and boot.version_major > 0 and isinstance(error, BootstrapTimeout)
and error.stage in ("wifi_start", "wifi_info") and not self._stop.is_set())
outcome = self._ladder.failed(address, strategy, ladder_failure=ladder_failure, video_seen=self._video_seen,
version_major=boot.version_major if boot is not None else 0, adapter=adapter)
if outcome:
self.log("ladder_failed", outcome=outcome, rung=strategy.rung, next=self._ladder.strategy(address, base_modern).rung,
version_major=boot.version_major)
if source == "cache" and not self._stop.is_set() and drops_cached_channel(self._status["last_stage"], error):
self._remember_channel(address, None) # the car never answered there; ask it over SDP next time
self.log("rfcomm_cache_dropped", channel=channel)
@@ -1632,6 +1655,7 @@ class Supervisor:
try:
self._stream(session, encoder, source, watch, interval)
finally:
self._note_streamed(session)
if session.audio is not None:
session.audio.close()
if getattr(session, "nav", None) is not None:
@@ -1645,6 +1669,20 @@ class Supervisor:
except Exception:
pass
def _note_streamed(self, session: ProjectionSession) -> None:
"""Video was acknowledged: later failures in this session say nothing about the handshake. A session that streamed for
a minute without a handshake failure makes the strategy it ran with the one this car starts with next time."""
if not session.video_confirmed:
return
self._video_seen = True
strategy, address = self._strategy, self.config["receiver_address"]
streamed = time.monotonic() - session.video_confirmed_at
if strategy is None or not address or streamed < setup_ladder.CLEAN_SECONDS:
return
self._ladder.worked(address, strategy, t_ready_s=session.video_confirmed_at - self._run_started)
self.log("ladder_worked", rung=strategy.rung, streamed_s=round(streamed, 1),
t_ready_s=round(session.video_confirmed_at - self._run_started, 1))
# ANDROID AUTO VIDEO PERFORMANCE GUARDRAIL: The bounded ACK/input drain and conditional waits keep video and touches responsive.
# Altering this can degrade video smoothness or introduce lag on Android Auto.
# Do not change it autonomously; require an explicit user request specifically for this video optimization.
@@ -492,3 +492,17 @@ def test_unknown_setup_values_are_dropped(tmp_path):
events[0] = (0, 0, "session_start", {"setup": {"type": "<script>", "profile": "x" * 90, "profile_source": "guess"}})
setup = br.session_record(write_session(tmp_path, 1, events))["setup"]
assert "type" not in setup and "profile_source" not in setup and len(setup["profile"]) <= 40
def test_report_shows_what_the_ladder_tried_and_what_worked(tmp_path):
events = [list(item) for item in STREAMED]
events.insert(1, (0, 1, "ladder_strategy", {"rung": "standard", "learned": False}))
events.insert(2, (0, 5, "ladder_failed", {"outcome": "advanced", "rung": "standard", "next": "other_reply"}))
events.insert(3, (0, 6, "ladder_strategy", {"rung": "other_reply", "learned": False}))
events.append((1, 50, "ladder_worked", {"rung": "other_reply", "streamed_s": 62.0, "t_ready_s": 14.5}))
record = br.session_record(write_session(tmp_path, 1, events))
assert record["ladder"] == [{"rung": "standard", "learned": False, "at_s": 1.0, "outcome": "advanced"},
{"rung": "other_reply", "learned": False, "at_s": 6.0, "outcome": "worked", "streamed_s": 62.0, "t_ready_s": 14.5}]
tags = br.drive_event([record])["tags"]
assert (tags["aa.ladder_rung"], tags["aa.ladder_learned"]) == ("other_reply", "no")
assert br.session_record(write_session(tmp_path, 2, STREAMED))["ladder"] == [] # older logs report nothing, without error
@@ -0,0 +1,213 @@
"""The setup ladder moves only when a car that answered the handshake refuses it before any video; nothing else moves it."""
import json
import pytest
from openpilot.starpilot.system.starport import setup_ladder as sl
from openpilot.starpilot.system.starport.bootstrap import CHANNEL_24GHZ, CHANNEL_DUAL, REFUSAL_PATIENCE
CAR = "AA:BB:CC:DD:EE:FF"
@pytest.fixture
def ladder(tmp_path):
state = {"boot": "boot-1"}
instance = sl.SetupLadder(tmp_path / "setup_state.json", boot_id=lambda: state["boot"], clock=lambda: 1000.0)
instance.state = state
return instance
def refuse(ladder, strategy, **overrides):
values = dict(ladder_failure=True, video_seen=False, version_major=4, adapter=False)
values.update(overrides)
return ladder.failed(CAR, strategy, **values)
def test_the_first_strategy_is_exactly_what_ran_before(ladder):
for base in (False, True):
strategy = ladder.strategy(CAR, base)
assert (strategy.rung, strategy.modern_reply, strategy.channel_type, strategy.patience, strategy.experiments) == (
"standard", base, None, REFUSAL_PATIENCE, False)
@pytest.mark.parametrize("name,overrides", [
("a car that never answered the handshake (asleep, refused, local trouble)", {"ladder_failure": False}),
("anything after video was acknowledged in the session (car-off teardown)", {"video_seen": True}),
])
def test_failures_that_say_nothing_about_the_handshake_never_move_the_ladder(ladder, name, overrides):
strategy = ladder.strategy(CAR, False)
for _ in range(50):
assert refuse(ladder, strategy, **overrides) is None
assert ladder.strategy(CAR, False).rung == "standard" and not ladder.exhausted(CAR)
assert not (ladder.path.exists() and json.loads(ladder.path.read_text())["cars"]) # nothing was even recorded
def test_a_silent_car_walks_the_rungs_in_order_and_each_is_a_knob_that_already_existed(ladder):
seen = []
for step in range(5):
strategy = ladder.strategy(CAR, False)
seen.append(strategy)
assert refuse(ladder, strategy) == ("advanced" if step < 4 else "wrapped") # the last rung ends the pass
assert [s.rung for s in seen] == ["standard", "other_reply", "modern_dual", "modern_24ghz", "patient"]
assert [s.modern_reply for s in seen] == [False, True, True, True, False]
assert [s.channel_type for s in seen] == [None, None, CHANNEL_DUAL, CHANNEL_24GHZ, None]
assert seen[4].patience == sl.LONG_PATIENCE and all(not s.experiments for s in seen)
def test_the_other_reply_is_relative_to_what_the_car_was_using(ladder):
assert ladder.strategy(CAR, True).modern_reply is True
refuse(ladder, ladder.strategy(CAR, True))
assert ladder.strategy(CAR, True).modern_reply is False
def test_a_car_older_than_protocol_four_skips_the_phone_style_rungs(ladder):
refuse(ladder, ladder.strategy(CAR, False), version_major=3)
assert ladder.strategy(CAR, False).rung == "patient"
assert ladder.snapshot(CAR)["steps"][1]["state"] == "skipped"
def test_the_adapter_experiments_run_last_and_only_for_adapters(ladder):
for _ in range(5):
refuse(ladder, ladder.strategy(CAR, False), adapter=True)
strategy = ladder.strategy(CAR, False)
assert strategy.rung == "experiments" and strategy.experiments
assert refuse(ladder, strategy, adapter=True) == "wrapped"
other = sl.SetupLadder(ladder.path.with_name("other.json"), boot_id=lambda: "b")
for _ in range(5):
refuse(other, other.strategy(CAR, False))
assert other.strategy(CAR, False).rung == "standard" # no adapter, no experiments rung: back to the start
assert "experiments" not in [step["id"] for step in other.snapshot(CAR)["steps"]]
def test_three_passes_per_boot_then_it_stops_trying_new_things(ladder):
results = []
for _ in range(5 * sl.MAX_PASSES):
results.append(refuse(ladder, ladder.strategy(CAR, False)))
assert results.count("exhausted") == 1 and ladder.exhausted(CAR)
assert ladder.strategy(CAR, False).rung == "standard" # settles on today's behavior
assert refuse(ladder, ladder.strategy(CAR, False)) is None
assert ladder.snapshot(CAR)["exhausted"]
def test_the_next_ignition_gets_its_passes_back_and_resumes_at_the_first_untried_rung(ladder):
for _ in range(2):
refuse(ladder, ladder.strategy(CAR, False))
ladder.state["boot"] = "boot-2"
assert ladder.strategy(CAR, False).rung == "modern_dual"
assert ladder.snapshot(CAR)["passes"] == 0
for _ in range(5 * sl.MAX_PASSES):
refuse(ladder, ladder.strategy(CAR, False))
assert ladder.exhausted(CAR)
ladder.state["boot"] = "boot-3"
assert not ladder.exhausted(CAR) and ladder.snapshot(CAR)["passes"] == 0
def test_what_worked_is_used_first_next_time_even_for_a_different_remembered_reply(ladder):
refuse(ladder, ladder.strategy(CAR, False))
worked = ladder.strategy(CAR, False)
assert worked.rung == "other_reply"
ladder.worked(CAR, worked, t_ready_s=18.4)
# The reply style is now remembered as the phone-style one; "the other reply" must not flip back to standard.
again = ladder.strategy(CAR, True)
assert (again.rung, again.modern_reply) == ("other_reply", True)
snapshot = ladder.snapshot(CAR)
assert snapshot["learned"] and snapshot["t_ready_s"] == 18.4 and snapshot["successes"] == 1
assert [step["state"] for step in snapshot["steps"]][1] == "worked"
def test_a_learned_strategy_is_forgotten_only_after_two_handshake_failures(ladder):
ladder.worked(CAR, sl.resolve("modern_dual", False))
learned = ladder.strategy(CAR, False)
assert refuse(ladder, learned) == "counted" and ladder.strategy(CAR, False).rung == "modern_dual"
assert refuse(ladder, learned, ladder_failure=False) is None and refuse(ladder, learned, video_seen=True) is None
assert refuse(ladder, learned) == "dropped"
assert ladder.strategy(CAR, False).rung == "standard" and not ladder.snapshot(CAR)["learned"]
def test_state_survives_a_restart_and_a_damaged_file_means_a_fresh_start(tmp_path):
path = tmp_path / "setup_state.json"
first = sl.SetupLadder(path, boot_id=lambda: "b")
refuse(first, first.strategy(CAR, False))
first.worked(CAR, sl.resolve("patient", False), 12.0)
second = sl.SetupLadder(path, boot_id=lambda: "b")
assert second.strategy(CAR.lower(), False).rung == "patient" and path.stat().st_mode & 0o777 == 0o600
for damage in ("not json", json.dumps({"version": 99, "cars": {}}), json.dumps({"version": 1, "cars": {CAR: {"working": {"rung": "x"}}}}),
json.dumps({"version": 1, "cars": [1]}), ""):
path.write_text(damage)
assert sl.SetupLadder(path, boot_id=lambda: "b").strategy(CAR, False).rung == "standard"
def test_a_read_only_disk_never_stops_a_connection(ladder):
ladder.path = ladder.path / "blocked" / "x" # parent is a missing path under a file below
ladder.path.parent.parent.parent.mkdir(exist_ok=True)
(ladder.path.parent.parent).write_text("a file where a directory should be")
refuse(ladder, ladder.strategy(CAR, False)) # must not raise
assert ladder.strategy(CAR, False).rung == "other_reply"
def test_only_sixteen_cars_are_kept_newest_first(tmp_path):
clock = iter(range(1000, 2000))
ladder = sl.SetupLadder(tmp_path / "s.json", boot_id=lambda: "b", clock=lambda: next(clock))
for number in range(20):
ladder.worked(f"AA:00:00:00:00:{number:02X}", sl.resolve("patient", False))
reloaded = sl.SetupLadder(tmp_path / "s.json", boot_id=lambda: "b")
assert len(reloaded.cars) == sl.MAX_CARS and "AA:00:00:00:00:13" in reloaded.cars and "AA:00:00:00:00:00" not in reloaded.cars
# ---- the supervisor's use of it -------------------------------------------------------------------------------------
import time as _time # noqa: E402
from types import SimpleNamespace # noqa: E402
from openpilot.starpilot.system.starport.tests.fake_head_unit import make_identity # noqa: E402
from openpilot.starpilot.system.starport.tests.test_starport import make_supervisor # noqa: E402
RECEIVER = "AA:BB:CC:DD:EE:01" # the car make_supervisor selects
@pytest.fixture(scope="module")
def identity(tmp_path_factory):
return make_identity(tmp_path_factory.mktemp("identity"))
@pytest.fixture
def sup(identity, tmp_path, monkeypatch):
supervisor, _ = make_supervisor(identity, tmp_path, monkeypatch, lambda *a, **k: None)
return supervisor
def streamed(seconds):
return SimpleNamespace(video_confirmed=True, video_confirmed_at=_time.monotonic() - seconds)
def test_the_supervisor_keeps_its_ladder_beside_the_config_so_tests_never_touch_real_state(sup):
from openpilot.starpilot.system.starport import identity as identity_store
assert sup._ladder.path == identity_store.CONFIG_PATH.with_name("setup_state.json")
def test_a_minute_of_video_makes_the_strategy_the_one_this_car_starts_with(sup):
strategy = sl.resolve("other_reply", False)
sup._strategy, sup._run_started = strategy, _time.monotonic() - 150
sup._note_streamed(streamed(30)) # too short to count, but video was seen
assert sup._video_seen and not sup._ladder.snapshot(RECEIVER)["learned"]
sup._note_streamed(streamed(90))
snapshot = sup._ladder.snapshot(RECEIVER)
assert snapshot["learned"] and snapshot["current"] == "other_reply" and 50 < snapshot["t_ready_s"] < 70
def test_a_session_that_never_got_video_learns_nothing(sup):
sup._strategy = sl.resolve("patient", False)
sup._note_streamed(SimpleNamespace(video_confirmed=False, video_confirmed_at=0.0))
assert not sup._video_seen and not sup._ladder.snapshot(RECEIVER)["learned"]
def test_status_shows_the_ladder_the_switch_and_the_cars_to_choose_between(sup):
status = sup.status()
assert status["new_setup"] in (True, False) and status["car_candidates"] == []
assert [step["id"] for step in status["setup_ladder"]["steps"]][:2] == ["standard", "other_reply"]
assert status["setup_ladder"]["max_passes"] == sl.MAX_PASSES and not status["setup_ladder"]["learned"]
def test_forgetting_a_car_forgets_what_was_learned_about_it(sup):
sup._ladder.worked(RECEIVER, sl.resolve("patient", False), 10.0)
assert sup.forget_receiver(RECEIVER) is True
assert not sup._ladder.snapshot(RECEIVER)["learned"] and RECEIVER not in sup._ladder.cars