"""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, retries: int = 3, retry_delay: float = 2.0) -> bool: """Start the BLE GATT server. Args: retries: Number of startup attempts (for boot timing issues) retry_delay: Delay between retries in seconds Returns: True if started successfully, False otherwise """ if self._is_running: logger.warning("BLE server already running") return True last_error = None for attempt in range(retries): 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: last_error = e if attempt < retries - 1: logger.warning( "BLE server start attempt %d/%d failed: %s, retrying in %.1fs...", attempt + 1, retries, e, retry_delay ) # Clean up failed server before retry if self.server: try: await self.server.stop() except Exception: pass self.server = None await asyncio.sleep(retry_delay) else: logger.error("Failed to start BLE server after %d attempts: %s", retries, 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()