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

106 lines
3.4 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: unregister → register with debounce."""
from __future__ import annotations
import logging
import struct
import time
from typing import Any, Callable, List, Optional
from config import (
ADV_DEBOUNCE_S,
ADV_VERSION,
COMPANY_ID,
N,
SERVICE_UUID_16,
)
log = logging.getLogger("collector.adv")
def build_manufacturer_data(bitmap: int, reports: List[bytes]) -> bytes:
"""ver=1 + for N=4: bitmap + 4×4 B reports (fits in 31-byte AD with UUID)."""
if N == 1 and reports:
# ver + single report (legacy compact form from spec sketch)
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:
"""Full AD structure excluding Flags (BlueZ/bless adds Flags itself).
03 03 00 A0 ; Complete list of 16-bit UUIDs
LL FF FF FF <mfr...> ; Manufacturer Specific Data
"""
uuid_ad = bytes((0x03, 0x03, SERVICE_UUID_16 & 0xFF, (SERVICE_UUID_16 >> 8) & 0xFF))
# company id little-endian + payload
mfr_body = struct.pack("<H", COMPANY_ID) + mfr
mfr_ad = bytes((1 + len(mfr_body), 0xFF)) + mfr_body
return uuid_ad + mfr_ad
class Advertiser:
"""Debounced advertising re-registration wrapper."""
def __init__(
self,
register_fn: Callable[[bytes], Any],
unregister_fn: Callable[[], Any],
debounce_s: float = ADV_DEBOUNCE_S,
):
self._register = register_fn
self._unregister = unregister_fn
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 tick(self) -> bool:
"""Apply pending update if debounce elapsed. Returns True if re-registered."""
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
try:
self._unregister()
except Exception as e:
log.warning("adv unregister: %s", e)
try:
ad = build_ad_payload(payload)
self._register(ad)
self._current = payload
self._pending = None
self._force = False
self._last_apply = now
log.debug("adv re-registered (%d bytes mfr)", len(payload))
return True
except Exception as e:
log.error("adv register failed: %s", e)
self._force = False
return False
def force_reregister(self) -> None:
self._force = True
if self._pending is None:
self._pending = self._current