"""BLE Manager for Dangerous Pi. Handles Bluetooth Low Energy notifications for updates, backups, and battery alerts using the Pi Zero 2 W built-in Bluetooth. """ import asyncio import json from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Optional, Dict, Any, List try: import dbus import dbus.mainloop.glib from gi.repository import GLib DBUS_AVAILABLE = True except ImportError: DBUS_AVAILABLE = False from .. import config class NotificationType(str, Enum): """BLE notification type enum.""" UPDATE_AVAILABLE = "update_available" UPDATE_COMPLETE = "update_complete" BACKUP_COMPLETE = "backup_complete" BATTERY_WARNING = "battery_warning" BATTERY_CRITICAL = "battery_critical" SHUTDOWN_INITIATED = "shutdown_initiated" PM3_STATUS = "pm3_status" @dataclass class BLENotification: """BLE notification data.""" type: NotificationType message: str data: Dict[str, Any] timestamp: str class BLEManager: """Manages BLE notifications via Pi Zero 2 W Bluetooth.""" def __init__(self): """Initialize the BLE manager.""" self._enabled = config.BLE_ENABLED self._device_name = config.BLE_DEVICE_NAME self._is_available = False self._is_advertising = False self._connected_devices: List[str] = [] self._notification_queue: asyncio.Queue = asyncio.Queue() self._service_uuid = "12345678-1234-5678-1234-56789abcdef0" # Custom service UUID self._char_uuid = "12345678-1234-5678-1234-56789abcdef1" # Notification characteristic self._bus = None self._adapter = None async def initialize(self) -> bool: """Initialize BLE adapter and check availability. Returns: True if initialization successful, False otherwise """ if not self._enabled: print("BLE is disabled in configuration") return False if not DBUS_AVAILABLE: print("BLE not available: dbus/GLib libraries not installed") return False try: # Check if Bluetooth adapter is available has_adapter = await self._check_bluetooth_adapter() if has_adapter: self._is_available = True print(f"BLE initialized: {self._device_name}") return True else: print("No Bluetooth adapter found") return False except Exception as e: print(f"Failed to initialize BLE: {e}") return False async def start_advertising(self): """Start BLE advertising to allow device connections.""" if not self._is_available: print("BLE not available, cannot start advertising") return try: # Set device name await self._set_device_name(self._device_name) # Make device discoverable await self._set_discoverable(True) self._is_advertising = True print(f"BLE advertising started: {self._device_name}") except Exception as e: print(f"Failed to start BLE advertising: {e}") self._is_advertising = False async def stop_advertising(self): """Stop BLE advertising.""" if self._is_advertising: try: await self._set_discoverable(False) self._is_advertising = False print("BLE advertising stopped") except Exception as e: print(f"Failed to stop BLE advertising: {e}") async def send_notification( self, notification_type: NotificationType, message: str, data: Optional[Dict[str, Any]] = None ): """Send a BLE notification to connected devices. Args: notification_type: Type of notification message: Human-readable message data: Optional additional data """ if not self._is_available: return notification = BLENotification( type=notification_type, message=message, data=data or {}, timestamp=datetime.utcnow().isoformat() ) await self._notification_queue.put(notification) # Process notification if self._connected_devices: await self._broadcast_notification(notification) else: print(f"BLE notification queued (no devices connected): {message}") async def get_status(self) -> Dict[str, Any]: """Get BLE manager status. Returns: Dictionary with BLE status information """ return { "enabled": self._enabled, "available": self._is_available, "advertising": self._is_advertising, "connected_devices": len(self._connected_devices), "device_name": self._device_name, "queued_notifications": self._notification_queue.qsize() } async def _check_bluetooth_adapter(self) -> bool: """Check if Bluetooth adapter is available. Returns: True if adapter is available, False otherwise """ try: # Use bluetoothctl to check for adapter process = await asyncio.create_subprocess_exec( "bluetoothctl", "list", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() # If we get output with "Controller", we have an adapter return b"Controller" in stdout except FileNotFoundError: print("bluetoothctl not found - BlueZ not installed") return False except Exception as e: print(f"Error checking Bluetooth adapter: {e}") return False async def _set_device_name(self, name: str): """Set the Bluetooth device name. Args: name: Device name to set """ try: process = await asyncio.create_subprocess_exec( "bluetoothctl", "system-alias", name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() except Exception as e: print(f"Failed to set device name: {e}") async def _set_discoverable(self, enabled: bool): """Set Bluetooth discoverable state. Args: enabled: True to enable discoverable, False to disable """ try: command = "on" if enabled else "off" process = await asyncio.create_subprocess_exec( "bluetoothctl", "discoverable", command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() except Exception as e: print(f"Failed to set discoverable: {e}") async def _broadcast_notification(self, notification: BLENotification): """Broadcast notification to all connected devices. Args: notification: Notification to broadcast """ # Convert notification to JSON payload = { "type": notification.type.value, "message": notification.message, "data": notification.data, "timestamp": notification.timestamp } # In a full implementation, this would write to a BLE characteristic # that connected devices are subscribed to. For now, we'll just log it. print(f"BLE Notification: {notification.type.value} - {notification.message}") # If we had connected devices, we would send the notification here # This would require setting up a GATT server with proper characteristics # For simplicity in this MVP, we're using a notification-based approach def is_available(self) -> bool: """Check if BLE is available. Returns: True if BLE is available, False otherwise """ return self._is_available def is_advertising(self) -> bool: """Check if BLE is currently advertising. Returns: True if advertising, False otherwise """ return self._is_advertising def get_connected_devices(self) -> List[str]: """Get list of connected device addresses. Returns: List of connected device MAC addresses """ return self._connected_devices.copy() # Global BLE manager instance _ble_manager: Optional[BLEManager] = None def get_ble_manager() -> BLEManager: """Get the global BLE manager instance.""" global _ble_manager if _ble_manager is None: _ble_manager = BLEManager() return _ble_manager