Report how the channel was found and what the ladder tried; wait longer for a slow car

Reports carry the SDP outcome (where the RFCOMM channel came from and how
many retries it took). The Discord post names the handshake options tried
and how each ended. A car that once needed 70 s to be ready now gets that
many stage timeouts to answer the Wi-Fi handshake, never fewer than the
standard wait and capped at nine.
This commit is contained in:
dirwin31
2026-11-01 14:18:06 -08:00
committed by Danny
parent 0783710c9a
commit 99a9025afe
7 changed files with 56 additions and 2 deletions
+14 -1
View File
@@ -104,10 +104,22 @@ def _setup(log_dir: Path | None, logs_to_check: int = 3) -> str:
return ""
def _handshake(log_dir: Path | None, logs_to_check: int = 3) -> str:
"""What the setup ladder tried in the newest log that recorded it, e.g. "standard (advanced) > other_reply (worked)"."""
from openpilot.starpilot.system.starport import compat_report
from openpilot.starpilot.system.starport.bugsink_report import session_ladder
for path in compat_report.session_logs(log_dir or identity_store.LOG_DIR)[:logs_to_check]:
tried = session_ladder(compat_report.load_events(path))
if tried:
return " > ".join(f"{item['rung']}{' learned' if item.get('learned') else ''} ({item.get('outcome', 'tried')})" for item in tried[-6:])
return ""
def device_summary(log_dir: Path | None = None) -> dict:
from openpilot.starpilot.system.starport.bugsink_transport import device_metadata
summary = device_metadata()
for key, read in (("car_fingerprint", _car_fingerprint), ("head_unit", lambda: _head_unit(log_dir)), ("setup", lambda: _setup(log_dir))):
for key, read in (("car_fingerprint", _car_fingerprint), ("head_unit", lambda: _head_unit(log_dir)), ("setup", lambda: _setup(log_dir)),
("handshake", lambda: _handshake(log_dir))):
try:
summary[key] = read()
except Exception as error:
@@ -163,6 +175,7 @@ def send(data: bytes, name: str, note: str, summary: dict, url: str | None = Non
f"Car: {summary.get('car_fingerprint') or 'unknown'}",
f"Head unit: {summary.get('head_unit') or 'unknown'}",
f"Setup: {summary.get('setup') or 'unknown'}",
f"Handshake: {summary.get('handshake') or 'unknown'}",
f"Branch {summary.get('branch', '?')} @ {str(summary.get('commit', '?'))[:10]}",
f"Note: {note.strip() or '(none)'}"]
payload = {"content": "\n".join(lines)[:1900], "username": "StarPilot diagnostics", "allowed_mentions": {"parse": []}}
@@ -61,6 +61,7 @@ class SendTest(unittest.TestCase):
self.assertIn("Note: Dropped at the light", call["payload"]["content"])
self.assertIn("Head unit: Honda Display Audio", call["payload"]["content"])
self.assertIn("Setup: unknown", call["payload"]["content"]) # no session log recorded a setup in this fixture
self.assertIn("Handshake: unknown", call["payload"]["content"])
self.assertNotIn(SUMMARY["device_id"], call["payload"]["content"])
self.assertEqual(query(call), {"wait": "true"})
self.assertEqual(call["files"]["files[0]"], ("logs.zip", b"zip", "application/zip"))
@@ -190,6 +190,17 @@ def offered_modes(events: list[dict]) -> list[str]:
return modes[:MAX_OFFERED_MODES]
def session_sdp(events: list[dict]) -> dict:
"""How the car's Android Auto RFCOMM channel was found: where it came from, and how many SDP retries it took."""
found = next((event for event in events if event["event"] == "rfcomm_channel"), {})
retries = sum(1 for event in events if event["event"] == "sdp_retry")
if not found and not retries:
return {}
return {key: value for key, value in (("channel", found.get("channel") if isinstance(found.get("channel"), int) else None),
("source", str(found["source"])[:12] if found.get("source") else None),
("retries", retries or None)) if value is not None}
def session_setup(events: list[dict]) -> dict:
"""The setup type and profile the owner chose, as recorded when the session started."""
start = next((event for event in events if event["event"] == "session_start"), {})
@@ -265,6 +276,7 @@ def session_record(path: Path) -> dict:
"phone_address_sent": bool(opened.get("phone_address")) if opened is not None else None,
"strategy": session_strategy(events, setup),
"ladder": session_ladder(events),
"sdp": session_sdp(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),
@@ -239,6 +239,14 @@ class SetupLadder:
entry["t_ready"] = (entry["t_ready"] + [round(float(t_ready_s), 1)])[-8:]
self.save()
def ready_patience(self, address: str, stage_timeout: float = 20.0) -> int:
"""Stage timeouts a slow car needs: it was ready after t_ready_s once, so the bootstrap must be willing to wait that long.
Never less than the standard patience, and 0 (no opinion) for a car nothing is known about."""
entry = self.cars.get(address.upper())
times = entry["t_ready"] if entry else []
return min(9, int(-(-max(times) // stage_timeout))) if times else 0
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
@@ -1341,7 +1341,9 @@ class Supervisor:
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"] or strategy.experiments else "",
modern_reply=strategy.modern_reply, patience=strategy.patience, channel_override=strategy.channel_type)
modern_reply=strategy.modern_reply, channel_override=strategy.channel_type,
# a car that once took 70 s to be ready gets more than the standard wait for the Wi-Fi handshake
patience=max(strategy.patience, self._ladder.ready_patience(address, 20.0)))
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:
@@ -506,3 +506,11 @@ def test_report_shows_what_the_ladder_tried_and_what_worked(tmp_path):
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
def test_report_says_how_the_rfcomm_channel_was_found(tmp_path):
events = [list(item) for item in STREAMED]
events.insert(1, (0, 1, "sdp_retry", {"error": "x"}))
events.insert(2, (0, 2, "rfcomm_channel", {"channel": 8, "source": "sdp"}))
assert br.session_record(write_session(tmp_path, 1, events))["sdp"] == {"channel": 8, "source": "sdp", "retries": 1}
assert br.session_record(write_session(tmp_path, 2, STREAMED))["sdp"] == {}
@@ -211,3 +211,13 @@ 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
def test_a_slow_car_gets_the_patience_it_once_needed_and_an_unknown_car_gets_no_opinion(ladder):
assert ladder.ready_patience(CAR) == 0
ladder.worked(CAR, sl.resolve("standard", False), 14.0)
assert ladder.ready_patience(CAR) == 1
ladder.worked(CAR, sl.resolve("standard", False), 70.0)
assert ladder.ready_patience(CAR) == 4 # 70 s at 20 s per stage timeout
ladder.worked(CAR, sl.resolve("standard", False), 3000.0)
assert ladder.ready_patience(CAR) == 9 # capped