"""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()