Files
StarPilot/openpilot/starpilot/system/android_auto/protocol.py
T
Danny a531afd02a Bluetooth: Coordinate companion pairing and Android Auto ownership
Expose pairing readiness and coordinate adapter hand-offs with Android Auto.
Show app pairings separately, support individual removal, and improve pairing
controls on both comma screens. Add Galaxy companion endpoints and web asset
manifests, preserve the plain camera look, and fix headless speed-limit shapes.
2026-10-22 15:37:20 -07:00

99 lines
3.6 KiB
Python

"""Client for android_autod's local control socket (used by the settings UI)."""
from __future__ import annotations
import json
import os
import socket
from typing import Any
ANDROID_AUTO_SOCKET_PATH = "/tmp/starpilot-android-auto.sock"
COMMAND_TIMEOUTS = {"stop": 12.0, "prepare_pairing": 20.0, "devices": 10.0, "select_receiver": 10.0, "bluetooth_action": 25.0, "bluetooth_power": 25.0,
# Opening a window may wait on a slow BlueZ right after boot, and a closed one is given time to wind down.
"phone_window": 25.0,
"forget_receiver": 15.0}
class AndroidAutoClient:
def __init__(self, socket_path: str = ANDROID_AUTO_SOCKET_PATH, timeout: float = 5.0):
self.socket_path = socket_path
self.timeout = timeout
@property
def available(self) -> bool:
return os.path.exists(self.socket_path)
def call(self, command: str, **payload: Any) -> dict[str, Any]:
if not self.available:
raise RuntimeError("Android Auto service is not running (turn on Bluetooth)")
request = json.dumps({"command": command, **payload}, separators=(",", ":")).encode() + b"\n"
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.settimeout(COMMAND_TIMEOUTS.get(command, self.timeout))
sock.connect(self.socket_path)
sock.sendall(request)
response = bytearray()
while not response.endswith(b"\n"):
chunk = sock.recv(65536)
if not chunk:
break
response.extend(chunk)
if not response:
raise RuntimeError("Android Auto service returned no response")
result = json.loads(response)
if not result.get("ok", False):
raise RuntimeError(str(result.get("error", "Android Auto operation failed")))
return result
def status(self) -> dict[str, Any]:
return self.call("status").get("status", {})
def start(self) -> None:
self.call("start")
def stop(self) -> None:
self.call("stop")
def select_receiver(self, address: str, name: str = "") -> None:
self.call("select_receiver", address=address, name=name)
def set_view(self, view: str) -> None:
self.call("set_view", view=view)
def set_connection(self, connection: str) -> None:
self.call("set_connection", connection=connection)
def prepare_pairing(self) -> None:
self.call("prepare_pairing")
def set_auto_connect(self, enabled: bool) -> None:
self.call("set_auto_connect", enabled=enabled)
def devices(self) -> list[dict[str, Any]]:
return list(self.call("devices").get("devices", []))
def forget_car(address: str, client: AndroidAutoClient | None = None) -> bool:
"""Clear Android Auto's chosen car (or companion) after its pairing was deleted; True if it was the chosen car.
Goes through android_autod when it runs, so its in-memory settings stay in step. Otherwise nothing
holds the settings, and the saved file is edited directly.
"""
client = client or AndroidAutoClient()
if client.available:
return bool(client.call("forget_receiver", address=address).get("cleared"))
from openpilot.starpilot.system.android_auto import identity
address = address.strip().upper()
config = identity.load_config()
chosen = config["receiver_address"].upper() == address
companion = config["companion_address"].upper() == address
cache = dict(config["rfcomm_cache"])
if cache.pop(address, None) is None and not (chosen or companion):
return False
config["rfcomm_cache"] = cache
if chosen:
config.update(receiver_address="", receiver_name="", companion_address="", companion_name="")
elif companion:
config.update(companion_address="", companion_name="")
identity.save_config(config)
return chosen