Files
2026-11-02 01:23:50 +03:00

155 lines
5.3 KiB
Python

#!/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