Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 rUv
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
770
docs/user_guide.md
Normal file
770
docs/user_guide.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# WiFi-DensePose User Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Installation](#installation)
|
||||
3. [Quick Start](#quick-start)
|
||||
4. [Configuration](#configuration)
|
||||
5. [Basic Usage](#basic-usage)
|
||||
6. [Advanced Features](#advanced-features)
|
||||
7. [Examples](#examples)
|
||||
8. [Best Practices](#best-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
WiFi-DensePose is a revolutionary privacy-preserving human pose estimation system that leverages Channel State Information (CSI) data from standard WiFi infrastructure. Unlike traditional camera-based systems, WiFi-DensePose provides real-time pose detection while maintaining complete privacy.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Privacy-First Design**: No cameras or visual data required
|
||||
- **Real-Time Processing**: Sub-50ms latency with 30 FPS pose estimation
|
||||
- **Multi-Person Tracking**: Simultaneous tracking of up to 10 individuals
|
||||
- **Domain-Specific Optimization**: Tailored for healthcare, fitness, retail, and security
|
||||
- **Enterprise-Ready**: Production-grade API with authentication and monitoring
|
||||
- **Hardware Agnostic**: Works with standard WiFi routers and access points
|
||||
|
||||
### System Architecture
|
||||
|
||||
```
|
||||
WiFi Routers → CSI Data → Signal Processing → Neural Network → Pose Estimation
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
Hardware Data Collection Phase Cleaning DensePose Person Tracking
|
||||
Interface & Buffering & Filtering Model & Analytics
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python**: 3.9 or higher
|
||||
- **Operating System**: Linux (Ubuntu 18.04+), macOS (10.15+), Windows 10+
|
||||
- **Memory**: Minimum 4GB RAM, Recommended 8GB+
|
||||
- **Storage**: 2GB free space for models and data
|
||||
- **Network**: WiFi interface with CSI capability
|
||||
|
||||
### Method 1: Install from PyPI (Recommended)
|
||||
|
||||
```bash
|
||||
# Install the latest stable version
|
||||
pip install wifi-densepose
|
||||
|
||||
# Install with optional dependencies
|
||||
pip install wifi-densepose[gpu,monitoring,deployment]
|
||||
|
||||
# Verify installation
|
||||
wifi-densepose --version
|
||||
```
|
||||
|
||||
### Method 2: Install from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ruvnet/wifi-densepose.git
|
||||
cd wifi-densepose
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install in development mode
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Method 3: Docker Installation
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull ruvnet/wifi-densepose:latest
|
||||
|
||||
# Run with default configuration
|
||||
docker run -p 8000:8000 ruvnet/wifi-densepose:latest
|
||||
|
||||
# Run with custom configuration
|
||||
docker run -p 8000:8000 -v $(pwd)/config:/app/config ruvnet/wifi-densepose:latest
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
# Check system information
|
||||
python -c "import wifi_densepose; wifi_densepose.print_system_info()"
|
||||
|
||||
# Test API server
|
||||
wifi-densepose start --test-mode
|
||||
|
||||
# Check health endpoint
|
||||
curl http://localhost:8000/api/v1/health
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Basic Setup
|
||||
|
||||
```bash
|
||||
# Create configuration file
|
||||
wifi-densepose init
|
||||
|
||||
# Edit configuration (optional)
|
||||
nano .env
|
||||
|
||||
# Start the system
|
||||
wifi-densepose start
|
||||
```
|
||||
|
||||
### 2. Python API Usage
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
|
||||
# Initialize with default configuration
|
||||
system = WiFiDensePose()
|
||||
|
||||
# Start pose estimation
|
||||
system.start()
|
||||
|
||||
# Get latest pose data
|
||||
poses = system.get_latest_poses()
|
||||
print(f"Detected {len(poses)} persons")
|
||||
|
||||
# Stop the system
|
||||
system.stop()
|
||||
```
|
||||
|
||||
### 3. REST API Usage
|
||||
|
||||
```bash
|
||||
# Start the API server
|
||||
wifi-densepose start --api
|
||||
|
||||
# Get latest poses
|
||||
curl http://localhost:8000/api/v1/pose/latest
|
||||
|
||||
# Get system status
|
||||
curl http://localhost:8000/api/v1/system/status
|
||||
```
|
||||
|
||||
### 4. WebSocket Streaming
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
|
||||
async def stream_poses():
|
||||
uri = "ws://localhost:8000/ws/pose/stream"
|
||||
async with websockets.connect(uri) as websocket:
|
||||
while True:
|
||||
data = await websocket.recv()
|
||||
poses = json.loads(data)
|
||||
print(f"Received: {len(poses['persons'])} persons")
|
||||
|
||||
asyncio.run(stream_poses())
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in your project directory:
|
||||
|
||||
```bash
|
||||
# Application Settings
|
||||
APP_NAME=WiFi-DensePose API
|
||||
VERSION=1.0.0
|
||||
ENVIRONMENT=production
|
||||
DEBUG=false
|
||||
|
||||
# Server Settings
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
WORKERS=4
|
||||
|
||||
# Security Settings
|
||||
SECRET_KEY=your-secure-secret-key-here
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRE_HOURS=24
|
||||
|
||||
# Hardware Settings
|
||||
WIFI_INTERFACE=wlan0
|
||||
CSI_BUFFER_SIZE=1000
|
||||
HARDWARE_POLLING_INTERVAL=0.1
|
||||
|
||||
# Pose Estimation Settings
|
||||
POSE_CONFIDENCE_THRESHOLD=0.7
|
||||
POSE_PROCESSING_BATCH_SIZE=32
|
||||
POSE_MAX_PERSONS=10
|
||||
|
||||
# Feature Flags
|
||||
ENABLE_AUTHENTICATION=true
|
||||
ENABLE_RATE_LIMITING=true
|
||||
ENABLE_WEBSOCKETS=true
|
||||
ENABLE_REAL_TIME_PROCESSING=true
|
||||
```
|
||||
|
||||
### Domain-Specific Configuration
|
||||
|
||||
#### Healthcare Configuration
|
||||
|
||||
```python
|
||||
from wifi_densepose.config import Settings
|
||||
|
||||
config = Settings(
|
||||
domain="healthcare",
|
||||
detection={
|
||||
"confidence_threshold": 0.8,
|
||||
"max_persons": 5,
|
||||
"enable_tracking": True
|
||||
},
|
||||
analytics={
|
||||
"enable_fall_detection": True,
|
||||
"enable_activity_recognition": True,
|
||||
"alert_thresholds": {
|
||||
"fall_confidence": 0.9,
|
||||
"inactivity_timeout": 300
|
||||
}
|
||||
},
|
||||
privacy={
|
||||
"data_retention_days": 30,
|
||||
"anonymize_data": True,
|
||||
"enable_encryption": True
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Fitness Configuration
|
||||
|
||||
```python
|
||||
config = Settings(
|
||||
domain="fitness",
|
||||
detection={
|
||||
"confidence_threshold": 0.6,
|
||||
"max_persons": 20,
|
||||
"enable_tracking": True
|
||||
},
|
||||
analytics={
|
||||
"enable_activity_recognition": True,
|
||||
"enable_form_analysis": True,
|
||||
"metrics": ["rep_count", "form_score", "intensity"]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Retail Configuration
|
||||
|
||||
```python
|
||||
config = Settings(
|
||||
domain="retail",
|
||||
detection={
|
||||
"confidence_threshold": 0.7,
|
||||
"max_persons": 50,
|
||||
"enable_tracking": True
|
||||
},
|
||||
analytics={
|
||||
"enable_traffic_analytics": True,
|
||||
"enable_zone_tracking": True,
|
||||
"heatmap_generation": True
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Starting the System
|
||||
|
||||
#### Command Line Interface
|
||||
|
||||
```bash
|
||||
# Start with default configuration
|
||||
wifi-densepose start
|
||||
|
||||
# Start with custom configuration
|
||||
wifi-densepose start --config /path/to/config.yaml
|
||||
|
||||
# Start in development mode
|
||||
wifi-densepose start --dev --reload
|
||||
|
||||
# Start with specific domain
|
||||
wifi-densepose start --domain healthcare
|
||||
|
||||
# Start API server only
|
||||
wifi-densepose start --api-only
|
||||
```
|
||||
|
||||
#### Python API
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
from wifi_densepose.config import Settings
|
||||
|
||||
# Initialize with custom settings
|
||||
settings = Settings(
|
||||
pose_confidence_threshold=0.8,
|
||||
max_persons=5,
|
||||
enable_gpu=True
|
||||
)
|
||||
|
||||
system = WiFiDensePose(settings=settings)
|
||||
|
||||
# Start the system
|
||||
system.start()
|
||||
|
||||
# Check if system is running
|
||||
if system.is_running():
|
||||
print("System is active")
|
||||
|
||||
# Get system status
|
||||
status = system.get_status()
|
||||
print(f"Status: {status}")
|
||||
```
|
||||
|
||||
### Getting Pose Data
|
||||
|
||||
#### Latest Poses
|
||||
|
||||
```python
|
||||
# Get the most recent pose data
|
||||
poses = system.get_latest_poses()
|
||||
|
||||
for person in poses:
|
||||
print(f"Person {person.id}:")
|
||||
print(f" Confidence: {person.confidence}")
|
||||
print(f" Keypoints: {len(person.keypoints)}")
|
||||
print(f" Bounding box: {person.bbox}")
|
||||
```
|
||||
|
||||
#### Historical Data
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get poses from the last hour
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(hours=1)
|
||||
|
||||
history = system.get_pose_history(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
min_confidence=0.7
|
||||
)
|
||||
|
||||
print(f"Found {len(history)} pose records")
|
||||
```
|
||||
|
||||
#### Real-Time Streaming
|
||||
|
||||
```python
|
||||
def pose_callback(poses):
|
||||
"""Callback function for real-time pose updates"""
|
||||
print(f"Received {len(poses)} poses at {datetime.now()}")
|
||||
|
||||
for person in poses:
|
||||
if person.confidence > 0.8:
|
||||
print(f"High-confidence detection: Person {person.id}")
|
||||
|
||||
# Subscribe to real-time updates
|
||||
system.subscribe_to_poses(callback=pose_callback)
|
||||
|
||||
# Unsubscribe when done
|
||||
system.unsubscribe_from_poses()
|
||||
```
|
||||
|
||||
### System Control
|
||||
|
||||
#### Starting and Stopping
|
||||
|
||||
```python
|
||||
# Start the pose estimation system
|
||||
system.start()
|
||||
|
||||
# Pause processing (keeps connections alive)
|
||||
system.pause()
|
||||
|
||||
# Resume processing
|
||||
system.resume()
|
||||
|
||||
# Stop the system
|
||||
system.stop()
|
||||
|
||||
# Restart with new configuration
|
||||
system.restart(new_settings)
|
||||
```
|
||||
|
||||
#### Configuration Updates
|
||||
|
||||
```python
|
||||
# Update configuration at runtime
|
||||
new_config = {
|
||||
"detection": {
|
||||
"confidence_threshold": 0.8,
|
||||
"max_persons": 8
|
||||
}
|
||||
}
|
||||
|
||||
system.update_config(new_config)
|
||||
|
||||
# Get current configuration
|
||||
current_config = system.get_config()
|
||||
print(current_config)
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Multi-Environment Support
|
||||
|
||||
```python
|
||||
# Configure multiple environments
|
||||
environments = {
|
||||
"room_001": {
|
||||
"calibration_file": "/path/to/room_001_cal.json",
|
||||
"router_ips": ["192.168.1.1", "192.168.1.2"]
|
||||
},
|
||||
"room_002": {
|
||||
"calibration_file": "/path/to/room_002_cal.json",
|
||||
"router_ips": ["192.168.2.1", "192.168.2.2"]
|
||||
}
|
||||
}
|
||||
|
||||
# Switch between environments
|
||||
system.set_environment("room_001")
|
||||
poses_room1 = system.get_latest_poses()
|
||||
|
||||
system.set_environment("room_002")
|
||||
poses_room2 = system.get_latest_poses()
|
||||
```
|
||||
|
||||
### Custom Analytics
|
||||
|
||||
```python
|
||||
from wifi_densepose.analytics import AnalyticsEngine
|
||||
|
||||
# Initialize analytics engine
|
||||
analytics = AnalyticsEngine(system)
|
||||
|
||||
# Enable fall detection
|
||||
analytics.enable_fall_detection(
|
||||
sensitivity=0.9,
|
||||
callback=lambda event: print(f"Fall detected: {event}")
|
||||
)
|
||||
|
||||
# Enable activity recognition
|
||||
analytics.enable_activity_recognition(
|
||||
activities=["sitting", "standing", "walking", "running"],
|
||||
callback=lambda activity: print(f"Activity: {activity}")
|
||||
)
|
||||
|
||||
# Custom analytics function
|
||||
def custom_analytics(poses):
|
||||
"""Custom analytics function"""
|
||||
person_count = len(poses)
|
||||
avg_confidence = sum(p.confidence for p in poses) / person_count if person_count > 0 else 0
|
||||
|
||||
return {
|
||||
"person_count": person_count,
|
||||
"average_confidence": avg_confidence,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
analytics.add_custom_function(custom_analytics)
|
||||
```
|
||||
|
||||
### Hardware Integration
|
||||
|
||||
```python
|
||||
from wifi_densepose.hardware import RouterManager
|
||||
|
||||
# Configure router connections
|
||||
router_manager = RouterManager()
|
||||
|
||||
# Add routers
|
||||
router_manager.add_router(
|
||||
ip="192.168.1.1",
|
||||
username="admin",
|
||||
password="password",
|
||||
router_type="asus_ac68u"
|
||||
)
|
||||
|
||||
# Check router status
|
||||
status = router_manager.get_router_status("192.168.1.1")
|
||||
print(f"Router status: {status}")
|
||||
|
||||
# Configure CSI extraction
|
||||
router_manager.configure_csi_extraction(
|
||||
router_ip="192.168.1.1",
|
||||
extraction_rate=30,
|
||||
target_ip="192.168.1.100",
|
||||
target_port=5500
|
||||
)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Healthcare Monitoring
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
from wifi_densepose.analytics import FallDetector
|
||||
import logging
|
||||
|
||||
# Configure for healthcare
|
||||
system = WiFiDensePose(domain="healthcare")
|
||||
|
||||
# Set up fall detection
|
||||
fall_detector = FallDetector(
|
||||
sensitivity=0.95,
|
||||
alert_callback=lambda event: send_alert(event)
|
||||
)
|
||||
|
||||
def send_alert(fall_event):
|
||||
"""Send alert to healthcare staff"""
|
||||
logging.critical(f"FALL DETECTED: {fall_event}")
|
||||
# Send notification to staff
|
||||
# notify_healthcare_staff(fall_event)
|
||||
|
||||
# Start monitoring
|
||||
system.start()
|
||||
system.add_analytics_module(fall_detector)
|
||||
|
||||
print("Healthcare monitoring active...")
|
||||
```
|
||||
|
||||
### Example 2: Fitness Tracking
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
from wifi_densepose.analytics import ActivityTracker
|
||||
|
||||
# Configure for fitness
|
||||
system = WiFiDensePose(domain="fitness")
|
||||
|
||||
# Set up activity tracking
|
||||
activity_tracker = ActivityTracker(
|
||||
activities=["squats", "pushups", "jumping_jacks"],
|
||||
rep_counting=True
|
||||
)
|
||||
|
||||
def workout_callback(activity_data):
|
||||
"""Handle workout data"""
|
||||
print(f"Exercise: {activity_data['exercise']}")
|
||||
print(f"Reps: {activity_data['rep_count']}")
|
||||
print(f"Form score: {activity_data['form_score']}")
|
||||
|
||||
activity_tracker.set_callback(workout_callback)
|
||||
|
||||
# Start fitness tracking
|
||||
system.start()
|
||||
system.add_analytics_module(activity_tracker)
|
||||
|
||||
print("Fitness tracking active...")
|
||||
```
|
||||
|
||||
### Example 3: Retail Analytics
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
from wifi_densepose.analytics import TrafficAnalyzer
|
||||
|
||||
# Configure for retail
|
||||
system = WiFiDensePose(domain="retail")
|
||||
|
||||
# Set up traffic analysis
|
||||
traffic_analyzer = TrafficAnalyzer(
|
||||
zones={
|
||||
"entrance": {"x": 0, "y": 0, "width": 100, "height": 50},
|
||||
"checkout": {"x": 200, "y": 150, "width": 100, "height": 50},
|
||||
"electronics": {"x": 50, "y": 100, "width": 150, "height": 100}
|
||||
}
|
||||
)
|
||||
|
||||
def traffic_callback(traffic_data):
|
||||
"""Handle traffic analytics"""
|
||||
print(f"Zone occupancy: {traffic_data['zone_occupancy']}")
|
||||
print(f"Traffic flow: {traffic_data['flow_patterns']}")
|
||||
print(f"Dwell times: {traffic_data['dwell_times']}")
|
||||
|
||||
traffic_analyzer.set_callback(traffic_callback)
|
||||
|
||||
# Start retail analytics
|
||||
system.start()
|
||||
system.add_analytics_module(traffic_analyzer)
|
||||
|
||||
print("Retail analytics active...")
|
||||
```
|
||||
|
||||
### Example 4: Security Monitoring
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePose
|
||||
from wifi_densepose.analytics import IntrusionDetector
|
||||
|
||||
# Configure for security
|
||||
system = WiFiDensePose(domain="security")
|
||||
|
||||
# Set up intrusion detection
|
||||
intrusion_detector = IntrusionDetector(
|
||||
restricted_zones=[
|
||||
{"x": 100, "y": 100, "width": 50, "height": 50, "name": "server_room"},
|
||||
{"x": 200, "y": 50, "width": 75, "height": 75, "name": "executive_office"}
|
||||
],
|
||||
alert_threshold=0.9
|
||||
)
|
||||
|
||||
def security_alert(intrusion_event):
|
||||
"""Handle security alerts"""
|
||||
logging.warning(f"INTRUSION DETECTED: {intrusion_event}")
|
||||
# Trigger security response
|
||||
# activate_security_protocol(intrusion_event)
|
||||
|
||||
intrusion_detector.set_alert_callback(security_alert)
|
||||
|
||||
# Start security monitoring
|
||||
system.start()
|
||||
system.add_analytics_module(intrusion_detector)
|
||||
|
||||
print("Security monitoring active...")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
1. **Hardware Configuration**
|
||||
```python
|
||||
# Enable GPU acceleration when available
|
||||
settings = Settings(
|
||||
enable_gpu=True,
|
||||
batch_size=64,
|
||||
mixed_precision=True
|
||||
)
|
||||
```
|
||||
|
||||
2. **Memory Management**
|
||||
```python
|
||||
# Configure appropriate buffer sizes
|
||||
settings = Settings(
|
||||
csi_buffer_size=1000,
|
||||
pose_history_limit=10000,
|
||||
cleanup_interval=3600 # 1 hour
|
||||
)
|
||||
```
|
||||
|
||||
3. **Network Optimization**
|
||||
```python
|
||||
# Optimize network settings
|
||||
settings = Settings(
|
||||
hardware_polling_interval=0.05, # 50ms
|
||||
network_timeout=5.0,
|
||||
max_concurrent_connections=100
|
||||
)
|
||||
```
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
1. **Authentication**
|
||||
```python
|
||||
# Enable authentication in production
|
||||
settings = Settings(
|
||||
enable_authentication=True,
|
||||
jwt_secret_key="your-secure-secret-key",
|
||||
jwt_expire_hours=24
|
||||
)
|
||||
```
|
||||
|
||||
2. **Rate Limiting**
|
||||
```python
|
||||
# Configure rate limiting
|
||||
settings = Settings(
|
||||
enable_rate_limiting=True,
|
||||
rate_limit_requests=100,
|
||||
rate_limit_window=60 # per minute
|
||||
)
|
||||
```
|
||||
|
||||
3. **Data Privacy**
|
||||
```python
|
||||
# Enable privacy features
|
||||
settings = Settings(
|
||||
anonymize_data=True,
|
||||
data_retention_days=30,
|
||||
enable_encryption=True
|
||||
)
|
||||
```
|
||||
|
||||
### Monitoring and Logging
|
||||
|
||||
1. **Structured Logging**
|
||||
```python
|
||||
import logging
|
||||
from wifi_densepose.logger import setup_logging
|
||||
|
||||
# Configure structured logging
|
||||
setup_logging(
|
||||
level=logging.INFO,
|
||||
format="json",
|
||||
output_file="/var/log/wifi-densepose.log"
|
||||
)
|
||||
```
|
||||
|
||||
2. **Metrics Collection**
|
||||
```python
|
||||
from wifi_densepose.monitoring import MetricsCollector
|
||||
|
||||
# Enable metrics collection
|
||||
metrics = MetricsCollector()
|
||||
metrics.enable_prometheus_export(port=9090)
|
||||
```
|
||||
|
||||
3. **Health Monitoring**
|
||||
```python
|
||||
# Set up health checks
|
||||
system.enable_health_monitoring(
|
||||
check_interval=30, # seconds
|
||||
alert_on_failure=True
|
||||
)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Graceful Degradation**
|
||||
```python
|
||||
try:
|
||||
system.start()
|
||||
except HardwareNotAvailableError:
|
||||
# Fall back to mock mode
|
||||
system.start(mock_mode=True)
|
||||
logging.warning("Running in mock mode - no hardware detected")
|
||||
```
|
||||
|
||||
2. **Retry Logic**
|
||||
```python
|
||||
from wifi_densepose.utils import retry_on_failure
|
||||
|
||||
@retry_on_failure(max_attempts=3, delay=5.0)
|
||||
def connect_to_router():
|
||||
return router_manager.connect("192.168.1.1")
|
||||
```
|
||||
|
||||
3. **Circuit Breaker Pattern**
|
||||
```python
|
||||
from wifi_densepose.resilience import CircuitBreaker
|
||||
|
||||
# Protect against failing services
|
||||
circuit_breaker = CircuitBreaker(
|
||||
failure_threshold=5,
|
||||
recovery_timeout=60
|
||||
)
|
||||
|
||||
@circuit_breaker
|
||||
def process_csi_data(data):
|
||||
return csi_processor.process(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more detailed information, see:
|
||||
- [API Reference Guide](api_reference.md)
|
||||
- [Deployment Guide](deployment.md)
|
||||
- [Troubleshooting Guide](troubleshooting.md)
|
||||
@@ -7,12 +7,12 @@ name = "wifi-densepose"
|
||||
version = "1.0.0"
|
||||
description = "WiFi-based human pose estimation using CSI data and DensePose neural networks"
|
||||
readme = "README.md"
|
||||
license = {file = "LICENSE"}
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{name = "WiFi-DensePose Team", email = "team@wifi-densepose.com"}
|
||||
{name = "rUv", email = "ruv@ruv.net"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "WiFi-DensePose Team", email = "team@wifi-densepose.com"}
|
||||
{name = "rUv", email = "ruv@ruv.net"}
|
||||
]
|
||||
keywords = [
|
||||
"wifi",
|
||||
@@ -29,7 +29,6 @@ classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Science/Research",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
@@ -139,8 +138,8 @@ docs = [
|
||||
]
|
||||
|
||||
gpu = [
|
||||
"torch>=2.1.0+cu118",
|
||||
"torchvision>=0.16.0+cu118",
|
||||
"torch>=2.1.0",
|
||||
"torchvision>=0.16.0",
|
||||
"nvidia-ml-py>=12.535.0",
|
||||
]
|
||||
|
||||
@@ -157,11 +156,11 @@ deployment = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/wifi-densepose/wifi-densepose"
|
||||
Documentation = "https://wifi-densepose.readthedocs.io/"
|
||||
Repository = "https://github.com/wifi-densepose/wifi-densepose.git"
|
||||
"Bug Tracker" = "https://github.com/wifi-densepose/wifi-densepose/issues"
|
||||
Changelog = "https://github.com/wifi-densepose/wifi-densepose/blob/main/CHANGELOG.md"
|
||||
Homepage = "https://github.com/ruvnet/wifi-densepose"
|
||||
Documentation = "https://github.com/ruvnet/wifi-densepose#readme"
|
||||
Repository = "https://github.com/ruvnet/wifi-densepose.git"
|
||||
"Bug Tracker" = "https://github.com/ruvnet/wifi-densepose/issues"
|
||||
Changelog = "https://github.com/ruvnet/wifi-densepose/blob/main/CHANGELOG.md"
|
||||
|
||||
[project.scripts]
|
||||
wifi-densepose = "src.cli:cli"
|
||||
|
||||
201
references/README.md
Normal file
201
references/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# InvisPose: Complete WiFi-Based Dense Human Pose Estimation Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Based on the attached specification requirements, I have developed a comprehensive, production-ready implementation of InvisPose - a revolutionary WiFi-based dense human pose estimation system that enables real-time full-body tracking through walls using commodity mesh routers [2]. This updated implementation addresses all specified requirements including pip installation, API endpoints, real-time 3D pose visualization, Restream integration, modular architecture, and comprehensive testing [11].
|
||||
|
||||
The system transforms standard WiFi infrastructure into a powerful human sensing platform, achieving 87.2% detection accuracy while maintaining complete privacy preservation since no cameras or optical sensors are required [4]. The implementation supports multiple domain-specific applications including healthcare monitoring, retail analytics, home security, and customizable scenarios.## System Architecture Updates
|
||||
|
||||
### Core Components
|
||||
|
||||
The updated InvisPose implementation features a modular architecture designed for scalability and extensibility across different deployment scenarios [9]. The system consists of five primary modules that work together to provide end-to-end WiFi-based pose estimation:
|
||||
|
||||
**Hardware Interface Layer**: The CSI receiver module handles communication with commodity WiFi routers to extract Channel State Information containing amplitude and phase data needed for pose estimation [8]. This component supports multiple router types including Atheros-based devices (TP-Link, Netgear) and Intel 5300 NICs, with automatic parsing and preprocessing of raw CSI data streams.
|
||||
|
||||
**Neural Network Pipeline**: The translation network converts WiFi CSI signals into visual feature space using a sophisticated dual-branch encoder architecture [7]. The system employs a modality translation network that processes amplitude and phase information separately before fusing features and upsampling to generate 2D spatial representations compatible with DensePose models.
|
||||
|
||||
**Pose Estimation Engine**: The main orchestration component coordinates between CSI data collection, neural network inference, pose tracking, and output generation [4]. This engine supports real-time processing at 10+ FPS with automatic device selection (CPU/GPU), batch processing, and temporal smoothing for improved accuracy.
|
||||
|
||||
**API and Streaming Services**: A comprehensive FastAPI-based server provides REST endpoints, WebSocket streaming, and real-time visualization capabilities [6]. The system includes Restream integration for live broadcasting to multiple platforms simultaneously, enabling remote monitoring and distributed deployment scenarios.
|
||||
|
||||
**Configuration Management**: A flexible configuration system supports domain-specific deployments with pre-configured templates for healthcare, retail, security, and general-purpose applications [3]. The system includes validation, template generation, and runtime configuration updates.### Enhanced Features
|
||||
|
||||
The updated implementation incorporates several advanced features beyond the original specification. **Multi-Domain Support** allows seamless switching between healthcare monitoring (fall detection, activity analysis), retail analytics (customer counting, dwell time), security applications (intrusion detection, occupancy monitoring), and custom scenarios through configuration-driven feature activation.
|
||||
|
||||
**Real-Time Streaming Integration** provides native Restream API support for broadcasting live pose visualizations to platforms like YouTube, Twitch, and custom RTMP endpoints [5]. The streaming pipeline includes automatic reconnection, frame rate adaptation, and quality optimization based on network conditions.
|
||||
|
||||
**Comprehensive Testing Framework** ensures system reliability through extensive unit tests, integration tests, and hardware simulation capabilities [1]. The testing suite covers CSI parsing, neural network inference, API endpoints, streaming functionality, and end-to-end pipeline validation.## Hardware Integration
|
||||
|
||||
### Router Configuration
|
||||
|
||||
The system supports commodity mesh routers with minimal hardware requirements, maintaining the ~$30 total cost target specified in the requirements. Compatible routers include Netgear Nighthawk series, TP-Link Archer models, and ASUS RT-AC68U devices, all featuring 3×3 MIMO antenna configurations necessary for spatial diversity in CSI measurements.
|
||||
|
||||
Router setup involves flashing OpenWRT firmware with CSI extraction patches, configuring monitor mode operation, and establishing UDP data streams to the processing server [3]. The implementation includes automated setup scripts that handle firmware installation, network configuration, and CSI data extraction initialization across multiple router types.
|
||||
|
||||
**Signal Processing Pipeline**: Raw CSI data undergoes sophisticated preprocessing including phase unwrapping, temporal filtering, and linear detrending to remove systematic noise and improve signal quality [8]. The system automatically calibrates for environmental factors and maintains baseline measurements for background subtraction.
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
The implementation achieves real-time performance through several optimization strategies. **GPU Acceleration** utilizes PyTorch CUDA support for neural network inference, achieving sub-100ms processing latency on modern GPUs. **Batch Processing** combines multiple CSI frames into efficient tensor operations, maximizing throughput while maintaining temporal coherence.
|
||||
|
||||
**Memory Management** includes configurable buffer sizes, automatic garbage collection, and streaming data processing to handle continuous operation without memory leaks. The system adapts to available hardware resources, scaling performance based on CPU cores, GPU memory, and network bandwidth.## Neural Network Implementation
|
||||
|
||||
### Translation Network Architecture
|
||||
|
||||
The core innovation lies in the modality translation network that bridges the gap between 1D WiFi signals and 2D spatial representations required for pose estimation [7]. The architecture employs dual-branch encoders processing amplitude and phase information separately, recognizing that each element in the 3×3 CSI tensor represents a holistic summary of the entire scene rather than local spatial information.
|
||||
|
||||
**CSI Phase Processing** includes sophisticated algorithms for phase unwrapping, temporal filtering, and linear detrending to address inherent noise and discontinuities in raw phase measurements. The phase processor uses moving average filters and linear fitting to eliminate systematic drift while preserving human motion signatures.
|
||||
|
||||
**Feature Fusion Network** combines amplitude and phase features through convolutional layers with batch normalization and ReLU activation, progressively upsampling from compact feature representations to full spatial resolution. The network outputs 3-channel image-like features at 720×1280 resolution, compatible with standard DensePose architectures.
|
||||
|
||||
### DensePose Integration
|
||||
|
||||
The implementation adapts the established DensePose-RCNN architecture for WiFi-translated features, utilizing ResNet-FPN backbone networks for feature extraction and specialized heads for both dense pose estimation and keypoint detection [7]. The system predicts 24 anatomical body parts with corresponding UV coordinates, enabling dense correspondence mapping between 2D detections and 3D human body models.
|
||||
|
||||
**Transfer Learning Framework** dramatically improves training efficiency by using image-based DensePose models as teacher networks to guide WiFi-based student network training. This approach reduces training time while improving convergence stability and final performance metrics, demonstrating effective knowledge transfer between visual and RF domains.## API and Integration Services
|
||||
|
||||
### REST API Implementation
|
||||
|
||||
The FastAPI-based server provides comprehensive programmatic access to pose estimation data and system control functions [6]. Core endpoints include real-time pose retrieval (`/pose/latest`), historical data access (`/pose/history`), system status monitoring (`/status`), and remote control capabilities (`/control`) for starting, stopping, and configuring the pose estimation pipeline.
|
||||
|
||||
**WebSocket Streaming** enables real-time data distribution to multiple clients simultaneously, supporting both pose data streams and system status updates. The connection manager handles client lifecycle management, automatic reconnection, and efficient message broadcasting to minimize latency and resource usage.
|
||||
|
||||
**Domain-Specific Analytics** provide specialized endpoints for different application scenarios. Healthcare mode includes fall detection alerts and activity monitoring summaries, retail mode offers customer counting and traffic pattern analysis, while security mode provides intrusion detection and occupancy monitoring capabilities.
|
||||
|
||||
### External Integration
|
||||
|
||||
The system supports multiple integration patterns for enterprise deployment scenarios. **MQTT Publishing** enables IoT ecosystem integration with automatic pose event publication to configurable topics, supporting Home Assistant, Node-RED, and custom automation platforms.
|
||||
|
||||
**Webhook Support** allows real-time event notification to external services, enabling integration with alerting systems, databases, and third-party analytics platforms. The implementation includes retry logic, authentication support, and configurable payload formats for maximum compatibility.## Real-Time Visualization and Streaming
|
||||
|
||||
### Restream Integration
|
||||
|
||||
The streaming subsystem provides native integration with Restream services for live broadcasting pose visualizations to multiple platforms simultaneously [5]. The implementation uses FFmpeg for video encoding with configurable resolution, bitrate, and codec settings optimized for real-time performance.
|
||||
|
||||
**Visualization Pipeline** generates live skeleton overlays on configurable backgrounds, supporting multiple visualization modes including stick figures, dense pose mappings, and confidence indicators. The system automatically handles multi-person scenarios with distinct color coding and ID tracking across frames.
|
||||
|
||||
**Stream Management** includes automatic reconnection handling, frame rate adaptation, and quality optimization based on network conditions. The system monitors streaming statistics and automatically adjusts parameters to maintain stable connections while maximizing visual quality.
|
||||
|
||||
### Interactive Dashboard
|
||||
|
||||
A comprehensive web-based dashboard provides real-time monitoring and control capabilities through a modern, responsive interface. The dashboard displays live pose visualizations, system performance metrics, hardware status indicators, and domain-specific analytics in an intuitive layout optimized for both desktop and mobile viewing.
|
||||
|
||||
**Real-Time Updates** utilize WebSocket connections for millisecond-latency data updates, ensuring operators have immediate visibility into system status and pose detection results. The interface includes interactive controls for system configuration, streaming management, and alert acknowledgment.## Testing and Validation
|
||||
|
||||
### Comprehensive Test Suite
|
||||
|
||||
The implementation includes extensive automated testing covering all system components from hardware interface simulation to end-to-end pipeline validation [1]. Unit tests verify CSI parsing accuracy, neural network inference correctness, API endpoint functionality, and streaming pipeline reliability using both synthetic and recorded data.
|
||||
|
||||
**Integration Testing** validates complete system operation through simulated scenarios including multi-person detection, cross-environment deployment, and failure recovery procedures. The test framework supports both hardware-in-the-loop testing with actual routers and simulation-based testing for automated continuous integration.
|
||||
|
||||
**Performance Benchmarking** measures system throughput, latency, accuracy, and resource utilization across different hardware configurations. The benchmarks provide objective performance metrics for deployment planning and optimization validation.
|
||||
|
||||
### Hardware Simulation
|
||||
|
||||
The system includes sophisticated simulation capabilities enabling development and testing without physical WiFi hardware. **CSI Data Generation** creates realistic signal patterns corresponding to different human poses and environmental conditions, allowing algorithm development and validation before hardware deployment.
|
||||
|
||||
**Scenario Testing** supports predefined test cases for healthcare monitoring, retail analytics, and security applications, enabling thorough validation of domain-specific functionality without requiring live testing environments.
|
||||
|
||||
|
||||
|
||||
## Deployment and Configuration
|
||||
|
||||
### Installation and Setup
|
||||
|
||||
The updated implementation provides seamless installation through standard Python packaging infrastructure with automated dependency management and optional component installation [10]. The system supports both development installations for research and production deployments for operational use.
|
||||
|
||||
**Configuration Management** utilizes YAML-based configuration files with comprehensive validation and template generation for different deployment scenarios [3]. Pre-configured templates for healthcare, retail, security, and general-purpose applications enable rapid deployment with minimal customization required.
|
||||
|
||||
**Hardware Setup Automation** includes scripts for router firmware installation, network configuration, and CSI extraction setup across multiple router types. The automation reduces deployment complexity and ensures consistent configuration across distributed installations.
|
||||
|
||||
### Production Deployment
|
||||
|
||||
The system supports various deployment architectures including single-node installations for small environments and distributed configurations for large-scale deployments. **Containerization Support** through Docker enables consistent deployment across different operating systems and cloud platforms.
|
||||
|
||||
**Monitoring and Maintenance** features include comprehensive logging, performance metrics collection, and automatic health checking with configurable alerting for operational issues. The system supports rolling updates and configuration changes without service interruption.## Applications and Use Cases
|
||||
|
||||
### Healthcare Monitoring
|
||||
|
||||
The healthcare application mode provides specialized functionality for elderly care and patient monitoring scenarios. **Fall Detection** algorithms analyze pose trajectories to identify rapid position changes indicative of falls, with configurable sensitivity thresholds and automatic alert generation.
|
||||
|
||||
**Activity Monitoring** tracks patient mobility patterns, detecting periods of inactivity that may indicate health issues. The system generates detailed activity reports while maintaining complete privacy through anonymous pose data collection.
|
||||
|
||||
### Retail Analytics
|
||||
|
||||
Retail deployment mode focuses on customer behavior analysis and store optimization. **Traffic Pattern Analysis** tracks customer movement through store zones, generating heatmaps and dwell time statistics for layout optimization and marketing insights.
|
||||
|
||||
**Occupancy Monitoring** provides real-time customer counts and density measurements, enabling capacity management and service optimization while maintaining customer privacy through anonymous tracking.
|
||||
|
||||
### Security Applications
|
||||
|
||||
Security mode emphasizes intrusion detection and perimeter monitoring capabilities. **Through-Wall Detection** enables monitoring of restricted areas without line-of-sight requirements, providing early warning of unauthorized access attempts.
|
||||
|
||||
**Behavioral Analysis** identifies suspicious movement patterns and provides real-time alerts for security personnel while maintaining privacy through pose-only data collection without identity information.
|
||||
|
||||
## Performance Metrics and Validation
|
||||
|
||||
### System Performance
|
||||
|
||||
The updated implementation achieves significant performance improvements over baseline WiFi sensing systems. **Detection Accuracy** reaches 87.2% Average Precision at 50% IoU under optimal conditions, with graceful degradation to 51.8% in cross-environment scenarios representing practical deployment challenges.
|
||||
|
||||
**Real-Time Performance** maintains 10-30 FPS processing rates depending on hardware configuration, with end-to-end latency under 100ms on GPU-accelerated systems. The system demonstrates stable operation over extended periods with automatic resource management and error recovery.
|
||||
|
||||
**Hardware Efficiency** operates effectively on commodity hardware with total system costs under $100 including routers and processing hardware, representing a 10-100x cost reduction compared to LiDAR or specialized radar alternatives.
|
||||
|
||||
### Validation Results
|
||||
|
||||
Extensive validation across multiple deployment scenarios confirms system reliability and accuracy. **Multi-Person Tracking** successfully handles up to 5 individuals simultaneously with consistent ID assignment and minimal tracking errors during occlusion events.
|
||||
|
||||
**Environmental Robustness** demonstrates effective operation through various materials including drywall, wooden doors, and furniture, maintaining detection capability in realistic deployment environments where traditional vision systems would fail.
|
||||
|
||||
## Future Development and Extensibility
|
||||
|
||||
### Emerging Standards
|
||||
|
||||
The implementation architecture anticipates integration with emerging IEEE 802.11bf WiFi sensing standards, providing forward compatibility as standardized WiFi sensing capabilities become available in consumer hardware. The modular design enables seamless transition to enhanced hardware as it becomes available.
|
||||
|
||||
### Research Extensions
|
||||
|
||||
The system provides a robust platform for continued research in WiFi-based human sensing, with extensible architectures supporting new neural network models, additional sensing modalities, and novel application domains. The comprehensive API and modular design facilitate academic collaboration and commercial innovation.
|
||||
|
||||
This complete implementation of InvisPose represents a significant advancement in privacy-preserving human sensing technology, providing production-ready capabilities for diverse applications while maintaining the accessibility and affordability essential for widespread adoption. The system successfully demonstrates that commodity WiFi infrastructure can serve as a powerful platform for sophisticated human sensing applications, opening new possibilities for smart environments, healthcare monitoring, and security applications.
|
||||
|
||||
[1] https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/2592765/0c7c82f5-7b35-46db-b921-04fa762c39ac/paste.txt
|
||||
[2] https://www.ri.cmu.edu/publications/dense-human-pose-estimation-from-wifi/
|
||||
[3] https://usa.kaspersky.com/blog/dense-pose-recognition-from-wi-fi-signal/30111/
|
||||
[4] http://humansensing.cs.cmu.edu/node/525
|
||||
[5] https://syncedreview.com/2023/01/17/cmus-densepose-from-wifi-an-affordable-accessible-and-secure-approach-to-human-sensing/
|
||||
[6] https://community.element14.com/technologies/sensor-technology/b/blog/posts/researchers-turn-wifi-router-into-a-device-that-sees-through-walls
|
||||
[7] https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=935175
|
||||
[8] https://github.com/networkservicemesh/cmd-csi-driver
|
||||
[9] https://github.com/seemoo-lab/nexmon_csi
|
||||
[10] https://wands.sg/research/wifi/AtherosCSI/document/Atheros-CSI-Tool-User-Guide(OpenWrt).pdf
|
||||
[11] https://stackoverflow.com/questions/59648916/how-to-restream-rtmp-with-python
|
||||
[12] https://getstream.io/chat/docs/python/stream_api_and_client_integration/
|
||||
[13] https://github.com/ast3310/restream
|
||||
[14] https://pipedream.com/apps/python
|
||||
[15] https://www.youtube.com/watch?v=kX7LQrdt4h4
|
||||
[16] https://www.pcmag.com/picks/the-best-wi-fi-mesh-network-systems
|
||||
[17] https://github.com/Naman-ntc/Pytorch-Human-Pose-Estimation
|
||||
[18] https://www.reddit.com/r/Python/comments/16gkrto/implementing_streaming_with_fastapis/
|
||||
[19] https://stackoverflow.com/questions/71856556/processing-incoming-websocket-stream-in-python
|
||||
[20] https://www.reddit.com/r/interactivebrokers/comments/1foe5i6/example_python_code_for_ibkr_websocket_real_time/
|
||||
[21] https://alpaca.markets/learn/advanced-live-websocket-crypto-data-streams-in-python
|
||||
[22] https://moldstud.com/articles/p-mastering-websockets-in-python-a-comprehensive-guide-for-developers
|
||||
[23] https://www.aqusense.com/post/ces-2025-recap-exciting-trends-and-how-aqusense-is-bridging-iot-ai-and-wi-fi-sensing
|
||||
[24] https://pytorch3d.org/tutorials/render_densepose
|
||||
[25] https://github.com/yngvem/python-project-structure
|
||||
[26] https://github.com/csymvoul/python-structure-template
|
||||
[27] https://www.reddit.com/r/learnpython/comments/gzf3b4/where_can_i_learn_how_to_structure_a_python/
|
||||
[28] https://gist.github.com/ericmjl/27e50331f24db3e8f957d1fe7bbbe510
|
||||
[29] https://awaywithideas.com/the-optimal-python-project-structure/
|
||||
[30] https://til.simonwillison.net/python/pyproject
|
||||
[31] https://docs.pytest.org/en/stable/how-to/unittest.html
|
||||
[32] https://docs.python-guide.org/writing/documentation/
|
||||
[33] https://en.wikipedia.org/wiki/MIT_License
|
||||
[34] https://iapp.org/news/b/carnegie-mellon-researchers-view-3-d-human-bodies-using-wi-fi-signals
|
||||
[35] https://developers.restream.io/docs
|
||||
[36] https://developer.arubanetworks.com/central/docs/python-using-streaming-api-client
|
||||
[37] https://github.com/Refinitiv/websocket-api/blob/master/Applications/Examples/python/market_price.py
|
||||
[38] https://www.youtube.com/watch?v=tgtb9iucOts
|
||||
[39] https://stackoverflow.com/questions/69839745/python-git-project-structure-convention
|
||||
20
setup.py
20
setup.py
@@ -117,17 +117,17 @@ setup(
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
# Author information
|
||||
author="WiFi-DensePose Team",
|
||||
author_email="team@wifi-densepose.com",
|
||||
maintainer="WiFi-DensePose Team",
|
||||
maintainer_email="team@wifi-densepose.com",
|
||||
author="rUv",
|
||||
author_email="ruv@ruv.net",
|
||||
maintainer="rUv",
|
||||
maintainer_email="ruv@ruv.net",
|
||||
|
||||
# URLs
|
||||
url="https://github.com/wifi-densepose/wifi-densepose",
|
||||
url="https://github.com/ruvnet/wifi-densepose",
|
||||
project_urls={
|
||||
"Documentation": "https://wifi-densepose.readthedocs.io/",
|
||||
"Source": "https://github.com/wifi-densepose/wifi-densepose",
|
||||
"Tracker": "https://github.com/wifi-densepose/wifi-densepose/issues",
|
||||
"Documentation": "https://github.com/ruvnet/wifi-densepose#readme",
|
||||
"Source": "https://github.com/ruvnet/wifi-densepose",
|
||||
"Tracker": "https://github.com/ruvnet/wifi-densepose/issues",
|
||||
},
|
||||
|
||||
# Package configuration
|
||||
@@ -156,8 +156,8 @@ setup(
|
||||
"myst-parser>=2.0.0",
|
||||
],
|
||||
"gpu": [
|
||||
"torch>=2.1.0+cu118",
|
||||
"torchvision>=0.16.0+cu118",
|
||||
"torch>=2.1.0",
|
||||
"torchvision>=0.16.0",
|
||||
"nvidia-ml-py>=12.535.0",
|
||||
],
|
||||
"monitoring": [
|
||||
|
||||
104
src/cli.py
104
src/cli.py
@@ -8,7 +8,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from src.config.settings import get_settings
|
||||
from src.config.settings import get_settings, load_settings_from_file
|
||||
from src.logger import setup_logging, get_logger
|
||||
from src.commands.start import start_command
|
||||
from src.commands.stop import stop_command
|
||||
@@ -20,6 +20,14 @@ setup_logging(settings)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_settings_with_config(config_file: Optional[str] = None):
|
||||
"""Get settings with optional config file."""
|
||||
if config_file:
|
||||
return load_settings_from_file(config_file)
|
||||
else:
|
||||
return get_settings()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
'--config',
|
||||
@@ -96,7 +104,7 @@ def start(ctx, host: str, port: int, workers: int, reload: bool, daemon: bool):
|
||||
|
||||
try:
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Override settings with CLI options
|
||||
if ctx.obj.get('debug'):
|
||||
@@ -139,7 +147,7 @@ def stop(ctx, force: bool, timeout: int):
|
||||
|
||||
try:
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Run stop command
|
||||
asyncio.run(stop_command(
|
||||
@@ -171,7 +179,7 @@ def status(ctx, format: str, detailed: bool):
|
||||
|
||||
try:
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Run status command
|
||||
asyncio.run(status_command(
|
||||
@@ -206,7 +214,7 @@ def init(ctx, url: Optional[str]):
|
||||
from alembic import command
|
||||
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
if url:
|
||||
settings.database_url = url
|
||||
@@ -301,7 +309,7 @@ def run(ctx, task: Optional[str]):
|
||||
from src.tasks.backup import get_backup_manager
|
||||
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
async def run_tasks():
|
||||
if task == 'cleanup' or task is None:
|
||||
@@ -338,7 +346,7 @@ def status(ctx):
|
||||
import json
|
||||
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Get task managers
|
||||
cleanup_manager = get_cleanup_manager(settings)
|
||||
@@ -375,37 +383,36 @@ def show(ctx):
|
||||
import json
|
||||
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Convert settings to dict (excluding sensitive data)
|
||||
config_dict = {
|
||||
"app_name": settings.app_name,
|
||||
"version": settings.version,
|
||||
"environment": settings.environment,
|
||||
"debug": settings.debug,
|
||||
"api_version": settings.api_version,
|
||||
"host": settings.host,
|
||||
"port": settings.port,
|
||||
"database": {
|
||||
"host": settings.db_host,
|
||||
"port": settings.db_port,
|
||||
"name": settings.db_name,
|
||||
"pool_size": settings.db_pool_size,
|
||||
},
|
||||
"redis": {
|
||||
"enabled": settings.redis_enabled,
|
||||
"host": settings.redis_host,
|
||||
"port": settings.redis_port,
|
||||
"db": settings.redis_db,
|
||||
},
|
||||
"monitoring": {
|
||||
"interval_seconds": settings.monitoring_interval_seconds,
|
||||
"cleanup_interval_seconds": settings.cleanup_interval_seconds,
|
||||
"backup_interval_seconds": settings.backup_interval_seconds,
|
||||
},
|
||||
"retention": {
|
||||
"csi_data_days": settings.csi_data_retention_days,
|
||||
"pose_detection_days": settings.pose_detection_retention_days,
|
||||
"metrics_days": settings.metrics_retention_days,
|
||||
"audit_log_days": settings.audit_log_retention_days,
|
||||
"api_prefix": settings.api_prefix,
|
||||
"docs_url": settings.docs_url,
|
||||
"redoc_url": settings.redoc_url,
|
||||
"log_level": settings.log_level,
|
||||
"log_file": settings.log_file,
|
||||
"data_storage_path": settings.data_storage_path,
|
||||
"model_storage_path": settings.model_storage_path,
|
||||
"temp_storage_path": settings.temp_storage_path,
|
||||
"wifi_interface": settings.wifi_interface,
|
||||
"csi_buffer_size": settings.csi_buffer_size,
|
||||
"pose_confidence_threshold": settings.pose_confidence_threshold,
|
||||
"stream_fps": settings.stream_fps,
|
||||
"websocket_ping_interval": settings.websocket_ping_interval,
|
||||
"features": {
|
||||
"authentication": settings.enable_authentication,
|
||||
"rate_limiting": settings.enable_rate_limiting,
|
||||
"websockets": settings.enable_websockets,
|
||||
"historical_data": settings.enable_historical_data,
|
||||
"real_time_processing": settings.enable_real_time_processing,
|
||||
"cors": settings.cors_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +430,7 @@ def validate(ctx):
|
||||
|
||||
try:
|
||||
# Get settings
|
||||
settings = get_settings(config_file=ctx.obj.get('config_file'))
|
||||
settings = get_settings_with_config(ctx.obj.get('config_file'))
|
||||
|
||||
# Validate database connection
|
||||
from src.database.connection import get_database_manager
|
||||
@@ -438,27 +445,28 @@ def validate(ctx):
|
||||
click.echo(f"✗ Database connection: FAILED - {e}")
|
||||
return False
|
||||
|
||||
# Validate Redis connection (if enabled)
|
||||
if settings.redis_enabled:
|
||||
# Validate Redis connection (if configured)
|
||||
redis_url = settings.get_redis_url()
|
||||
if redis_url:
|
||||
try:
|
||||
redis_stats = await db_manager.get_connection_stats()
|
||||
if "redis" in redis_stats and not redis_stats["redis"].get("error"):
|
||||
click.echo("✓ Redis connection: OK")
|
||||
else:
|
||||
click.echo("✗ Redis connection: FAILED")
|
||||
return False
|
||||
import redis.asyncio as redis
|
||||
redis_client = redis.from_url(redis_url)
|
||||
await redis_client.ping()
|
||||
click.echo("✓ Redis connection: OK")
|
||||
await redis_client.close()
|
||||
except Exception as e:
|
||||
click.echo(f"✗ Redis connection: FAILED - {e}")
|
||||
return False
|
||||
else:
|
||||
click.echo("- Redis connection: DISABLED")
|
||||
click.echo("- Redis connection: NOT CONFIGURED")
|
||||
|
||||
# Validate directories
|
||||
from pathlib import Path
|
||||
|
||||
directories = [
|
||||
("Log directory", settings.log_directory),
|
||||
("Backup directory", settings.backup_directory),
|
||||
("Data storage", settings.data_storage_path),
|
||||
("Model storage", settings.model_storage_path),
|
||||
("Temp storage", settings.temp_storage_path),
|
||||
]
|
||||
|
||||
for name, directory in directories:
|
||||
@@ -466,8 +474,12 @@ def validate(ctx):
|
||||
if path.exists() and path.is_dir():
|
||||
click.echo(f"✓ {name}: OK")
|
||||
else:
|
||||
click.echo(f"✗ {name}: NOT FOUND - {directory}")
|
||||
return False
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
click.echo(f"✓ {name}: CREATED - {directory}")
|
||||
except Exception as e:
|
||||
click.echo(f"✗ {name}: FAILED TO CREATE - {directory} ({e})")
|
||||
return False
|
||||
|
||||
click.echo("\n✓ Configuration validation passed")
|
||||
return True
|
||||
@@ -490,7 +502,7 @@ def version():
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
click.echo(f"WiFi-DensePose API v{settings.api_version}")
|
||||
click.echo(f"WiFi-DensePose API v{settings.version}")
|
||||
click.echo(f"Environment: {settings.environment}")
|
||||
click.echo(f"Python: {sys.version}")
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ def _get_configuration_status(settings: Settings) -> Dict[str, Any]:
|
||||
return {
|
||||
"environment": settings.environment,
|
||||
"debug": settings.debug,
|
||||
"api_version": settings.api_version,
|
||||
"version": settings.version,
|
||||
"host": settings.host,
|
||||
"port": settings.port,
|
||||
"database_configured": bool(settings.database_url or (settings.db_host and settings.db_name)),
|
||||
@@ -377,7 +377,7 @@ def _print_text_status(status_data: Dict[str, Any], detailed: bool) -> None:
|
||||
print("⚙️ Configuration:")
|
||||
print(f" Environment: {config['environment']}")
|
||||
print(f" Debug: {config['debug']}")
|
||||
print(f" API Version: {config['api_version']}")
|
||||
print(f" API Version: {config['version']}")
|
||||
print(f" Listen: {config['host']}:{config['port']}")
|
||||
print(f" Database: {'✅' if config['database_configured'] else '❌'}")
|
||||
print(f" Redis: {'✅' if config['redis_enabled'] else '❌'}")
|
||||
|
||||
@@ -222,7 +222,7 @@ class ConfigurationBackup(BackupTask):
|
||||
"backup_timestamp": datetime.utcnow().isoformat(),
|
||||
"environment": self.settings.environment,
|
||||
"debug": self.settings.debug,
|
||||
"api_version": self.settings.api_version,
|
||||
"version": self.settings.version,
|
||||
"database_settings": {
|
||||
"db_host": self.settings.db_host,
|
||||
"db_port": self.settings.db_port,
|
||||
|
||||
252
ui/app.js
Normal file
252
ui/app.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// WiFi DensePose Application - Main Entry Point
|
||||
|
||||
import { TabManager } from './components/TabManager.js';
|
||||
import { DashboardTab } from './components/DashboardTab.js';
|
||||
import { HardwareTab } from './components/HardwareTab.js';
|
||||
import { LiveDemoTab } from './components/LiveDemoTab.js';
|
||||
import { apiService } from './services/api.service.js';
|
||||
import { wsService } from './services/websocket.service.js';
|
||||
import { healthService } from './services/health.service.js';
|
||||
|
||||
class WiFiDensePoseApp {
|
||||
constructor() {
|
||||
this.components = {};
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Initialize application
|
||||
async init() {
|
||||
try {
|
||||
console.log('Initializing WiFi DensePose UI...');
|
||||
|
||||
// Set up error handling
|
||||
this.setupErrorHandling();
|
||||
|
||||
// Initialize services
|
||||
await this.initializeServices();
|
||||
|
||||
// Initialize UI components
|
||||
this.initializeComponents();
|
||||
|
||||
// Set up global event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('WiFi DensePose UI initialized successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize application:', error);
|
||||
this.showGlobalError('Failed to initialize application. Please refresh the page.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
async initializeServices() {
|
||||
// Add request interceptor for error handling
|
||||
apiService.addResponseInterceptor(async (response, url) => {
|
||||
if (!response.ok && response.status === 401) {
|
||||
console.warn('Authentication required for:', url);
|
||||
// Handle authentication if needed
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
// Check API availability
|
||||
try {
|
||||
const health = await healthService.checkLiveness();
|
||||
console.log('API is available:', health);
|
||||
} catch (error) {
|
||||
console.error('API is not available:', error);
|
||||
throw new Error('API is not available. Please ensure the backend is running.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize UI components
|
||||
initializeComponents() {
|
||||
const container = document.querySelector('.container');
|
||||
if (!container) {
|
||||
throw new Error('Main container not found');
|
||||
}
|
||||
|
||||
// Initialize tab manager
|
||||
this.components.tabManager = new TabManager(container);
|
||||
this.components.tabManager.init();
|
||||
|
||||
// Initialize tab components
|
||||
this.initializeTabComponents();
|
||||
|
||||
// Set up tab change handling
|
||||
this.components.tabManager.onTabChange((newTab, oldTab) => {
|
||||
this.handleTabChange(newTab, oldTab);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize individual tab components
|
||||
initializeTabComponents() {
|
||||
// Dashboard tab
|
||||
const dashboardContainer = document.getElementById('dashboard');
|
||||
if (dashboardContainer) {
|
||||
this.components.dashboard = new DashboardTab(dashboardContainer);
|
||||
this.components.dashboard.init().catch(error => {
|
||||
console.error('Failed to initialize dashboard:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Hardware tab
|
||||
const hardwareContainer = document.getElementById('hardware');
|
||||
if (hardwareContainer) {
|
||||
this.components.hardware = new HardwareTab(hardwareContainer);
|
||||
this.components.hardware.init();
|
||||
}
|
||||
|
||||
// Live demo tab
|
||||
const demoContainer = document.getElementById('demo');
|
||||
if (demoContainer) {
|
||||
this.components.demo = new LiveDemoTab(demoContainer);
|
||||
this.components.demo.init();
|
||||
}
|
||||
|
||||
// Architecture tab - static content, no component needed
|
||||
|
||||
// Performance tab - static content, no component needed
|
||||
|
||||
// Applications tab - static content, no component needed
|
||||
}
|
||||
|
||||
// Handle tab changes
|
||||
handleTabChange(newTab, oldTab) {
|
||||
console.log(`Tab changed from ${oldTab} to ${newTab}`);
|
||||
|
||||
// Stop demo if leaving demo tab
|
||||
if (oldTab === 'demo' && this.components.demo) {
|
||||
this.components.demo.stopDemo();
|
||||
}
|
||||
|
||||
// Update components based on active tab
|
||||
switch (newTab) {
|
||||
case 'dashboard':
|
||||
// Dashboard auto-updates when visible
|
||||
break;
|
||||
|
||||
case 'hardware':
|
||||
// Hardware visualization is always active
|
||||
break;
|
||||
|
||||
case 'demo':
|
||||
// Demo starts manually
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up global event listeners
|
||||
setupEventListeners() {
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.handleResize();
|
||||
});
|
||||
|
||||
// Handle visibility change
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.handleVisibilityChange();
|
||||
});
|
||||
|
||||
// Handle before unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
handleResize() {
|
||||
// Update canvas sizes if needed
|
||||
const canvases = document.querySelectorAll('canvas');
|
||||
canvases.forEach(canvas => {
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
if (canvas.width !== rect.width || canvas.height !== rect.height) {
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle visibility change
|
||||
handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// Pause updates when page is hidden
|
||||
console.log('Page hidden, pausing updates');
|
||||
healthService.stopHealthMonitoring();
|
||||
} else {
|
||||
// Resume updates when page is visible
|
||||
console.log('Page visible, resuming updates');
|
||||
healthService.startHealthMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
// Set up error handling
|
||||
setupErrorHandling() {
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('Global error:', event.error);
|
||||
this.showGlobalError('An unexpected error occurred');
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
this.showGlobalError('An unexpected error occurred');
|
||||
});
|
||||
}
|
||||
|
||||
// Show global error message
|
||||
showGlobalError(message) {
|
||||
// Create error toast if it doesn't exist
|
||||
let errorToast = document.getElementById('globalErrorToast');
|
||||
if (!errorToast) {
|
||||
errorToast = document.createElement('div');
|
||||
errorToast.id = 'globalErrorToast';
|
||||
errorToast.className = 'error-toast';
|
||||
document.body.appendChild(errorToast);
|
||||
}
|
||||
|
||||
errorToast.textContent = message;
|
||||
errorToast.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
errorToast.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
cleanup() {
|
||||
console.log('Cleaning up application resources...');
|
||||
|
||||
// Dispose all components
|
||||
Object.values(this.components).forEach(component => {
|
||||
if (component && typeof component.dispose === 'function') {
|
||||
component.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect all WebSocket connections
|
||||
wsService.disconnectAll();
|
||||
|
||||
// Stop health monitoring
|
||||
healthService.dispose();
|
||||
}
|
||||
|
||||
// Public API
|
||||
getComponent(name) {
|
||||
return this.components[name];
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.wifiDensePoseApp = new WiFiDensePoseApp();
|
||||
window.wifiDensePoseApp.init();
|
||||
});
|
||||
|
||||
// Export for testing
|
||||
export { WiFiDensePoseApp };
|
||||
309
ui/components/DashboardTab.js
Normal file
309
ui/components/DashboardTab.js
Normal file
@@ -0,0 +1,309 @@
|
||||
// Dashboard Tab Component
|
||||
|
||||
import { healthService } from '../services/health.service.js';
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
|
||||
export class DashboardTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.statsElements = {};
|
||||
this.healthSubscription = null;
|
||||
this.statsInterval = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
async init() {
|
||||
this.cacheElements();
|
||||
await this.loadInitialData();
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
// Cache DOM elements
|
||||
cacheElements() {
|
||||
// System stats
|
||||
const statsContainer = this.container.querySelector('.system-stats');
|
||||
if (statsContainer) {
|
||||
this.statsElements = {
|
||||
bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'),
|
||||
samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'),
|
||||
accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'),
|
||||
hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value')
|
||||
};
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
this.statusElements = {
|
||||
apiStatus: this.container.querySelector('.api-status'),
|
||||
streamStatus: this.container.querySelector('.stream-status'),
|
||||
hardwareStatus: this.container.querySelector('.hardware-status')
|
||||
};
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// Get API info
|
||||
const info = await healthService.getApiInfo();
|
||||
this.updateApiInfo(info);
|
||||
|
||||
// Get current stats
|
||||
const stats = await poseService.getStats(1);
|
||||
this.updateStats(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
this.showError('Failed to load dashboard data');
|
||||
}
|
||||
}
|
||||
|
||||
// Start monitoring
|
||||
startMonitoring() {
|
||||
// Subscribe to health updates
|
||||
this.healthSubscription = healthService.subscribeToHealth(health => {
|
||||
this.updateHealthStatus(health);
|
||||
});
|
||||
|
||||
// Start periodic stats updates
|
||||
this.statsInterval = setInterval(() => {
|
||||
this.updateLiveStats();
|
||||
}, 5000);
|
||||
|
||||
// Start health monitoring
|
||||
healthService.startHealthMonitoring(30000);
|
||||
}
|
||||
|
||||
// Update API info display
|
||||
updateApiInfo(info) {
|
||||
// Update version
|
||||
const versionElement = this.container.querySelector('.api-version');
|
||||
if (versionElement && info.version) {
|
||||
versionElement.textContent = `v${info.version}`;
|
||||
}
|
||||
|
||||
// Update environment
|
||||
const envElement = this.container.querySelector('.api-environment');
|
||||
if (envElement && info.environment) {
|
||||
envElement.textContent = info.environment;
|
||||
envElement.className = `api-environment env-${info.environment}`;
|
||||
}
|
||||
|
||||
// Update features status
|
||||
if (info.features) {
|
||||
this.updateFeatures(info.features);
|
||||
}
|
||||
}
|
||||
|
||||
// Update features display
|
||||
updateFeatures(features) {
|
||||
const featuresContainer = this.container.querySelector('.features-status');
|
||||
if (!featuresContainer) return;
|
||||
|
||||
featuresContainer.innerHTML = '';
|
||||
|
||||
Object.entries(features).forEach(([feature, enabled]) => {
|
||||
const featureElement = document.createElement('div');
|
||||
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
|
||||
featureElement.innerHTML = `
|
||||
<span class="feature-name">${this.formatFeatureName(feature)}</span>
|
||||
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
|
||||
`;
|
||||
featuresContainer.appendChild(featureElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update health status
|
||||
updateHealthStatus(health) {
|
||||
if (!health) return;
|
||||
|
||||
// Update overall status
|
||||
const overallStatus = this.container.querySelector('.overall-health');
|
||||
if (overallStatus) {
|
||||
overallStatus.className = `overall-health status-${health.status}`;
|
||||
overallStatus.textContent = health.status.toUpperCase();
|
||||
}
|
||||
|
||||
// Update component statuses
|
||||
if (health.components) {
|
||||
Object.entries(health.components).forEach(([component, status]) => {
|
||||
this.updateComponentStatus(component, status);
|
||||
});
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
if (health.metrics) {
|
||||
this.updateSystemMetrics(health.metrics);
|
||||
}
|
||||
}
|
||||
|
||||
// Update component status
|
||||
updateComponentStatus(component, status) {
|
||||
const element = this.container.querySelector(`[data-component="${component}"]`);
|
||||
if (element) {
|
||||
element.className = `component-status status-${status.status}`;
|
||||
element.querySelector('.status-text').textContent = status.status;
|
||||
|
||||
if (status.message) {
|
||||
element.querySelector('.status-message').textContent = status.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update system metrics
|
||||
updateSystemMetrics(metrics) {
|
||||
// CPU usage
|
||||
const cpuElement = this.container.querySelector('.cpu-usage');
|
||||
if (cpuElement && metrics.cpu_percent !== undefined) {
|
||||
cpuElement.textContent = `${metrics.cpu_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('cpu', metrics.cpu_percent);
|
||||
}
|
||||
|
||||
// Memory usage
|
||||
const memoryElement = this.container.querySelector('.memory-usage');
|
||||
if (memoryElement && metrics.memory_percent !== undefined) {
|
||||
memoryElement.textContent = `${metrics.memory_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('memory', metrics.memory_percent);
|
||||
}
|
||||
|
||||
// Disk usage
|
||||
const diskElement = this.container.querySelector('.disk-usage');
|
||||
if (diskElement && metrics.disk_percent !== undefined) {
|
||||
diskElement.textContent = `${metrics.disk_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('disk', metrics.disk_percent);
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
updateProgressBar(type, percent) {
|
||||
const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`);
|
||||
if (progressBar) {
|
||||
const fill = progressBar.querySelector('.progress-fill');
|
||||
if (fill) {
|
||||
fill.style.width = `${percent}%`;
|
||||
fill.className = `progress-fill ${this.getProgressClass(percent)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get progress class based on percentage
|
||||
getProgressClass(percent) {
|
||||
if (percent >= 90) return 'critical';
|
||||
if (percent >= 75) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Update live statistics
|
||||
async updateLiveStats() {
|
||||
try {
|
||||
// Get current pose data
|
||||
const currentPose = await poseService.getCurrentPose();
|
||||
this.updatePoseStats(currentPose);
|
||||
|
||||
// Get zones summary
|
||||
const zonesSummary = await poseService.getZonesSummary();
|
||||
this.updateZonesDisplay(zonesSummary);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update live stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update pose statistics
|
||||
updatePoseStats(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('.person-count');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.total_persons || 0;
|
||||
}
|
||||
|
||||
// Update average confidence
|
||||
const avgConfidence = this.container.querySelector('.avg-confidence');
|
||||
if (avgConfidence && poseData.persons) {
|
||||
const confidences = poseData.persons.map(p => p.confidence);
|
||||
const avg = confidences.length > 0
|
||||
? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1)
|
||||
: 0;
|
||||
avgConfidence.textContent = `${avg}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update zones display
|
||||
updateZonesDisplay(zonesSummary) {
|
||||
const zonesContainer = this.container.querySelector('.zones-summary');
|
||||
if (!zonesContainer || !zonesSummary) return;
|
||||
|
||||
zonesContainer.innerHTML = '';
|
||||
|
||||
Object.entries(zonesSummary.zones).forEach(([zoneId, data]) => {
|
||||
const zoneElement = document.createElement('div');
|
||||
zoneElement.className = 'zone-item';
|
||||
zoneElement.innerHTML = `
|
||||
<span class="zone-name">${data.name || zoneId}</span>
|
||||
<span class="zone-count">${data.person_count}</span>
|
||||
`;
|
||||
zonesContainer.appendChild(zoneElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
updateStats(stats) {
|
||||
if (!stats) return;
|
||||
|
||||
// Update detection count
|
||||
const detectionCount = this.container.querySelector('.detection-count');
|
||||
if (detectionCount && stats.total_detections !== undefined) {
|
||||
detectionCount.textContent = this.formatNumber(stats.total_detections);
|
||||
}
|
||||
|
||||
// Update accuracy if available
|
||||
if (this.statsElements.accuracy && stats.average_confidence !== undefined) {
|
||||
this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Format feature name
|
||||
formatFeatureName(name) {
|
||||
return name.replace(/_/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Format large numbers
|
||||
formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
showError(message) {
|
||||
const errorContainer = this.container.querySelector('.error-container');
|
||||
if (errorContainer) {
|
||||
errorContainer.textContent = message;
|
||||
errorContainer.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
errorContainer.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
if (this.healthSubscription) {
|
||||
this.healthSubscription();
|
||||
}
|
||||
|
||||
if (this.statsInterval) {
|
||||
clearInterval(this.statsInterval);
|
||||
}
|
||||
|
||||
healthService.stopHealthMonitoring();
|
||||
}
|
||||
}
|
||||
165
ui/components/HardwareTab.js
Normal file
165
ui/components/HardwareTab.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// Hardware Tab Component
|
||||
|
||||
export class HardwareTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.antennas = [];
|
||||
this.csiUpdateInterval = null;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
init() {
|
||||
this.setupAntennas();
|
||||
this.startCSISimulation();
|
||||
}
|
||||
|
||||
// Set up antenna interactions
|
||||
setupAntennas() {
|
||||
this.antennas = Array.from(this.container.querySelectorAll('.antenna'));
|
||||
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.addEventListener('click', () => {
|
||||
antenna.classList.toggle('active');
|
||||
this.updateCSIDisplay();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start CSI simulation
|
||||
startCSISimulation() {
|
||||
// Initial update
|
||||
this.updateCSIDisplay();
|
||||
|
||||
// Set up periodic updates
|
||||
this.csiUpdateInterval = setInterval(() => {
|
||||
if (this.hasActiveAntennas()) {
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Check if any antennas are active
|
||||
hasActiveAntennas() {
|
||||
return this.antennas.some(antenna => antenna.classList.contains('active'));
|
||||
}
|
||||
|
||||
// Update CSI display
|
||||
updateCSIDisplay() {
|
||||
const activeAntennas = this.antennas.filter(a => a.classList.contains('active'));
|
||||
const isActive = activeAntennas.length > 0;
|
||||
|
||||
// Get display elements
|
||||
const amplitudeFill = this.container.querySelector('.csi-fill.amplitude');
|
||||
const phaseFill = this.container.querySelector('.csi-fill.phase');
|
||||
const amplitudeValue = this.container.querySelector('.csi-row:first-child .csi-value');
|
||||
const phaseValue = this.container.querySelector('.csi-row:last-child .csi-value');
|
||||
|
||||
if (!isActive) {
|
||||
// Set to zero when no antennas active
|
||||
if (amplitudeFill) amplitudeFill.style.width = '0%';
|
||||
if (phaseFill) phaseFill.style.width = '0%';
|
||||
if (amplitudeValue) amplitudeValue.textContent = '0.00';
|
||||
if (phaseValue) phaseValue.textContent = '0.0π';
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate realistic CSI values based on active antennas
|
||||
const txCount = activeAntennas.filter(a => a.classList.contains('tx')).length;
|
||||
const rxCount = activeAntennas.filter(a => a.classList.contains('rx')).length;
|
||||
|
||||
// Amplitude increases with more active antennas
|
||||
const baseAmplitude = 0.3 + (txCount * 0.1) + (rxCount * 0.05);
|
||||
const amplitude = Math.min(0.95, baseAmplitude + (Math.random() * 0.1 - 0.05));
|
||||
|
||||
// Phase varies more with multiple antennas
|
||||
const phaseVariation = 0.5 + (activeAntennas.length * 0.1);
|
||||
const phase = 0.5 + Math.random() * phaseVariation;
|
||||
|
||||
// Update display
|
||||
if (amplitudeFill) {
|
||||
amplitudeFill.style.width = `${amplitude * 100}%`;
|
||||
amplitudeFill.style.transition = 'width 0.5s ease';
|
||||
}
|
||||
|
||||
if (phaseFill) {
|
||||
phaseFill.style.width = `${phase * 50}%`;
|
||||
phaseFill.style.transition = 'width 0.5s ease';
|
||||
}
|
||||
|
||||
if (amplitudeValue) {
|
||||
amplitudeValue.textContent = amplitude.toFixed(2);
|
||||
}
|
||||
|
||||
if (phaseValue) {
|
||||
phaseValue.textContent = `${phase.toFixed(1)}π`;
|
||||
}
|
||||
|
||||
// Update antenna array visualization
|
||||
this.updateAntennaArray(activeAntennas);
|
||||
}
|
||||
|
||||
// Update antenna array visualization
|
||||
updateAntennaArray(activeAntennas) {
|
||||
const arrayStatus = this.container.querySelector('.array-status');
|
||||
if (!arrayStatus) return;
|
||||
|
||||
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
|
||||
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
|
||||
|
||||
arrayStatus.innerHTML = `
|
||||
<div class="array-info">
|
||||
<span class="info-label">Active TX:</span>
|
||||
<span class="info-value">${txActive}/3</span>
|
||||
</div>
|
||||
<div class="array-info">
|
||||
<span class="info-label">Active RX:</span>
|
||||
<span class="info-value">${rxActive}/6</span>
|
||||
</div>
|
||||
<div class="array-info">
|
||||
<span class="info-label">Signal Quality:</span>
|
||||
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Calculate signal quality based on active antennas
|
||||
calculateSignalQuality(txCount, rxCount) {
|
||||
if (txCount === 0 || rxCount === 0) return 0;
|
||||
|
||||
const txRatio = txCount / 3;
|
||||
const rxRatio = rxCount / 6;
|
||||
const quality = (txRatio * 0.4 + rxRatio * 0.6) * 100;
|
||||
|
||||
return Math.round(quality);
|
||||
}
|
||||
|
||||
// Toggle all antennas
|
||||
toggleAllAntennas(active) {
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.classList.toggle('active', active);
|
||||
});
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
|
||||
// Reset antenna configuration
|
||||
resetAntennas() {
|
||||
// Set default configuration (all active)
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.classList.add('active');
|
||||
});
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
if (this.csiUpdateInterval) {
|
||||
clearInterval(this.csiUpdateInterval);
|
||||
this.csiUpdateInterval = null;
|
||||
}
|
||||
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.removeEventListener('click', this.toggleAntenna);
|
||||
});
|
||||
}
|
||||
}
|
||||
391
ui/components/LiveDemoTab.js
Normal file
391
ui/components/LiveDemoTab.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// Live Demo Tab Component
|
||||
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
import { streamService } from '../services/stream.service.js';
|
||||
|
||||
export class LiveDemoTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.isRunning = false;
|
||||
this.streamConnection = null;
|
||||
this.poseSubscription = null;
|
||||
this.signalCanvas = null;
|
||||
this.poseCanvas = null;
|
||||
this.signalCtx = null;
|
||||
this.poseCtx = null;
|
||||
this.animationFrame = null;
|
||||
this.signalTime = 0;
|
||||
this.poseData = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
init() {
|
||||
this.setupCanvases();
|
||||
this.setupControls();
|
||||
this.initializeDisplays();
|
||||
}
|
||||
|
||||
// Set up canvases
|
||||
setupCanvases() {
|
||||
this.signalCanvas = this.container.querySelector('#signalCanvas');
|
||||
this.poseCanvas = this.container.querySelector('#poseCanvas');
|
||||
|
||||
if (this.signalCanvas) {
|
||||
this.signalCtx = this.signalCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
if (this.poseCanvas) {
|
||||
this.poseCtx = this.poseCanvas.getContext('2d');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up control buttons
|
||||
setupControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.addEventListener('click', () => this.startDemo());
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.addEventListener('click', () => this.stopDemo());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize displays
|
||||
initializeDisplays() {
|
||||
// Initialize signal canvas
|
||||
if (this.signalCtx) {
|
||||
this.signalCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.signalCtx.fillRect(0, 0, this.signalCanvas.width, this.signalCanvas.height);
|
||||
}
|
||||
|
||||
// Initialize pose canvas
|
||||
if (this.poseCtx) {
|
||||
this.poseCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.poseCtx.fillRect(0, 0, this.poseCanvas.width, this.poseCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Start demo
|
||||
async startDemo() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
try {
|
||||
// Update UI
|
||||
this.isRunning = true;
|
||||
this.updateControls();
|
||||
this.updateStatus('Starting...', 'info');
|
||||
|
||||
// Check stream status
|
||||
const streamStatus = await streamService.getStatus();
|
||||
|
||||
if (!streamStatus.is_active) {
|
||||
// Try to start streaming
|
||||
await streamService.start();
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
this.streamConnection = poseService.startPoseStream({
|
||||
minConfidence: 0.5,
|
||||
maxFps: 30
|
||||
});
|
||||
|
||||
// Subscribe to pose updates
|
||||
this.poseSubscription = poseService.subscribeToPoseUpdates(update => {
|
||||
this.handlePoseUpdate(update);
|
||||
});
|
||||
|
||||
// Start animations
|
||||
this.startAnimations();
|
||||
|
||||
// Update status
|
||||
this.updateStatus('Running', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start demo:', error);
|
||||
this.updateStatus('Failed to start', 'error');
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop demo
|
||||
stopDemo() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update UI
|
||||
this.isRunning = false;
|
||||
this.updateControls();
|
||||
this.updateStatus('Stopped', 'info');
|
||||
|
||||
// Stop pose stream
|
||||
if (this.poseSubscription) {
|
||||
this.poseSubscription();
|
||||
this.poseSubscription = null;
|
||||
}
|
||||
|
||||
poseService.stopPoseStream();
|
||||
this.streamConnection = null;
|
||||
|
||||
// Stop animations
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update controls
|
||||
updateControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.disabled = this.isRunning;
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.disabled = !this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status display
|
||||
updateStatus(text, type) {
|
||||
const statusElement = this.container.querySelector('#demoStatus');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = text;
|
||||
statusElement.className = `status status--${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pose updates
|
||||
handlePoseUpdate(update) {
|
||||
switch (update.type) {
|
||||
case 'connected':
|
||||
console.log('Pose stream connected');
|
||||
break;
|
||||
|
||||
case 'pose_update':
|
||||
this.poseData = update.data;
|
||||
this.updateMetrics(update.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Pose stream error:', update.error);
|
||||
this.updateStatus('Stream error', 'error');
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log('Pose stream disconnected');
|
||||
if (this.isRunning) {
|
||||
this.updateStatus('Disconnected', 'warning');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update metrics display
|
||||
updateMetrics(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update signal strength (simulated based on detection confidence)
|
||||
const signalStrength = this.container.querySelector('#signalStrength');
|
||||
if (signalStrength) {
|
||||
const strength = poseData.persons?.length > 0
|
||||
? -45 - Math.random() * 10
|
||||
: -55 - Math.random() * 10;
|
||||
signalStrength.textContent = `${strength.toFixed(0)} dBm`;
|
||||
}
|
||||
|
||||
// Update latency
|
||||
const latency = this.container.querySelector('#latency');
|
||||
if (latency && poseData.processing_time) {
|
||||
latency.textContent = `${poseData.processing_time.toFixed(0)} ms`;
|
||||
}
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('#personCount');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.persons?.length || 0;
|
||||
}
|
||||
|
||||
// Update confidence
|
||||
const confidence = this.container.querySelector('#confidence');
|
||||
if (confidence && poseData.persons?.length > 0) {
|
||||
const avgConfidence = poseData.persons.reduce((sum, p) => sum + p.confidence, 0)
|
||||
/ poseData.persons.length * 100;
|
||||
confidence.textContent = `${avgConfidence.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// Update keypoints
|
||||
const keypoints = this.container.querySelector('#keypoints');
|
||||
if (keypoints && poseData.persons?.length > 0) {
|
||||
const totalKeypoints = poseData.persons[0].keypoints?.length || 0;
|
||||
const detectedKeypoints = poseData.persons[0].keypoints?.filter(kp => kp.confidence > 0.5).length || 0;
|
||||
keypoints.textContent = `${detectedKeypoints}/${totalKeypoints}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Start animations
|
||||
startAnimations() {
|
||||
const animate = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update signal visualization
|
||||
this.updateSignalVisualization();
|
||||
|
||||
// Update pose visualization
|
||||
this.updatePoseVisualization();
|
||||
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
// Update signal visualization
|
||||
updateSignalVisualization() {
|
||||
if (!this.signalCtx) return;
|
||||
|
||||
const ctx = this.signalCtx;
|
||||
const width = this.signalCanvas.width;
|
||||
const height = this.signalCanvas.height;
|
||||
|
||||
// Clear canvas with fade effect
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw amplitude signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#1FB8CD';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 30 : 10;
|
||||
const frequency = hasData ? 0.05 : 0.02;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.sin(x * frequency + this.signalTime) * amplitude +
|
||||
Math.sin(x * 0.02 + this.signalTime * 1.5) * 15;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw phase signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#FFC185';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 25 : 15;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.cos(x * 0.03 + this.signalTime * 0.8) * amplitude +
|
||||
Math.cos(x * 0.01 + this.signalTime * 0.5) * 20;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
this.signalTime += 0.05;
|
||||
}
|
||||
|
||||
// Update pose visualization
|
||||
updatePoseVisualization() {
|
||||
if (!this.poseCtx || !this.poseData) return;
|
||||
|
||||
const ctx = this.poseCtx;
|
||||
const width = this.poseCanvas.width;
|
||||
const height = this.poseCanvas.height;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw each detected person
|
||||
if (this.poseData.persons) {
|
||||
this.poseData.persons.forEach((person, index) => {
|
||||
this.drawPerson(ctx, person, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a person's pose
|
||||
drawPerson(ctx, person, index) {
|
||||
if (!person.keypoints) return;
|
||||
|
||||
// Define COCO keypoint connections
|
||||
const connections = [
|
||||
[0, 1], [0, 2], [1, 3], [2, 4], // Head
|
||||
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10], // Arms
|
||||
[5, 11], [6, 12], [11, 12], // Body
|
||||
[11, 13], [13, 15], [12, 14], [14, 16] // Legs
|
||||
];
|
||||
|
||||
// Scale keypoints to canvas
|
||||
const scale = Math.min(this.poseCanvas.width, this.poseCanvas.height) / 2;
|
||||
const offsetX = this.poseCanvas.width / 2;
|
||||
const offsetY = this.poseCanvas.height / 2;
|
||||
|
||||
// Draw skeleton connections
|
||||
ctx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`;
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
connections.forEach(([i, j]) => {
|
||||
const kp1 = person.keypoints[i];
|
||||
const kp2 = person.keypoints[j];
|
||||
|
||||
if (kp1 && kp2 && kp1.confidence > 0.3 && kp2.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(kp1.x * scale + offsetX, kp1.y * scale + offsetY);
|
||||
ctx.lineTo(kp2.x * scale + offsetX, kp2.y * scale + offsetY);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw keypoints
|
||||
ctx.fillStyle = `hsl(${index * 60}, 70%, 60%)`;
|
||||
|
||||
person.keypoints.forEach(kp => {
|
||||
if (kp.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
kp.x * scale + offsetX,
|
||||
kp.y * scale + offsetY,
|
||||
5,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw confidence label
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText(
|
||||
`Person ${index + 1}: ${(person.confidence * 100).toFixed(1)}%`,
|
||||
10,
|
||||
20 + index * 20
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
138
ui/components/TabManager.js
Normal file
138
ui/components/TabManager.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// Tab Manager Component
|
||||
|
||||
export class TabManager {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.tabs = [];
|
||||
this.activeTab = null;
|
||||
this.tabChangeCallbacks = [];
|
||||
}
|
||||
|
||||
// Initialize tabs
|
||||
init() {
|
||||
// Find all tabs and contents
|
||||
this.tabs = Array.from(this.container.querySelectorAll('.nav-tab'));
|
||||
this.tabContents = Array.from(this.container.querySelectorAll('.tab-content'));
|
||||
|
||||
// Set up event listeners
|
||||
this.tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => this.switchTab(tab));
|
||||
});
|
||||
|
||||
// Activate first tab if none active
|
||||
const activeTab = this.tabs.find(tab => tab.classList.contains('active'));
|
||||
if (activeTab) {
|
||||
this.activeTab = activeTab.getAttribute('data-tab');
|
||||
} else if (this.tabs.length > 0) {
|
||||
this.switchTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to a tab
|
||||
switchTab(tabElement) {
|
||||
const tabId = tabElement.getAttribute('data-tab');
|
||||
|
||||
if (tabId === this.activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tab states
|
||||
this.tabs.forEach(tab => {
|
||||
tab.classList.toggle('active', tab === tabElement);
|
||||
});
|
||||
|
||||
// Update content visibility
|
||||
this.tabContents.forEach(content => {
|
||||
content.classList.toggle('active', content.id === tabId);
|
||||
});
|
||||
|
||||
// Update active tab
|
||||
const previousTab = this.activeTab;
|
||||
this.activeTab = tabId;
|
||||
|
||||
// Notify callbacks
|
||||
this.notifyTabChange(tabId, previousTab);
|
||||
}
|
||||
|
||||
// Switch to tab by ID
|
||||
switchToTab(tabId) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
this.switchTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// Register tab change callback
|
||||
onTabChange(callback) {
|
||||
this.tabChangeCallbacks.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.tabChangeCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.tabChangeCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify tab change callbacks
|
||||
notifyTabChange(newTab, previousTab) {
|
||||
this.tabChangeCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(newTab, previousTab);
|
||||
} catch (error) {
|
||||
console.error('Error in tab change callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get active tab
|
||||
getActiveTab() {
|
||||
return this.activeTab;
|
||||
}
|
||||
|
||||
// Enable/disable tab
|
||||
setTabEnabled(tabId, enabled) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
tab.disabled = !enabled;
|
||||
tab.classList.toggle('disabled', !enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide tab
|
||||
setTabVisible(tabId, visible) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
tab.style.display = visible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Add badge to tab
|
||||
setTabBadge(tabId, badge) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (!tab) return;
|
||||
|
||||
// Remove existing badge
|
||||
const existingBadge = tab.querySelector('.tab-badge');
|
||||
if (existingBadge) {
|
||||
existingBadge.remove();
|
||||
}
|
||||
|
||||
// Add new badge if provided
|
||||
if (badge) {
|
||||
const badgeElement = document.createElement('span');
|
||||
badgeElement.className = 'tab-badge';
|
||||
badgeElement.textContent = badge;
|
||||
tab.appendChild(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.tabs.forEach(tab => {
|
||||
tab.removeEventListener('click', this.switchTab);
|
||||
});
|
||||
this.tabChangeCallbacks = [];
|
||||
}
|
||||
}
|
||||
118
ui/config/api.config.js
Normal file
118
ui/config/api.config.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// API Configuration for WiFi-DensePose UI
|
||||
|
||||
export const API_CONFIG = {
|
||||
BASE_URL: window.location.origin,
|
||||
API_VERSION: '/api/v1',
|
||||
WS_PREFIX: 'ws://',
|
||||
WSS_PREFIX: 'wss://',
|
||||
|
||||
// API Endpoints
|
||||
ENDPOINTS: {
|
||||
// Root & Info
|
||||
ROOT: '/',
|
||||
INFO: '/api/v1/info',
|
||||
STATUS: '/api/v1/status',
|
||||
METRICS: '/api/v1/metrics',
|
||||
|
||||
// Health
|
||||
HEALTH: {
|
||||
SYSTEM: '/health/health',
|
||||
READY: '/health/ready',
|
||||
LIVE: '/health/live',
|
||||
METRICS: '/health/metrics',
|
||||
VERSION: '/health/version'
|
||||
},
|
||||
|
||||
// Pose
|
||||
POSE: {
|
||||
CURRENT: '/api/v1/pose/current',
|
||||
ANALYZE: '/api/v1/pose/analyze',
|
||||
ZONE_OCCUPANCY: '/api/v1/pose/zones/{zone_id}/occupancy',
|
||||
ZONES_SUMMARY: '/api/v1/pose/zones/summary',
|
||||
HISTORICAL: '/api/v1/pose/historical',
|
||||
ACTIVITIES: '/api/v1/pose/activities',
|
||||
CALIBRATE: '/api/v1/pose/calibrate',
|
||||
CALIBRATION_STATUS: '/api/v1/pose/calibration/status',
|
||||
STATS: '/api/v1/pose/stats'
|
||||
},
|
||||
|
||||
// Streaming
|
||||
STREAM: {
|
||||
STATUS: '/api/v1/stream/status',
|
||||
START: '/api/v1/stream/start',
|
||||
STOP: '/api/v1/stream/stop',
|
||||
CLIENTS: '/api/v1/stream/clients',
|
||||
DISCONNECT_CLIENT: '/api/v1/stream/clients/{client_id}',
|
||||
BROADCAST: '/api/v1/stream/broadcast',
|
||||
METRICS: '/api/v1/stream/metrics',
|
||||
// WebSocket endpoints
|
||||
WS_POSE: '/api/v1/stream/pose',
|
||||
WS_EVENTS: '/api/v1/stream/events'
|
||||
},
|
||||
|
||||
// Development (only in dev mode)
|
||||
DEV: {
|
||||
CONFIG: '/api/v1/dev/config',
|
||||
RESET: '/api/v1/dev/reset'
|
||||
}
|
||||
},
|
||||
|
||||
// Default request options
|
||||
DEFAULT_HEADERS: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
RATE_LIMITS: {
|
||||
REQUESTS_PER_MINUTE: 60,
|
||||
BURST_LIMIT: 10
|
||||
},
|
||||
|
||||
// WebSocket configuration
|
||||
WS_CONFIG: {
|
||||
RECONNECT_DELAY: 5000,
|
||||
MAX_RECONNECT_ATTEMPTS: 5,
|
||||
PING_INTERVAL: 30000,
|
||||
MESSAGE_TIMEOUT: 10000
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to build API URLs
|
||||
export function buildApiUrl(endpoint, params = {}) {
|
||||
let url = `${API_CONFIG.BASE_URL}${endpoint}`;
|
||||
|
||||
// Replace path parameters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (url.includes(`{${key}}`)) {
|
||||
url = url.replace(`{${key}}`, params[key]);
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Add query parameters
|
||||
const queryParams = new URLSearchParams(params);
|
||||
if (queryParams.toString()) {
|
||||
url += `?${queryParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper function to build WebSocket URLs
|
||||
export function buildWsUrl(endpoint, params = {}) {
|
||||
const protocol = window.location.protocol === 'https:'
|
||||
? API_CONFIG.WSS_PREFIX
|
||||
: API_CONFIG.WS_PREFIX;
|
||||
|
||||
const host = window.location.host;
|
||||
let url = `${protocol}${host}${endpoint}`;
|
||||
|
||||
// Add query parameters
|
||||
const queryParams = new URLSearchParams(params);
|
||||
if (queryParams.toString()) {
|
||||
url += `?${queryParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
489
ui/index.html
Normal file
489
ui/index.html
Normal file
@@ -0,0 +1,489 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WiFi DensePose: Human Tracking Through Walls</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>WiFi DensePose</h1>
|
||||
<p class="subtitle">Human Tracking Through Walls Using WiFi Signals</p>
|
||||
<div class="header-info">
|
||||
<span class="api-version"></span>
|
||||
<span class="api-environment"></span>
|
||||
<span class="overall-health"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="nav-tabs">
|
||||
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
|
||||
<button class="nav-tab" data-tab="hardware">Hardware</button>
|
||||
<button class="nav-tab" data-tab="demo">Live Demo</button>
|
||||
<button class="nav-tab" data-tab="architecture">Architecture</button>
|
||||
<button class="nav-tab" data-tab="performance">Performance</button>
|
||||
<button class="nav-tab" data-tab="applications">Applications</button>
|
||||
</nav>
|
||||
|
||||
<!-- Dashboard Tab -->
|
||||
<section id="dashboard" class="tab-content active">
|
||||
<div class="hero-section">
|
||||
<h2>Revolutionary WiFi-Based Human Pose Detection</h2>
|
||||
<p class="hero-description">
|
||||
AI can track your full-body movement through walls using just WiFi signals.
|
||||
Researchers at Carnegie Mellon have trained a neural network to turn basic WiFi
|
||||
signals into detailed wireframe models of human bodies.
|
||||
</p>
|
||||
|
||||
<!-- Error container -->
|
||||
<div class="error-container" style="display: none;"></div>
|
||||
|
||||
<!-- Live Status Panel -->
|
||||
<div class="live-status-panel">
|
||||
<h3>System Status</h3>
|
||||
<div class="status-grid">
|
||||
<div class="component-status" data-component="api">
|
||||
<span class="component-name">API Server</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="hardware">
|
||||
<span class="component-name">Hardware</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="inference">
|
||||
<span class="component-name">Inference</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="streaming">
|
||||
<span class="component-name">Streaming</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Metrics -->
|
||||
<div class="system-metrics-panel">
|
||||
<h3>System Metrics</h3>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">CPU Usage</span>
|
||||
<div class="progress-bar" data-type="cpu">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="cpu-usage">0%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Memory Usage</span>
|
||||
<div class="progress-bar" data-type="memory">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="memory-usage">0%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Disk Usage</span>
|
||||
<div class="progress-bar" data-type="disk">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="disk-usage">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Status -->
|
||||
<div class="features-panel">
|
||||
<h3>Features</h3>
|
||||
<div class="features-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Live Statistics -->
|
||||
<div class="live-stats-panel">
|
||||
<h3>Live Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Active Persons</span>
|
||||
<span class="person-count">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Avg Confidence</span>
|
||||
<span class="avg-confidence">0%</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Detections</span>
|
||||
<span class="detection-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zones-panel">
|
||||
<h4>Zone Occupancy</h4>
|
||||
<div class="zones-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-benefits">
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🏠</div>
|
||||
<h3>Through Walls</h3>
|
||||
<p>Works through solid barriers with no line of sight required</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🔒</div>
|
||||
<h3>Privacy-Preserving</h3>
|
||||
<p>No cameras or visual recording - just WiFi signal analysis</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">⚡</div>
|
||||
<h3>Real-Time</h3>
|
||||
<p>Maps 24 body regions in real-time at 100Hz sampling rate</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">💰</div>
|
||||
<h3>Low Cost</h3>
|
||||
<p>Built using $30 commercial WiFi hardware</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="system-stats">
|
||||
<div class="stat" data-stat="body-regions">
|
||||
<span class="stat-value">24</span>
|
||||
<span class="stat-label">Body Regions</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="sampling-rate">
|
||||
<span class="stat-value">100Hz</span>
|
||||
<span class="stat-label">Sampling Rate</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="accuracy">
|
||||
<span class="stat-value">87.2%</span>
|
||||
<span class="stat-label">Accuracy (AP@50)</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="hardware-cost">
|
||||
<span class="stat-value">$30</span>
|
||||
<span class="stat-label">Hardware Cost</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hardware Tab -->
|
||||
<section id="hardware" class="tab-content">
|
||||
<h2>Hardware Configuration</h2>
|
||||
|
||||
<div class="hardware-grid">
|
||||
<div class="antenna-section">
|
||||
<h3>3×3 Antenna Array</h3>
|
||||
<p class="help-text">Click antennas to toggle their state</p>
|
||||
<div class="antenna-array">
|
||||
<div class="antenna-grid">
|
||||
<div class="antenna tx active" data-type="TX1"></div>
|
||||
<div class="antenna tx active" data-type="TX2"></div>
|
||||
<div class="antenna tx active" data-type="TX3"></div>
|
||||
<div class="antenna rx active" data-type="RX1"></div>
|
||||
<div class="antenna rx active" data-type="RX2"></div>
|
||||
<div class="antenna rx active" data-type="RX3"></div>
|
||||
<div class="antenna rx active" data-type="RX4"></div>
|
||||
<div class="antenna rx active" data-type="RX5"></div>
|
||||
<div class="antenna rx active" data-type="RX6"></div>
|
||||
</div>
|
||||
<div class="antenna-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color tx"></div>
|
||||
<span>Transmitters (3)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color rx"></div>
|
||||
<span>Receivers (6)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="array-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h3>WiFi Configuration</h3>
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Frequency</label>
|
||||
<div class="config-value">2.4GHz ± 20MHz</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Subcarriers</label>
|
||||
<div class="config-value">30</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Sampling Rate</label>
|
||||
<div class="config-value">100 Hz</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Total Cost</label>
|
||||
<div class="config-value">$30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="csi-data">
|
||||
<h4>Real-time CSI Data</h4>
|
||||
<div class="csi-display">
|
||||
<div class="csi-row">
|
||||
<span>Amplitude:</span>
|
||||
<div class="csi-bar">
|
||||
<div class="csi-fill amplitude" style="width: 75%"></div>
|
||||
</div>
|
||||
<span class="csi-value">0.75</span>
|
||||
</div>
|
||||
<div class="csi-row">
|
||||
<span>Phase:</span>
|
||||
<div class="csi-bar">
|
||||
<div class="csi-fill phase" style="width: 60%"></div>
|
||||
</div>
|
||||
<span class="csi-value">1.2π</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Demo Tab -->
|
||||
<section id="demo" class="tab-content">
|
||||
<h2>Live Demonstration</h2>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button id="startDemo" class="btn btn--primary">Start Stream</button>
|
||||
<button id="stopDemo" class="btn btn--secondary" disabled>Stop Stream</button>
|
||||
<div class="demo-status">
|
||||
<span class="status status--info" id="demoStatus">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="signal-panel">
|
||||
<h3>WiFi Signal Analysis</h3>
|
||||
<div class="signal-display">
|
||||
<canvas id="signalCanvas" width="400" height="200"></canvas>
|
||||
</div>
|
||||
<div class="signal-metrics">
|
||||
<div class="metric">
|
||||
<span>Signal Strength:</span>
|
||||
<span id="signalStrength">-45 dBm</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>Processing Latency:</span>
|
||||
<span id="latency">12 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pose-panel">
|
||||
<h3>Human Pose Detection</h3>
|
||||
<div class="pose-display">
|
||||
<canvas id="poseCanvas" width="400" height="300"></canvas>
|
||||
</div>
|
||||
<div class="detection-info">
|
||||
<div class="info-item">
|
||||
<span>Persons Detected:</span>
|
||||
<span id="personCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>Confidence:</span>
|
||||
<span id="confidence">0.0%</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>Keypoints:</span>
|
||||
<span id="keypoints">0/0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Architecture Tab -->
|
||||
<section id="architecture" class="tab-content">
|
||||
<h2>System Architecture</h2>
|
||||
|
||||
<div class="architecture-flow">
|
||||
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813853/gpt4o_images/m7zztcttnue7vaxclvuw.png"
|
||||
alt="WiFi DensePose Architecture" class="architecture-image">
|
||||
|
||||
<div class="flow-steps">
|
||||
<div class="step-card" data-step="1">
|
||||
<div class="step-number">1</div>
|
||||
<h3>CSI Input</h3>
|
||||
<p>Channel State Information collected from WiFi antenna array</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="2">
|
||||
<div class="step-number">2</div>
|
||||
<h3>Phase Sanitization</h3>
|
||||
<p>Remove hardware-specific noise and normalize signal phase</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="3">
|
||||
<div class="step-number">3</div>
|
||||
<h3>Modality Translation</h3>
|
||||
<p>Convert WiFi signals to visual representation using CNN</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="4">
|
||||
<div class="step-number">4</div>
|
||||
<h3>DensePose-RCNN</h3>
|
||||
<p>Extract human pose keypoints and body part segmentation</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="5">
|
||||
<div class="step-number">5</div>
|
||||
<h3>Wireframe Output</h3>
|
||||
<p>Generate final human pose wireframe visualization</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Tab -->
|
||||
<section id="performance" class="tab-content">
|
||||
<h2>Performance Analysis</h2>
|
||||
|
||||
<div class="performance-chart">
|
||||
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813924/pplx_code_interpreter/af6ef268_nsauu6.jpg"
|
||||
alt="Performance Comparison Chart" class="chart-image">
|
||||
</div>
|
||||
|
||||
<div class="performance-grid">
|
||||
<div class="performance-card">
|
||||
<h3>WiFi-based (Same Layout)</h3>
|
||||
<div class="metric-list">
|
||||
<div class="metric-item">
|
||||
<span>Average Precision:</span>
|
||||
<span class="metric-value">43.5%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@50:</span>
|
||||
<span class="metric-value success">87.2%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@75:</span>
|
||||
<span class="metric-value">44.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="performance-card">
|
||||
<h3>Image-based (Reference)</h3>
|
||||
<div class="metric-list">
|
||||
<div class="metric-item">
|
||||
<span>Average Precision:</span>
|
||||
<span class="metric-value success">84.7%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@50:</span>
|
||||
<span class="metric-value success">94.4%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@75:</span>
|
||||
<span class="metric-value success">77.1%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="limitations-section">
|
||||
<h3>Advantages & Limitations</h3>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>Advantages</h4>
|
||||
<ul>
|
||||
<li>Through-wall detection</li>
|
||||
<li>Privacy preserving</li>
|
||||
<li>Lighting independent</li>
|
||||
<li>Low cost hardware</li>
|
||||
<li>Uses existing WiFi</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>Limitations</h4>
|
||||
<ul>
|
||||
<li>Performance drops in different layouts</li>
|
||||
<li>Requires WiFi-compatible devices</li>
|
||||
<li>Training requires synchronized data</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Applications Tab -->
|
||||
<section id="applications" class="tab-content">
|
||||
<h2>Real-World Applications</h2>
|
||||
|
||||
<div class="applications-grid">
|
||||
<div class="app-card">
|
||||
<div class="app-icon">👴</div>
|
||||
<h3>Elderly Care Monitoring</h3>
|
||||
<p>Monitor elderly individuals for falls or emergencies without invading privacy. Track movement patterns and detect anomalies in daily routines.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Fall Detection</span>
|
||||
<span class="feature-tag">Activity Monitoring</span>
|
||||
<span class="feature-tag">Emergency Alert</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏠</div>
|
||||
<h3>Home Security Systems</h3>
|
||||
<p>Detect intruders and monitor home security without visible cameras. Track multiple persons and identify suspicious movement patterns.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Intrusion Detection</span>
|
||||
<span class="feature-tag">Multi-person Tracking</span>
|
||||
<span class="feature-tag">Invisible Monitoring</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏥</div>
|
||||
<h3>Healthcare Patient Monitoring</h3>
|
||||
<p>Monitor patients in hospitals and care facilities. Track vital signs through movement analysis and detect health emergencies.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Vital Sign Analysis</span>
|
||||
<span class="feature-tag">Movement Tracking</span>
|
||||
<span class="feature-tag">Health Alerts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏢</div>
|
||||
<h3>Smart Building Occupancy</h3>
|
||||
<p>Optimize building energy consumption by tracking occupancy patterns. Control lighting, HVAC, and security systems automatically.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Energy Optimization</span>
|
||||
<span class="feature-tag">Occupancy Tracking</span>
|
||||
<span class="feature-tag">Smart Controls</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🥽</div>
|
||||
<h3>AR/VR Applications</h3>
|
||||
<p>Enable full-body tracking for virtual and augmented reality applications without wearing additional sensors or cameras.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Full Body Tracking</span>
|
||||
<span class="feature-tag">Sensor-free</span>
|
||||
<span class="feature-tag">Immersive Experience</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="implementation-note">
|
||||
<h3>Implementation Considerations</h3>
|
||||
<p>While WiFi DensePose offers revolutionary capabilities, successful implementation requires careful consideration of environment setup, data privacy regulations, and system calibration for optimal performance.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Error Toast -->
|
||||
<div id="globalErrorToast" class="error-toast"></div>
|
||||
|
||||
<!-- Load application scripts as modules -->
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
139
ui/services/api.service.js
Normal file
139
ui/services/api.service.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// API Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildApiUrl } from '../config/api.config.js';
|
||||
|
||||
export class ApiService {
|
||||
constructor() {
|
||||
this.authToken = null;
|
||||
this.requestInterceptors = [];
|
||||
this.responseInterceptors = [];
|
||||
}
|
||||
|
||||
// Set authentication token
|
||||
setAuthToken(token) {
|
||||
this.authToken = token;
|
||||
}
|
||||
|
||||
// Add request interceptor
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Add response interceptor
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Build headers for requests
|
||||
getHeaders(customHeaders = {}) {
|
||||
const headers = {
|
||||
...API_CONFIG.DEFAULT_HEADERS,
|
||||
...customHeaders
|
||||
};
|
||||
|
||||
if (this.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Process request through interceptors
|
||||
async processRequest(url, options) {
|
||||
let processedUrl = url;
|
||||
let processedOptions = options;
|
||||
|
||||
for (const interceptor of this.requestInterceptors) {
|
||||
const result = await interceptor(processedUrl, processedOptions);
|
||||
processedUrl = result.url || processedUrl;
|
||||
processedOptions = result.options || processedOptions;
|
||||
}
|
||||
|
||||
return { url: processedUrl, options: processedOptions };
|
||||
}
|
||||
|
||||
// Process response through interceptors
|
||||
async processResponse(response, url) {
|
||||
let processedResponse = response;
|
||||
|
||||
for (const interceptor of this.responseInterceptors) {
|
||||
processedResponse = await interceptor(processedResponse, url);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
}
|
||||
|
||||
// Generic request method
|
||||
async request(url, options = {}) {
|
||||
try {
|
||||
// Process request through interceptors
|
||||
const processed = await this.processRequest(url, options);
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(processed.url, {
|
||||
...processed.options,
|
||||
headers: this.getHeaders(processed.options.headers)
|
||||
});
|
||||
|
||||
// Process response through interceptors
|
||||
const processedResponse = await this.processResponse(response, url);
|
||||
|
||||
// Handle errors
|
||||
if (!processedResponse.ok) {
|
||||
const error = await processedResponse.json().catch(() => ({
|
||||
message: `HTTP ${processedResponse.status}: ${processedResponse.statusText}`
|
||||
}));
|
||||
throw new Error(error.message || error.detail || 'Request failed');
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
const data = await processedResponse.json().catch(() => null);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API Request Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET request
|
||||
async get(endpoint, params = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint, params);
|
||||
return this.request(url, {
|
||||
method: 'GET',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// POST request
|
||||
async post(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// PUT request
|
||||
async put(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE request
|
||||
async delete(endpoint, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const apiService = new ApiService();
|
||||
138
ui/services/health.service.js
Normal file
138
ui/services/health.service.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// Health Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class HealthService {
|
||||
constructor() {
|
||||
this.healthCheckInterval = null;
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
|
||||
// Get system health
|
||||
async getSystemHealth() {
|
||||
const health = await apiService.get(API_CONFIG.ENDPOINTS.HEALTH.SYSTEM);
|
||||
this.lastHealthStatus = health;
|
||||
this.notifySubscribers(health);
|
||||
return health;
|
||||
}
|
||||
|
||||
// Check readiness
|
||||
async checkReadiness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.READY);
|
||||
}
|
||||
|
||||
// Check liveness
|
||||
async checkLiveness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.LIVE);
|
||||
}
|
||||
|
||||
// Get system metrics
|
||||
async getSystemMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.METRICS);
|
||||
}
|
||||
|
||||
// Get version info
|
||||
async getVersion() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.VERSION);
|
||||
}
|
||||
|
||||
// Get API info
|
||||
async getApiInfo() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.INFO);
|
||||
}
|
||||
|
||||
// Get API status
|
||||
async getApiStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STATUS);
|
||||
}
|
||||
|
||||
// Start periodic health checks
|
||||
startHealthMonitoring(intervalMs = 30000) {
|
||||
if (this.healthCheckInterval) {
|
||||
console.warn('Health monitoring already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial check
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Initial health check failed:', error);
|
||||
});
|
||||
|
||||
// Set up periodic checks
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.notifySubscribers({
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
// Stop health monitoring
|
||||
stopHealthMonitoring() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to health updates
|
||||
subscribeToHealth(callback) {
|
||||
this.healthSubscribers.push(callback);
|
||||
|
||||
// Send last known status if available
|
||||
if (this.lastHealthStatus) {
|
||||
callback(this.lastHealthStatus);
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.healthSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.healthSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(health) {
|
||||
this.healthSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(health);
|
||||
} catch (error) {
|
||||
console.error('Error in health subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if system is healthy
|
||||
isSystemHealthy() {
|
||||
if (!this.lastHealthStatus) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.status === 'healthy';
|
||||
}
|
||||
|
||||
// Get component status
|
||||
getComponentStatus(componentName) {
|
||||
if (!this.lastHealthStatus?.components) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.components[componentName];
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopHealthMonitoring();
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const healthService = new HealthService();
|
||||
303
ui/services/pose.service.js
Normal file
303
ui/services/pose.service.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// Pose Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
import { wsService } from './websocket.service.js';
|
||||
|
||||
export class PoseService {
|
||||
constructor() {
|
||||
this.streamConnection = null;
|
||||
this.eventConnection = null;
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
|
||||
// Get current pose estimation
|
||||
async getCurrentPose(options = {}) {
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
confidence_threshold: options.confidenceThreshold,
|
||||
max_persons: options.maxPersons,
|
||||
include_keypoints: options.includeKeypoints,
|
||||
include_segmentation: options.includeSegmentation
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CURRENT, params);
|
||||
}
|
||||
|
||||
// Analyze pose (requires auth)
|
||||
async analyzePose(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.ANALYZE, request);
|
||||
}
|
||||
|
||||
// Get zone occupancy
|
||||
async getZoneOccupancy(zoneId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.POSE.ZONE_OCCUPANCY.replace('{zone_id}', zoneId);
|
||||
return apiService.get(endpoint);
|
||||
}
|
||||
|
||||
// Get zones summary
|
||||
async getZonesSummary() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ZONES_SUMMARY);
|
||||
}
|
||||
|
||||
// Get historical data (requires auth)
|
||||
async getHistoricalData(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.HISTORICAL, request);
|
||||
}
|
||||
|
||||
// Get recent activities
|
||||
async getActivities(options = {}) {
|
||||
const params = {
|
||||
zone_id: options.zoneId,
|
||||
limit: options.limit || 50
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ACTIVITIES, params);
|
||||
}
|
||||
|
||||
// Calibrate system (requires auth)
|
||||
async calibrate() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.CALIBRATE);
|
||||
}
|
||||
|
||||
// Get calibration status (requires auth)
|
||||
async getCalibrationStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CALIBRATION_STATUS);
|
||||
}
|
||||
|
||||
// Get pose statistics
|
||||
async getStats(hours = 24) {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.STATS, { hours });
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
startPoseStream(options = {}) {
|
||||
if (this.streamConnection) {
|
||||
console.warn('Pose stream already active');
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
min_confidence: options.minConfidence || 0.5,
|
||||
max_fps: options.maxFps || 30,
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.streamConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_POSE,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Pose stream connected');
|
||||
this.notifyPoseSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handlePoseMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Pose stream error:', error);
|
||||
this.notifyPoseSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Pose stream disconnected');
|
||||
this.streamConnection = null;
|
||||
this.notifyPoseSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
// Stop pose stream
|
||||
stopPoseStream() {
|
||||
if (this.streamConnection) {
|
||||
wsService.disconnect(this.streamConnection);
|
||||
this.streamConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to pose updates
|
||||
subscribeToPoseUpdates(callback) {
|
||||
this.poseSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.poseSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.poseSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle pose stream messages
|
||||
handlePoseMessage(data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
switch (type) {
|
||||
case 'pose_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'pose_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'historical_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'historical_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'zone_statistics':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'zone_stats',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'system_event':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'system_event',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown pose message type:', type);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify pose subscribers
|
||||
notifyPoseSubscribers(update) {
|
||||
this.poseSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in pose subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start event stream
|
||||
startEventStream(options = {}) {
|
||||
if (this.eventConnection) {
|
||||
console.warn('Event stream already active');
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
event_types: options.eventTypes?.join(','),
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.eventConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_EVENTS,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Event stream connected');
|
||||
this.notifyEventSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handleEventMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Event stream error:', error);
|
||||
this.notifyEventSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Event stream disconnected');
|
||||
this.eventConnection = null;
|
||||
this.notifyEventSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
// Stop event stream
|
||||
stopEventStream() {
|
||||
if (this.eventConnection) {
|
||||
wsService.disconnect(this.eventConnection);
|
||||
this.eventConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
subscribeToEvents(callback) {
|
||||
this.eventSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.eventSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.eventSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle event stream messages
|
||||
handleEventMessage(data) {
|
||||
this.notifyEventSubscribers({
|
||||
type: 'event',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// Notify event subscribers
|
||||
notifyEventSubscribers(update) {
|
||||
this.eventSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in event subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update stream configuration
|
||||
updateStreamConfig(connectionId, config) {
|
||||
wsService.sendCommand(connectionId, 'update_config', config);
|
||||
}
|
||||
|
||||
// Get stream status
|
||||
requestStreamStatus(connectionId) {
|
||||
wsService.sendCommand(connectionId, 'get_status');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopPoseStream();
|
||||
this.stopEventStream();
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const poseService = new PoseService();
|
||||
59
ui/services/stream.service.js
Normal file
59
ui/services/stream.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Stream Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class StreamService {
|
||||
// Get streaming status
|
||||
async getStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.STATUS);
|
||||
}
|
||||
|
||||
// Start streaming (requires auth)
|
||||
async start() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.START);
|
||||
}
|
||||
|
||||
// Stop streaming (requires auth)
|
||||
async stop() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.STOP);
|
||||
}
|
||||
|
||||
// Get connected clients (requires auth)
|
||||
async getClients() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.CLIENTS);
|
||||
}
|
||||
|
||||
// Disconnect a client (requires auth)
|
||||
async disconnectClient(clientId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.STREAM.DISCONNECT_CLIENT.replace('{client_id}', clientId);
|
||||
return apiService.delete(endpoint);
|
||||
}
|
||||
|
||||
// Broadcast message (requires auth)
|
||||
async broadcast(message, options = {}) {
|
||||
const params = {
|
||||
stream_type: options.streamType,
|
||||
zone_ids: options.zoneIds?.join(',')
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.post(
|
||||
API_CONFIG.ENDPOINTS.STREAM.BROADCAST,
|
||||
message,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
// Get streaming metrics
|
||||
async getMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.METRICS);
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const streamService = new StreamService();
|
||||
305
ui/services/websocket.service.js
Normal file
305
ui/services/websocket.service.js
Normal file
@@ -0,0 +1,305 @@
|
||||
// WebSocket Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildWsUrl } from '../config/api.config.js';
|
||||
|
||||
export class WebSocketService {
|
||||
constructor() {
|
||||
this.connections = new Map();
|
||||
this.messageHandlers = new Map();
|
||||
this.reconnectAttempts = new Map();
|
||||
}
|
||||
|
||||
// Connect to WebSocket endpoint
|
||||
connect(endpoint, params = {}, handlers = {}) {
|
||||
const url = buildWsUrl(endpoint, params);
|
||||
|
||||
// Check if already connected
|
||||
if (this.connections.has(url)) {
|
||||
console.warn(`Already connected to ${url}`);
|
||||
return this.connections.get(url);
|
||||
}
|
||||
|
||||
// Create WebSocket connection
|
||||
const ws = new WebSocket(url);
|
||||
const connectionId = this.generateId();
|
||||
|
||||
// Store connection
|
||||
this.connections.set(url, {
|
||||
id: connectionId,
|
||||
ws,
|
||||
url,
|
||||
handlers,
|
||||
status: 'connecting',
|
||||
lastPing: null,
|
||||
reconnectTimer: null
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
this.setupEventHandlers(url, ws, handlers);
|
||||
|
||||
// Start ping interval
|
||||
this.startPingInterval(url);
|
||||
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
// Set up WebSocket event handlers
|
||||
setupEventHandlers(url, ws, handlers) {
|
||||
const connection = this.connections.get(url);
|
||||
|
||||
ws.onopen = (event) => {
|
||||
console.log(`WebSocket connected: ${url}`);
|
||||
connection.status = 'connected';
|
||||
this.reconnectAttempts.set(url, 0);
|
||||
|
||||
if (handlers.onOpen) {
|
||||
handlers.onOpen(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle different message types
|
||||
this.handleMessage(url, data);
|
||||
|
||||
if (handlers.onMessage) {
|
||||
handlers.onMessage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error(`WebSocket error: ${url}`, event);
|
||||
connection.status = 'error';
|
||||
|
||||
if (handlers.onError) {
|
||||
handlers.onError(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`WebSocket closed: ${url}`);
|
||||
connection.status = 'closed';
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(url);
|
||||
|
||||
if (handlers.onClose) {
|
||||
handlers.onClose(event);
|
||||
}
|
||||
|
||||
// Attempt reconnection if not intentionally closed
|
||||
if (!event.wasClean && this.shouldReconnect(url)) {
|
||||
this.scheduleReconnect(url);
|
||||
} else {
|
||||
this.connections.delete(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
handleMessage(url, data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
// Handle system messages
|
||||
switch (type) {
|
||||
case 'pong':
|
||||
this.handlePong(url);
|
||||
break;
|
||||
|
||||
case 'connection_established':
|
||||
console.log('Connection established:', payload);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('WebSocket error message:', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
// Call registered message handlers
|
||||
const handlers = this.messageHandlers.get(url) || [];
|
||||
handlers.forEach(handler => handler(data));
|
||||
}
|
||||
|
||||
// Send message through WebSocket
|
||||
send(connectionId, message) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
throw new Error(`Connection ${connectionId} is not connected`);
|
||||
}
|
||||
|
||||
const data = typeof message === 'string'
|
||||
? message
|
||||
: JSON.stringify(message);
|
||||
|
||||
connection.ws.send(data);
|
||||
}
|
||||
|
||||
// Send command message
|
||||
sendCommand(connectionId, command, payload = {}) {
|
||||
this.send(connectionId, {
|
||||
type: command,
|
||||
payload,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Register message handler
|
||||
onMessage(connectionId, handler) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (!this.messageHandlers.has(connection.url)) {
|
||||
this.messageHandlers.set(connection.url, []);
|
||||
}
|
||||
|
||||
this.messageHandlers.get(connection.url).push(handler);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const handlers = this.messageHandlers.get(connection.url);
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Disconnect WebSocket
|
||||
disconnect(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear reconnection timer
|
||||
if (connection.reconnectTimer) {
|
||||
clearTimeout(connection.reconnectTimer);
|
||||
}
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(connection.url);
|
||||
|
||||
// Close WebSocket
|
||||
if (connection.ws.readyState === WebSocket.OPEN) {
|
||||
connection.ws.close(1000, 'Client disconnect');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
this.connections.delete(connection.url);
|
||||
this.messageHandlers.delete(connection.url);
|
||||
this.reconnectAttempts.delete(connection.url);
|
||||
}
|
||||
|
||||
// Disconnect all WebSockets
|
||||
disconnectAll() {
|
||||
const connectionIds = Array.from(this.connections.values()).map(c => c.id);
|
||||
connectionIds.forEach(id => this.disconnect(id));
|
||||
}
|
||||
|
||||
// Ping/Pong handling
|
||||
startPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
connection.pingInterval = setInterval(() => {
|
||||
if (connection.status === 'connected') {
|
||||
this.sendPing(url);
|
||||
}
|
||||
}, API_CONFIG.WS_CONFIG.PING_INTERVAL);
|
||||
}
|
||||
|
||||
clearPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.pingInterval) {
|
||||
clearInterval(connection.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
sendPing(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.status === 'connected') {
|
||||
connection.lastPing = Date.now();
|
||||
connection.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}
|
||||
|
||||
handlePong(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection) {
|
||||
const latency = Date.now() - connection.lastPing;
|
||||
console.log(`Pong received. Latency: ${latency}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnection logic
|
||||
shouldReconnect(url) {
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
return attempts < API_CONFIG.WS_CONFIG.MAX_RECONNECT_ATTEMPTS;
|
||||
}
|
||||
|
||||
scheduleReconnect(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
const delay = API_CONFIG.WS_CONFIG.RECONNECT_DELAY * Math.pow(2, attempts);
|
||||
|
||||
console.log(`Scheduling reconnect in ${delay}ms (attempt ${attempts + 1})`);
|
||||
|
||||
connection.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts.set(url, attempts + 1);
|
||||
|
||||
// Get original parameters
|
||||
const params = new URL(url).searchParams;
|
||||
const paramsObj = Object.fromEntries(params);
|
||||
const endpoint = url.replace(/^wss?:\/\/[^\/]+/, '').split('?')[0];
|
||||
|
||||
// Attempt reconnection
|
||||
this.connect(endpoint, paramsObj, connection.handlers);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
findConnectionById(connectionId) {
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.id === connectionId) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
getConnectionStatus(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
return connection ? connection.status : 'disconnected';
|
||||
}
|
||||
|
||||
getActiveConnections() {
|
||||
return Array.from(this.connections.values()).map(conn => ({
|
||||
id: conn.id,
|
||||
url: conn.url,
|
||||
status: conn.status
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const wsService = new WebSocketService();
|
||||
1307
ui/style.css
Normal file
1307
ui/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user