collector: survive bluetoothd hangs and restarts
- every BlueZ call runs off the asyncio thread with a timeout; if bluetoothd stays unresponsive for 30 s the collector restarts bluetooth.service and exits for a clean re-registration - Type=notify + WatchdogSec for buttontask-collector, never give up restarting - bluetooth.service drop-in: Restart=always (BlueZ 5.55 crashes on assert/SEGV), installed by device-install.sh and OTA (bt-update) - bt-service: bluez-restart action for the collector Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+47
-47
@@ -1,9 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BlueZ helpers for connected devices (RSSI / disconnect). Advertising is via bless."""
|
||||
"""BlueZ helpers for connected devices (RSSI / disconnect). Advertising is via bless.
|
||||
|
||||
All calls here are blocking (python3-dbus). main.py runs them in a worker
|
||||
thread with a timeout, so they must raise on failure instead of returning
|
||||
empty results: an empty list would look like "everyone disconnected".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
log = logging.getLogger("collector.bluez_adv")
|
||||
|
||||
@@ -16,8 +21,11 @@ except Exception: # pragma: no cover
|
||||
dbus = None
|
||||
|
||||
BLUEZ = "org.bluez"
|
||||
PROPS = "org.freedesktop.DBus.Properties"
|
||||
OBJECT_MANAGER = "org.freedesktop.DBus.ObjectManager"
|
||||
DEVICE_IFACE = "org.bluez.Device1"
|
||||
|
||||
# Per-call D-Bus timeout (python3-dbus default is 25 s).
|
||||
CALL_TIMEOUT_S = 5.0
|
||||
|
||||
|
||||
def system_bus():
|
||||
@@ -26,32 +34,45 @@ def system_bus():
|
||||
return dbus.SystemBus()
|
||||
|
||||
|
||||
def read_device_rssi(bus, device_path: str) -> Optional[int]:
|
||||
if dbus is None:
|
||||
return None
|
||||
def _managed_objects(bus) -> dict:
|
||||
# introspect=False: no extra Introspect round-trip (it has its own 25 s timeout).
|
||||
obj = bus.get_object(BLUEZ, "/", introspect=False)
|
||||
mgr = dbus.Interface(obj, OBJECT_MANAGER)
|
||||
return mgr.GetManagedObjects(timeout=CALL_TIMEOUT_S)
|
||||
|
||||
|
||||
def bluez_owner(bus) -> Optional[str]:
|
||||
"""Unique bus name of the running bluetoothd, or None if it is not running.
|
||||
|
||||
Asks dbus-daemon only, so it does not depend on bluetoothd being responsive.
|
||||
"""
|
||||
try:
|
||||
obj = bus.get_object(BLUEZ, device_path)
|
||||
props = dbus.Interface(obj, PROPS)
|
||||
return int(props.Get("org.bluez.Device1", "RSSI"))
|
||||
except Exception:
|
||||
return None
|
||||
return str(bus.get_name_owner(BLUEZ))
|
||||
except dbus.exceptions.DBusException as e:
|
||||
if e.get_dbus_name() == "org.freedesktop.DBus.Error.NameHasNoOwner":
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def connected_devices(bus) -> Dict[str, Optional[int]]:
|
||||
"""{ADDR: rssi or None} for all connected devices. Raises on D-Bus errors."""
|
||||
out: Dict[str, Optional[int]] = {}
|
||||
for _path, ifaces in _managed_objects(bus).items():
|
||||
dev = ifaces.get(DEVICE_IFACE)
|
||||
if not dev or not bool(dev.get("Connected", False)):
|
||||
continue
|
||||
addr = str(dev.get("Address", "")).upper()
|
||||
rssi = dev.get("RSSI")
|
||||
out[addr] = int(rssi) if rssi is not None else None
|
||||
return out
|
||||
|
||||
|
||||
def find_device_path(bus, addr: str) -> Optional[str]:
|
||||
if dbus is None:
|
||||
return None
|
||||
addr_u = (addr or "").upper()
|
||||
try:
|
||||
obj = bus.get_object(BLUEZ, "/")
|
||||
mgr = dbus.Interface(obj, OBJECT_MANAGER)
|
||||
for path, ifaces in mgr.GetManagedObjects().items():
|
||||
dev = ifaces.get("org.bluez.Device1")
|
||||
if not dev:
|
||||
continue
|
||||
if str(dev.get("Address", "")).upper() == addr_u:
|
||||
return str(path)
|
||||
except Exception as e:
|
||||
log.debug("find_device: %s", e)
|
||||
for path, ifaces in _managed_objects(bus).items():
|
||||
dev = ifaces.get(DEVICE_IFACE)
|
||||
if dev and str(dev.get("Address", "")).upper() == addr_u:
|
||||
return str(path)
|
||||
return None
|
||||
|
||||
|
||||
@@ -59,26 +80,5 @@ def disconnect_device(bus, addr: str) -> None:
|
||||
path = find_device_path(bus, addr)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
obj = bus.get_object(BLUEZ, path)
|
||||
dbus.Interface(obj, "org.bluez.Device1").Disconnect()
|
||||
except Exception as e:
|
||||
log.warning("disconnect %s: %s", addr, e)
|
||||
|
||||
|
||||
def list_connected_addresses(bus) -> list:
|
||||
if dbus is None:
|
||||
return []
|
||||
out = []
|
||||
try:
|
||||
obj = bus.get_object(BLUEZ, "/")
|
||||
mgr = dbus.Interface(obj, OBJECT_MANAGER)
|
||||
for _path, ifaces in mgr.GetManagedObjects().items():
|
||||
dev = ifaces.get("org.bluez.Device1")
|
||||
if not dev:
|
||||
continue
|
||||
if bool(dev.get("Connected", False)):
|
||||
out.append(str(dev.get("Address", "")).upper())
|
||||
except Exception as e:
|
||||
log.debug("list_connected: %s", e)
|
||||
return out
|
||||
obj = bus.get_object(BLUEZ, path, introspect=False)
|
||||
dbus.Interface(obj, DEVICE_IFACE).Disconnect(timeout=CALL_TIMEOUT_S)
|
||||
|
||||
Reference in New Issue
Block a user