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>
318 lines
8.4 KiB
Python
318 lines
8.4 KiB
Python
"""WiFi management API endpoints."""
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional
|
|
|
|
from ..managers.wifi_manager import (
|
|
wifi_manager,
|
|
WiFiMode,
|
|
WiFiStatus,
|
|
WiFiNetwork,
|
|
WiFiInterface,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class WiFiStatusResponse(BaseModel):
|
|
"""WiFi status response."""
|
|
mode: str
|
|
interfaces: List[dict]
|
|
current_ssid: Optional[str]
|
|
current_ip: Optional[str]
|
|
ap_ssid: Optional[str]
|
|
ap_ip: Optional[str]
|
|
supports_dual: bool
|
|
|
|
|
|
class WiFiNetworkResponse(BaseModel):
|
|
"""WiFi network response."""
|
|
ssid: str
|
|
bssid: str
|
|
signal_strength: int
|
|
frequency: int
|
|
encrypted: bool
|
|
in_use: bool
|
|
|
|
|
|
class SetModeRequest(BaseModel):
|
|
"""Request to set WiFi mode."""
|
|
mode: WiFiMode
|
|
|
|
|
|
class ConnectRequest(BaseModel):
|
|
"""Request to connect to network."""
|
|
ssid: str
|
|
password: Optional[str] = None
|
|
interface: Optional[str] = None
|
|
hidden: bool = False
|
|
|
|
|
|
@router.get("/status", response_model=WiFiStatusResponse)
|
|
async def get_wifi_status():
|
|
"""Get current WiFi status and available interfaces.
|
|
|
|
Returns WiFi mode, interface information, and connection status.
|
|
"""
|
|
try:
|
|
status: WiFiStatus = await wifi_manager.get_status()
|
|
|
|
return WiFiStatusResponse(
|
|
mode=status.mode.value,
|
|
interfaces=[
|
|
{
|
|
"name": iface.name,
|
|
"mac": iface.mac,
|
|
"is_usb": iface.is_usb,
|
|
"is_up": iface.is_up,
|
|
"connected": iface.connected,
|
|
"ssid": iface.ssid,
|
|
"ip_address": iface.ip_address,
|
|
}
|
|
for iface in status.interfaces
|
|
],
|
|
current_ssid=status.current_ssid,
|
|
current_ip=status.current_ip,
|
|
ap_ssid=status.ap_ssid,
|
|
ap_ip=status.ap_ip,
|
|
supports_dual=status.supports_dual,
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get WiFi status: {e}")
|
|
|
|
|
|
@router.get("/scan", response_model=List[WiFiNetworkResponse])
|
|
async def scan_networks(interface: Optional[str] = None):
|
|
"""Scan for available WiFi networks.
|
|
|
|
Args:
|
|
interface: Optional interface to scan with
|
|
|
|
Returns:
|
|
List of available networks
|
|
"""
|
|
try:
|
|
networks = await wifi_manager.scan_networks(interface)
|
|
|
|
return [
|
|
WiFiNetworkResponse(
|
|
ssid=net.ssid,
|
|
bssid=net.bssid,
|
|
signal_strength=net.signal_strength,
|
|
frequency=net.frequency,
|
|
encrypted=net.encrypted,
|
|
in_use=net.in_use,
|
|
)
|
|
for net in networks
|
|
]
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to scan networks: {e}")
|
|
|
|
|
|
@router.post("/mode")
|
|
async def set_wifi_mode(request: SetModeRequest):
|
|
"""Set WiFi operation mode.
|
|
|
|
Args:
|
|
request: Mode to set (ap, client, dual, auto, off)
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.set_mode(request.mode)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to set WiFi mode")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"WiFi mode set to {request.mode.value}",
|
|
"mode": request.mode.value,
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to set WiFi mode: {e}")
|
|
|
|
|
|
@router.post("/connect")
|
|
async def connect_to_network(request: ConnectRequest):
|
|
"""Connect to a WiFi network.
|
|
|
|
Args:
|
|
request: Connection details (SSID, password, interface, hidden)
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.connect_to_network(
|
|
ssid=request.ssid,
|
|
password=request.password,
|
|
interface=request.interface,
|
|
hidden=request.hidden,
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to connect to network")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Connected to {request.ssid}",
|
|
"ssid": request.ssid,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to connect to network: {e}")
|
|
|
|
|
|
@router.get("/interfaces")
|
|
async def get_interfaces():
|
|
"""Get available WiFi interfaces.
|
|
|
|
Returns:
|
|
List of WiFi interfaces with their status
|
|
"""
|
|
try:
|
|
interfaces = await wifi_manager.detect_interfaces()
|
|
|
|
return {
|
|
"interfaces": [
|
|
{
|
|
"name": iface.name,
|
|
"mac": iface.mac,
|
|
"is_usb": iface.is_usb,
|
|
"is_up": iface.is_up,
|
|
"connected": iface.connected,
|
|
"ssid": iface.ssid,
|
|
"ip_address": iface.ip_address,
|
|
}
|
|
for iface in interfaces
|
|
],
|
|
"supports_dual": len(interfaces) >= 2,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get interfaces: {e}")
|
|
|
|
|
|
@router.post("/disconnect")
|
|
async def disconnect_from_network(interface: Optional[str] = None):
|
|
"""Disconnect from current network.
|
|
|
|
Args:
|
|
interface: Interface to disconnect (optional)
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.disconnect_from_network(interface)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to disconnect")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Disconnected from network",
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to disconnect: {e}")
|
|
|
|
|
|
@router.get("/saved")
|
|
async def get_saved_networks():
|
|
"""Get list of saved networks.
|
|
|
|
Returns:
|
|
List of saved networks
|
|
"""
|
|
try:
|
|
networks = await wifi_manager.get_saved_networks()
|
|
return {"networks": networks}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get saved networks: {e}")
|
|
|
|
|
|
@router.delete("/saved/{ssid}")
|
|
async def forget_network(ssid: str):
|
|
"""Forget a saved network.
|
|
|
|
Args:
|
|
ssid: SSID of network to forget
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.forget_network(ssid)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Network {ssid} not found")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Forgot network {ssid}",
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to forget network: {e}")
|
|
|
|
|
|
@router.post("/static-ip")
|
|
async def set_static_ip(
|
|
interface: str,
|
|
ip_address: str,
|
|
netmask: str = "255.255.255.0",
|
|
gateway: Optional[str] = None,
|
|
dns: Optional[List[str]] = None,
|
|
):
|
|
"""Set static IP for interface.
|
|
|
|
Args:
|
|
interface: Interface name
|
|
ip_address: Static IP address
|
|
netmask: Network mask (default: 255.255.255.0)
|
|
gateway: Gateway IP (optional)
|
|
dns: DNS servers (optional)
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.set_static_ip(
|
|
interface, ip_address, netmask, gateway, dns
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to set static IP")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Static IP {ip_address} set for {interface}",
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to set static IP: {e}")
|
|
|
|
|
|
@router.post("/dhcp")
|
|
async def enable_dhcp(interface: str):
|
|
"""Enable DHCP for interface.
|
|
|
|
Args:
|
|
interface: Interface name
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
try:
|
|
success = await wifi_manager.enable_dhcp(interface)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to enable DHCP")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"DHCP enabled for {interface}",
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to enable DHCP: {e}")
|