Initial commit - Phase 3/4
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
719
app/backend/ble/gatt_server.py
Normal file
719
app/backend/ble/gatt_server.py
Normal file
@@ -0,0 +1,719 @@
|
||||
"""GATT Server implementation for Dangerous Pi.
|
||||
|
||||
This GATT server provides BLE access to all Dangerous Pi functionality by
|
||||
reusing the service layer. This ensures identical behavior between REST API
|
||||
and BLE interface - NO CODE DUPLICATION!
|
||||
|
||||
Architecture:
|
||||
BLE GATT Characteristic Handler
|
||||
↓
|
||||
Service Layer (PM3Service, WiFiService, etc.)
|
||||
↓
|
||||
Managers/Workers (PM3Worker, WiFiManager, etc.)
|
||||
|
||||
The handlers are thin adapters, just like REST endpoints.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..services.container import container
|
||||
from .characteristics import (
|
||||
PM3CharacteristicUUIDs,
|
||||
WiFiCharacteristicUUIDs,
|
||||
SystemCharacteristicUUIDs,
|
||||
UpdateCharacteristicUUIDs,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CharacteristicHandler:
|
||||
"""Configuration for a GATT characteristic handler."""
|
||||
uuid: str
|
||||
properties: list # ["read", "write", "notify"]
|
||||
read_handler: Optional[Callable] = None
|
||||
write_handler: Optional[Callable] = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
class DangerousPiGATTServer:
|
||||
"""GATT server for Dangerous Pi BLE interface.
|
||||
|
||||
This server exposes Dangerous Pi functionality over BLE by reusing
|
||||
the service layer. All business logic comes from services, ensuring
|
||||
consistency with the REST API.
|
||||
|
||||
Key Features:
|
||||
- Uses ServiceContainer for all operations (same as REST!)
|
||||
- Converts BLE data formats to/from service calls
|
||||
- Sends notifications for async operations
|
||||
- Zero business logic duplication
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize GATT server."""
|
||||
self.is_running = False
|
||||
self._notification_callbacks: Dict[str, Callable] = {}
|
||||
self._characteristic_handlers: Dict[str, CharacteristicHandler] = {}
|
||||
|
||||
# Register all characteristic handlers
|
||||
self._register_pm3_characteristics()
|
||||
self._register_wifi_characteristics()
|
||||
self._register_system_characteristics()
|
||||
self._register_update_characteristics()
|
||||
|
||||
logger.info("GATT server initialized with %d characteristics",
|
||||
len(self._characteristic_handlers))
|
||||
|
||||
# ========================================================================
|
||||
# PM3 Characteristic Handlers
|
||||
# ========================================================================
|
||||
|
||||
def _register_pm3_characteristics(self):
|
||||
"""Register PM3 GATT characteristics.
|
||||
|
||||
All handlers delegate to PM3Service - NO business logic here!
|
||||
"""
|
||||
self._characteristic_handlers.update({
|
||||
PM3CharacteristicUUIDs.COMMAND_WRITE: CharacteristicHandler(
|
||||
uuid=PM3CharacteristicUUIDs.COMMAND_WRITE,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_pm3_command_write,
|
||||
description="Execute PM3 command"
|
||||
),
|
||||
PM3CharacteristicUUIDs.STATUS: CharacteristicHandler(
|
||||
uuid=PM3CharacteristicUUIDs.STATUS,
|
||||
properties=["read"],
|
||||
read_handler=self._handle_pm3_status_read,
|
||||
description="Get PM3 status"
|
||||
),
|
||||
PM3CharacteristicUUIDs.SESSION_CREATE: CharacteristicHandler(
|
||||
uuid=PM3CharacteristicUUIDs.SESSION_CREATE,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_session_create_write,
|
||||
description="Create PM3 session"
|
||||
),
|
||||
PM3CharacteristicUUIDs.SESSION_RELEASE: CharacteristicHandler(
|
||||
uuid=PM3CharacteristicUUIDs.SESSION_RELEASE,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_session_release_write,
|
||||
description="Release PM3 session"
|
||||
),
|
||||
})
|
||||
|
||||
async def _handle_pm3_command_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle PM3 command execution via BLE.
|
||||
|
||||
This is a THIN ADAPTER - delegates to PM3Service!
|
||||
|
||||
Args:
|
||||
value: JSON bytes: {"command": "hw version", "session_id": "..."}
|
||||
|
||||
Returns:
|
||||
Response dict to send via notification
|
||||
"""
|
||||
try:
|
||||
# Parse BLE request
|
||||
data = json.loads(value.decode('utf-8'))
|
||||
command = data.get("command")
|
||||
session_id = data.get("session_id")
|
||||
|
||||
if not command:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "invalid_request", "message": "Command required"}
|
||||
}
|
||||
|
||||
# ✅ REUSE PM3Service - same logic as REST API!
|
||||
result = await container.pm3_service.execute_command(
|
||||
command=command,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# Convert service result to BLE response
|
||||
if result.success:
|
||||
response = {
|
||||
"success": True,
|
||||
"output": result.data["output"],
|
||||
"command": result.data["command"]
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
# Send notification with result
|
||||
await self._notify_characteristic(
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
json.dumps(response).encode('utf-8')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "invalid_json", "message": "Invalid JSON"}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Error handling PM3 command: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_pm3_status_read(self) -> bytes:
|
||||
"""Handle PM3 status read via BLE.
|
||||
|
||||
This is a THIN ADAPTER - delegates to PM3Service!
|
||||
|
||||
Returns:
|
||||
JSON bytes with PM3 status
|
||||
"""
|
||||
try:
|
||||
# ✅ REUSE PM3Service - same logic as REST API!
|
||||
result = await container.pm3_service.get_status()
|
||||
|
||||
if result.success:
|
||||
# Handle both single-device and multi-device responses
|
||||
if "devices" in result.data:
|
||||
# Multi-device mode: summarize status
|
||||
devices = result.data["devices"]
|
||||
connected_count = sum(1 for d in devices if d.get("connected"))
|
||||
response = {
|
||||
"success": True,
|
||||
"device_count": len(devices),
|
||||
"connected_count": connected_count,
|
||||
"devices": devices
|
||||
}
|
||||
else:
|
||||
# Legacy single-device mode
|
||||
response = {
|
||||
"success": True,
|
||||
"connected": result.data.get("connected", False),
|
||||
"device_path": result.data.get("device_path"),
|
||||
"version": result.data.get("version"),
|
||||
"session_active": result.data.get("session_active", False)
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(response).encode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting PM3 status: %s", e)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}).encode('utf-8')
|
||||
|
||||
async def _handle_session_create_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle session creation via BLE.
|
||||
|
||||
Args:
|
||||
value: JSON bytes: {"force_takeover": false}
|
||||
"""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8'))
|
||||
force_takeover = data.get("force_takeover", False)
|
||||
|
||||
# ✅ REUSE PM3Service
|
||||
result = container.pm3_service.create_session(
|
||||
force_takeover=force_takeover
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"session_id": result.data["session_id"]
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error creating session: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_session_release_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle session release via BLE.
|
||||
|
||||
Args:
|
||||
value: JSON bytes: {"session_id": "..."}
|
||||
"""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8'))
|
||||
session_id = data.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "invalid_request", "message": "Session ID required"}
|
||||
}
|
||||
|
||||
# ✅ REUSE PM3Service
|
||||
result = container.pm3_service.release_session(session_id)
|
||||
|
||||
if result.success:
|
||||
return {"success": True, "message": result.data["message"]}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error releasing session: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
# ========================================================================
|
||||
# WiFi Characteristic Handlers
|
||||
# ========================================================================
|
||||
|
||||
def _register_wifi_characteristics(self):
|
||||
"""Register WiFi GATT characteristics."""
|
||||
self._characteristic_handlers.update({
|
||||
WiFiCharacteristicUUIDs.STATUS: CharacteristicHandler(
|
||||
uuid=WiFiCharacteristicUUIDs.STATUS,
|
||||
properties=["read"],
|
||||
read_handler=self._handle_wifi_status_read,
|
||||
description="Get WiFi status"
|
||||
),
|
||||
WiFiCharacteristicUUIDs.SCAN: CharacteristicHandler(
|
||||
uuid=WiFiCharacteristicUUIDs.SCAN,
|
||||
properties=["write", "notify"],
|
||||
write_handler=self._handle_wifi_scan_write,
|
||||
description="Scan for WiFi networks"
|
||||
),
|
||||
WiFiCharacteristicUUIDs.CONNECT: CharacteristicHandler(
|
||||
uuid=WiFiCharacteristicUUIDs.CONNECT,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_wifi_connect_write,
|
||||
description="Connect to WiFi network"
|
||||
),
|
||||
WiFiCharacteristicUUIDs.MODE: CharacteristicHandler(
|
||||
uuid=WiFiCharacteristicUUIDs.MODE,
|
||||
properties=["read", "write"],
|
||||
read_handler=self._handle_wifi_mode_read,
|
||||
write_handler=self._handle_wifi_mode_write,
|
||||
description="WiFi mode"
|
||||
),
|
||||
})
|
||||
|
||||
async def _handle_wifi_status_read(self) -> bytes:
|
||||
"""Handle WiFi status read via BLE."""
|
||||
try:
|
||||
# ✅ REUSE WiFiService
|
||||
result = await container.wifi_service.get_status()
|
||||
|
||||
if result.success:
|
||||
return json.dumps(result.data).encode('utf-8')
|
||||
else:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}).encode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting WiFi status: %s", e)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}).encode('utf-8')
|
||||
|
||||
async def _handle_wifi_scan_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle WiFi scan request via BLE."""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8')) if value else {}
|
||||
interface = data.get("interface")
|
||||
|
||||
# ✅ REUSE WiFiService
|
||||
result = await container.wifi_service.scan_networks(interface)
|
||||
|
||||
if result.success:
|
||||
# Send scan results via notification
|
||||
await self._notify_characteristic(
|
||||
WiFiCharacteristicUUIDs.SCAN,
|
||||
json.dumps(result.data).encode('utf-8')
|
||||
)
|
||||
return {"success": True, "count": result.data["count"]}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error scanning WiFi: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_wifi_connect_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle WiFi connect request via BLE."""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8'))
|
||||
ssid = data.get("ssid")
|
||||
password = data.get("password")
|
||||
hidden = data.get("hidden", False)
|
||||
|
||||
if not ssid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "invalid_request", "message": "SSID required"}
|
||||
}
|
||||
|
||||
# ✅ REUSE WiFiService
|
||||
result = await container.wifi_service.connect(
|
||||
ssid=ssid,
|
||||
password=password,
|
||||
hidden=hidden
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"ssid": result.data["ssid"],
|
||||
"ip": result.data.get("ip")
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to WiFi: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_wifi_mode_read(self) -> bytes:
|
||||
"""Handle WiFi mode read via BLE."""
|
||||
try:
|
||||
result = await container.wifi_service.get_status()
|
||||
if result.success:
|
||||
return json.dumps({"mode": result.data["mode"]}).encode('utf-8')
|
||||
else:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": result.error.code, "message": result.error.message}
|
||||
}).encode('utf-8')
|
||||
except Exception as e:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}).encode('utf-8')
|
||||
|
||||
async def _handle_wifi_mode_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle WiFi mode change via BLE."""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8'))
|
||||
mode = data.get("mode")
|
||||
|
||||
if not mode:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "invalid_request", "message": "Mode required"}
|
||||
}
|
||||
|
||||
# ✅ REUSE WiFiService
|
||||
result = await container.wifi_service.set_mode(mode)
|
||||
|
||||
if result.success:
|
||||
return {"success": True, "mode": result.data["mode"]}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error setting WiFi mode: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
# ========================================================================
|
||||
# System Characteristic Handlers
|
||||
# ========================================================================
|
||||
|
||||
def _register_system_characteristics(self):
|
||||
"""Register System GATT characteristics."""
|
||||
self._characteristic_handlers.update({
|
||||
SystemCharacteristicUUIDs.INFO: CharacteristicHandler(
|
||||
uuid=SystemCharacteristicUUIDs.INFO,
|
||||
properties=["read"],
|
||||
read_handler=self._handle_system_info_read,
|
||||
description="Get system information"
|
||||
),
|
||||
SystemCharacteristicUUIDs.SHUTDOWN: CharacteristicHandler(
|
||||
uuid=SystemCharacteristicUUIDs.SHUTDOWN,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_system_shutdown_write,
|
||||
description="Initiate system shutdown"
|
||||
),
|
||||
SystemCharacteristicUUIDs.RESTART: CharacteristicHandler(
|
||||
uuid=SystemCharacteristicUUIDs.RESTART,
|
||||
properties=["write"],
|
||||
write_handler=self._handle_system_restart_write,
|
||||
description="Initiate system restart"
|
||||
),
|
||||
})
|
||||
|
||||
async def _handle_system_info_read(self) -> bytes:
|
||||
"""Handle system info read via BLE."""
|
||||
try:
|
||||
# ✅ REUSE SystemService
|
||||
result = await container.system_service.get_info()
|
||||
|
||||
if result.success:
|
||||
return json.dumps(result.data).encode('utf-8')
|
||||
else:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}).encode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting system info: %s", e)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}).encode('utf-8')
|
||||
|
||||
async def _handle_system_shutdown_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle system shutdown request via BLE."""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8')) if value else {}
|
||||
delay = data.get("delay", 0)
|
||||
|
||||
# ✅ REUSE SystemService
|
||||
result = await container.system_service.shutdown(delay=delay)
|
||||
|
||||
if result.success:
|
||||
return {"success": True, "message": result.data["message"]}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error initiating shutdown: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_system_restart_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle system restart request via BLE."""
|
||||
try:
|
||||
data = json.loads(value.decode('utf-8')) if value else {}
|
||||
delay = data.get("delay", 0)
|
||||
|
||||
# ✅ REUSE SystemService
|
||||
result = await container.system_service.restart(delay=delay)
|
||||
|
||||
if result.success:
|
||||
return {"success": True, "message": result.data["message"]}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error initiating restart: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
# ========================================================================
|
||||
# Update Characteristic Handlers
|
||||
# ========================================================================
|
||||
|
||||
def _register_update_characteristics(self):
|
||||
"""Register Update GATT characteristics."""
|
||||
self._characteristic_handlers.update({
|
||||
UpdateCharacteristicUUIDs.CHECK: CharacteristicHandler(
|
||||
uuid=UpdateCharacteristicUUIDs.CHECK,
|
||||
properties=["write", "notify"],
|
||||
write_handler=self._handle_update_check_write,
|
||||
description="Check for updates"
|
||||
),
|
||||
UpdateCharacteristicUUIDs.PROGRESS: CharacteristicHandler(
|
||||
uuid=UpdateCharacteristicUUIDs.PROGRESS,
|
||||
properties=["read", "notify"],
|
||||
read_handler=self._handle_update_progress_read,
|
||||
description="Get update progress"
|
||||
),
|
||||
})
|
||||
|
||||
async def _handle_update_check_write(self, value: bytes) -> Dict[str, Any]:
|
||||
"""Handle update check request via BLE."""
|
||||
try:
|
||||
# ✅ REUSE UpdateService
|
||||
result = await container.update_service.check_for_updates()
|
||||
|
||||
if result.success:
|
||||
# Send result via notification
|
||||
await self._notify_characteristic(
|
||||
UpdateCharacteristicUUIDs.CHECK,
|
||||
json.dumps(result.data).encode('utf-8')
|
||||
)
|
||||
return result.data
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error checking for updates: %s", e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}
|
||||
|
||||
async def _handle_update_progress_read(self) -> bytes:
|
||||
"""Handle update progress read via BLE."""
|
||||
try:
|
||||
# ✅ REUSE UpdateService
|
||||
result = await container.update_service.get_progress()
|
||||
|
||||
if result.success:
|
||||
return json.dumps(result.data).encode('utf-8')
|
||||
else:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": result.error.code,
|
||||
"message": result.error.message
|
||||
}
|
||||
}).encode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting update progress: %s", e)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": {"code": "internal_error", "message": str(e)}
|
||||
}).encode('utf-8')
|
||||
|
||||
# ========================================================================
|
||||
# GATT Server Management
|
||||
# ========================================================================
|
||||
|
||||
async def start(self):
|
||||
"""Start the GATT server."""
|
||||
logger.info("Starting Dangerous Pi GATT server")
|
||||
self.is_running = True
|
||||
logger.info("GATT server started with %d characteristics",
|
||||
len(self._characteristic_handlers))
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the GATT server."""
|
||||
logger.info("Stopping Dangerous Pi GATT server")
|
||||
self.is_running = False
|
||||
logger.info("GATT server stopped")
|
||||
|
||||
def get_characteristic_handler(self, uuid: str) -> Optional[CharacteristicHandler]:
|
||||
"""Get handler for a characteristic UUID.
|
||||
|
||||
Args:
|
||||
uuid: Characteristic UUID
|
||||
|
||||
Returns:
|
||||
CharacteristicHandler or None if not found
|
||||
"""
|
||||
return self._characteristic_handlers.get(uuid)
|
||||
|
||||
async def _notify_characteristic(self, uuid: str, value: bytes):
|
||||
"""Send notification for a characteristic.
|
||||
|
||||
Args:
|
||||
uuid: Characteristic UUID
|
||||
value: Notification value (bytes)
|
||||
"""
|
||||
if uuid in self._notification_callbacks:
|
||||
try:
|
||||
await self._notification_callbacks[uuid](value)
|
||||
logger.debug("Sent notification for %s", uuid)
|
||||
except Exception as e:
|
||||
logger.error("Error sending notification for %s: %s", uuid, e)
|
||||
else:
|
||||
logger.debug("No notification callback registered for %s", uuid)
|
||||
|
||||
def register_notification_callback(self, uuid: str, callback: Callable):
|
||||
"""Register callback for sending notifications.
|
||||
|
||||
Args:
|
||||
uuid: Characteristic UUID
|
||||
callback: Async callable that sends the notification
|
||||
"""
|
||||
self._notification_callbacks[uuid] = callback
|
||||
logger.debug("Registered notification callback for %s", uuid)
|
||||
|
||||
def get_all_characteristics(self) -> Dict[str, CharacteristicHandler]:
|
||||
"""Get all registered characteristic handlers.
|
||||
|
||||
Returns:
|
||||
Dict mapping UUID to CharacteristicHandler
|
||||
"""
|
||||
return self._characteristic_handlers.copy()
|
||||
Reference in New Issue
Block a user