Files
Button/collector/adv.py
T
2026-10-16 07:19:06 +03:00

153 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Advertising mirror for bless 0.3.x (BlueZ): stop → register with ManufacturerData."""
from __future__ import annotations
import logging
import time
from typing import Any, List, Optional
from config import (
ADV_DEBOUNCE_S,
ADV_VERSION,
COMPANY_ID,
LOCAL_NAME,
N,
SERVICE_UUID_16,
)
log = logging.getLogger("collector.adv")
def build_manufacturer_data(bitmap: int, reports: List[bytes]) -> bytes:
"""ver=1 + bitmap + N×4 B reports."""
if N == 1 and reports:
return bytes((ADV_VERSION,)) + reports[0][:4]
body = bytearray()
body.append(ADV_VERSION)
body.append(bitmap & 0xFF)
for i in range(N):
r = reports[i] if i < len(reports) else bytes((0xFF, 0, 0xFF, 0xFF))
body.extend(r[:4])
return bytes(body)
def build_ad_payload(mfr: bytes) -> bytes:
"""Legacy AD bytes (debug)."""
uuid_ad = bytes((0x03, 0x03, SERVICE_UUID_16 & 0xFF, (SERVICE_UUID_16 >> 8) & 0xFF))
mfr_body = COMPANY_ID.to_bytes(2, "little") + mfr
mfr_ad = bytes((1 + len(mfr_body), 0xFF)) + mfr_body
return uuid_ad + mfr_ad
class BlessAdvertiser:
"""Debounced advertising updates through a running BlessServer (0.3.x)."""
def __init__(self, server: Any, debounce_s: float = ADV_DEBOUNCE_S):
self._server = server
self._debounce_s = debounce_s
self._last_apply = 0.0
self._pending: Optional[bytes] = None
self._current: Optional[bytes] = None
self._force = False
def set_mirror(self, mfr_payload: bytes, force: bool = False) -> None:
self._pending = mfr_payload
if force:
self._force = True
def force_reregister(self) -> None:
self._force = True
if self._pending is None:
self._pending = self._current
async def tick(self) -> bool:
if self._pending is None and not self._force:
return False
now = time.monotonic()
if not self._force and (now - self._last_apply) < self._debounce_s:
return False
payload = self._pending if self._pending is not None else self._current
if payload is None:
self._force = False
return False
if not self._force and payload == self._current:
self._pending = None
return False
ok = await self._apply(payload)
if ok:
self._current = payload
self._pending = None
self._force = False
self._last_apply = now
else:
self._force = False
return ok
async def _apply(self, mfr_payload: bytes) -> bool:
server = self._server
try:
setup = getattr(server, "setup_task", None)
if setup is not None:
await setup
except Exception as e:
log.debug("setup_task: %s", e)
app = getattr(server, "app", None)
adapter = getattr(server, "adapter", None)
if app is None or adapter is None:
log.error("bless server has no app/adapter — cannot update adv")
return False
try:
from dbus_next.signature import Variant # type: ignore
from bless.backends.bluezdbus.dbus.advertisement import ( # type: ignore
BlueZLEAdvertisement,
Type,
)
except Exception as e:
log.error("bless bluez advertisement import failed: %s", e)
return False
# Drop previous advertisement(s) owned by this app.
try:
while getattr(app, "advertisements", None):
await app.stop_advertising(adapter)
except Exception as e:
log.debug("stop_advertising: %s", e)
try:
await app.set_name(adapter, LOCAL_NAME)
advertisement = BlueZLEAdvertisement(
Type.PERIPHERAL, len(app.advertisements) + 1, app
)
app.advertisements.append(advertisement)
if app.services:
advertisement._service_uuids.append(app.services[0].UUID)
else:
advertisement._service_uuids.append(f"{SERVICE_UUID_16:04x}")
advertisement._local_name = LOCAL_NAME
advertisement._manufacturer_data = {
COMPANY_ID: Variant("ay", bytes(mfr_payload))
}
# Avoid experimental interval props on BlueZ 5.55 if possible —
# leave bless defaults; they are ignored without -E.
app.bus.export(advertisement.path, advertisement)
iface = adapter.get_interface("org.bluez.LEAdvertisingManager1")
await iface.call_register_advertisement(advertisement.path, {})
log.info(
"advertisement registered (mfr %d bytes, path=%s)",
len(mfr_payload),
advertisement.path,
)
return True
except Exception as e:
log.error("adv register failed: %s", e)
return False
# Back-compat
Advertiser = BlessAdvertiser