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:
michael
2026-01-06 13:45:29 -08:00
parent 1da6730735
commit 4f35df1781
323 changed files with 98287 additions and 1195 deletions

View File

@@ -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.