Files
StarPilot/openpilot/starpilot/system/android_auto/daemon.py
T
2026-10-04 17:23:26 -05:00

222 lines
9.2 KiB
Python

"""android_autod: local control socket for wireless Android Auto projection.
Runs only when Android Auto and Bluetooth are enabled. Projection starts when the user presses
Start, or on its own when auto-connect is on and the chosen car is on (see auto_connect.py);
until then nothing but a Bluetooth status check and the car's hands-free gateway runs. Commands are one JSON line per
connection on ``ANDROID_AUTO_SOCKET_PATH``, mirroring bluetooth_managerd.
python -m openpilot.starpilot.system.android_auto.daemon # service
python -m openpilot.starpilot.system.android_auto.daemon --once # foreground diagnostic run
python -m openpilot.starpilot.system.android_auto.daemon --once --synthetic # test pattern, no UI needed
"""
from __future__ import annotations
import argparse
from contextlib import ExitStack
import json
import os
import signal
import socketserver
import sys
import threading
import time
from typing import Any
from openpilot.starpilot.system.android_auto.protocol import ANDROID_AUTO_SOCKET_PATH
from openpilot.starpilot.system.android_auto.source_verifier import GalaxySourceVerifier
from openpilot.starpilot.system.android_auto.supervisor import Supervisor
PAIRING_COMMANDS = {"prepare_pairing", "pairing_status", "pairing_response", "cancel_pairing", "pair_device"}
AUTHENTICATED_COMMANDS = {"start", "set_auto_connect", "select_receiver", "set_view", "set_connection",
"prepare_pairing", "pairing_status", "pairing_response", "cancel_pairing", "pair_device", "devices", "bluetooth_action"}
def _offroad() -> bool:
try:
from openpilot.common.params import Params
return Params().get_bool("IsOffroad")
except Exception:
return False
def _enabled() -> bool:
try:
from openpilot.common.params import Params
return Params().get_bool('AndroidAutoEnabled')
except Exception:
return False
def handle(supervisor: Supervisor, request: dict[str, Any], verifier: GalaxySourceVerifier | None = None, *, parked=None) -> dict[str, Any]:
command = str(request.get("command", ""))
if command not in {"status", "stop"}:
from openpilot.common.params import Params
if not Params().get_bool("AndroidAutoEnabled"):
raise RuntimeError("Enable Android Auto under Toggles → Android Auto first")
if command in AUTHENTICATED_COMMANDS:
source = request.get('source')
if not isinstance(source, str) or verifier is None or not verifier.valid(source):
raise RuntimeError('Authenticated Galaxy Android Auto source required')
if command != 'bluetooth_action':
supervisor.bind_source_session(('galaxy', source))
if command in PAIRING_COMMANDS and not (parked or _offroad)():
raise RuntimeError("Disengage and shift into Park before pairing")
if command == 'bluetooth_action':
from openpilot.starpilot.bluetooth.owner import BluetoothRejected, BluetoothUnavailable
if set(request) != {'command', 'source', 'operation', 'address'}:
raise ValueError('Invalid companion Bluetooth request')
if not (parked or _offroad)():
return {'handled': True, 'error_code': 'park_required'}
try:
result = supervisor.companion_bluetooth_action(request['operation'], request['address'], ('galaxy', request['source']))
except (BluetoothRejected, BluetoothUnavailable) as error:
return {'handled': True, 'error_code': error.code}
except (OSError, RuntimeError):
return {'handled': True, 'error_code': 'service_unavailable'}
return {'handled': result is not None, **({'bluetooth': result} if result is not None else {})}
if command == "status":
return {"status": supervisor.status()}
if command == "start":
supervisor.user_start()
elif command == "stop":
supervisor.user_stop()
elif command == "set_auto_connect":
supervisor.set_auto_connect(bool(request.get("enabled", True)))
elif command == "select_receiver":
supervisor.select_receiver(str(request.get("address", "")), str(request.get("name", "")))
elif command == "set_view":
supervisor.set_view(str(request.get("view", "")))
elif command == "set_connection":
supervisor.set_connection(str(request.get("connection", "")))
elif command == "prepare_pairing":
supervisor.prepare_pairing()
elif command == "pairing_status":
return {"pairing": supervisor.pairing_status()}
elif command == "pair_device":
address = request.get("address")
if not isinstance(address, str):
raise ValueError("Invalid Bluetooth address")
supervisor.pair_device(address)
elif command == "pairing_response":
prompt, accepted = request.get('prompt_id'), request.get('accepted')
if not isinstance(prompt, str) or type(accepted) is not bool:
raise ValueError('Invalid pairing response')
supervisor.pairing_response(prompt, accepted, request.get('value', ''))
elif command == "cancel_pairing":
supervisor.cancel_pairing()
elif command == "devices":
return {"devices": supervisor.devices()}
else:
raise RuntimeError(f"Unknown Android Auto command: {command}")
return {}
class RequestHandler(socketserver.StreamRequestHandler):
def handle(self) -> None:
try:
request = json.loads(self.rfile.readline(64 * 1024))
response = {"ok": True, **handle(self.server.supervisor, request, self.server.source_verifier, parked=self.server.parked)}
except Exception as error:
response = {"ok": False, "error": str(error)}
self.wfile.write(json.dumps(response, separators=(",", ":"), default=str).encode() + b"\n")
class Server(socketserver.ThreadingUnixStreamServer):
daemon_threads = True
def __init__(self, path: str, supervisor: Supervisor, source_verifier: GalaxySourceVerifier, parked=_offroad):
self.supervisor = supervisor
self.source_verifier = source_verifier
self.parked = parked
super().__init__(path, RequestHandler)
def run_once(duration: float, synthetic: bool) -> int:
"""Foreground diagnostic: start, print state changes, stop after ``duration`` seconds or Ctrl-C."""
supervisor = Supervisor(synthetic=synthetic)
stop = threading.Event()
signal.signal(signal.SIGINT, lambda *_: stop.set())
signal.signal(signal.SIGTERM, lambda *_: stop.set())
supervisor.start()
deadline = time.monotonic() + duration
last = None
try:
while not stop.is_set() and time.monotonic() < deadline:
status = supervisor.status()
summary = (status["state"], status["detail"], status["error"])
if summary != last:
print(json.dumps({key: status[key] for key in ("state", "label", "detail", "error", "mode", "head_unit")}, default=str), flush=True)
last = summary
if status["state"] == "streaming" and int(time.monotonic()) % 5 == 0:
print(json.dumps({"stats": status["stats"]}), flush=True)
stop.wait(1.0)
finally:
supervisor.close()
return 0 if supervisor.status()["error"] == "" else 1
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--once", action="store_true", help="run one foreground session for diagnostics")
parser.add_argument("--duration", type=float, default=900.0)
parser.add_argument("--synthetic", action="store_true", help="with --once: send a moving test pattern instead of the UI")
args = parser.parse_args()
if args.once:
return run_once(args.duration, args.synthetic)
try:
os.unlink(ANDROID_AUTO_SOCKET_PATH)
except FileNotFoundError:
pass
from openpilot.starpilot.system.android_auto.bluetooth_bridge import SharedBluetoothOwner
source_verifier = GalaxySourceVerifier()
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.evidence import EvidenceSource
from openpilot.starpilot.galaxy.settings import LiveContextSource
with ExitStack() as cleanup:
evidence = EvidenceSource().start()
cleanup.callback(evidence.close)
authority = LiveContextSource(Params(), messages=evidence, evidence_wait_ms=0)
cleanup.callback(authority.close)
bluetooth_owner = SharedBluetoothOwner(authority.configuration_allowed,
session_valid=lambda session: len(session) == 2 and session[0] == 'galaxy' and source_verifier.valid(session[1]))
cleanup.callback(bluetooth_owner.close)
bluetooth_owner.recover_phone_role()
supervisor = Supervisor(shared_bluetooth_owner=bluetooth_owner, projection_enabled=_enabled)
cleanup.callback(supervisor.close)
exit_event = threading.Event()
cleanup.callback(exit_event.set)
def housekeeping():
while not exit_event.wait(2.0):
try:
supervisor.maintain()
except Exception:
pass
threading.Thread(target=housekeeping, daemon=True).start()
server = Server(ANDROID_AUTO_SOCKET_PATH, supervisor, source_verifier, authority.configuration_allowed)
cleanup.callback(server.server_close)
def shutdown(*_):
exit_event.set()
threading.Thread(target=server.shutdown, daemon=True).start()
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
try:
os.chmod(ANDROID_AUTO_SOCKET_PATH, 0o660)
server.serve_forever(poll_interval=0.5)
finally:
exit_event.set()
try:
os.unlink(ANDROID_AUTO_SOCKET_PATH)
except FileNotFoundError:
pass
return 0
if __name__ == "__main__":
sys.exit(main())