diff --git a/openpilot/starpilot/system/starport/bugsink_report.py b/openpilot/starpilot/system/starport/bugsink_report.py index 4f341c8202..c4860ff879 100644 --- a/openpilot/starpilot/system/starport/bugsink_report.py +++ b/openpilot/starpilot/system/starport/bugsink_report.py @@ -161,6 +161,61 @@ def bounded_trace(trace: list[dict], limit: int) -> dict: return {"events": kept, "omitted": len(trace) - len(kept)} +MAX_OFFERED_MODES = 16 +SETUP_TYPES = ("builtin", "adapter", "usb", "app") + + +def offered_modes(events: list[dict]) -> list[str]: + """Every video mode the head unit offered, e.g. "1280x720 H.264 margins 0x240", from its service discovery. + + Kept for working pairings too, so the choice of mode can later be replayed against real cars.""" + from openpilot.starpilot.system.starport.session import CODEC_H264_BP, CODEC_NAMES, RESOLUTION_NAMES + found = next((event for event in events if event["event"] == "discovered"), {}) + modes: list[str] = [] + for channel in found.get("channels") or []: + for config in (channel.get("video_configs") or []) if isinstance(channel, dict) else []: + if not isinstance(config, dict): + continue + + def field(number, default=None): + values = config.get(str(number), config.get(number)) + return values[0] if isinstance(values, list) and values and isinstance(values[0], int) else default + + resolution, codec = field(1), field(10, CODEC_H264_BP) + text = f"{RESOLUTION_NAMES.get(resolution, f'resolution {resolution}')} {CODEC_NAMES.get(codec, f'codec {codec}')}" + if field(3, 0) or field(4, 0): + text += f" margins {field(3, 0)}x{field(4, 0)}" + modes.append(text) + return modes[:MAX_OFFERED_MODES] + + +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"), {}) + setup = start.get("setup") if isinstance(start.get("setup"), dict) else {} + kind = setup.get("type") + return {key: value for key, value in ( + ("type", kind if kind in SETUP_TYPES else None), + ("profile", scrub(setup.get("profile", ""), 40) or None), + ("profile_source", setup.get("profile_source") if setup.get("profile_source") in ("override", "detected", "standard") else None), + ) if value} + + +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): + return {} + ready = next((event for event in reversed(events) if event["event"] == "video_setup"), {}) + reply = next((event for event in events if event["event"] == "bootstrap_version_reply"), {}) + mode = ready.get("mode") if isinstance(ready.get("mode"), dict) else {} + # Only a wireless bootstrap has a version reply; the modern one is logged, the standard one is its absence. + style = reply.get("style") or ("standard" if any(event["event"] == "bootstrap_start_response" for event in events) else None) + strategy = {"profile": setup.get("profile"), "window_offered": ready.get("window"), "window_used": ready.get("used_window"), + "reply_style": style, "channel_type": reply.get("channel_type"), + "config_index": mode.get("config_index")} + return {key: value for key, value in strategy.items() if value is not None} + + def session_record(path: Path) -> dict: """One finished session log reduced to its outcome, stage timings, video and the car it talked to.""" from openpilot.starpilot.system.starport import compat_report @@ -184,7 +239,14 @@ def session_record(path: Path) -> dict: car = report["car"] start = next((e for e in events if e["event"] == "session_start"), {}) software, settings = start.get("software", {}), start.get("settings") + setup = session_setup(events) + opened = next((e for e in events if e["event"] == "bluetooth_opened"), None) record = { + "setup": setup, + "offered_modes": offered_modes(events), + # 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), "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), @@ -395,6 +457,8 @@ def drive_event(sessions: list[dict]) -> dict: head_unit = next((s["head_unit"] for s in reversed(sessions) if s["head_unit"]), {}) video = next((s["video"] for s in reversed(sessions) if s["video"].get("resolution")), {}) 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) 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" @@ -425,6 +489,11 @@ def drive_event(sessions: list[dict]) -> dict: "aa.encoder": video.get("encoder", "unknown"), "aa.fps": str(summary["fps_avg"]) if summary["fps_avg"] is not None else "none", "aa.failures": str(len(failures)), + # Low-cardinality choices only; long values (offered modes, strategy) live in the contexts below. + **({"aa.setup_type": setup["type"]} if setup.get("type") else {}), + **({"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.attention": ",".join(reasons)[:190]} if reasons else {}), **({"aa.cause": latest["cause"], "aa.stage": latest["stage"], "aa.error_kind": latest["error_kind"]} if latest else {})}, "breadcrumbs": {"values": attempt_breadcrumbs(sessions)}, diff --git a/openpilot/starpilot/system/starport/bugsink_transport.py b/openpilot/starpilot/system/starport/bugsink_transport.py index 0f5acf7364..969ba752ef 100644 --- a/openpilot/starpilot/system/starport/bugsink_transport.py +++ b/openpilot/starpilot/system/starport/bugsink_transport.py @@ -38,7 +38,7 @@ class Sender: self.opener = opener or urllib.request.build_opener(NoRedirect()) self.active = active self.logger, self.client = logger, client - self.schema_tags = {"aa.schema": "2"} if schema_tags is None else schema_tags + self.schema_tags = {"aa.schema": "3"} if schema_tags is None else schema_tags def __call__(self, **event) -> bool: if not self.active(): diff --git a/openpilot/starpilot/system/starport/supervisor.py b/openpilot/starpilot/system/starport/supervisor.py index 8e69a8f5c4..20152d392a 100644 --- a/openpilot/starpilot/system/starport/supervisor.py +++ b/openpilot/starpilot/system/starport/supervisor.py @@ -1134,9 +1134,13 @@ class Supervisor: receiver = {"wired": "usb", "headunitpad": "headunitpad", "openheadunit": "openheadunit"}.get(connection, self.config["receiver_name"]) from openpilot.starpilot.system.starport.bugsink_transport import device_metadata software = device_metadata() + profile, profile_source = car_profiles.resolve(self.config) self.log("session_start", receiver=receiver, generation=generation, trigger=trigger, software={key: software.get(key, "unknown") for key in ("commit", "branch")}, - settings={key: self.config.get(key) for key in REPORTED_SETTINGS}) + settings={key: self.config.get(key) for key in REPORTED_SETTINGS}, + # What the owner chose, so a report can tell a built-in unit from a dongle and say which profile applied. + setup={"type": {"wired": "usb", "headunitpad": "app", "openheadunit": "app"}.get(connection) or self.car_type(), + "profile": profile, "profile_source": profile_source}) lease = self._lease() if wireless else NoLease() attempt = 0 car_first = trigger == "onroad" and wireless diff --git a/openpilot/starpilot/system/starport/tests/test_bugsink_report.py b/openpilot/starpilot/system/starport/tests/test_bugsink_report.py index d03e3e2e63..3221f8114b 100644 --- a/openpilot/starpilot/system/starport/tests/test_bugsink_report.py +++ b/openpilot/starpilot/system/starport/tests/test_bugsink_report.py @@ -441,3 +441,54 @@ def test_pairing_that_failed_carries_a_device_slice_but_success_does_not(tmp_pat assert event["contexts"]["starport_pairing_system"]["bluetooth"]["lines"] == ["bt '' up"] assert calls[0][1] - calls[0][0] == 30 assert "starport_pairing_system" not in br.pairing_event({**record, "result": "paired"}, collector)["contexts"] + + +OFFER = {"channels": [{"id": 3, "video_configs": [ + {"1": [2], "10": [3], "3": [0], "4": [240]}, + {"1": [8], "10": [7], "3": [288], "4": [1378]}, + {"1": [7], "10": [7], "3": [0], "4": [811]}]}], "head_unit": {"2": ["Honda"]}} + + +def adapter_session(): + events = [list(item) for item in STREAMED] + events[0] = (0, 0, "session_start", {"receiver": "AAWireless", "trigger": "auto", + "setup": {"type": "adapter", "profile": "aawireless", "profile_source": "override"}}) + events[5] = (0, 10, "discovered", {**OFFER, "head_unit": STREAMED[5][3]["head_unit"]}) + events.insert(6, (0, 10, "bootstrap_start_response", {})) + events.insert(7, (0, 10, "bootstrap_version_reply", {"style": "modern", "channel_type": 1})) + events.insert(8, (0, 10, "bluetooth_opened", {"channel": 6, "phone_address": "39:90:21:64:07:00"})) + events.insert(9, (0, 11, "video_setup", {"mode": {"config_index": 0}, "window": 30, "used_window": 30})) + return events + + +def test_report_keeps_setup_offered_modes_and_the_working_strategy(tmp_path): + record = br.session_record(write_session(tmp_path, 1, adapter_session())) + assert record["setup"] == {"type": "adapter", "profile": "aawireless", "profile_source": "override"} + assert record["offered_modes"] == ["1280x720 H.264 margins 0x240", "1440x2560 H.265 margins 288x1378", "1080x1920 H.265 margins 0x811"] + assert record["phone_address_sent"] is True + assert record["strategy"] == {"profile": "aawireless", "window_offered": 30, "window_used": 30, + "reply_style": "modern", "channel_type": 1, "config_index": 0} + event = br.drive_event([record]) + tags = event["tags"] + assert (tags["aa.setup_type"], tags["aa.adapter_model"], tags["aa.profile_source"], tags["aa.phone_address_sent"]) == ( + "adapter", "aawireless", "override", "yes") + context = event["contexts"]["starport_session_1"] + assert context["offered_modes"] == record["offered_modes"] and context["strategy"]["reply_style"] == "modern" + assert "39:90:21:64:07:00" not in json.dumps(event) # the comma's own address is sanitized from every field + + +def test_report_records_that_the_address_was_not_sent_and_old_logs_still_report(tmp_path): + events = [list(item) for item in STREAMED] + events.insert(6, (0, 10, "bluetooth_opened", {"channel": 6, "phone_address": ""})) + assert br.drive_event([br.session_record(write_session(tmp_path, 1, events))])["tags"]["aa.phone_address_sent"] == "no" + old = br.session_record(write_session(tmp_path, 2, STREAMED)) # a log written before these fields existed + assert old["setup"] == {} and old["offered_modes"] == [] and old["phone_address_sent"] is None and old["strategy"] == {} + tags = br.drive_event([old])["tags"] + assert not any(key in tags for key in ("aa.setup_type", "aa.adapter_model", "aa.profile_source", "aa.phone_address_sent")) + + +def test_unknown_setup_values_are_dropped(tmp_path): + events = [list(item) for item in STREAMED] + events[0] = (0, 0, "session_start", {"setup": {"type": "