Files
StarPilot/openpilot/starpilot/ui/bluetooth_status.py
T

90 lines
3.3 KiB
Python

"""Read-only BlueZ state for the compact Home indicator: off, on, or a device connected.
This does not enable Bluetooth or manage devices. The system-bus query runs off
the render thread and expires after a short gap.
"""
from collections.abc import Callable
from concurrent.futures import Future, ThreadPoolExecutor
import json
import subprocess
import time
STATES = ("off", "on", "connected")
def _clock_ns() -> int:
boot = getattr(time, "CLOCK_BOOTTIME", None)
return time.clock_gettime_ns(boot) if boot is not None else time.monotonic_ns()
def read_bluetooth_state() -> str:
"""One BlueZ object listing: the adapter's power and whether any of its devices is connected."""
try:
result = subprocess.run(("busctl", "--system", "--timeout=1", "call", "org.bluez", "/", "org.freedesktop.DBus.ObjectManager",
"GetManagedObjects", "--json=short"), capture_output=True, text=True, timeout=1.5, check=False)
except (OSError, subprocess.TimeoutExpired):
return "off"
if result.returncode != 0:
return "off"
try:
objects = json.loads(result.stdout)["data"][0]
def prop(path, interface, name):
return objects.get(path, {}).get(interface, {}).get(name, {}).get("data")
if prop("/org/bluez/hci0", "org.bluez.Adapter1", "Powered") is not True:
return "off"
connected = any(path.startswith("/org/bluez/hci0/dev_") and prop(path, "org.bluez.Device1", "Connected") is True
for path in objects)
except (ValueError, KeyError, IndexError, TypeError, AttributeError):
return "off"
return "connected" if connected else "on"
class BluetoothStatusSource:
"""One bounded background read; stale, failed, or absent status is false."""
REFRESH_NS = 2_000_000_000
MAX_AGE_NS = 4_000_000_000
def __init__(self, request: Callable[[], str] = read_bluetooth_state,
clock: Callable[[], int] = _clock_ns):
self.request = request
self.clock = clock
self.executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="bluetooth-status")
self.pending: Future[tuple[str, int]] | None = None
self.next_read_ns = 0
self.state = "off"
self.observed_ns: int | None = None
self.closed = False
def _poll(self) -> tuple[str, int]:
try:
state = self.request()
except (OSError, RuntimeError, ValueError, subprocess.TimeoutExpired):
state = "off"
return state if state in STATES else "off", self.clock()
def snapshot(self) -> str:
if self.closed:
return "off"
now = self.clock()
if self.pending is not None and self.pending.done():
try:
self.state, self.observed_ns = self.pending.result()
except (OSError, RuntimeError, ValueError):
self.state, self.observed_ns = "off", None
self.pending = None
if now < self.next_read_ns - self.REFRESH_NS:
self.next_read_ns = 0
self.state, self.observed_ns = "off", None
if self.pending is None and now >= self.next_read_ns:
self.pending = self.executor.submit(self._poll)
self.next_read_ns = now + self.REFRESH_NS
fresh = self.observed_ns is not None and 0 <= now - self.observed_ns <= self.MAX_AGE_NS
return self.state if fresh else "off"
def close(self) -> None:
self.closed = True
self.executor.shutdown(wait=False, cancel_futures=True)