diff --git a/openpilot/starpilot/galaxy/starport_send.py b/openpilot/starpilot/galaxy/starport_send.py index 52ad1e0371..fc1045dbe4 100644 --- a/openpilot/starpilot/galaxy/starport_send.py +++ b/openpilot/starpilot/galaxy/starport_send.py @@ -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": []}} diff --git a/openpilot/starpilot/galaxy/tests/test_starport_send.py b/openpilot/starpilot/galaxy/tests/test_starport_send.py index 8a24310d8a..59e086efee 100644 --- a/openpilot/starpilot/galaxy/tests/test_starport_send.py +++ b/openpilot/starpilot/galaxy/tests/test_starport_send.py @@ -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")) diff --git a/openpilot/starpilot/system/starport/bugsink_report.py b/openpilot/starpilot/system/starport/bugsink_report.py index 40d0d6e967..c859969afa 100644 --- a/openpilot/starpilot/system/starport/bugsink_report.py +++ b/openpilot/starpilot/system/starport/bugsink_report.py @@ -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), diff --git a/openpilot/starpilot/system/starport/setup_ladder.py b/openpilot/starpilot/system/starport/setup_ladder.py index cb68e094c6..49f696f7dc 100644 --- a/openpilot/starpilot/system/starport/setup_ladder.py +++ b/openpilot/starpilot/system/starport/setup_ladder.py @@ -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 diff --git a/openpilot/starpilot/system/starport/supervisor.py b/openpilot/starpilot/system/starport/supervisor.py index 1e559175d5..7089f827ae 100644 --- a/openpilot/starpilot/system/starport/supervisor.py +++ b/openpilot/starpilot/system/starport/supervisor.py @@ -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: diff --git a/openpilot/starpilot/system/starport/tests/test_bugsink_report.py b/openpilot/starpilot/system/starport/tests/test_bugsink_report.py index 2e300c8974..8f59569e23 100644 --- a/openpilot/starpilot/system/starport/tests/test_bugsink_report.py +++ b/openpilot/starpilot/system/starport/tests/test_bugsink_report.py @@ -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"] == {} diff --git a/openpilot/starpilot/system/starport/tests/test_setup_ladder.py b/openpilot/starpilot/system/starport/tests/test_setup_ladder.py index be0826f4b8..1ba7e0f6e0 100644 --- a/openpilot/starpilot/system/starport/tests/test_setup_ladder.py +++ b/openpilot/starpilot/system/starport/tests/test_setup_ladder.py @@ -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