This commit adds the complete Dangerous Pi web management interface with all MVP features implemented and tested locally. ## New Features ### Backend (Python + FastAPI) - Complete FastAPI backend with async support - 40+ API endpoints (Health, PM3, WiFi, Updates, UPS, BLE, Plugins) - 6 managers: Session, WiFi, Update, UPS, BLE, Plugin - SQLite database with sessions, config, history, crash reports - Server-Sent Events (SSE) for real-time notifications - Mock PM3 worker for development without hardware ### WiFi Manager - Interface detection (USB vs built-in) - Network scanning with signal strength - Mode switching (AP/Client/Dual/Auto/Off) - Network connection with password support - Hidden SSID and saved networks support - Static IP and DHCP configuration - 10 WiFi API endpoints ### Update Manager - GitHub releases API integration - Automatic periodic update checks - Semantic version comparison - Update download with progress tracking - SHA256 checksum verification - Automatic installation with backup and rollback - PM3 client rebuild after updates - 6 Update API endpoints ### UPS Manager - I2C battery monitoring (MAX17040-compatible) - Battery percentage, voltage, current tracking - Power source detection (AC/Battery) - Safe shutdown triggers at configurable thresholds - Event callbacks for battery warnings - SSE and BLE notification integration - 3 UPS API endpoints ### BLE Manager - Bluetooth Low Energy notification support - Auto-detects BLE capability - Multiple notification types (updates, battery, shutdown, etc.) - BLE advertising management - Device connection tracking - 4 BLE API endpoints ### Plugin Framework - Dynamic plugin loading/unloading - Plugin lifecycle management (load, enable, disable, unload) - Hook system for extensibility - JSON-based metadata - Example "Hello World" plugin included - 7 Plugin API endpoints ### Frontend (Remix.js + React) - Cyberpunk-themed responsive UI - Dashboard with system status - PM3 command interface with history - Settings page with WiFi and Update management - Command logs viewer - Theme toggle (Dark/Light/Auto) - Server-side rendering (SSR) - Mobile-first responsive design ### System Integration - Systemd service with security hardening - Automated install/uninstall scripts - Environment configuration template - Hardware access groups (i2c, bluetooth, gpio, dialout) - Pi-gen stage 04 integration for OS image building - Port conflict resolution with ttyd-bash - I2C interface auto-enable for UPS HAT ### Testing - test_backend.py - Backend API tests - test_ups.py - UPS manager tests - test_ble.py - BLE manager tests - test_plugins.py - Plugin manager tests - All tests passing locally ### Documentation - 12 comprehensive documentation files - claude.md - AI development guide - WIFI_MANAGER.md - WiFi management guide - UPDATE_MANAGER.md - Update system guide - PORT_CONFLICT.md - Port conflict resolution guide - MVP_COMPLETE.md - MVP implementation summary - PROJECT_STATUS.md - Project status and roadmap - systemd/README.md - Service management docs - pi-gen integration documentation ## Technical Details - ~5,000+ lines of backend code - 11 Python dependencies (smbus2 added for UPS) - FastAPI with async/await throughout - Type hints and docstrings on all functions - RESTful API design with SSE for notifications - Security hardening (non-root, protected dirs, resource limits) ## Next Steps - Deploy to Raspberry Pi Zero 2 W hardware - Test with real Proxmark3 device - Test UPS HAT integration - Test BLE on Pi hardware - Build custom OS image with pi-gen - Performance optimization for Pi Zero 2 W 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
166 lines
4.9 KiB
Python
166 lines
4.9 KiB
Python
"""SSE event endpoints for real-time notifications."""
|
|
import asyncio
|
|
import json
|
|
from typing import AsyncGenerator
|
|
from fastapi import APIRouter
|
|
from sse_starlette.sse import EventSourceResponse
|
|
|
|
router = APIRouter()
|
|
|
|
# Global event queue for broadcasting events
|
|
event_queues: list[asyncio.Queue] = []
|
|
|
|
|
|
class EventBroadcaster:
|
|
"""Broadcasts events to all connected SSE clients."""
|
|
|
|
@staticmethod
|
|
async def broadcast(event_type: str, data: dict):
|
|
"""Broadcast an event to all connected clients.
|
|
|
|
Args:
|
|
event_type: Type of event (e.g., "update_available", "backup_complete")
|
|
data: Event data to send
|
|
"""
|
|
message = {
|
|
"type": event_type,
|
|
"data": data
|
|
}
|
|
|
|
# Send to all connected clients
|
|
dead_queues = []
|
|
for queue in event_queues:
|
|
try:
|
|
await queue.put(message)
|
|
except Exception:
|
|
# Queue is dead, mark for removal
|
|
dead_queues.append(queue)
|
|
|
|
# Remove dead queues
|
|
for dead_queue in dead_queues:
|
|
event_queues.remove(dead_queue)
|
|
|
|
|
|
broadcaster = EventBroadcaster()
|
|
|
|
|
|
async def event_generator() -> AsyncGenerator[dict, None]:
|
|
"""Generate SSE events for a client connection."""
|
|
# Create a new queue for this client
|
|
queue = asyncio.Queue()
|
|
event_queues.append(queue)
|
|
|
|
try:
|
|
# Send initial connection event
|
|
yield {
|
|
"event": "connected",
|
|
"data": json.dumps({"message": "Connected to Dangerous Pi event stream"})
|
|
}
|
|
|
|
# Keep connection alive and send events
|
|
while True:
|
|
try:
|
|
# Wait for events with timeout to send keep-alive
|
|
message = await asyncio.wait_for(queue.get(), timeout=30.0)
|
|
|
|
yield {
|
|
"event": message["type"],
|
|
"data": json.dumps(message["data"])
|
|
}
|
|
except asyncio.TimeoutError:
|
|
# Send keep-alive comment
|
|
yield {
|
|
"comment": "keep-alive"
|
|
}
|
|
except asyncio.CancelledError:
|
|
# Client disconnected
|
|
pass
|
|
finally:
|
|
# Remove queue when client disconnects
|
|
if queue in event_queues:
|
|
event_queues.remove(queue)
|
|
|
|
|
|
@router.get("/events")
|
|
async def stream_events():
|
|
"""SSE endpoint for streaming events to clients.
|
|
|
|
Events include:
|
|
- update_available: New version available on GitHub
|
|
- update_downloading: Update download in progress
|
|
- update_complete: Update downloaded and ready to install
|
|
- backup_complete: Backup operation completed
|
|
- ups_low_battery: UPS battery below threshold
|
|
- pm3_rebuild_required: PM3 client rebuild needed
|
|
- command_complete: Long-running command completed
|
|
"""
|
|
return EventSourceResponse(event_generator())
|
|
|
|
|
|
# Helper functions for sending specific event types
|
|
|
|
async def notify_update_available(version: str, url: str):
|
|
"""Notify clients that an update is available."""
|
|
await broadcaster.broadcast("update_available", {
|
|
"version": version,
|
|
"url": url
|
|
})
|
|
|
|
|
|
async def notify_update_progress(progress: float, status: str):
|
|
"""Notify clients of update download progress."""
|
|
await broadcaster.broadcast("update_downloading", {
|
|
"progress": progress,
|
|
"status": status
|
|
})
|
|
|
|
|
|
async def notify_backup_complete(backup_path: str, size_bytes: int):
|
|
"""Notify clients that backup is complete."""
|
|
await broadcaster.broadcast("backup_complete", {
|
|
"path": backup_path,
|
|
"size": size_bytes
|
|
})
|
|
|
|
|
|
async def notify_ups_battery(percentage: float, voltage: float):
|
|
"""Notify clients of UPS battery status."""
|
|
await broadcaster.broadcast("ups_battery", {
|
|
"percentage": percentage,
|
|
"voltage": voltage
|
|
})
|
|
|
|
|
|
async def notify_ups_warning(percentage: float, threshold: float):
|
|
"""Notify clients of low battery warning."""
|
|
await broadcaster.broadcast("ups_warning", {
|
|
"percentage": percentage,
|
|
"threshold": threshold,
|
|
"message": f"Battery low: {percentage}%"
|
|
})
|
|
|
|
|
|
async def notify_ups_critical(percentage: float):
|
|
"""Notify clients of critical battery level."""
|
|
await broadcaster.broadcast("ups_critical", {
|
|
"percentage": percentage,
|
|
"message": f"Battery critical: {percentage}%"
|
|
})
|
|
|
|
|
|
async def notify_ups_shutdown(delay: int, percentage: float):
|
|
"""Notify clients that shutdown has been initiated."""
|
|
await broadcaster.broadcast("ups_shutdown", {
|
|
"delay": delay,
|
|
"percentage": percentage,
|
|
"message": f"Shutdown initiated due to low battery ({percentage}%)"
|
|
})
|
|
|
|
|
|
async def notify_pm3_status(connected: bool, message: str):
|
|
"""Notify clients of PM3 status changes."""
|
|
await broadcaster.broadcast("pm3_status", {
|
|
"connected": connected,
|
|
"message": message
|
|
})
|