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:
48
app/backend/ble/__init__.py
Normal file
48
app/backend/ble/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""BLE GATT server implementation for Dangerous Pi.
|
||||
|
||||
This module provides Bluetooth Low Energy (BLE) GATT server functionality
|
||||
that reuses the service layer for business logic, ensuring consistency
|
||||
with the REST API.
|
||||
|
||||
Components:
|
||||
- DangerousPiGATTServer: GATT handlers that call service layer
|
||||
- BlueZGATTAdapter: Bridges bless library to GATT handlers
|
||||
- Characteristic UUIDs: Service and characteristic definitions
|
||||
"""
|
||||
|
||||
from .gatt_server import DangerousPiGATTServer
|
||||
from .characteristics import (
|
||||
PM3CharacteristicUUIDs,
|
||||
WiFiCharacteristicUUIDs,
|
||||
SystemCharacteristicUUIDs,
|
||||
UpdateCharacteristicUUIDs,
|
||||
)
|
||||
|
||||
# BlueZ adapter (requires bless library)
|
||||
try:
|
||||
from .bluez_adapter import (
|
||||
BlueZGATTAdapter,
|
||||
get_ble_adapter,
|
||||
start_ble_server,
|
||||
stop_ble_server,
|
||||
)
|
||||
BLESS_AVAILABLE = True
|
||||
except ImportError:
|
||||
BlueZGATTAdapter = None
|
||||
get_ble_adapter = None
|
||||
start_ble_server = None
|
||||
stop_ble_server = None
|
||||
BLESS_AVAILABLE = False
|
||||
|
||||
__all__ = [
|
||||
"DangerousPiGATTServer",
|
||||
"PM3CharacteristicUUIDs",
|
||||
"WiFiCharacteristicUUIDs",
|
||||
"SystemCharacteristicUUIDs",
|
||||
"UpdateCharacteristicUUIDs",
|
||||
"BlueZGATTAdapter",
|
||||
"get_ble_adapter",
|
||||
"start_ble_server",
|
||||
"stop_ble_server",
|
||||
"BLESS_AVAILABLE",
|
||||
]
|
||||
457
app/backend/ble/bluez_adapter.py
Normal file
457
app/backend/ble/bluez_adapter.py
Normal file
@@ -0,0 +1,457 @@
|
||||
"""BlueZ GATT adapter using the bless library.
|
||||
|
||||
This module bridges our GATT server handlers to BlueZ via bless,
|
||||
enabling BLE peripheral functionality on Linux.
|
||||
|
||||
Architecture:
|
||||
bless BLEServer (handles BlueZ D-Bus)
|
||||
|
|
||||
BlueZGATTAdapter (this file - bridges handlers)
|
||||
|
|
||||
DangerousPiGATTServer (handlers call service layer)
|
||||
|
|
||||
Service Layer (business logic)
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
from typing import Dict, Optional, Any, Union
|
||||
|
||||
from bless import (
|
||||
BlessServer,
|
||||
BlessGATTCharacteristic,
|
||||
GATTCharacteristicProperties,
|
||||
GATTAttributePermissions,
|
||||
)
|
||||
|
||||
from .gatt_server import DangerousPiGATTServer
|
||||
from .characteristics import (
|
||||
PM3CharacteristicUUIDs,
|
||||
WiFiCharacteristicUUIDs,
|
||||
SystemCharacteristicUUIDs,
|
||||
UpdateCharacteristicUUIDs,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Device name for BLE advertising
|
||||
DEFAULT_DEVICE_NAME = "Dangerous-Pi"
|
||||
|
||||
|
||||
class BlueZGATTAdapter:
|
||||
"""Adapter connecting bless BLE server to our GATT handlers.
|
||||
|
||||
This class:
|
||||
- Initializes the bless BLE server
|
||||
- Registers all services and characteristics via GATT dictionary
|
||||
- Routes read/write requests to DangerousPiGATTServer handlers
|
||||
- Sends notifications via bless
|
||||
"""
|
||||
|
||||
def __init__(self, device_name: str = DEFAULT_DEVICE_NAME):
|
||||
"""Initialize the BlueZ GATT adapter.
|
||||
|
||||
Args:
|
||||
device_name: BLE device name for advertising
|
||||
"""
|
||||
self.device_name = device_name
|
||||
self.server: Optional[BlessServer] = None
|
||||
self.gatt_server = DangerousPiGATTServer()
|
||||
self._is_running = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
# Platform-specific trigger for async coordination
|
||||
self._trigger: Union[asyncio.Event, threading.Event]
|
||||
if sys.platform in ["darwin", "win32"]:
|
||||
self._trigger = threading.Event()
|
||||
else:
|
||||
self._trigger = asyncio.Event()
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if BLE server is running."""
|
||||
return self._is_running
|
||||
|
||||
def _build_gatt_dict(self) -> Dict:
|
||||
"""Build the GATT dictionary for bless.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping service UUIDs to characteristic definitions
|
||||
"""
|
||||
return {
|
||||
# PM3 Service
|
||||
PM3CharacteristicUUIDs.SERVICE: {
|
||||
PM3CharacteristicUUIDs.COMMAND_WRITE: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.read |
|
||||
GATTCharacteristicProperties.notify
|
||||
),
|
||||
"Permissions": GATTAttributePermissions.readable,
|
||||
"Value": bytearray(b'{}'),
|
||||
},
|
||||
PM3CharacteristicUUIDs.STATUS: {
|
||||
"Properties": GATTCharacteristicProperties.read,
|
||||
"Permissions": GATTAttributePermissions.readable,
|
||||
"Value": bytearray(b'{"connected": false}'),
|
||||
},
|
||||
PM3CharacteristicUUIDs.SESSION_CREATE: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
PM3CharacteristicUUIDs.SESSION_RELEASE: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
},
|
||||
# WiFi Service
|
||||
WiFiCharacteristicUUIDs.SERVICE: {
|
||||
WiFiCharacteristicUUIDs.STATUS: {
|
||||
"Properties": GATTCharacteristicProperties.read,
|
||||
"Permissions": GATTAttributePermissions.readable,
|
||||
"Value": bytearray(b'{"mode": "unknown"}'),
|
||||
},
|
||||
WiFiCharacteristicUUIDs.SCAN: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.write |
|
||||
GATTCharacteristicProperties.notify
|
||||
),
|
||||
"Permissions": (
|
||||
GATTAttributePermissions.readable |
|
||||
GATTAttributePermissions.writeable
|
||||
),
|
||||
"Value": None,
|
||||
},
|
||||
WiFiCharacteristicUUIDs.CONNECT: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
WiFiCharacteristicUUIDs.MODE: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.read |
|
||||
GATTCharacteristicProperties.write
|
||||
),
|
||||
"Permissions": (
|
||||
GATTAttributePermissions.readable |
|
||||
GATTAttributePermissions.writeable
|
||||
),
|
||||
"Value": bytearray(b'client'),
|
||||
},
|
||||
},
|
||||
# System Service
|
||||
SystemCharacteristicUUIDs.SERVICE: {
|
||||
SystemCharacteristicUUIDs.INFO: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.read |
|
||||
GATTCharacteristicProperties.notify
|
||||
),
|
||||
"Permissions": GATTAttributePermissions.readable,
|
||||
"Value": bytearray(b'{}'),
|
||||
},
|
||||
SystemCharacteristicUUIDs.SHUTDOWN: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
SystemCharacteristicUUIDs.RESTART: {
|
||||
"Properties": GATTCharacteristicProperties.write,
|
||||
"Permissions": GATTAttributePermissions.writeable,
|
||||
"Value": None,
|
||||
},
|
||||
},
|
||||
# Update Service
|
||||
UpdateCharacteristicUUIDs.SERVICE: {
|
||||
UpdateCharacteristicUUIDs.CHECK: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.write |
|
||||
GATTCharacteristicProperties.notify
|
||||
),
|
||||
"Permissions": (
|
||||
GATTAttributePermissions.readable |
|
||||
GATTAttributePermissions.writeable
|
||||
),
|
||||
"Value": None,
|
||||
},
|
||||
UpdateCharacteristicUUIDs.PROGRESS: {
|
||||
"Properties": (
|
||||
GATTCharacteristicProperties.read |
|
||||
GATTCharacteristicProperties.notify
|
||||
),
|
||||
"Permissions": GATTAttributePermissions.readable,
|
||||
"Value": bytearray(b'{"progress": 0}'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def _on_read(self, characteristic: BlessGATTCharacteristic) -> bytearray:
|
||||
"""Handle BLE read request.
|
||||
|
||||
Note: This callback is called synchronously from the D-Bus event handler.
|
||||
We cannot block on async operations here as it would deadlock the event loop.
|
||||
Instead, we return cached values and schedule async updates in the background.
|
||||
|
||||
Args:
|
||||
characteristic: The characteristic being read
|
||||
|
||||
Returns:
|
||||
Characteristic value as bytearray
|
||||
"""
|
||||
uuid = str(characteristic.uuid).lower()
|
||||
logger.info("BLE Read request for characteristic %s", uuid)
|
||||
|
||||
# Return the current cached value (synchronously)
|
||||
# Async handlers update these values in the background
|
||||
if characteristic.value:
|
||||
logger.info("Read returning cached: %s", characteristic.value[:50] if len(characteristic.value) > 50 else characteristic.value)
|
||||
return characteristic.value
|
||||
|
||||
# Return default JSON if no cached value
|
||||
default_value = bytearray(b'{"status": "initializing"}')
|
||||
logger.info("Read returning default: %s", default_value)
|
||||
return default_value
|
||||
|
||||
def _on_write(self, characteristic: BlessGATTCharacteristic, value: Any):
|
||||
"""Handle BLE write request.
|
||||
|
||||
Args:
|
||||
characteristic: The characteristic being written
|
||||
value: Value being written
|
||||
"""
|
||||
uuid = str(characteristic.uuid).lower() # Use lowercase to match our UUID format
|
||||
logger.info("BLE Write request for characteristic %s: %s", uuid, value)
|
||||
|
||||
# Update the characteristic value
|
||||
characteristic.value = value
|
||||
|
||||
handler = self.gatt_server.get_characteristic_handler(uuid)
|
||||
if handler and handler.write_handler:
|
||||
try:
|
||||
# Convert value to bytes if needed
|
||||
if isinstance(value, bytearray):
|
||||
data = bytes(value)
|
||||
elif isinstance(value, bytes):
|
||||
data = value
|
||||
else:
|
||||
data = bytes(value)
|
||||
|
||||
# Run async handler in event loop
|
||||
if self._loop and self._loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
handler.write_handler(data),
|
||||
self._loop
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error handling write for %s: %s", uuid, e)
|
||||
|
||||
def _on_subscribe(self, characteristic: BlessGATTCharacteristic, **kwargs):
|
||||
"""Handle subscription to characteristic notifications."""
|
||||
logger.info("Client subscribed to %s", characteristic.uuid)
|
||||
|
||||
def _on_unsubscribe(self, characteristic: BlessGATTCharacteristic, **kwargs):
|
||||
"""Handle unsubscription from characteristic notifications."""
|
||||
logger.info("Client unsubscribed from %s", characteristic.uuid)
|
||||
|
||||
async def start(self) -> bool:
|
||||
"""Start the BLE GATT server.
|
||||
|
||||
Returns:
|
||||
True if started successfully, False otherwise
|
||||
"""
|
||||
if self._is_running:
|
||||
logger.warning("BLE server already running")
|
||||
return True
|
||||
|
||||
try:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
# Create bless server
|
||||
self.server = BlessServer(name=self.device_name, loop=self._loop)
|
||||
|
||||
# Set up callbacks (bless 0.3.0+ API)
|
||||
self.server.read_request_func = self._on_read
|
||||
self.server.write_request_func = self._on_write
|
||||
|
||||
# Build and add GATT structure
|
||||
gatt = self._build_gatt_dict()
|
||||
await self.server.add_gatt(gatt)
|
||||
|
||||
# Start the server (begins advertising)
|
||||
await self.server.start()
|
||||
|
||||
# Start GATT server handlers
|
||||
await self.gatt_server.start()
|
||||
|
||||
# Register notification callbacks
|
||||
self._setup_notification_callbacks()
|
||||
|
||||
self._is_running = True
|
||||
logger.info("BLE GATT server started, advertising as '%s'", self.device_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to start BLE server: %s", e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self._is_running = False
|
||||
return False
|
||||
|
||||
def _setup_notification_callbacks(self):
|
||||
"""Set up notification callbacks for characteristics that support notify."""
|
||||
notify_chars = [
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
WiFiCharacteristicUUIDs.SCAN,
|
||||
SystemCharacteristicUUIDs.INFO,
|
||||
UpdateCharacteristicUUIDs.CHECK,
|
||||
UpdateCharacteristicUUIDs.PROGRESS,
|
||||
]
|
||||
|
||||
for uuid in notify_chars:
|
||||
self._register_notification_callback(uuid)
|
||||
|
||||
def _register_notification_callback(self, uuid: str):
|
||||
"""Register notification callback for a characteristic.
|
||||
|
||||
Args:
|
||||
uuid: Characteristic UUID
|
||||
"""
|
||||
async def send_notification(value: bytes):
|
||||
if self.server and self._is_running:
|
||||
try:
|
||||
char = self.server.get_characteristic(uuid)
|
||||
if char:
|
||||
char.value = bytearray(value)
|
||||
service_uuid = self._get_service_uuid_for_characteristic(uuid)
|
||||
# update_value is not async in bless 0.3+
|
||||
self.server.update_value(service_uuid, uuid)
|
||||
logger.debug("Notification sent for %s", uuid)
|
||||
except Exception as e:
|
||||
logger.error("Failed to send notification for %s: %s", uuid, e)
|
||||
|
||||
self.gatt_server.register_notification_callback(uuid, send_notification)
|
||||
|
||||
def _get_service_uuid_for_characteristic(self, char_uuid: str) -> str:
|
||||
"""Get the service UUID that contains a characteristic.
|
||||
|
||||
Args:
|
||||
char_uuid: Characteristic UUID
|
||||
|
||||
Returns:
|
||||
Service UUID
|
||||
"""
|
||||
# Check each service's characteristics
|
||||
if char_uuid in [
|
||||
PM3CharacteristicUUIDs.COMMAND_WRITE,
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
PM3CharacteristicUUIDs.STATUS,
|
||||
PM3CharacteristicUUIDs.SESSION_CREATE,
|
||||
PM3CharacteristicUUIDs.SESSION_RELEASE,
|
||||
]:
|
||||
return PM3CharacteristicUUIDs.SERVICE
|
||||
elif char_uuid in [
|
||||
WiFiCharacteristicUUIDs.STATUS,
|
||||
WiFiCharacteristicUUIDs.SCAN,
|
||||
WiFiCharacteristicUUIDs.CONNECT,
|
||||
WiFiCharacteristicUUIDs.MODE,
|
||||
]:
|
||||
return WiFiCharacteristicUUIDs.SERVICE
|
||||
elif char_uuid in [
|
||||
SystemCharacteristicUUIDs.INFO,
|
||||
SystemCharacteristicUUIDs.SHUTDOWN,
|
||||
SystemCharacteristicUUIDs.RESTART,
|
||||
]:
|
||||
return SystemCharacteristicUUIDs.SERVICE
|
||||
elif char_uuid in [
|
||||
UpdateCharacteristicUUIDs.CHECK,
|
||||
UpdateCharacteristicUUIDs.PROGRESS,
|
||||
]:
|
||||
return UpdateCharacteristicUUIDs.SERVICE
|
||||
else:
|
||||
return PM3CharacteristicUUIDs.SERVICE # Default
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the BLE GATT server."""
|
||||
if not self._is_running:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.gatt_server.stop()
|
||||
|
||||
if self.server:
|
||||
await self.server.stop()
|
||||
self.server = None
|
||||
|
||||
self._is_running = False
|
||||
logger.info("BLE server stopped")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error stopping BLE server: %s", e)
|
||||
|
||||
async def send_notification(self, uuid: str, value: bytes) -> bool:
|
||||
"""Send a notification for a characteristic.
|
||||
|
||||
Args:
|
||||
uuid: Characteristic UUID
|
||||
value: Notification value
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
"""
|
||||
if not self.server or not self._is_running:
|
||||
return False
|
||||
|
||||
try:
|
||||
char = self.server.get_characteristic(uuid)
|
||||
if char:
|
||||
char.value = bytearray(value)
|
||||
service_uuid = self._get_service_uuid_for_characteristic(uuid)
|
||||
# update_value is not async in bless 0.3+
|
||||
self.server.update_value(service_uuid, uuid)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Error sending notification for %s: %s", uuid, e)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Singleton instance for application use
|
||||
_adapter_instance: Optional[BlueZGATTAdapter] = None
|
||||
|
||||
|
||||
def get_ble_adapter() -> BlueZGATTAdapter:
|
||||
"""Get or create the singleton BLE adapter instance.
|
||||
|
||||
Returns:
|
||||
BlueZGATTAdapter instance
|
||||
"""
|
||||
global _adapter_instance
|
||||
if _adapter_instance is None:
|
||||
_adapter_instance = BlueZGATTAdapter()
|
||||
return _adapter_instance
|
||||
|
||||
|
||||
async def start_ble_server(device_name: str = DEFAULT_DEVICE_NAME) -> bool:
|
||||
"""Start the BLE GATT server.
|
||||
|
||||
Args:
|
||||
device_name: BLE device name for advertising
|
||||
|
||||
Returns:
|
||||
True if started successfully
|
||||
"""
|
||||
adapter = get_ble_adapter()
|
||||
adapter.device_name = device_name
|
||||
return await adapter.start()
|
||||
|
||||
|
||||
async def stop_ble_server():
|
||||
"""Stop the BLE GATT server."""
|
||||
adapter = get_ble_adapter()
|
||||
await adapter.stop()
|
||||
112
app/backend/ble/characteristics.py
Normal file
112
app/backend/ble/characteristics.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""GATT Characteristic UUIDs for Dangerous Pi BLE interface.
|
||||
|
||||
This module defines the UUIDs for all GATT services and characteristics
|
||||
used by the Dangerous Pi BLE interface.
|
||||
|
||||
UUID Namespace: Dangerous Pi uses custom 128-bit UUIDs.
|
||||
Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12 hex chars)
|
||||
|
||||
Services are differentiated by the 5th group:
|
||||
- PM3: 0000xxxx (0000-000f)
|
||||
- WiFi: 0001xxxx (0010-001f)
|
||||
- System: 0002xxxx (0020-002f)
|
||||
- Update: 0003xxxx (0030-003f)
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def _uuid(suffix: str) -> str:
|
||||
"""Generate a Dangerous Pi UUID with the given 4-char suffix.
|
||||
|
||||
Args:
|
||||
suffix: 4-character hex suffix (e.g., "0001", "0010")
|
||||
|
||||
Returns:
|
||||
Full 128-bit UUID string
|
||||
"""
|
||||
return f"d4c3b2a1-0000-1000-8000-00805f9b{suffix}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PM3CharacteristicUUIDs:
|
||||
"""PM3 GATT Service and Characteristics.
|
||||
|
||||
Service for Proxmark3 operations (command execution, status).
|
||||
"""
|
||||
# Service UUID
|
||||
SERVICE = _uuid("0000")
|
||||
|
||||
# Characteristics
|
||||
COMMAND_WRITE = _uuid("0001") # Write: Execute PM3 command
|
||||
COMMAND_RESULT = _uuid("0002") # Notify: Command result
|
||||
STATUS = _uuid("0003") # Read: Get PM3 status
|
||||
SESSION_CREATE = _uuid("0004") # Write: Create session
|
||||
SESSION_RELEASE = _uuid("0005") # Write: Release session
|
||||
SESSION_INFO = _uuid("0006") # Read: Get session info
|
||||
|
||||
|
||||
@dataclass
|
||||
class WiFiCharacteristicUUIDs:
|
||||
"""WiFi GATT Service and Characteristics.
|
||||
|
||||
Service for WiFi network management (scan, connect, status).
|
||||
"""
|
||||
# Service UUID
|
||||
SERVICE = _uuid("0010")
|
||||
|
||||
# Characteristics
|
||||
STATUS = _uuid("0011") # Read: Get WiFi status
|
||||
SCAN = _uuid("0012") # Write: Trigger scan, Notify: Results
|
||||
CONNECT = _uuid("0013") # Write: Connect to network
|
||||
DISCONNECT = _uuid("0014") # Write: Disconnect
|
||||
MODE = _uuid("0015") # Read/Write: WiFi mode
|
||||
SAVED_NETWORKS = _uuid("0016") # Read: Get saved networks
|
||||
FORGET_NETWORK = _uuid("0017") # Write: Forget network
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemCharacteristicUUIDs:
|
||||
"""System GATT Service and Characteristics.
|
||||
|
||||
Service for system operations (info, shutdown, restart).
|
||||
"""
|
||||
# Service UUID
|
||||
SERVICE = _uuid("0020")
|
||||
|
||||
# Characteristics
|
||||
INFO = _uuid("0021") # Read: Get system info
|
||||
SHUTDOWN = _uuid("0022") # Write: Initiate shutdown
|
||||
RESTART = _uuid("0023") # Write: Initiate restart
|
||||
LOGS = _uuid("0024") # Read: Get service logs
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateCharacteristicUUIDs:
|
||||
"""Update GATT Service and Characteristics.
|
||||
|
||||
Service for software update management.
|
||||
"""
|
||||
# Service UUID
|
||||
SERVICE = _uuid("0030")
|
||||
|
||||
# Characteristics
|
||||
CHECK = _uuid("0031") # Write: Check for updates, Notify: Result
|
||||
DOWNLOAD = _uuid("0032") # Write: Download update
|
||||
INSTALL = _uuid("0033") # Write: Install update
|
||||
PROGRESS = _uuid("0034") # Read/Notify: Update progress
|
||||
RELEASE_NOTES = _uuid("0035") # Read: Get release notes
|
||||
|
||||
|
||||
# Characteristic properties
|
||||
class CharacteristicProperties:
|
||||
"""Standard GATT characteristic properties."""
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
WRITE_WITHOUT_RESPONSE = "write-without-response"
|
||||
NOTIFY = "notify"
|
||||
INDICATE = "indicate"
|
||||
|
||||
|
||||
# Characteristic descriptors
|
||||
CHARACTERISTIC_USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb"
|
||||
CLIENT_CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb"
|
||||
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