122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""GATT service 0xA000 — Sensor Report + Slot & Config."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Callable, Optional
|
|
|
|
from config import CHAR_REPORT_UUID, CHAR_SLOT_UUID, POLL_PERIOD_S, SERVICE_UUID
|
|
from protocol import (
|
|
CFG_JOIN_OPEN,
|
|
CFG_POOL_FULL,
|
|
SLOT_UNASSIGNED,
|
|
pack_slot_config,
|
|
unpack_report,
|
|
)
|
|
from registry import Registry
|
|
|
|
log = logging.getLogger("collector.gatt")
|
|
|
|
WriteHandler = Callable[[str, bytes], None]
|
|
ReadSlotHandler = Callable[[str], bytes]
|
|
ConnectHandler = Callable[[str], bool] # return False → should disconnect
|
|
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.on_write: Optional[WriteHandler] = None
|
|
self.on_read_slot: Optional[ReadSlotHandler] = 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 handle_write(self, characteristic, value: bytearray, **kwargs) -> None:
|
|
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)
|
|
|
|
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_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, POLL_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)
|
|
|
|
# Signature: (service, char, properties, value, permissions)
|
|
# 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,
|
|
)
|
|
|
|
# Slot & Config — read
|
|
await server.add_new_characteristic(
|
|
SERVICE_UUID,
|
|
CHAR_SLOT_UUID,
|
|
GATTCharacteristicProperties.read,
|
|
bytearray(gatt.default_slot_value("")),
|
|
GATTAttributePermissions.readable,
|
|
)
|
|
|
|
return server
|