#!/usr/bin/env python3 """GATT service 0xA000 — Sensor Report + Interval + Slot & Config.""" from __future__ import annotations import logging import time from typing import Callable, Optional from config import ( CHAR_INTERVAL_UUID, CHAR_REPORT_UUID, CHAR_SLOT_UUID, DEFAULT_REPORT_INTERVAL_MIN, SERVICE_UUID, ) from protocol import ( CFG_JOIN_OPEN, CFG_POOL_FULL, SLOT_UNASSIGNED, pack_interval_min, pack_slot_config, unpack_report, ) from registry import Registry log = logging.getLogger("collector.gatt") WriteHandler = Callable[[str, bytes], None] ReadSlotHandler = Callable[[str], bytes] ReadIntervalHandler = Callable[[], bytes] ConnectHandler = Callable[[str], bool] DisconnectHandler = Callable[[str], None] class CollectorGatt: """GATT callbacks over bless; address resolution is injected by main.""" def __init__(self, registry: Registry): self.registry = registry self._current_addr: Optional[str] = None self.report_interval_min: int = DEFAULT_REPORT_INTERVAL_MIN self.on_write: Optional[WriteHandler] = None self.on_read_slot: Optional[ReadSlotHandler] = None self.on_read_interval: Optional[ReadIntervalHandler] = None self.on_connect: Optional[ConnectHandler] = None self.on_disconnect: Optional[DisconnectHandler] = None def set_current_addr(self, addr: Optional[str]) -> None: self._current_addr = addr def current_addr(self) -> Optional[str]: return self._current_addr def period_s(self) -> int: return max(1, int(self.report_interval_min) * 60) def handle_write(self, characteristic, value: bytearray, **kwargs) -> None: t0 = time.monotonic() try: data = bytes(value) if value is not None else b"" addr = self._current_addr or kwargs.get("address") or "" uuid = str(getattr(characteristic, "uuid", "") or "").lower() if CHAR_REPORT_UUID.lower() not in uuid and "a001" not in uuid: log.debug("write to unexpected char %s", uuid) return if len(data) != 4: log.warning("bad report length=%d from %s — discarded", len(data), addr) return report = unpack_report(data) if report is None: log.warning("bad report parse from %s — discarded", addr) return if self.on_write: self.on_write(addr, data) except Exception as e: log.error("write handler error: %s", e) finally: dt_ms = (time.monotonic() - t0) * 1000.0 if dt_ms >= 5.0: log.info("write-handler %.1f ms", dt_ms) else: log.debug("write-handler %.2f ms", dt_ms) def handle_read(self, characteristic, **kwargs) -> bytearray: try: uuid = str(getattr(characteristic, "uuid", "") or "").lower() addr = self._current_addr or kwargs.get("address") or "" if CHAR_INTERVAL_UUID.lower() in uuid or "a002" in uuid: if self.on_read_interval: return bytearray(self.on_read_interval()) return bytearray(pack_interval_min(self.report_interval_min)) if CHAR_SLOT_UUID.lower() in uuid or "a011" in uuid: if self.on_read_slot: return bytearray(self.on_read_slot(addr)) return bytearray(self.default_slot_value(addr)) return bytearray(b"\x00\x00\x00\x00") except Exception as e: log.error("read handler error: %s", e) return bytearray(b"\xff\x00\x00\x00") def default_slot_value(self, addr: str) -> bytes: sensor = self.registry.get(addr) if addr else None slot = sensor.slot if sensor else SLOT_UNASSIGNED flags = 0 if not self.registry.pool_full(): flags |= CFG_JOIN_OPEN else: flags |= CFG_POOL_FULL return pack_slot_config(slot, flags, self.period_s()) async def build_bless_server(gatt: CollectorGatt, loop, name: str): """Create and configure a BlessServer. Returns the server instance.""" from bless import ( # type: ignore BlessServer, GATTAttributePermissions, GATTCharacteristicProperties, ) server = BlessServer(name=name, loop=loop) server.read_request_func = gatt.handle_read server.write_request_func = gatt.handle_write await server.add_new_service(SERVICE_UUID) # Sensor Report — write-without-response await server.add_new_characteristic( SERVICE_UUID, CHAR_REPORT_UUID, GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.write, bytearray(b"\x00\x00\x00\x00"), GATTAttributePermissions.writeable, ) # Report Interval — read (2 bytes LE, minutes) await server.add_new_characteristic( SERVICE_UUID, CHAR_INTERVAL_UUID, GATTCharacteristicProperties.read, bytearray(pack_interval_min(gatt.report_interval_min)), GATTAttributePermissions.readable, ) # Slot & Config — read await server.add_new_characteristic( SERVICE_UUID, CHAR_SLOT_UUID, GATTCharacteristicProperties.read, bytearray(gatt.default_slot_value("")), GATTAttributePermissions.readable, ) return server