from unittest.mock import Mock import socket import ssl import struct import threading import time from pathlib import Path import pytest from openpilot.starpilot.system.android_auto import bootstrap as bs from openpilot.starpilot.system.android_auto import hfp, sdp from openpilot.starpilot.system.android_auto.frame_source import FrameConsumer, FrameProducer, FrameRequest, fit_content from openpilot.starpilot.system.android_auto.session import (AuthenticationRejected, ProjectionSession, VideoMode, choose_video_mode) from openpilot.starpilot.system.android_auto.tests.fake_head_unit import FakeHeadUnit, make_identity, rfcomm_head_unit from openpilot.starpilot.system.android_auto.wire import field, one, parse_fields, signed @pytest.fixture(scope="module") def identity(tmp_path_factory): return make_identity(tmp_path_factory.mktemp("identity")) # ---------------------------------------------------------------------- wire def test_wire_roundtrip_and_negative_ints(): data = field(1, 5) + field(2, "abc") + field(3, -1) fields = parse_fields(data) assert one(fields, 1) == 5 and one(fields, 2) == b"abc" and signed(one(fields, 3)) == -1 def test_wire_rejects_truncation(): with pytest.raises(ValueError): parse_fields(field(2, "abcdef")[:-2]) # ----------------------------------------------------------------------- sdp def sdp_response(attributes: bytes, continuation: bytes = b"") -> bytes: params = struct.pack(">H", len(attributes)) + attributes + bytes([len(continuation)]) + continuation return struct.pack(">BHH", sdp.PDU_SEARCH_ATTRIBUTE_RESPONSE, 0, len(params)) + params PROTOCOLS = b"\x35\x20\x09\x00\x04\x35\x0c\x35\x03\x19\x01\x00\x35\x05\x19\x00\x03\x08\x08" # L2CAP + RFCOMM ch 8 AA_RECORD = PROTOCOLS + b"\x09\x00\x01\x35\x11\x1c" + sdp.AA_WIRELESS_UUID.bytes class FakeSdpSocket: def __init__(self, responses): self.responses = list(responses) self.requests = [] def settimeout(self, _): pass def send(self, data): self.requests.append(data) def recv(self, _): return self.responses.pop(0) class ClosableSdpSocket(FakeSdpSocket): def __enter__(self): return self def __exit__(self, *_): pass def shutdown(self, *_): pass def close(self): pass def test_sdp_request_matches_phone_shape(): request = sdp.build_request(b"") assert request[0] == sdp.PDU_SEARCH_ATTRIBUTE_REQUEST and request[1:3] == b"\x00\x00" assert sdp.AA_WIRELESS_UUID.bytes in request and b"\x03\xf0" in request and request.endswith(b"\x00") def test_sdp_follows_continuation_and_finds_channel(): first, second = AA_RECORD[:10], AA_RECORD[10:] sock = FakeSdpSocket([sdp_response(first, b"\x01\x02"), sdp_response(second)]) assert sdp.query_channel(sock) == 8 assert sock.requests[1].endswith(b"\x02\x01\x02") def test_sdp_missing_service_and_error_response(): with pytest.raises(sdp.SdpError, match="does not advertise"): sdp.query_channel(FakeSdpSocket([sdp_response(b"\x35\x00")])) error = struct.pack(">BHHH", sdp.PDU_ERROR_RESPONSE, 0, 2, 3) with pytest.raises(sdp.SdpError, match="invalid request syntax"): sdp.query_channel(FakeSdpSocket([error])) def test_sdp_channel_encodings(): assert sdp.rfcomm_channel(b"\x19\x00\x03\x09\x00\x0c") == 12 assert sdp.rfcomm_channel(b"\x19\x00\x03\x08\x00" + b"\x19\x00\x03\x08\x05") == 5 assert sdp.rfcomm_channel(b"\x19\x01\x00") is None # ----------------------------------------------------------------- bootstrap def unexpected_join(_): raise AssertionError("Unexpected join") def run_bootstrap(start_request_delay=5.0, initial_kick_delay=bs.INITIAL_KICK_SECONDS, **hu_options): phone, car = socket.socketpair() joined = [] result_holder = {} def car_side(): try: result_holder["seen"] = rfcomm_head_unit(car, ("192.168.50.1", 5288), **hu_options) except BaseException as error: result_holder["error"] = error thread = threading.Thread(target=car_side, daemon=True) thread.start() events = [] boot = bs.WirelessBootstrap(phone, lambda name, **values: events.append((name, values)), stage_timeout=5.0, start_request_delay=start_request_delay, initial_kick_delay=initial_kick_delay) def join(credentials): time.sleep(0.3) # the car pings while we join joined.append(credentials) result = boot.run(join) thread.join(5) phone.close() car.close() assert "error" not in result_holder, result_holder.get("error") return result, joined, result_holder["seen"], events def test_bootstrap_standard_flow(): result, joined, seen, events = run_bootstrap() assert (result.endpoint.ip, result.endpoint.port) == ("192.168.50.1", 5288) creds = joined[0] assert (creds.ssid, creds.key, creds.bssid, creds.security) == ("HondaAA", "secret-key", "AA:BB:CC:DD:EE:FF", 8) assert signed(one(seen["start_response"], 3)) == 0 and 1 not in seen["start_response"] assert signed(one(seen["connect_status"], 1)) == 0 assert all("secret-key" not in repr(values) for _, values in events) def test_bootstrap_version_first_and_fragmented(): result, joined, seen, _ = run_bootstrap(version_first=True, byte_by_byte=True) assert result.version == (1, 3) and result.head_unit.get("car_make") == "Honda" assert one(seen["version_response"], 1) == 1 and signed(one(seen["version_response"], 4)) == 0 assert joined[0].ssid == "HondaAA" def test_bootstrap_asks_car_to_start_projection(): result, joined, seen, events = run_bootstrap(start_request_delay=0.2, version_first=True, wait_for_phone_start=True) assert seen["phone_start_request"] == b"" and joined[0].ssid == "HondaAA" assert ("bootstrap_tx", {"message": "WifiStartRequest", "bytes": 0}) in events def test_bootstrap_takes_endpoint_from_start_response(): # 2025 Honda (session 25): version exchange, phone asks, car answers with a StartResponse. result, joined, seen, events = run_bootstrap(start_request_delay=0.1, version_first=True, wait_for_phone_start=True, answer_start=True) assert (result.endpoint.ip, result.endpoint.port) == ("192.168.50.1", 5288) and joined[0].ssid == "HondaAA" assert seen["messages"].count(bs.WIFI_START_REQUEST) == 1 assert not any(name == "bootstrap_ignored" for name, _ in events) def test_bootstrap_waits_out_a_busy_start_response(): # The car says "not ready" (status alone), then sends the endpoint on its own. result, joined, _, events = run_bootstrap(start_request_delay=0.1, version_first=True, wait_for_phone_start=True, answer_start=True, busy_replies=1, busy_follow_up=True) assert result.endpoint.port == 5288 and joined[0].ssid == "HondaAA" assert ("bootstrap_start_refused", {"status": -1, "endpoint": False}) in events def test_bootstrap_asks_again_after_a_busy_start_response(): result, joined, seen, _ = run_bootstrap(start_request_delay=0.1, version_first=True, wait_for_phone_start=True, answer_start=True, busy_replies=2) assert result.endpoint.port == 5288 and joined[0].ssid == "HondaAA" assert seen["messages"].count(bs.WIFI_START_REQUEST) == 3 def test_bootstrap_start_response_without_endpoint_is_not_an_endpoint(monkeypatch): now = [0.0] monkeypatch.setattr(bs.time, "monotonic", lambda: now[0]) boot = bs.WirelessBootstrap(None, lambda *a, **k: None, stage_timeout=1, start_request_delay=0.3) sent = [] monkeypatch.setattr(boot, "send", lambda message, payload=b"": sent.append(message)) replies = [(bs.WIFI_START_RESPONSE, field(3, 0)), (bs.WIFI_START_RESPONSE, field(1, "10.0.0.1") + field(2, 5288) + field(3, -3))] def receive(timeout): now[0] += 0.2 if replies: return replies.pop(0) raise bs.BootstrapTimeout("wifi_start", "head unit did not answer in time") monkeypatch.setattr(boot, "next_frame", receive) with pytest.raises(bs.BootstrapTimeout): boot.run(unexpected_join) assert bs.WIFI_START_REQUEST in sent, "a refused start is asked again" def test_bootstrap_silent_peer_receives_one_prompt(): _, joined, seen, events = run_bootstrap(initial_kick_delay=0.05, wait_for_phone_start=True, pings=False) assert joined[0].ssid == "HondaAA" assert seen["messages"].count(bs.WIFI_START_REQUEST) == 1 assert sum(name == "bootstrap_initial_kick" for name, _ in events) == 1 def test_bootstrap_version_prompt_does_not_get_an_extra_initial_prompt(): _, _, seen, events = run_bootstrap(initial_kick_delay=0.05, start_request_delay=0.1, version_first=True, wait_for_phone_start=True) assert seen["messages"].count(bs.WIFI_START_REQUEST) == 1 assert not any(name == "bootstrap_initial_kick" for name, _ in events) @pytest.mark.parametrize("stage", ["wifi_start", "wifi_info"]) def test_bootstrap_stage_deadline_survives_continuous_pings(monkeypatch, stage): now = [0.0] monkeypatch.setattr(bs.time, "monotonic", lambda: now[0]) boot = bs.WirelessBootstrap(None, lambda *a, **k: None, stage_timeout=1) sent = [] monkeypatch.setattr(boot, "send", lambda message, payload=b"": sent.append(message)) first = [True] def receive(timeout): now[0] += 0.2 if first[0] and stage == "wifi_info": first[0] = False return bs.WIFI_START_REQUEST, field(1, "10.0.0.1") + field(2, 5288) return bs.WIFI_PING_REQUEST, b"" monkeypatch.setattr(boot, "next_frame", receive) with pytest.raises(bs.BootstrapTimeout) as error: boot.run(unexpected_join) assert error.value.stage == stage and now[0] < 1.5 assert bs.WIFI_PING_RESPONSE in sent and bs.WIFI_START_REQUEST not in sent def test_bootstrap_hint_cannot_hide_disconnect(): phone, car = socket.socketpair() try: car.sendall(bs.encode_frame(bs.WIFI_SETUP_INFO, field(4, field(1, "10.0.0.1") + field(2, 5288)))) car.close() with pytest.raises(bs.BootstrapError, match="closed"): bs.WirelessBootstrap(phone, lambda *a, **k: None).run(unexpected_join) finally: phone.close() car.close() def test_bootstrap_hint_grace_is_not_extended_by_pings(monkeypatch): now = [0.0] monkeypatch.setattr(bs.time, "monotonic", lambda: now[0]) boot = bs.WirelessBootstrap(None, lambda *a, **k: None, stage_timeout=10) monkeypatch.setattr(boot, "send", lambda *a: None) payload = field(4, field(1, "10.0.0.1") + field(2, 5288)) + field(5, field(1, "test") + field(3, "secret-key") + field(4, 8)) def receive(timeout): now[0] += 0.25 return (bs.WIFI_SETUP_INFO, payload) if now[0] == 0.25 else (bs.WIFI_PING_REQUEST, b"") monkeypatch.setattr(boot, "next_frame", receive) joined = [] result = boot.run(joined.append) assert result.endpoint.ip == "10.0.0.1" and joined[0].ssid == "test" assert 3 <= now[0] < 5 def test_bootstrap_detects_alternate_info_layout(): _, joined, _, _ = run_bootstrap(oaa_layout=True) assert joined[0].key == "secret-key" and joined[0].bssid == "AA:BB:CC:DD:EE:FF" def test_bootstrap_setup_info_without_start_request(): result, joined, seen, _ = run_bootstrap(setup_info_only=True, pings=False) assert result.endpoint.port == 5288 and joined[0].key == "secret-key" assert bs.WIFI_INFO_REQUEST not in seen["messages"] def test_bootstrap_join_failure_reports_status(): phone, car = socket.socketpair() seen = {} def car_side(): try: seen.update(rfcomm_head_unit(car, ("10.0.0.1", 5000), pings=False)) except BaseException as error: seen["error"] = error thread = threading.Thread(target=car_side, daemon=True) thread.start() def join(_): raise RuntimeError("wrong key") with pytest.raises(bs.BootstrapError) as error: bs.WirelessBootstrap(phone, lambda *a, **k: None, stage_timeout=5.0).run(join) assert error.value.stage == "joining_wifi" thread.join(5) phone.close() car.close() assert signed(one(seen["connect_status"], 1)) == bs.STATUS_NETWORK_UNAVAILABLE def test_bootstrap_timeout_names_stage(): phone, car = socket.socketpair() with pytest.raises(bs.BootstrapError) as error: bs.WirelessBootstrap(phone, lambda *a, **k: None, stage_timeout=0.3).run(lambda _: None) assert error.value.stage == "wifi_start" car.close() phone.close() def test_frame_reader_bounds(): reader = bs.FrameReader() assert reader.feed(bs.encode_frame(1, b"ab")[:3]) == [] assert reader.feed(bs.encode_frame(1, b"ab")[3:] + bs.encode_frame(8)) == [(1, b"ab"), (8, b"")] with pytest.raises(ValueError): reader.feed(struct.pack(">HH", 60000, 1)) # ----------------------------------------------------------------------- hfp def test_hfp_service_level_connection(): assert hfp.respond("AT+BRSF=767") == ["+BRSF: 995", "OK"] # a phone's feature set, not "nothing" assert hfp.respond("AT+BAC=1,2") == ["OK"] assert hfp.respond("AT+BCC") == ["ERROR"] # never start call audio we cannot carry assert hfp.respond("AT+BIA=1,1,1,1,1,1,1,0") == ["OK"] assert hfp.respond("AT+CIND=?")[0].startswith("+CIND: (\"call\"") assert hfp.respond("AT+CIND?") == ["+CIND: 0,0,1,5,0,5,0", "OK"] assert hfp.respond("AT+CMER=3,0,0,1") == ["OK"] assert hfp.respond("ATD5551234;") == ["ERROR"] assert hfp.respond("AT+UNKNOWN") == ["ERROR"] def test_hfp_serve_over_socket(): a, b = socket.socketpair() stop = threading.Event() thread = threading.Thread(target=hfp.serve, args=(a, stop, lambda *x, **y: None), daemon=True) thread.start() b.sendall(b"AT+BRSF=0\rAT+CIND=?\r") time.sleep(0.2) data = b.recv(4096) assert b"+BRSF: 995" in data and data.count(b"OK") == 2 b.close() thread.join(2) stop.set() # -------------------------------------------------------------- frame source def test_fit_content_letterboxes_compact_ui(): x, y, w, h = fit_content(536, 240, 1280, 720, 0, 240) assert (w, h) == (1072, 480) and x == 104 and y == 120 assert fit_content(536, 240, 800, 480, 0, 0) == (0, 60, 800, 358) def test_frame_handoff(tmp_path): path = str(tmp_path / "frames") consumer = FrameConsumer(path) producer = FrameProducer(path) request = FrameRequest(64, 32, 0, 0, 100_000) consumer.configure(request) assert producer.pending_request() is None # no demand yet consumer.demand(1.0) producer._next_open_check = 0 pending = producer.pending_request() assert pending == request now_ns = time.monotonic_ns() assert producer.due(pending, now_ns) producer.publish(pending, bytes([7]) * (64 * 32 * 4), now_ns) assert not producer.due(pending, now_ns + 10_000_000) # paced to 10 fps frame = consumer.latest() assert frame is not None and frame.data[:4] == b"\x07" * 4 and frame.captured_ns == now_ns assert consumer.latest() is None # nothing newer consumer.release_demand() assert producer.pending_request() is None consumer.close() def test_frame_consumer_rejects_torn_write(tmp_path): path = str(tmp_path / "frames") consumer = FrameConsumer(path) consumer.configure(FrameRequest(16, 16, 0, 0, 50_000)) consumer.demand() producer = FrameProducer(path) request = producer.pending_request() assert request is not None producer.publish(request, bytes(16 * 16 * 4), time.monotonic_ns()) struct.pack_into(" bytes: return b"\x00\x00\x00\x01\x09\xf0" + b"\x00\x00\x00\x01\x67\x42" + b"\x00\x00\x00\x01\x68\xce" + b"\x00\x00\x00\x01\x65" + bytes([tag]) def delta_au(tag: int) -> bytes: return b"\x00\x00\x00\x01\x09\xf0" + b"\x00\x00\x00\x01\x41" + bytes([tag]) def connect(hu: FakeHeadUnit, identity, verify=True) -> ProjectionSession: sock = socket.create_connection(("127.0.0.1", hu.port), timeout=5) return ProjectionSession(sock, str(identity["phone_cert"]), str(identity["phone_key"]), None, str(identity["root"]) if verify else None) def pump_until(session, predicate, timeout=5.0): deadline = time.monotonic() + timeout while not predicate(): assert time.monotonic() < deadline, "condition not reached" session.pump(0.05) def test_head_unit_tls_encode_drain_and_frame_share_send_lock(): hu = FakeHeadUnit.__new__(FakeHeadUnit) hu._send_lock = threading.Lock() pending = bytearray() packets = [] def write(data): assert hu._send_lock.locked() assert not pending pending.extend(data) return len(data) def read(): assert hu._send_lock.locked() data = bytes(pending) pending.clear() return data def sendall(data): assert hu._send_lock.locked() packets.append(data) hu.tls = Mock(write=Mock(side_effect=write)) hu.outgoing = Mock(read=Mock(side_effect=read)) hu.sock = Mock(sendall=Mock(side_effect=sendall)) start = threading.Barrier(3) failures = [] def send(kind, body): try: start.wait(timeout=5) hu._send(3, kind, body) except BaseException as error: failures.append(error) messages = ((0x8004, field(1, 1) + field(2, 1)), (0x8008, field(1, 2) + field(2, 1))) threads = [threading.Thread(target=send, args=message) for message in messages] for thread in threads: thread.start() start.wait(timeout=5) for thread in threads: thread.join(timeout=5) assert not thread.is_alive() assert not failures decoded = [] for packet in packets: channel, flags, size = struct.unpack(">BBH", packet[:4]) assert channel == 3 and flags == 11 and size == len(packet) - 4 and size > 2 decoded.append((struct.unpack(">H", packet[4:6])[0], packet[6:])) assert sorted(decoded) == sorted(messages) assert not pending def test_session_end_to_end_with_focus_epochs(identity): hu = FakeHeadUnit(identity) session = connect(hu, identity) session.authenticate() assert "Honda" in session.head_unit_subject mode = session.start("StarPilot", "comma.ai") assert (mode.width, mode.height, mode.margin_height) == (1280, 720, 240) pump_until(session, lambda: session.focused) with pytest.raises(ValueError): session.send_frame(delta_au(1), 1, keyframe=False) # a new epoch must start with a keyframe session.send_frame(keyframe_au(1), 1, keyframe=True) for i in range(2, 6): pump_until(session, session.can_send) session.send_frame(delta_au(i), i, keyframe=False) pump_until(session, lambda: session.acked == 5) hu.set_focus(False) # driver switched to the car's own screen pump_until(session, lambda: not session.focused) hu.set_focus(True) pump_until(session, lambda: session.focused) assert session.needs_keyframe and session.session_id == 2 session.send_frame(keyframe_au(9), 9, keyframe=True) pump_until(session, lambda: session.acked == 6) session.shutdown() session.peer.close() hu.thread.join(5) assert hu.error is None, hu.error assert hu.device_name == "StarPilot" and hu.start_indications == [1, 2] assert [sid for sid, _ in hu.frames] == [1, 1, 1, 1, 1, 2] and hu.shutdown_received.is_set() def test_session_can_return_to_native_screen_without_disconnect(identity): hu = FakeHeadUnit(identity) session = connect(hu, identity) session.authenticate() session.start("StarPilot", "comma.ai") pump_until(session, lambda: session.focused) assert session.request_native() pump_until(session, lambda: not session.focused) assert hu.focus_requests[-1] == (2, 2) assert not hu.shutdown_received.is_set() hu.set_focus(True) pump_until(session, lambda: session.focused) assert session.needs_keyframe and session.session_id == 2 session.shutdown() session.peer.close() hu.thread.join(5) assert hu.error is None, hu.error def test_session_accepts_newer_head_unit_protocol(identity): hu = FakeHeadUnit(identity, version=(4, 1)) # 2025 Honda Civic head unit session = connect(hu, identity) session.authenticate() mode = session.start("StarPilot", "comma.ai") assert hu.version_reply == (6, 1, 0) and mode.width == 1280 session.shutdown() session.peer.close() hu.thread.join(5) assert hu.error is None, hu.error @pytest.mark.parametrize("ack_codec_config", [True, False, 0]) def test_session_sends_codec_config_before_each_epoch(identity, ack_codec_config): hu = FakeHeadUnit(identity, ack_codec_config=ack_codec_config) session = connect(hu, identity) session.authenticate() session.start("StarPilot", "comma.ai") pump_until(session, lambda: session.focused) session.send_frame(keyframe_au(1), 1, keyframe=True) for i in range(2, 5): pump_until(session, session.can_send) session.send_frame(delta_au(i), i, keyframe=False) pump_until(session, lambda: session.acked == 4) hu.set_focus(False) pump_until(session, lambda: not session.focused) hu.set_focus(True) pump_until(session, lambda: session.focused) session.send_frame(keyframe_au(9), 9, keyframe=True) pump_until(session, lambda: session.acked == 5) session.shutdown() session.peer.close() hu.thread.join(5) assert hu.error is None, hu.error sps_pps = b"\x00\x00\x00\x01\x67\x42\x00\x00\x00\x01\x68\xce" assert hu.codec_configs == [(1, sps_pps, 0), (2, sps_pps, 4)] # one per epoch, before its keyframe def test_session_keeps_unsolicited_focus_grant(identity): hu = FakeHeadUnit(identity, unsolicited_focus=True) session = connect(hu, identity) session.authenticate() session.start("StarPilot", "comma.ai") pump_until(session, lambda: session.focused, timeout=2.0) session.send_frame(keyframe_au(1), 1, keyframe=True) pump_until(session, lambda: session.acked == 1) session.shutdown() session.peer.close() hu.thread.join(5) assert hu.error is None and hu.start_indications == [1] HONDA_SCREEN_TAKEBACK_S = 3.0 # the Civic gives its screen back after this long without video def streaming_session(hu: FakeHeadUnit, identity) -> ProjectionSession: session = connect(hu, identity) session.authenticate() session.start("StarPilot", "comma.ai") pump_until(session, lambda: session.focused) session.send_frame(keyframe_au(1), 1, keyframe=True) pump_until(session, lambda: session.acked == 1 and not session.pending) # ACKs are counts, not frame ids: read everything the car sent (the codec config's ACK too) so a late one is not # credited to the next frame. deadline = time.monotonic() + 5 while session.pump(0.2): assert time.monotonic() < deadline, "the car kept talking" return session def sent_frame(hu: FakeHeadUnit, session: ProjectionSession) -> float: """Send one frame and wait until the car has it; its ACK, if any, is left unread in the socket.""" received = len(hu.frames) session.send_frame(delta_au(2), 2, keyframe=False) deadline = time.monotonic() + 5 while len(hu.frames) == received: assert time.monotonic() < deadline, "the car never received the frame" time.sleep(0.01) time.sleep(0.1) return session.pending[0] def end_session(session: ProjectionSession, hu: FakeHeadUnit) -> None: hu.close() session.peer.close() def test_ack_timeout_leaves_room_for_a_stall_but_beats_the_car_taking_its_screen_back(): assert 1.5 < ProjectionSession.ACK_TIMEOUT < HONDA_SCREEN_TAKEBACK_S def test_ack_already_received_during_a_loop_stall_keeps_the_session(identity): # 2026-10-01: the dashcam and model starting at onroad stalled the comma's loop while the car kept acking. hu = FakeHeadUnit(identity) session = streaming_session(hu, identity) sent = sent_frame(hu, session) session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.5) # the waiting ACK is read, not timed out assert session.acked == 2 and not session.pending end_session(session, hu) def test_car_that_stops_acking_times_out_at_the_new_limit(identity): hu = FakeHeadUnit(identity) session = streaming_session(hu, identity) hu.hold_acks.set() sent = sent_frame(hu, session) session.check_progress(now=sent + 1.6) # past the old 1.5 s limit: no longer fatal with pytest.raises(TimeoutError, match="2.5 s"): session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05) end_session(session, hu) def test_session_accepts_a_large_video_window_and_caps_it(identity): hu = FakeHeadUnit(identity, window=100) # VW MIB3 offers 100 session = streaming_session(hu, identity) assert session.window == ProjectionSession.MAX_WINDOW end_session(session, hu) def test_session_authentication_rejected(identity): hu = FakeHeadUnit(identity, reject_auth=True) session = connect(hu, identity) with pytest.raises(AuthenticationRejected): session.authenticate() session.peer.close() hu.close() def test_session_refuses_untrusted_head_unit(identity, tmp_path): other = make_identity(tmp_path / "other") hu = FakeHeadUnit({**identity, "hu_cert": other["hu_cert"], "hu_key": other["hu_key"]}, require_client_cert=False) session = connect(hu, identity) with pytest.raises(ssl.SSLError): session.authenticate() session.peer.close() hu.close() # ---------------------------------------------------------------- supervisor class FakeLease: def __init__(self, *_): self.local_ip = "" self.acquired = [] self.releases = [] def acquire(self, credentials, cancelled=lambda: False, timeout=40.0): self.acquired.append(credentials.ssid) self.local_ip = "127.0.0.1" return self.local_ip def still_connected(self): return True def release(self, restore=True): self.releases.append(restore) class FakeBluez: def __init__(self, log): self.acquired = 0 self.released = 0 self.class_restored = 0 self.hfp_registrations = 0 self.trusted = [] self.adapter_ready = True self.connected = True self.paired_devices = [("AA:BB:CC:DD:EE:01", "Civic", True)] def acquire(self): self.acquired += 1 def release(self): self.released += 1 def restore_class(self): self.class_restored += 1 def register_hfp(self): self.hfp_registrations += 1 def set_trusted(self, address): self.trusted.append(address) def close(self): pass def device(self, address): return {"address": address, "paired": True, "connected": self.connected, "name": "Civic", "android_auto": True, "uuids": ["0000111e-0000-1000-8000-00805f9b34fb"]} def devices(self): return [{**self.device(address), "name": name, "android_auto": aa} for address, name, aa in self.paired_devices] def snapshot(self, address): return self.adapter_ready, self.device(address) def connect_device(self, address): pass def test_supervisor_full_wireless_session(identity, tmp_path, monkeypatch): from openpilot.starpilot.system.android_auto import bt_sockets, hw_encoder, identity as identity_store, supervisor as supervisor_module class TransportEncoder: backend = 'fixture' supports_nv12 = False last_encode_ms = 0.0 def __init__(self, width, height): self.width, self.height, self.frames = width, height, 0 def encode_rgba(self, pixels, *, keyframe=False): assert len(pixels) == self.width * self.height * 4 self.frames += 1 return (keyframe_au(self.frames), True) if keyframe or self.frames == 1 else (delta_au(self.frames), False) def close(self): pass monkeypatch.setattr(hw_encoder, 'create_encoder', lambda width, height, **_kwargs: (TransportEncoder(width, height), 15)) data = tmp_path / "aa" (data / "identity").mkdir(parents=True) for src, name in ((identity["phone_cert"], "phone-cert.pem"), (identity["phone_key"], "phone-key.pem"), (identity["root"], "root-cert.pem")): (data / "identity" / name).write_bytes(Path(src).read_bytes()) (data / "identity" / "phone-key.pem").chmod(0o600) monkeypatch.setattr(identity_store, "IDENTITY_DIR", data / "identity") monkeypatch.setattr(identity_store, "CONFIG_PATH", data / "config.json") monkeypatch.setattr(identity_store, "LOG_DIR", data / "logs") monkeypatch.setattr(supervisor_module, "SDP_SETTLE", (0.01,)) hu = FakeHeadUnit(identity) rfcomm_seen = {} def fake_l2cap(address, psm, timeout=10.0): return ClosableSdpSocket([sdp_response(AA_RECORD)]) def fake_rfcomm(address, channel, timeout=15.0): assert channel == 8 phone, car = socket.socketpair() def car_side(): with car: rfcomm_seen.update(rfcomm_head_unit(car, ("127.0.0.1", hu.port), pings=False)) time.sleep(1.0) threading.Thread(target=car_side, daemon=True).start() return phone monkeypatch.setattr(bt_sockets, "connect_l2cap", fake_l2cap) monkeypatch.setattr(bt_sockets, "connect_rfcomm", fake_rfcomm) frame_path = str(tmp_path / "frames") sup = supervisor_module.Supervisor(bluez_factory=FakeBluez, lease_factory=FakeLease, bluetooth_client=type("C", (), {"status": lambda s: type("St", (), {"selected_audio": ""})()})(), frame_path=frame_path) sup.select_receiver("AA:BB:CC:DD:EE:01", "Civic") lease_holder = {} original_lease = sup._lease sup._lease = Mock(side_effect=lambda: lease_holder.setdefault("lease", original_lease())) stop_producer = threading.Event() def producer_loop(): producer = FrameProducer(frame_path) while not stop_producer.is_set(): producer._next_open_check = 0 request = producer.pending_request() now_ns = time.monotonic_ns() if request is not None and producer.due(request, now_ns): producer.publish(request, bytes([now_ns % 251]) * (request.width * request.height * 4), now_ns) time.sleep(0.01) threading.Thread(target=producer_loop, daemon=True).start() try: sup.start() deadline = time.monotonic() + 20 while len(hu.frames) < 5: status = sup.status() assert time.monotonic() < deadline, (repr(status), repr(hu.error)) time.sleep(0.05) status = sup.status() assert status["state"] == "streaming" and status["mode"]["width"] == 1280 sup.stop() finally: stop_producer.set() hu.close() hu.thread.join(5) assert hu.error is None, hu.error assert hu.shutdown_received.is_set() lease = lease_holder["lease"] assert lease.acquired == ["HondaAA"] and lease.releases[-1] is True assert sup.status()["state"] == "idle" and sup.status()["error"] == "" assert signed(one(rfcomm_seen["connect_status"], 1)) == 0 logs = list((data / "logs").glob("session-*.jsonl")) assert logs and "secret-key" not in logs[0].read_text() def make_supervisor(identity, tmp_path, monkeypatch, rfcomm): from openpilot.starpilot.system.android_auto import bt_sockets, identity as identity_store, supervisor as supervisor_module data = tmp_path / "aa" (data / "identity").mkdir(parents=True) for src, name in ((identity["phone_cert"], "phone-cert.pem"), (identity["phone_key"], "phone-key.pem"), (identity["root"], "root-cert.pem")): (data / "identity" / name).write_bytes(Path(src).read_bytes()) (data / "identity" / "phone-key.pem").chmod(0o600) monkeypatch.setattr(identity_store, "IDENTITY_DIR", data / "identity") monkeypatch.setattr(identity_store, "CONFIG_PATH", data / "config.json") monkeypatch.setattr(identity_store, "LOG_DIR", data / "logs") monkeypatch.setattr(supervisor_module, "SDP_SETTLE", (0.01,)) monkeypatch.setattr(supervisor_module, "BACKOFF_SECONDS", (0.2,)) monkeypatch.setattr(bt_sockets, "connect_l2cap", lambda *a, **k: ClosableSdpSocket([sdp_response(AA_RECORD)] * 4)) monkeypatch.setattr(bt_sockets, "connect_rfcomm", rfcomm) leases = [] def lease_factory(*args): leases.append(FakeLease()) return leases[-1] client = type("C", (), {"status": lambda s: type("St", (), {"selected_audio": ""})()})() sup = supervisor_module.Supervisor(bluez_factory=FakeBluez, lease_factory=lease_factory, bluetooth_client=client, frame_path=str(tmp_path / "frames")) sup.select_receiver("AA:BB:CC:DD:EE:01", "Civic") return sup, leases def test_supervisor_retries_after_rejection(identity, tmp_path, monkeypatch): cars = [] threads = [] failures = [] stopping = threading.Event() def rfcomm(address, channel, timeout=15.0): hu = FakeHeadUnit(identity, reject_auth=True) cars.append(hu) phone, car = socket.socketpair() def car_side(): try: with car: rfcomm_head_unit(car, ("127.0.0.1", hu.port), pings=False) stopping.wait(1.0) except EOFError as error: if not stopping.is_set(): failures.append(error) except Exception as error: failures.append(error) thread = threading.Thread(target=car_side, daemon=True) threads.append(thread) thread.start() return phone sup, leases = make_supervisor(identity, tmp_path, monkeypatch, rfcomm) sup.start() deadline = time.monotonic() + 15 while len(cars) < 2: assert time.monotonic() < deadline, sup.status() time.sleep(0.05) assert "rejected" in sup.status()["error"] stopping.set() sup.stop() for hu in cars: hu.close() for thread in threads: thread.join(timeout=5) assert not thread.is_alive(), "RFCOMM fixture did not finish after stop" assert not failures, failures assert False in leases[0].releases and leases[0].releases[-1] is True # retries keep the old Wi-Fi parked assert sup.status()["state"] == "idle" def test_supervisor_stop_while_waiting_for_car(identity, tmp_path, monkeypatch): held = [] def rfcomm(address, channel, timeout=15.0): phone, car = socket.socketpair() held.append(car) # the car never answers return phone sup, leases = make_supervisor(identity, tmp_path, monkeypatch, rfcomm) sup.start() deadline = time.monotonic() + 5 while sup.status()["state"] != "wifi_start": assert time.monotonic() < deadline, sup.status() time.sleep(0.02) started = time.monotonic() sup.stop() assert time.monotonic() - started < 5 assert sup.status()["state"] == "idle" and leases[0].releases[-1] is True for car in held: car.close() # ------------------------------------------------------------------- network class FakeNetworkManager: def __init__(self, fail_with_bssid=False): self.fail_with_bssid = fail_with_bssid self.active_device_connection = "/active/home" self.states = {} self.added = [] self.calls = [] def call(self, path, interface, member, signature=None, body: tuple=(), timeout=10.0): from openpilot.starpilot.system.android_auto import network self.calls.append(member) if member == "GetDevices": return [["/dev/wlan0"]] if member == "AddAndActivateConnection2": settings = body[0] self.added.append(settings) active = f"/active/aa{len(self.added)}" locked = "bssid" in settings["802-11-wireless"] self.states[active] = network.ACTIVE_STATE_DEACTIVATED if (locked and self.fail_with_bssid) else network.ACTIVE_STATE_ACTIVATED self.active_device_connection = active return [f"/settings/aa{len(self.added)}", active] if member == "GetSettings": return [{"connection": {"id": ("s", "home" if path == "/settings/home" else "starpilot-android-auto")}}] if member == "ListConnections": return [[]] return [] def get(self, path, interface, name): values = {"WirelessEnabled": True, "DeviceType": 2, "Interface": "wlan0", "ActiveConnection": self.active_device_connection, "Connection": "/settings/home", "Ip4Config": "/ip4/1"} if name == "State": return self.states.get(path, 4) if name == "AddressData": return [{"address": ("s", "192.168.50.23")}] return values[name] def make_lease(nm): from openpilot.starpilot.system.android_auto.network import NetworkLease lease = NetworkLease(lambda *a, **k: None) router = Mock(wraps=type("R", (), {"close": lambda self: None})()) def reopen(*args, **kwargs): # the real lease reopens D-Bus lazily on its next call lease.router = router def call(*args, **kwargs): reopen() return nm.call(*args, **kwargs) def get(*args, **kwargs): reopen() return nm.get(*args, **kwargs) lease._call, lease._get = Mock(side_effect=call), Mock(side_effect=get) lease._delete_stale_profiles = Mock(side_effect=lambda: None) return lease def credentials(): return bs.WifiCredentials(ssid="HondaAA", key="secret-key", bssid="AA:BB:CC:DD:EE:FF", security=8, ap_type=1) def test_network_lease_settings_keep_internet_route(): from openpilot.starpilot.system.android_auto.network import connection_settings settings = connection_settings(credentials(), "wlan0") assert settings["ipv4"]["never-default"] == ("b", True) and settings["ipv4"]["ignore-auto-dns"] == ("b", True) assert settings["connection"]["autoconnect"] == ("b", False) and settings["802-11-wireless-security"]["psk"] == ("s", "secret-key") assert settings["802-11-wireless"]["bssid"] == ("ay", bytes.fromhex("AABBCCDDEEFF")) def test_network_lease_retries_without_bssid_and_restores_previous(): nm = FakeNetworkManager(fail_with_bssid=True) lease = make_lease(nm) assert lease.acquire(credentials(), timeout=2.0) == "192.168.50.23" assert "bssid" in nm.added[0]["802-11-wireless"] and "bssid" not in nm.added[1]["802-11-wireless"] lease.release(restore=True) assert nm.calls[-1] == "ActivateConnection" # home Wi-Fi comes back def test_network_lease_waits_for_a_slow_honda_access_point(monkeypatch): from openpilot.starpilot.system.android_auto import network lease = network.NetworkLease(lambda *a, **k: None) budgets = [] def attempt(credentials, timeout, cancelled): budgets.append((credentials.bssid, timeout)) raise network.NetworkError("not up yet") monkeypatch.setattr(lease, "_acquire_once", attempt) monkeypatch.setattr(lease, "_drop_attempt", lambda: None) with pytest.raises(network.NetworkError): lease.acquire(credentials()) assert budgets == [("AA:BB:CC:DD:EE:FF", 90.0), ("", 30.0)] def test_network_lease_does_not_undo_user_network_change(): nm = FakeNetworkManager() lease = make_lease(nm) lease.acquire(credentials(), timeout=2.0) nm.active_device_connection = "/active/user-picked" # user chose another network meanwhile lease.release(restore=True) assert "ActivateConnection" not in nm.calls def test_network_lease_retry_keeps_previous_for_final_stop(): nm = FakeNetworkManager() lease = make_lease(nm) lease.acquire(credentials(), timeout=2.0) lease.release(restore=False) assert "ActivateConnection" not in nm.calls and lease.previous_connection == "/settings/home" lease.acquire(credentials(), timeout=2.0) lease.release(restore=True) assert nm.calls.count("ActivateConnection") == 1