Files
StarPilot/openpilot/starpilot/bluetooth/native_owner.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

72 lines
3.0 KiB
Python

"""Native settings use Galaxy when Android Auto already owns the Bluetooth adapter."""
from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner, BluetoothRejected, BluetoothUnavailable
class NativeBluetoothOwner(BluetoothOwner):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.galaxy = None
self.delegated_pairing = False
self.delegated_scan = False
def _galaxy(self, operation, **kwargs):
if self.galaxy is None:
from openpilot.starpilot.galaxy_bluetooth.phone_ui import galaxy_call
self.galaxy = galaxy_call() # Keep the same authenticated session for prompts and polling.
if operation == 'status':
status, result = self.galaxy('GET', '/api/bluetooth/status')
else:
payload = {'operation': operation}
for key, value in kwargs.items():
if key in ('address', 'enabled', 'prompt_id', 'accepted') and value is not None:
payload['promptId' if key == 'prompt_id' else key] = value
if operation == 'pairing_response':
payload['value'] = kwargs.get('value', '')
status, result = self.galaxy('POST', '/api/bluetooth/action', payload)
if status == 409:
raise BluetoothRejected('Bluetooth change was not accepted', code=result.get('code', 'busy'))
if status != 200 or not isinstance(result.get('devices'), list):
raise BluetoothUnavailable('Galaxy Bluetooth service is unavailable')
if operation in ('pair', 'accept_pair'):
self.delegated_pairing = True
elif operation == 'cancel_pair':
self.delegated_pairing = False
if operation == 'scan':
self.delegated_scan = True
elif operation in ('stop_scan', 'pair', 'accept_pair'):
self.delegated_scan = False
return result
def request(self, operation, *, delegate=True, **kwargs):
session = kwargs.get('session')
if session is not None and not self.session_valid(session):
raise BluetoothRejected('Pairing session expired', code='session_expired')
if self.galaxy is None:
try:
return super().request(operation, **kwargs)
except AdapterBusyElsewhere:
if not delegate:
return super().snapshot(session=session)
elif not delegate:
return self.snapshot(session=session)
return self._galaxy(operation, **kwargs)
def snapshot(self, *, session=None):
if self.galaxy is not None:
return self._galaxy('status')
return super().snapshot(session=session)
def close(self):
try:
if self.galaxy is not None:
for operation, needed in (('cancel_pair', self.delegated_pairing), ('stop_scan', self.delegated_scan)):
if needed:
try:
self._galaxy(operation)
except (BluetoothRejected, BluetoothUnavailable, OSError, RuntimeError):
pass # The daemon also bounds scans and cancels expired pairing sessions.
finally:
self.galaxy = None
self.delegated_pairing = self.delegated_scan = False
super().close()