#!/usr/bin/env python3 """RSSI of a connected LE peer via the kernel Bluetooth Management API. BlueZ fills Device1.RSSI only while *scanning*; our sensors connect to us (we are the GATT server), so that property stays empty. The controller still measures the link: MGMT_OP_GET_CONN_INFO returns it (same as `btmgmt conn-info`). Needs CAP_NET_ADMIN (mgmt sockets without it are "untrusted" and this command is refused): buttontask-collector.service sets AmbientCapabilities. Blocking; main.py calls it in an executor with a timeout. """ from __future__ import annotations import ctypes import os import select import socket import struct import time from typing import Optional AF_BLUETOOTH = 31 BTPROTO_HCI = 1 HCI_DEV_NONE = 0xFFFF HCI_CHANNEL_CONTROL = 3 MGMT_OP_GET_CONN_INFO = 0x0031 MGMT_EV_CMD_COMPLETE = 0x0001 MGMT_EV_CMD_STATUS = 0x0002 MGMT_STATUS_NOT_CONNECTED = 0x02 MGMT_STATUS_PERMISSION_DENIED = 0x14 ADDR_TYPES = {"bredr": 0, "public": 1, "random": 2} RSSI_INVALID = 127 _libc = ctypes.CDLL(None, use_errno=True) class MgmtPermissionError(PermissionError): """The collector lacks CAP_NET_ADMIN.""" def _open() -> int: # Python's bind() for BTPROTO_HCI cannot select a channel, hence libc. # Stays a raw fd: socket.socket(fileno=...) calls getsockname(), which the # kernel refuses (EBADFD) on the control channel. fd = _libc.socket(AF_BLUETOOTH, socket.SOCK_RAW | socket.SOCK_CLOEXEC, BTPROTO_HCI) if fd < 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) sa = struct.pack(" bytes: parts = addr.split(":") if len(parts) != 6: raise ValueError(f"bad address {addr!r}") return bytes(int(p, 16) for p in reversed(parts)) def conn_rssi(addr: str, addr_type: str = "random", index: int = 0, timeout: float = 1.5) -> Optional[int]: """RSSI (dBm) of the connected peer, or None if not connected / unknown. Raises MgmtPermissionError without CAP_NET_ADMIN, OSError on socket errors. """ params = _bdaddr(addr) + bytes([ADDR_TYPES.get(addr_type, 2)]) cmd = struct.pack("