🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
720 lines
26 KiB
Python
720 lines
26 KiB
Python
"""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()
|