#!/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 ; 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(" 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