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:
@@ -1,15 +1,28 @@
|
||||
"""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.
|
||||
Handles Bluetooth Low Energy functionality including:
|
||||
- GATT server with full PM3/WiFi/System/Update services
|
||||
- Notifications for updates, backups, and battery alerts
|
||||
- Uses the Pi Zero 2 W built-in Bluetooth via bless library
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from .. import config
|
||||
|
||||
# Try to import bless for GATT server
|
||||
try:
|
||||
from ..ble.bluez_adapter import BlueZGATTAdapter, get_ble_adapter
|
||||
BLESS_AVAILABLE = True
|
||||
except ImportError:
|
||||
BLESS_AVAILABLE = False
|
||||
|
||||
# Fallback to dbus for basic operations
|
||||
try:
|
||||
import dbus
|
||||
import dbus.mainloop.glib
|
||||
@@ -18,7 +31,7 @@ try:
|
||||
except ImportError:
|
||||
DBUS_AVAILABLE = False
|
||||
|
||||
from .. import config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationType(str, Enum):
|
||||
@@ -30,6 +43,7 @@ class NotificationType(str, Enum):
|
||||
BATTERY_CRITICAL = "battery_critical"
|
||||
SHUTDOWN_INITIATED = "shutdown_initiated"
|
||||
PM3_STATUS = "pm3_status"
|
||||
CONFIG_REQUIRED = "config_required"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,7 +56,12 @@ class BLENotification:
|
||||
|
||||
|
||||
class BLEManager:
|
||||
"""Manages BLE notifications via Pi Zero 2 W Bluetooth."""
|
||||
"""Manages BLE functionality via Pi Zero 2 W Bluetooth.
|
||||
|
||||
Provides both:
|
||||
- Full GATT server with PM3/WiFi/System/Update services (via bless)
|
||||
- Simple notifications for system events
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the BLE manager."""
|
||||
@@ -50,12 +69,14 @@ class BLEManager:
|
||||
self._device_name = config.BLE_DEVICE_NAME
|
||||
self._is_available = False
|
||||
self._is_advertising = False
|
||||
self._gatt_server_running = 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
|
||||
self._gatt_adapter: Optional[BlueZGATTAdapter] = None
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize BLE adapter and check availability.
|
||||
@@ -64,58 +85,81 @@ class BLEManager:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
if not self._enabled:
|
||||
print("BLE is disabled in configuration")
|
||||
logger.info("BLE is disabled in configuration")
|
||||
return False
|
||||
|
||||
if not DBUS_AVAILABLE:
|
||||
print("BLE not available: dbus/GLib libraries not installed")
|
||||
# Check if Bluetooth adapter is available first
|
||||
has_adapter = await self._check_bluetooth_adapter()
|
||||
if not has_adapter:
|
||||
logger.warning("No Bluetooth adapter found")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check if Bluetooth adapter is available
|
||||
has_adapter = await self._check_bluetooth_adapter()
|
||||
self._is_available = True
|
||||
|
||||
if has_adapter:
|
||||
self._is_available = True
|
||||
print(f"BLE initialized: {self._device_name}")
|
||||
return True
|
||||
else:
|
||||
print("No Bluetooth adapter found")
|
||||
return False
|
||||
# Try to initialize GATT server with bless (preferred)
|
||||
if BLESS_AVAILABLE:
|
||||
try:
|
||||
self._gatt_adapter = get_ble_adapter()
|
||||
self._gatt_adapter.device_name = self._device_name
|
||||
logger.info("BLE GATT adapter initialized (bless): %s", self._device_name)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to initialize bless GATT adapter: %s", e)
|
||||
self._gatt_adapter = None
|
||||
else:
|
||||
logger.info("bless library not available, using basic BLE mode")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize BLE: {e}")
|
||||
return False
|
||||
logger.info("BLE initialized: %s", self._device_name)
|
||||
return True
|
||||
|
||||
async def start_advertising(self):
|
||||
"""Start BLE advertising to allow device connections."""
|
||||
"""Start BLE advertising and GATT server."""
|
||||
if not self._is_available:
|
||||
print("BLE not available, cannot start advertising")
|
||||
logger.warning("BLE not available, cannot start advertising")
|
||||
return
|
||||
|
||||
try:
|
||||
# Set device name
|
||||
await self._set_device_name(self._device_name)
|
||||
# Start full GATT server if available (preferred)
|
||||
if self._gatt_adapter:
|
||||
success = await self._gatt_adapter.start()
|
||||
if success:
|
||||
self._gatt_server_running = True
|
||||
self._is_advertising = True
|
||||
logger.info("BLE GATT server started: %s", self._device_name)
|
||||
return
|
||||
else:
|
||||
logger.warning("Failed to start GATT server, falling back to basic mode")
|
||||
|
||||
# Make device discoverable
|
||||
# Fallback to basic advertising via bluetoothctl
|
||||
await self._set_device_name(self._device_name)
|
||||
await self._set_discoverable(True)
|
||||
|
||||
self._is_advertising = True
|
||||
print(f"BLE advertising started: {self._device_name}")
|
||||
logger.info("BLE advertising started (basic mode): %s", self._device_name)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to start BLE advertising: {e}")
|
||||
logger.error("Failed to start BLE advertising: %s", e)
|
||||
self._is_advertising = False
|
||||
|
||||
async def stop_advertising(self):
|
||||
"""Stop BLE advertising."""
|
||||
if self._is_advertising:
|
||||
try:
|
||||
"""Stop BLE advertising and GATT server."""
|
||||
if not self._is_advertising:
|
||||
return
|
||||
|
||||
try:
|
||||
# Stop GATT server if running
|
||||
if self._gatt_adapter and self._gatt_server_running:
|
||||
await self._gatt_adapter.stop()
|
||||
self._gatt_server_running = False
|
||||
logger.info("BLE GATT server stopped")
|
||||
else:
|
||||
# Stop basic advertising
|
||||
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}")
|
||||
|
||||
self._is_advertising = False
|
||||
logger.info("BLE advertising stopped")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to stop BLE advertising: %s", e)
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
@@ -142,11 +186,11 @@ class BLEManager:
|
||||
|
||||
await self._notification_queue.put(notification)
|
||||
|
||||
# Process notification
|
||||
if self._connected_devices:
|
||||
# Process notification - always broadcast if GATT server is running
|
||||
if self._connected_devices or self._gatt_server_running:
|
||||
await self._broadcast_notification(notification)
|
||||
else:
|
||||
print(f"BLE notification queued (no devices connected): {message}")
|
||||
logger.debug("BLE notification queued (no devices connected): %s", message)
|
||||
|
||||
async def get_status(self) -> Dict[str, Any]:
|
||||
"""Get BLE manager status.
|
||||
@@ -158,6 +202,8 @@ class BLEManager:
|
||||
"enabled": self._enabled,
|
||||
"available": self._is_available,
|
||||
"advertising": self._is_advertising,
|
||||
"gatt_server_running": self._gatt_server_running,
|
||||
"gatt_available": BLESS_AVAILABLE,
|
||||
"connected_devices": len(self._connected_devices),
|
||||
"device_name": self._device_name,
|
||||
"queued_notifications": self._notification_queue.qsize()
|
||||
@@ -182,10 +228,10 @@ class BLEManager:
|
||||
return b"Controller" in stdout
|
||||
|
||||
except FileNotFoundError:
|
||||
print("bluetoothctl not found - BlueZ not installed")
|
||||
logger.warning("bluetoothctl not found - BlueZ not installed")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error checking Bluetooth adapter: {e}")
|
||||
logger.error("Error checking Bluetooth adapter: %s", e)
|
||||
return False
|
||||
|
||||
async def _set_device_name(self, name: str):
|
||||
@@ -202,7 +248,7 @@ class BLEManager:
|
||||
)
|
||||
await process.communicate()
|
||||
except Exception as e:
|
||||
print(f"Failed to set device name: {e}")
|
||||
logger.error("Failed to set device name: %s", e)
|
||||
|
||||
async def _set_discoverable(self, enabled: bool):
|
||||
"""Set Bluetooth discoverable state.
|
||||
@@ -219,7 +265,7 @@ class BLEManager:
|
||||
)
|
||||
await process.communicate()
|
||||
except Exception as e:
|
||||
print(f"Failed to set discoverable: {e}")
|
||||
logger.error("Failed to set discoverable: %s", e)
|
||||
|
||||
async def _broadcast_notification(self, notification: BLENotification):
|
||||
"""Broadcast notification to all connected devices.
|
||||
@@ -234,14 +280,24 @@ class BLEManager:
|
||||
"data": notification.data,
|
||||
"timestamp": notification.timestamp
|
||||
}
|
||||
payload_bytes = json.dumps(payload).encode('utf-8')
|
||||
|
||||
# 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}")
|
||||
# Use GATT server if available
|
||||
if self._gatt_adapter and self._gatt_server_running:
|
||||
try:
|
||||
# Send via system notification characteristic
|
||||
from ..ble.characteristics import SystemCharacteristicUUIDs
|
||||
await self._gatt_adapter.send_notification(
|
||||
SystemCharacteristicUUIDs.INFO,
|
||||
payload_bytes
|
||||
)
|
||||
logger.debug("BLE notification sent via GATT: %s", notification.type.value)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning("Failed to send GATT notification: %s", e)
|
||||
|
||||
# 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
|
||||
# Fallback: log the notification
|
||||
logger.info("BLE Notification: %s - %s", notification.type.value, notification.message)
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if BLE is available.
|
||||
|
||||
Reference in New Issue
Block a user