diff --git a/=3.0.0 b/=3.0.0
deleted file mode 100644
index 10482c6..0000000
--- a/=3.0.0
+++ /dev/null
@@ -1,18 +0,0 @@
-Collecting paramiko
- Downloading paramiko-3.5.1-py3-none-any.whl.metadata (4.6 kB)
-Collecting bcrypt>=3.2 (from paramiko)
- Downloading bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (10 kB)
-Collecting cryptography>=3.3 (from paramiko)
- Downloading cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl.metadata (5.7 kB)
-Collecting pynacl>=1.5 (from paramiko)
- Downloading PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.metadata (8.6 kB)
-Requirement already satisfied: cffi>=1.14 in /home/codespace/.local/lib/python3.12/site-packages (from cryptography>=3.3->paramiko) (1.17.1)
-Requirement already satisfied: pycparser in /home/codespace/.local/lib/python3.12/site-packages (from cffi>=1.14->cryptography>=3.3->paramiko) (2.22)
-Downloading paramiko-3.5.1-py3-none-any.whl (227 kB)
-Downloading bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl (284 kB)
-Downloading cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl (4.5 MB)
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 45.0 MB/s eta 0:00:00
-Downloading PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (856 kB)
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 856.7/856.7 kB 37.4 MB/s eta 0:00:00
-Installing collected packages: bcrypt, pynacl, cryptography, paramiko
-Successfully installed bcrypt-4.3.0 cryptography-45.0.3 paramiko-3.5.1 pynacl-1.5.0
diff --git a/example.env b/example.env
new file mode 100644
index 0000000..2238f7e
--- /dev/null
+++ b/example.env
@@ -0,0 +1,183 @@
+# WiFi-DensePose API Environment Configuration Template
+# Copy this file to .env and modify the values according to your setup
+
+# =============================================================================
+# APPLICATION SETTINGS
+# =============================================================================
+
+# Application metadata
+APP_NAME=WiFi-DensePose API
+VERSION=1.0.0
+ENVIRONMENT=development # Options: development, staging, production
+DEBUG=true
+
+# =============================================================================
+# SERVER SETTINGS
+# =============================================================================
+
+# Server configuration
+HOST=0.0.0.0
+PORT=8000
+RELOAD=true # Auto-reload on code changes (development only)
+WORKERS=1 # Number of worker processes
+
+# =============================================================================
+# SECURITY SETTINGS
+# =============================================================================
+
+# IMPORTANT: Change these values for production!
+SECRET_KEY=your-secret-key-here-change-for-production
+JWT_ALGORITHM=HS256
+JWT_EXPIRE_HOURS=24
+
+# Allowed hosts (restrict in production)
+ALLOWED_HOSTS=* # Use specific domains in production: example.com,api.example.com
+
+# CORS settings (restrict in production)
+CORS_ORIGINS=* # Use specific origins in production: https://example.com,https://app.example.com
+
+# =============================================================================
+# DATABASE SETTINGS
+# =============================================================================
+
+# Database connection (optional - defaults to SQLite in development)
+# DATABASE_URL=postgresql://user:password@localhost:5432/wifi_densepose
+# DATABASE_POOL_SIZE=10
+# DATABASE_MAX_OVERFLOW=20
+
+# =============================================================================
+# REDIS SETTINGS (Optional - for caching and rate limiting)
+# =============================================================================
+
+# Redis connection (optional - defaults to localhost in development)
+# REDIS_URL=redis://localhost:6379/0
+# REDIS_PASSWORD=your-redis-password
+# REDIS_DB=0
+
+# =============================================================================
+# HARDWARE SETTINGS
+# =============================================================================
+
+# WiFi interface configuration
+WIFI_INTERFACE=wlan0
+CSI_BUFFER_SIZE=1000
+HARDWARE_POLLING_INTERVAL=0.1
+
+# Hardware mock settings (for development/testing)
+MOCK_HARDWARE=true
+MOCK_POSE_DATA=true
+
+# =============================================================================
+# POSE ESTIMATION SETTINGS
+# =============================================================================
+
+# Model configuration
+# POSE_MODEL_PATH=/path/to/your/pose/model.pth
+POSE_CONFIDENCE_THRESHOLD=0.5
+POSE_PROCESSING_BATCH_SIZE=32
+POSE_MAX_PERSONS=10
+
+# =============================================================================
+# STREAMING SETTINGS
+# =============================================================================
+
+# Real-time streaming configuration
+STREAM_FPS=30
+STREAM_BUFFER_SIZE=100
+WEBSOCKET_PING_INTERVAL=60
+WEBSOCKET_TIMEOUT=300
+
+# =============================================================================
+# FEATURE FLAGS
+# =============================================================================
+
+# Enable/disable features
+ENABLE_AUTHENTICATION=false # Set to true for production
+ENABLE_RATE_LIMITING=false # Set to true for production
+ENABLE_WEBSOCKETS=true
+ENABLE_REAL_TIME_PROCESSING=true
+ENABLE_HISTORICAL_DATA=true
+
+# Development features
+ENABLE_TEST_ENDPOINTS=true # Set to false for production
+
+# =============================================================================
+# RATE LIMITING SETTINGS
+# =============================================================================
+
+# Rate limiting configuration
+RATE_LIMIT_REQUESTS=100
+RATE_LIMIT_AUTHENTICATED_REQUESTS=1000
+RATE_LIMIT_WINDOW=3600 # Window in seconds
+
+# =============================================================================
+# LOGGING SETTINGS
+# =============================================================================
+
+# Logging configuration
+LOG_LEVEL=INFO # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
+LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s
+# LOG_FILE=/path/to/logfile.log # Optional: specify log file path
+LOG_MAX_SIZE=10485760 # 10MB
+LOG_BACKUP_COUNT=5
+
+# =============================================================================
+# STORAGE SETTINGS
+# =============================================================================
+
+# Storage directories
+DATA_STORAGE_PATH=./data
+MODEL_STORAGE_PATH=./models
+TEMP_STORAGE_PATH=./temp
+MAX_STORAGE_SIZE_GB=100
+
+# =============================================================================
+# MONITORING SETTINGS
+# =============================================================================
+
+# Monitoring and metrics
+METRICS_ENABLED=true
+HEALTH_CHECK_INTERVAL=30
+PERFORMANCE_MONITORING=true
+
+# =============================================================================
+# API SETTINGS
+# =============================================================================
+
+# API configuration
+API_PREFIX=/api/v1
+DOCS_URL=/docs # Set to null to disable in production
+REDOC_URL=/redoc # Set to null to disable in production
+OPENAPI_URL=/openapi.json # Set to null to disable in production
+
+# =============================================================================
+# PRODUCTION SETTINGS
+# =============================================================================
+
+# For production deployment, ensure you:
+# 1. Set ENVIRONMENT=production
+# 2. Set DEBUG=false
+# 3. Use a strong SECRET_KEY
+# 4. Configure proper DATABASE_URL
+# 5. Restrict ALLOWED_HOSTS and CORS_ORIGINS
+# 6. Enable ENABLE_AUTHENTICATION=true
+# 7. Enable ENABLE_RATE_LIMITING=true
+# 8. Set ENABLE_TEST_ENDPOINTS=false
+# 9. Disable API documentation URLs (set to null)
+# 10. Configure proper logging with LOG_FILE
+
+# Example production settings:
+# ENVIRONMENT=production
+# DEBUG=false
+# SECRET_KEY=your-very-secure-secret-key-here
+# DATABASE_URL=postgresql://user:password@db-host:5432/wifi_densepose
+# REDIS_URL=redis://redis-host:6379/0
+# ALLOWED_HOSTS=yourdomain.com,api.yourdomain.com
+# CORS_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
+# ENABLE_AUTHENTICATION=true
+# ENABLE_RATE_LIMITING=true
+# ENABLE_TEST_ENDPOINTS=false
+# DOCS_URL=null
+# REDOC_URL=null
+# OPENAPI_URL=null
+# LOG_FILE=/var/log/wifi-densepose/app.log
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 7fd7bfe..97245ca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,6 +17,9 @@ fastapi>=0.95.0
uvicorn>=0.20.0
websockets>=10.4
pydantic>=1.10.0
+python-jose[cryptography]>=3.3.0
+python-multipart>=0.0.6
+passlib[bcrypt]>=1.7.4
# Hardware interface dependencies
asyncio-mqtt>=0.11.0
diff --git a/scripts/api_test_results_20250607_122720.json b/scripts/api_test_results_20250607_122720.json
new file mode 100644
index 0000000..b4b4459
--- /dev/null
+++ b/scripts/api_test_results_20250607_122720.json
@@ -0,0 +1,1967 @@
+{
+ "total_tests": 24,
+ "passed": 5,
+ "failed": 19,
+ "errors": [
+ "WebSocket /ws/pose - Exception: BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'",
+ "WebSocket /ws/hardware - Exception: BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'"
+ ],
+ "test_details": [
+ {
+ "test_name": "GET /health/health",
+ "description": "System health check",
+ "url": "http://localhost:8000/health/health",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1017.05,
+ "response_data": {
+ "status": "unhealthy",
+ "timestamp": "2025-06-07T12:27:19.698473",
+ "uptime_seconds": 0.0,
+ "components": {
+ "hardware": {
+ "name": "Hardware Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'HardwareService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:27:19.698473",
+ "uptime_seconds": null,
+ "metrics": null
+ },
+ "pose": {
+ "name": "Pose Service",
+ "status": "healthy",
+ "message": "Service is running normally",
+ "last_check": "2025-06-07T12:27:19.698473",
+ "uptime_seconds": 0.0,
+ "metrics": {
+ "total_processed": 5314,
+ "success_rate": 1.0,
+ "average_processing_time_ms": 0.8020579601053834
+ }
+ },
+ "stream": {
+ "name": "Stream Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'StreamService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:27:19.698473",
+ "uptime_seconds": null,
+ "metrics": null
+ }
+ },
+ "system_metrics": {
+ "cpu": {
+ "percent": 39.4,
+ "count": 2
+ },
+ "memory": {
+ "total_gb": 7.75,
+ "available_gb": 2.98,
+ "used_gb": 4.41,
+ "percent": 61.6
+ },
+ "disk": {
+ "total_gb": 31.33,
+ "free_gb": 7.99,
+ "used_gb": 21.72,
+ "percent": 69.34
+ },
+ "network": {
+ "bytes_sent": 3572468408,
+ "bytes_recv": 36997029117,
+ "packets_sent": 1132219,
+ "packets_recv": 25723413
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:27:20.703081"
+ },
+ {
+ "test_name": "GET /health/ready",
+ "description": "Readiness check",
+ "url": "http://localhost:8000/health/ready",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 2.5,
+ "response_data": {
+ "ready": false,
+ "timestamp": "2025-06-07T12:27:20.705155",
+ "checks": {},
+ "message": "Readiness check failed: 'HardwareService' object has no attribute 'is_ready'"
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:27:20.705747"
+ },
+ {
+ "test_name": "POST /pose/estimate",
+ "description": "Basic pose estimation",
+ "url": "http://localhost:8000/pose/estimate",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.99,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.706878"
+ },
+ {
+ "test_name": "POST /pose/estimate",
+ "description": "Pose estimation with parameters",
+ "url": "http://localhost:8000/pose/estimate",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.89,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.708132"
+ },
+ {
+ "test_name": "POST /pose/analyze",
+ "description": "Pose analysis",
+ "url": "http://localhost:8000/pose/analyze",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.91,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/analyze"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.709165"
+ },
+ {
+ "test_name": "GET /pose/zones/zone_1/occupancy",
+ "description": "Zone occupancy",
+ "url": "http://localhost:8000/pose/zones/zone_1/occupancy",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.78,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/zones/zone_1/occupancy"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.710222"
+ },
+ {
+ "test_name": "GET /pose/zones/summary",
+ "description": "All zones summary",
+ "url": "http://localhost:8000/pose/zones/summary",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.84,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/zones/summary"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.711403"
+ },
+ {
+ "test_name": "GET /pose/historical",
+ "description": "Historical pose data",
+ "url": "http://localhost:8000/pose/historical",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.83,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/historical"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.712646"
+ },
+ {
+ "test_name": "GET /pose/activities/recent",
+ "description": "Recent activities",
+ "url": "http://localhost:8000/pose/activities/recent",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.9,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/activities/recent"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.713667"
+ },
+ {
+ "test_name": "GET /calibration/status",
+ "description": "Calibration status",
+ "url": "http://localhost:8000/calibration/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.76,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/calibration/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.714763"
+ },
+ {
+ "test_name": "POST /calibration/start",
+ "description": "Start calibration",
+ "url": "http://localhost:8000/calibration/start",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.87,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/calibration/start"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.715985"
+ },
+ {
+ "test_name": "GET /statistics",
+ "description": "System statistics",
+ "url": "http://localhost:8000/statistics",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.93,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/statistics"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.717073"
+ },
+ {
+ "test_name": "GET /hardware/status",
+ "description": "Hardware status",
+ "url": "http://localhost:8000/hardware/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.79,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/hardware/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.718166"
+ },
+ {
+ "test_name": "GET /hardware/routers",
+ "description": "Router information",
+ "url": "http://localhost:8000/hardware/routers",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.84,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/hardware/routers"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.719352"
+ },
+ {
+ "test_name": "GET /hardware/routers/main_router",
+ "description": "Specific router info",
+ "url": "http://localhost:8000/hardware/routers/main_router",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.79,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/hardware/routers/main_router"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.720486"
+ },
+ {
+ "test_name": "GET /stream/status",
+ "description": "Stream status",
+ "url": "http://localhost:8000/stream/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.87,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/stream/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.721535"
+ },
+ {
+ "test_name": "POST /stream/start",
+ "description": "Start streaming",
+ "url": "http://localhost:8000/stream/start",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.78,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/stream/start"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.722650"
+ },
+ {
+ "test_name": "POST /stream/stop",
+ "description": "Stop streaming",
+ "url": "http://localhost:8000/stream/stop",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.98,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/stream/stop"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.723750"
+ },
+ {
+ "test_name": "WebSocket /ws/pose",
+ "description": "Pose data WebSocket",
+ "url": "ws://localhost:8000/ws/pose",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url, timeout=5) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 541, in __await_impl__\n self.connection = await self.create_connection()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 467, in create_connection\n _, connection = await loop.create_connection(factory, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nTypeError: BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'\n",
+ "timestamp": "2025-06-07T12:27:20.747316"
+ },
+ {
+ "test_name": "WebSocket /ws/hardware",
+ "description": "Hardware status WebSocket",
+ "url": "ws://localhost:8000/ws/hardware",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url, timeout=5) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 541, in __await_impl__\n self.connection = await self.create_connection()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 467, in create_connection\n _, connection = await loop.create_connection(factory, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nTypeError: BaseEventLoop.create_connection() got an unexpected keyword argument 'timeout'\n",
+ "timestamp": "2025-06-07T12:27:20.748123"
+ },
+ {
+ "test_name": "GET /docs",
+ "description": "API documentation",
+ "url": "http://localhost:8000/docs",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.71,
+ "response_data": {
+ "raw_response": "\n \n \n
\n \n \n WiFi-DensePose API - Swagger UI\n \n \n \n
\n \n \n \n \n \n "
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:27:20.749960"
+ },
+ {
+ "test_name": "GET /openapi.json",
+ "description": "OpenAPI schema",
+ "url": "http://localhost:8000/openapi.json",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.18,
+ "response_data": {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "WiFi-DensePose API",
+ "description": "WiFi-based human pose estimation and activity recognition API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/health/health": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Health Check",
+ "description": "Comprehensive system health check.",
+ "operationId": "health_check_health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SystemHealth"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/ready": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Readiness Check",
+ "description": "Check if system is ready to serve requests.",
+ "operationId": "readiness_check_health_ready_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReadinessCheck"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/live": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Liveness Check",
+ "description": "Simple liveness check for load balancers.",
+ "operationId": "liveness_check_health_live_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/metrics": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get System Metrics",
+ "description": "Get detailed system metrics.",
+ "operationId": "get_system_metrics_health_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/health/version": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get Version Info",
+ "description": "Get application version information.",
+ "operationId": "get_version_info_health_version_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/current": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Current Pose Estimation",
+ "description": "Get current pose estimation from WiFi signals.",
+ "operationId": "get_current_pose_estimation_api_v1_pose_current_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "confidence_threshold",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold"
+ }
+ },
+ {
+ "name": "max_persons",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50,
+ "minimum": 1
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons"
+ }
+ },
+ {
+ "name": "include_keypoints",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": true,
+ "title": "Include Keypoints"
+ }
+ },
+ {
+ "name": "include_segmentation",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": false,
+ "title": "Include Segmentation"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/analyze": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Analyze Pose Data",
+ "description": "Trigger pose analysis with custom parameters.",
+ "operationId": "analyze_pose_data_api_v1_pose_analyze_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/zones/{zone_id}/occupancy": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zone Occupancy",
+ "description": "Get current occupancy for a specific zone.",
+ "operationId": "get_zone_occupancy_api_v1_pose_zones__zone_id__occupancy_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Zone Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/zones/summary": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zones Summary",
+ "description": "Get occupancy summary for all zones.",
+ "operationId": "get_zones_summary_api_v1_pose_zones_summary_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/historical": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Historical Data",
+ "description": "Get historical pose estimation data.",
+ "operationId": "get_historical_data_api_v1_pose_historical_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HistoricalDataRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/activities": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Detected Activities",
+ "description": "Get recently detected activities.",
+ "operationId": "get_detected_activities_api_v1_pose_activities_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by zone ID",
+ "title": "Zone Id"
+ },
+ "description": "Filter by zone ID"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 100,
+ "minimum": 1,
+ "description": "Maximum number of activities",
+ "default": 10,
+ "title": "Limit"
+ },
+ "description": "Maximum number of activities"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/calibrate": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Calibrate Pose System",
+ "description": "Calibrate the pose estimation system.",
+ "operationId": "calibrate_pose_system_api_v1_pose_calibrate_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/calibration/status": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Calibration Status",
+ "description": "Get current calibration status.",
+ "operationId": "get_calibration_status_api_v1_pose_calibration_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/stats": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Pose Statistics",
+ "description": "Get pose estimation statistics.",
+ "operationId": "get_pose_statistics_api_v1_pose_stats_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "hours",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 168,
+ "minimum": 1,
+ "description": "Hours of data to analyze",
+ "default": 24,
+ "title": "Hours"
+ },
+ "description": "Hours of data to analyze"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/status": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Stream Status",
+ "description": "Get current streaming status.",
+ "operationId": "get_stream_status_api_v1_stream_status_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StreamStatus"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/start": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Start Streaming",
+ "description": "Start the streaming service.",
+ "operationId": "start_streaming_api_v1_stream_start_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/stop": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Stop Streaming",
+ "description": "Stop the streaming service.",
+ "operationId": "stop_streaming_api_v1_stream_stop_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Connected Clients",
+ "description": "Get list of connected WebSocket clients.",
+ "operationId": "get_connected_clients_api_v1_stream_clients_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients/{client_id}": {
+ "delete": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Disconnect Client",
+ "description": "Disconnect a specific WebSocket client.",
+ "operationId": "disconnect_client_api_v1_stream_clients__client_id__delete",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "client_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Client Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/broadcast": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Broadcast Message",
+ "description": "Broadcast a message to connected WebSocket clients.",
+ "operationId": "broadcast_message_api_v1_stream_broadcast_post",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "stream_type",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target stream type",
+ "title": "Stream Type"
+ },
+ "description": "Target stream type"
+ },
+ {
+ "name": "zone_ids",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target zone IDs",
+ "title": "Zone Ids"
+ },
+ "description": "Target zone IDs"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true,
+ "title": "Message"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/metrics": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Streaming Metrics",
+ "description": "Get streaming performance metrics.",
+ "operationId": "get_streaming_metrics_api_v1_stream_metrics_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/": {
+ "get": {
+ "summary": "Root",
+ "description": "Root endpoint with API information.",
+ "operationId": "root__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/info": {
+ "get": {
+ "summary": "Api Info",
+ "description": "Get detailed API information.",
+ "operationId": "api_info_api_v1_info_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/status": {
+ "get": {
+ "summary": "Api Status",
+ "description": "Get current API status.",
+ "operationId": "api_status_api_v1_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/metrics": {
+ "get": {
+ "summary": "Api Metrics",
+ "description": "Get API metrics.",
+ "operationId": "api_metrics_api_v1_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/config": {
+ "get": {
+ "summary": "Dev Config",
+ "description": "Get current configuration (development only).",
+ "operationId": "dev_config_api_v1_dev_config_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/reset": {
+ "post": {
+ "summary": "Dev Reset",
+ "description": "Reset services (development only).",
+ "operationId": "dev_reset_api_v1_dev_reset_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ComponentHealth": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Component name"
+ },
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Health status (healthy, degraded, unhealthy)"
+ },
+ "message": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Message",
+ "description": "Status message"
+ },
+ "last_check": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Last Check",
+ "description": "Last health check timestamp"
+ },
+ "uptime_seconds": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Uptime Seconds",
+ "description": "Component uptime"
+ },
+ "metrics": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Metrics",
+ "description": "Component metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "status",
+ "last_check"
+ ],
+ "title": "ComponentHealth",
+ "description": "Health status for a system component."
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "HistoricalDataRequest": {
+ "properties": {
+ "start_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Start Time",
+ "description": "Start time for data query"
+ },
+ "end_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "End Time",
+ "description": "End time for data query"
+ },
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Filter by specific zones"
+ },
+ "aggregation_interval": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 3600.0,
+ "minimum": 60.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Aggregation Interval",
+ "description": "Aggregation interval in seconds",
+ "default": 300
+ },
+ "include_raw_data": {
+ "type": "boolean",
+ "title": "Include Raw Data",
+ "description": "Include raw detection data",
+ "default": false
+ }
+ },
+ "type": "object",
+ "required": [
+ "start_time",
+ "end_time"
+ ],
+ "title": "HistoricalDataRequest",
+ "description": "Request model for historical pose data."
+ },
+ "PersonPose": {
+ "properties": {
+ "person_id": {
+ "type": "string",
+ "title": "Person Id",
+ "description": "Unique person identifier"
+ },
+ "confidence": {
+ "type": "number",
+ "title": "Confidence",
+ "description": "Detection confidence score"
+ },
+ "bounding_box": {
+ "additionalProperties": {
+ "type": "number"
+ },
+ "type": "object",
+ "title": "Bounding Box",
+ "description": "Person bounding box"
+ },
+ "keypoints": {
+ "anyOf": [
+ {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Keypoints",
+ "description": "Body keypoints with coordinates and confidence"
+ },
+ "segmentation": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Segmentation",
+ "description": "DensePose segmentation data"
+ },
+ "zone_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Id",
+ "description": "Zone where person is detected"
+ },
+ "activity": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Activity",
+ "description": "Detected activity"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Detection timestamp"
+ }
+ },
+ "type": "object",
+ "required": [
+ "person_id",
+ "confidence",
+ "bounding_box",
+ "timestamp"
+ ],
+ "title": "PersonPose",
+ "description": "Person pose data model."
+ },
+ "PoseEstimationRequest": {
+ "properties": {
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Specific zones to analyze (all zones if not specified)"
+ },
+ "confidence_threshold": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold",
+ "description": "Minimum confidence threshold for detections"
+ },
+ "max_persons": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50.0,
+ "minimum": 1.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons",
+ "description": "Maximum number of persons to detect"
+ },
+ "include_keypoints": {
+ "type": "boolean",
+ "title": "Include Keypoints",
+ "description": "Include detailed keypoint data",
+ "default": true
+ },
+ "include_segmentation": {
+ "type": "boolean",
+ "title": "Include Segmentation",
+ "description": "Include DensePose segmentation masks",
+ "default": false
+ }
+ },
+ "type": "object",
+ "title": "PoseEstimationRequest",
+ "description": "Request model for pose estimation."
+ },
+ "PoseEstimationResponse": {
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Analysis timestamp"
+ },
+ "frame_id": {
+ "type": "string",
+ "title": "Frame Id",
+ "description": "Unique frame identifier"
+ },
+ "persons": {
+ "items": {
+ "$ref": "#/components/schemas/PersonPose"
+ },
+ "type": "array",
+ "title": "Persons",
+ "description": "Detected persons"
+ },
+ "zone_summary": {
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "type": "object",
+ "title": "Zone Summary",
+ "description": "Person count per zone"
+ },
+ "processing_time_ms": {
+ "type": "number",
+ "title": "Processing Time Ms",
+ "description": "Processing time in milliseconds"
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Metadata",
+ "description": "Additional metadata"
+ }
+ },
+ "type": "object",
+ "required": [
+ "timestamp",
+ "frame_id",
+ "persons",
+ "zone_summary",
+ "processing_time_ms"
+ ],
+ "title": "PoseEstimationResponse",
+ "description": "Response model for pose estimation."
+ },
+ "ReadinessCheck": {
+ "properties": {
+ "ready": {
+ "type": "boolean",
+ "title": "Ready",
+ "description": "Whether system is ready to serve requests"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Readiness check timestamp"
+ },
+ "checks": {
+ "additionalProperties": {
+ "type": "boolean"
+ },
+ "type": "object",
+ "title": "Checks",
+ "description": "Individual readiness checks"
+ },
+ "message": {
+ "type": "string",
+ "title": "Message",
+ "description": "Readiness status message"
+ }
+ },
+ "type": "object",
+ "required": [
+ "ready",
+ "timestamp",
+ "checks",
+ "message"
+ ],
+ "title": "ReadinessCheck",
+ "description": "System readiness check result."
+ },
+ "StreamStatus": {
+ "properties": {
+ "is_active": {
+ "type": "boolean",
+ "title": "Is Active",
+ "description": "Whether streaming is active"
+ },
+ "connected_clients": {
+ "type": "integer",
+ "title": "Connected Clients",
+ "description": "Number of connected clients"
+ },
+ "streams": {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array",
+ "title": "Streams",
+ "description": "Active streams"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "Stream uptime in seconds"
+ }
+ },
+ "type": "object",
+ "required": [
+ "is_active",
+ "connected_clients",
+ "streams",
+ "uptime_seconds"
+ ],
+ "title": "StreamStatus",
+ "description": "Stream status model."
+ },
+ "SystemHealth": {
+ "properties": {
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Overall system status"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Health check timestamp"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "System uptime"
+ },
+ "components": {
+ "additionalProperties": {
+ "$ref": "#/components/schemas/ComponentHealth"
+ },
+ "type": "object",
+ "title": "Components",
+ "description": "Component health status"
+ },
+ "system_metrics": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "System Metrics",
+ "description": "System-level metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "status",
+ "timestamp",
+ "uptime_seconds",
+ "components",
+ "system_metrics"
+ ],
+ "title": "SystemHealth",
+ "description": "Overall system health status."
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ },
+ "securitySchemes": {
+ "HTTPBearer": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:27:20.751744"
+ },
+ {
+ "test_name": "GET /nonexistent",
+ "description": "Non-existent endpoint",
+ "url": "http://localhost:8000/nonexistent",
+ "method": "GET",
+ "expected_status": 404,
+ "actual_status": 404,
+ "response_time_ms": 0.75,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/nonexistent"
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:27:20.752801"
+ },
+ {
+ "test_name": "POST /pose/estimate",
+ "description": "Invalid request data",
+ "url": "http://localhost:8000/pose/estimate",
+ "method": "POST",
+ "expected_status": 422,
+ "actual_status": 404,
+ "response_time_ms": 0.89,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:27:20.753929"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/api_test_results_20250607_122856.json b/scripts/api_test_results_20250607_122856.json
new file mode 100644
index 0000000..90b0651
--- /dev/null
+++ b/scripts/api_test_results_20250607_122856.json
@@ -0,0 +1,1991 @@
+{
+ "total_tests": 24,
+ "passed": 7,
+ "failed": 17,
+ "errors": [
+ "WebSocket /ws/pose - Exception: server rejected WebSocket connection: HTTP 403",
+ "WebSocket /ws/hardware - Exception: server rejected WebSocket connection: HTTP 403"
+ ],
+ "test_details": [
+ {
+ "test_name": "GET /health/health",
+ "description": "System health check",
+ "url": "http://localhost:8000/health/health",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1007.28,
+ "response_data": {
+ "status": "unhealthy",
+ "timestamp": "2025-06-07T12:28:55.762672",
+ "uptime_seconds": 0.0,
+ "components": {
+ "hardware": {
+ "name": "Hardware Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'HardwareService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:28:55.762672",
+ "uptime_seconds": null,
+ "metrics": null
+ },
+ "pose": {
+ "name": "Pose Service",
+ "status": "healthy",
+ "message": "Service is running normally",
+ "last_check": "2025-06-07T12:28:55.762672",
+ "uptime_seconds": 0.0,
+ "metrics": {
+ "total_processed": 7957,
+ "success_rate": 1.0,
+ "average_processing_time_ms": 0.7798933014955369
+ }
+ },
+ "stream": {
+ "name": "Stream Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'StreamService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:28:55.762672",
+ "uptime_seconds": null,
+ "metrics": null
+ }
+ },
+ "system_metrics": {
+ "cpu": {
+ "percent": 42.2,
+ "count": 2
+ },
+ "memory": {
+ "total_gb": 7.75,
+ "available_gb": 3.4,
+ "used_gb": 3.99,
+ "percent": 56.2
+ },
+ "disk": {
+ "total_gb": 31.33,
+ "free_gb": 7.99,
+ "used_gb": 21.72,
+ "percent": 69.34
+ },
+ "network": {
+ "bytes_sent": 3735289492,
+ "bytes_recv": 37107794581,
+ "packets_sent": 1163504,
+ "packets_recv": 25763938
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.764536"
+ },
+ {
+ "test_name": "GET /health/ready",
+ "description": "Readiness check",
+ "url": "http://localhost:8000/health/ready",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 2.54,
+ "response_data": {
+ "ready": false,
+ "timestamp": "2025-06-07T12:28:56.766715",
+ "checks": {},
+ "message": "Readiness check failed: 'HardwareService' object has no attribute 'is_ready'"
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.767265"
+ },
+ {
+ "test_name": "POST /api/v1/pose/estimate",
+ "description": "Basic pose estimation",
+ "url": "http://localhost:8000/api/v1/pose/estimate",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.97,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.768369"
+ },
+ {
+ "test_name": "POST /api/v1/pose/estimate",
+ "description": "Pose estimation with parameters",
+ "url": "http://localhost:8000/api/v1/pose/estimate",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.87,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.769596"
+ },
+ {
+ "test_name": "POST /api/v1/pose/analyze",
+ "description": "Pose analysis",
+ "url": "http://localhost:8000/api/v1/pose/analyze",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.1,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/analyze"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.771001"
+ },
+ {
+ "test_name": "GET /api/v1/pose/zones/zone_1/occupancy",
+ "description": "Zone occupancy",
+ "url": "http://localhost:8000/api/v1/pose/zones/zone_1/occupancy",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.35,
+ "response_data": {
+ "zone_id": "zone_1",
+ "current_occupancy": 1,
+ "max_occupancy": 10,
+ "persons": [
+ {
+ "person_id": "person_0",
+ "confidence": 0.9419077493534322,
+ "activity": "walking"
+ }
+ ],
+ "timestamp": "2025-06-07T12:28:56.771914"
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.772481"
+ },
+ {
+ "test_name": "GET /api/v1/pose/zones/summary",
+ "description": "All zones summary",
+ "url": "http://localhost:8000/api/v1/pose/zones/summary",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.21,
+ "response_data": {
+ "timestamp": "2025-06-07T12:28:56.773322",
+ "total_persons": 7,
+ "zones": {
+ "zone_1": {
+ "occupancy": 2,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_2": {
+ "occupancy": 1,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_3": {
+ "occupancy": 2,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_4": {
+ "occupancy": 2,
+ "max_occupancy": 10,
+ "status": "active"
+ }
+ },
+ "active_zones": 4
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.774037"
+ },
+ {
+ "test_name": "GET /api/v1/pose/historical",
+ "description": "Historical pose data",
+ "url": "http://localhost:8000/api/v1/pose/historical",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 405,
+ "response_time_ms": 0.91,
+ "response_data": {
+ "error": {
+ "code": 405,
+ "message": "Method Not Allowed",
+ "type": "http_error",
+ "path": "/api/v1/pose/historical"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.775097"
+ },
+ {
+ "test_name": "GET /api/v1/pose/activities/recent",
+ "description": "Recent activities",
+ "url": "http://localhost:8000/api/v1/pose/activities/recent",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.78,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/pose/activities/recent"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.776228"
+ },
+ {
+ "test_name": "GET /api/v1/calibration/status",
+ "description": "Calibration status",
+ "url": "http://localhost:8000/api/v1/calibration/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.9,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/calibration/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.777282"
+ },
+ {
+ "test_name": "POST /api/v1/calibration/start",
+ "description": "Start calibration",
+ "url": "http://localhost:8000/api/v1/calibration/start",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.81,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/calibration/start"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.778372"
+ },
+ {
+ "test_name": "GET /api/v1/statistics",
+ "description": "System statistics",
+ "url": "http://localhost:8000/api/v1/statistics",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.87,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/statistics"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.779653"
+ },
+ {
+ "test_name": "GET /api/v1/hardware/status",
+ "description": "Hardware status",
+ "url": "http://localhost:8000/api/v1/hardware/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.9,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/hardware/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.780706"
+ },
+ {
+ "test_name": "GET /api/v1/hardware/routers",
+ "description": "Router information",
+ "url": "http://localhost:8000/api/v1/hardware/routers",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.75,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/hardware/routers"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.781763"
+ },
+ {
+ "test_name": "GET /api/v1/hardware/routers/main_router",
+ "description": "Specific router info",
+ "url": "http://localhost:8000/api/v1/hardware/routers/main_router",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 404,
+ "response_time_ms": 0.83,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/hardware/routers/main_router"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.782949"
+ },
+ {
+ "test_name": "GET /api/v1/stream/status",
+ "description": "Stream status",
+ "url": "http://localhost:8000/api/v1/stream/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 500,
+ "response_time_ms": 1.2,
+ "response_data": {
+ "error": {
+ "code": 500,
+ "message": "Failed to get stream status: 'is_active'",
+ "type": "http_error",
+ "path": "/api/v1/stream/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.784463"
+ },
+ {
+ "test_name": "POST /api/v1/stream/start",
+ "description": "Start streaming",
+ "url": "http://localhost:8000/api/v1/stream/start",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.3,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/stream/start"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.785880"
+ },
+ {
+ "test_name": "POST /api/v1/stream/stop",
+ "description": "Stop streaming",
+ "url": "http://localhost:8000/api/v1/stream/stop",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.27,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/stream/stop"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.787501"
+ },
+ {
+ "test_name": "WebSocket /ws/pose",
+ "description": "Pose data WebSocket",
+ "url": "ws://localhost:8000/ws/pose",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "server rejected WebSocket connection: HTTP 403",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 543, in __await_impl__\n await self.connection.handshake(\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 114, in handshake\n raise self.protocol.handshake_exc\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 325, in parse\n self.process_response(response)\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 142, in process_response\n raise InvalidStatus(response)\nwebsockets.exceptions.InvalidStatus: server rejected WebSocket connection: HTTP 403\n",
+ "timestamp": "2025-06-07T12:28:56.810344"
+ },
+ {
+ "test_name": "WebSocket /ws/hardware",
+ "description": "Hardware status WebSocket",
+ "url": "ws://localhost:8000/ws/hardware",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "server rejected WebSocket connection: HTTP 403",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 543, in __await_impl__\n await self.connection.handshake(\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 114, in handshake\n raise self.protocol.handshake_exc\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 325, in parse\n self.process_response(response)\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 142, in process_response\n raise InvalidStatus(response)\nwebsockets.exceptions.InvalidStatus: server rejected WebSocket connection: HTTP 403\n",
+ "timestamp": "2025-06-07T12:28:56.813660"
+ },
+ {
+ "test_name": "GET /docs",
+ "description": "API documentation",
+ "url": "http://localhost:8000/docs",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 0.93,
+ "response_data": {
+ "raw_response": "\n \n \n \n \n \n WiFi-DensePose API - Swagger UI\n \n \n \n
\n \n \n \n \n \n "
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.814706"
+ },
+ {
+ "test_name": "GET /openapi.json",
+ "description": "OpenAPI schema",
+ "url": "http://localhost:8000/openapi.json",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.01,
+ "response_data": {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "WiFi-DensePose API",
+ "description": "WiFi-based human pose estimation and activity recognition API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/health/health": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Health Check",
+ "description": "Comprehensive system health check.",
+ "operationId": "health_check_health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SystemHealth"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/ready": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Readiness Check",
+ "description": "Check if system is ready to serve requests.",
+ "operationId": "readiness_check_health_ready_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReadinessCheck"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/live": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Liveness Check",
+ "description": "Simple liveness check for load balancers.",
+ "operationId": "liveness_check_health_live_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/metrics": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get System Metrics",
+ "description": "Get detailed system metrics.",
+ "operationId": "get_system_metrics_health_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/health/version": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get Version Info",
+ "description": "Get application version information.",
+ "operationId": "get_version_info_health_version_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/current": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Current Pose Estimation",
+ "description": "Get current pose estimation from WiFi signals.",
+ "operationId": "get_current_pose_estimation_api_v1_pose_current_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "confidence_threshold",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold"
+ }
+ },
+ {
+ "name": "max_persons",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50,
+ "minimum": 1
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons"
+ }
+ },
+ {
+ "name": "include_keypoints",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": true,
+ "title": "Include Keypoints"
+ }
+ },
+ {
+ "name": "include_segmentation",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": false,
+ "title": "Include Segmentation"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/analyze": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Analyze Pose Data",
+ "description": "Trigger pose analysis with custom parameters.",
+ "operationId": "analyze_pose_data_api_v1_pose_analyze_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/zones/{zone_id}/occupancy": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zone Occupancy",
+ "description": "Get current occupancy for a specific zone.",
+ "operationId": "get_zone_occupancy_api_v1_pose_zones__zone_id__occupancy_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Zone Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/zones/summary": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zones Summary",
+ "description": "Get occupancy summary for all zones.",
+ "operationId": "get_zones_summary_api_v1_pose_zones_summary_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/historical": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Historical Data",
+ "description": "Get historical pose estimation data.",
+ "operationId": "get_historical_data_api_v1_pose_historical_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HistoricalDataRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/activities": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Detected Activities",
+ "description": "Get recently detected activities.",
+ "operationId": "get_detected_activities_api_v1_pose_activities_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by zone ID",
+ "title": "Zone Id"
+ },
+ "description": "Filter by zone ID"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 100,
+ "minimum": 1,
+ "description": "Maximum number of activities",
+ "default": 10,
+ "title": "Limit"
+ },
+ "description": "Maximum number of activities"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/calibrate": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Calibrate Pose System",
+ "description": "Calibrate the pose estimation system.",
+ "operationId": "calibrate_pose_system_api_v1_pose_calibrate_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/calibration/status": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Calibration Status",
+ "description": "Get current calibration status.",
+ "operationId": "get_calibration_status_api_v1_pose_calibration_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/stats": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Pose Statistics",
+ "description": "Get pose estimation statistics.",
+ "operationId": "get_pose_statistics_api_v1_pose_stats_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "hours",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 168,
+ "minimum": 1,
+ "description": "Hours of data to analyze",
+ "default": 24,
+ "title": "Hours"
+ },
+ "description": "Hours of data to analyze"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/status": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Stream Status",
+ "description": "Get current streaming status.",
+ "operationId": "get_stream_status_api_v1_stream_status_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StreamStatus"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/start": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Start Streaming",
+ "description": "Start the streaming service.",
+ "operationId": "start_streaming_api_v1_stream_start_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/stop": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Stop Streaming",
+ "description": "Stop the streaming service.",
+ "operationId": "stop_streaming_api_v1_stream_stop_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Connected Clients",
+ "description": "Get list of connected WebSocket clients.",
+ "operationId": "get_connected_clients_api_v1_stream_clients_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients/{client_id}": {
+ "delete": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Disconnect Client",
+ "description": "Disconnect a specific WebSocket client.",
+ "operationId": "disconnect_client_api_v1_stream_clients__client_id__delete",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "client_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Client Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/broadcast": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Broadcast Message",
+ "description": "Broadcast a message to connected WebSocket clients.",
+ "operationId": "broadcast_message_api_v1_stream_broadcast_post",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "stream_type",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target stream type",
+ "title": "Stream Type"
+ },
+ "description": "Target stream type"
+ },
+ {
+ "name": "zone_ids",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target zone IDs",
+ "title": "Zone Ids"
+ },
+ "description": "Target zone IDs"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true,
+ "title": "Message"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/metrics": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Streaming Metrics",
+ "description": "Get streaming performance metrics.",
+ "operationId": "get_streaming_metrics_api_v1_stream_metrics_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/": {
+ "get": {
+ "summary": "Root",
+ "description": "Root endpoint with API information.",
+ "operationId": "root__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/info": {
+ "get": {
+ "summary": "Api Info",
+ "description": "Get detailed API information.",
+ "operationId": "api_info_api_v1_info_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/status": {
+ "get": {
+ "summary": "Api Status",
+ "description": "Get current API status.",
+ "operationId": "api_status_api_v1_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/metrics": {
+ "get": {
+ "summary": "Api Metrics",
+ "description": "Get API metrics.",
+ "operationId": "api_metrics_api_v1_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/config": {
+ "get": {
+ "summary": "Dev Config",
+ "description": "Get current configuration (development only).",
+ "operationId": "dev_config_api_v1_dev_config_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/reset": {
+ "post": {
+ "summary": "Dev Reset",
+ "description": "Reset services (development only).",
+ "operationId": "dev_reset_api_v1_dev_reset_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ComponentHealth": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Component name"
+ },
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Health status (healthy, degraded, unhealthy)"
+ },
+ "message": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Message",
+ "description": "Status message"
+ },
+ "last_check": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Last Check",
+ "description": "Last health check timestamp"
+ },
+ "uptime_seconds": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Uptime Seconds",
+ "description": "Component uptime"
+ },
+ "metrics": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Metrics",
+ "description": "Component metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "status",
+ "last_check"
+ ],
+ "title": "ComponentHealth",
+ "description": "Health status for a system component."
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "HistoricalDataRequest": {
+ "properties": {
+ "start_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Start Time",
+ "description": "Start time for data query"
+ },
+ "end_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "End Time",
+ "description": "End time for data query"
+ },
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Filter by specific zones"
+ },
+ "aggregation_interval": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 3600.0,
+ "minimum": 60.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Aggregation Interval",
+ "description": "Aggregation interval in seconds",
+ "default": 300
+ },
+ "include_raw_data": {
+ "type": "boolean",
+ "title": "Include Raw Data",
+ "description": "Include raw detection data",
+ "default": false
+ }
+ },
+ "type": "object",
+ "required": [
+ "start_time",
+ "end_time"
+ ],
+ "title": "HistoricalDataRequest",
+ "description": "Request model for historical pose data."
+ },
+ "PersonPose": {
+ "properties": {
+ "person_id": {
+ "type": "string",
+ "title": "Person Id",
+ "description": "Unique person identifier"
+ },
+ "confidence": {
+ "type": "number",
+ "title": "Confidence",
+ "description": "Detection confidence score"
+ },
+ "bounding_box": {
+ "additionalProperties": {
+ "type": "number"
+ },
+ "type": "object",
+ "title": "Bounding Box",
+ "description": "Person bounding box"
+ },
+ "keypoints": {
+ "anyOf": [
+ {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Keypoints",
+ "description": "Body keypoints with coordinates and confidence"
+ },
+ "segmentation": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Segmentation",
+ "description": "DensePose segmentation data"
+ },
+ "zone_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Id",
+ "description": "Zone where person is detected"
+ },
+ "activity": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Activity",
+ "description": "Detected activity"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Detection timestamp"
+ }
+ },
+ "type": "object",
+ "required": [
+ "person_id",
+ "confidence",
+ "bounding_box",
+ "timestamp"
+ ],
+ "title": "PersonPose",
+ "description": "Person pose data model."
+ },
+ "PoseEstimationRequest": {
+ "properties": {
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Specific zones to analyze (all zones if not specified)"
+ },
+ "confidence_threshold": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold",
+ "description": "Minimum confidence threshold for detections"
+ },
+ "max_persons": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50.0,
+ "minimum": 1.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons",
+ "description": "Maximum number of persons to detect"
+ },
+ "include_keypoints": {
+ "type": "boolean",
+ "title": "Include Keypoints",
+ "description": "Include detailed keypoint data",
+ "default": true
+ },
+ "include_segmentation": {
+ "type": "boolean",
+ "title": "Include Segmentation",
+ "description": "Include DensePose segmentation masks",
+ "default": false
+ }
+ },
+ "type": "object",
+ "title": "PoseEstimationRequest",
+ "description": "Request model for pose estimation."
+ },
+ "PoseEstimationResponse": {
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Analysis timestamp"
+ },
+ "frame_id": {
+ "type": "string",
+ "title": "Frame Id",
+ "description": "Unique frame identifier"
+ },
+ "persons": {
+ "items": {
+ "$ref": "#/components/schemas/PersonPose"
+ },
+ "type": "array",
+ "title": "Persons",
+ "description": "Detected persons"
+ },
+ "zone_summary": {
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "type": "object",
+ "title": "Zone Summary",
+ "description": "Person count per zone"
+ },
+ "processing_time_ms": {
+ "type": "number",
+ "title": "Processing Time Ms",
+ "description": "Processing time in milliseconds"
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Metadata",
+ "description": "Additional metadata"
+ }
+ },
+ "type": "object",
+ "required": [
+ "timestamp",
+ "frame_id",
+ "persons",
+ "zone_summary",
+ "processing_time_ms"
+ ],
+ "title": "PoseEstimationResponse",
+ "description": "Response model for pose estimation."
+ },
+ "ReadinessCheck": {
+ "properties": {
+ "ready": {
+ "type": "boolean",
+ "title": "Ready",
+ "description": "Whether system is ready to serve requests"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Readiness check timestamp"
+ },
+ "checks": {
+ "additionalProperties": {
+ "type": "boolean"
+ },
+ "type": "object",
+ "title": "Checks",
+ "description": "Individual readiness checks"
+ },
+ "message": {
+ "type": "string",
+ "title": "Message",
+ "description": "Readiness status message"
+ }
+ },
+ "type": "object",
+ "required": [
+ "ready",
+ "timestamp",
+ "checks",
+ "message"
+ ],
+ "title": "ReadinessCheck",
+ "description": "System readiness check result."
+ },
+ "StreamStatus": {
+ "properties": {
+ "is_active": {
+ "type": "boolean",
+ "title": "Is Active",
+ "description": "Whether streaming is active"
+ },
+ "connected_clients": {
+ "type": "integer",
+ "title": "Connected Clients",
+ "description": "Number of connected clients"
+ },
+ "streams": {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array",
+ "title": "Streams",
+ "description": "Active streams"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "Stream uptime in seconds"
+ }
+ },
+ "type": "object",
+ "required": [
+ "is_active",
+ "connected_clients",
+ "streams",
+ "uptime_seconds"
+ ],
+ "title": "StreamStatus",
+ "description": "Stream status model."
+ },
+ "SystemHealth": {
+ "properties": {
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Overall system status"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Health check timestamp"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "System uptime"
+ },
+ "components": {
+ "additionalProperties": {
+ "$ref": "#/components/schemas/ComponentHealth"
+ },
+ "type": "object",
+ "title": "Components",
+ "description": "Component health status"
+ },
+ "system_metrics": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "System Metrics",
+ "description": "System-level metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "status",
+ "timestamp",
+ "uptime_seconds",
+ "components",
+ "system_metrics"
+ ],
+ "title": "SystemHealth",
+ "description": "Overall system health status."
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ },
+ "securitySchemes": {
+ "HTTPBearer": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.816300"
+ },
+ {
+ "test_name": "GET /nonexistent",
+ "description": "Non-existent endpoint",
+ "url": "http://localhost:8000/nonexistent",
+ "method": "GET",
+ "expected_status": 404,
+ "actual_status": 404,
+ "response_time_ms": 0.87,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/nonexistent"
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:28:56.817299"
+ },
+ {
+ "test_name": "POST /api/v1/pose/estimate",
+ "description": "Invalid request data",
+ "url": "http://localhost:8000/api/v1/pose/estimate",
+ "method": "POST",
+ "expected_status": 422,
+ "actual_status": 404,
+ "response_time_ms": 0.9,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/api/v1/pose/estimate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:28:56.818290"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/api_test_results_20250607_123111.json b/scripts/api_test_results_20250607_123111.json
new file mode 100644
index 0000000..5758131
--- /dev/null
+++ b/scripts/api_test_results_20250607_123111.json
@@ -0,0 +1,2961 @@
+{
+ "total_tests": 26,
+ "passed": 15,
+ "failed": 11,
+ "errors": [
+ "WebSocket /ws/pose - Exception: server rejected WebSocket connection: HTTP 403",
+ "WebSocket /ws/hardware - Exception: server rejected WebSocket connection: HTTP 403"
+ ],
+ "test_details": [
+ {
+ "test_name": "GET /health/health",
+ "description": "System health check",
+ "url": "http://localhost:8000/health/health",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1007.09,
+ "response_data": {
+ "status": "unhealthy",
+ "timestamp": "2025-06-07T12:31:10.227495",
+ "uptime_seconds": 0.0,
+ "components": {
+ "hardware": {
+ "name": "Hardware Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'HardwareService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:31:10.227495",
+ "uptime_seconds": null,
+ "metrics": null
+ },
+ "pose": {
+ "name": "Pose Service",
+ "status": "healthy",
+ "message": "Service is running normally",
+ "last_check": "2025-06-07T12:31:10.227495",
+ "uptime_seconds": 0.0,
+ "metrics": {
+ "total_processed": 11593,
+ "success_rate": 1.0,
+ "average_processing_time_ms": 0.7697883205382553
+ }
+ },
+ "stream": {
+ "name": "Stream Service",
+ "status": "unhealthy",
+ "message": "Health check failed: 'StreamService' object has no attribute 'health_check'",
+ "last_check": "2025-06-07T12:31:10.227495",
+ "uptime_seconds": null,
+ "metrics": null
+ }
+ },
+ "system_metrics": {
+ "cpu": {
+ "percent": 41.6,
+ "count": 2
+ },
+ "memory": {
+ "total_gb": 7.75,
+ "available_gb": 3.42,
+ "used_gb": 3.96,
+ "percent": 55.8
+ },
+ "disk": {
+ "total_gb": 31.33,
+ "free_gb": 7.99,
+ "used_gb": 21.72,
+ "percent": 69.34
+ },
+ "network": {
+ "bytes_sent": 3966865385,
+ "bytes_recv": 37266864859,
+ "packets_sent": 1208137,
+ "packets_recv": 25822484
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.229468"
+ },
+ {
+ "test_name": "GET /health/ready",
+ "description": "Readiness check",
+ "url": "http://localhost:8000/health/ready",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 2.8,
+ "response_data": {
+ "ready": false,
+ "timestamp": "2025-06-07T12:31:11.231718",
+ "checks": {},
+ "message": "Readiness check failed: 'HardwareService' object has no attribute 'is_ready'"
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.232458"
+ },
+ {
+ "test_name": "GET /api/v1/pose/current",
+ "description": "Current pose estimation",
+ "url": "http://localhost:8000/api/v1/pose/current",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 4.78,
+ "response_data": {
+ "timestamp": "2025-06-07T12:31:11.236304",
+ "frame_id": "frame_1749299471",
+ "persons": [
+ {
+ "person_id": "0",
+ "confidence": 0.7293420300231561,
+ "bounding_box": {
+ "x": 0.20030137087111707,
+ "y": 0.34797650745590625,
+ "width": 0.3981145986577058,
+ "height": 0.37970677378320483
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.44823595638505365,
+ "y": 0.28977208775102437,
+ "confidence": 0.5542371581192655
+ },
+ {
+ "name": "left_eye",
+ "x": 0.4952690740858373,
+ "y": 0.5432792007858821,
+ "confidence": 0.8389099658520509
+ },
+ {
+ "name": "right_eye",
+ "x": 0.5804648567981479,
+ "y": 0.4647719962179129,
+ "confidence": 0.8566402128570042
+ },
+ {
+ "name": "left_ear",
+ "x": 0.3908451092263244,
+ "y": 0.47632227436288144,
+ "confidence": 0.8663735676067241
+ },
+ {
+ "name": "right_ear",
+ "x": 0.7239991852126997,
+ "y": 0.23663541301872126,
+ "confidence": 0.8765906890203725
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.8047991402971765,
+ "y": 0.5945560513865605,
+ "confidence": 0.6733604589224793
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.5827385517549469,
+ "y": 0.6480707247526286,
+ "confidence": 0.6619337464371322
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.22838735429686896,
+ "y": 0.5384875625869312,
+ "confidence": 0.8898981616721842
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.30698440179370057,
+ "y": 0.7681933243920521,
+ "confidence": 0.8650395359923496
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.2513618929990984,
+ "y": 0.7888295208071133,
+ "confidence": 0.7868846288735598
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.7451812973100521,
+ "y": 0.8656266393186364,
+ "confidence": 0.6986352734789892
+ },
+ {
+ "name": "left_hip",
+ "x": 0.8711882610447488,
+ "y": 0.21107445509375788,
+ "confidence": 0.7641797518172958
+ },
+ {
+ "name": "right_hip",
+ "x": 0.6886993071914757,
+ "y": 0.8831958965641219,
+ "confidence": 0.607316198276865
+ },
+ {
+ "name": "left_knee",
+ "x": 0.8309229095457401,
+ "y": 0.6179393131368978,
+ "confidence": 0.9484639425058705
+ },
+ {
+ "name": "right_knee",
+ "x": 0.41084910063004776,
+ "y": 0.871048879535313,
+ "confidence": 0.5869033936285174
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.868380526885448,
+ "y": 0.29994798097554654,
+ "confidence": 0.8711292170158544
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.23919791092843745,
+ "y": 0.7835125578080285,
+ "confidence": 0.9296263841499632
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "standing",
+ "timestamp": "2025-06-07T12:31:11.236213"
+ },
+ {
+ "person_id": "1",
+ "confidence": 0.8305294582546161,
+ "bounding_box": {
+ "x": 0.10170118349182375,
+ "y": 0.509137090786002,
+ "width": 0.3259154094555018,
+ "height": 0.4358340805173928
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.7996096118501625,
+ "y": 0.4189010321170784,
+ "confidence": 0.7875794312662672
+ },
+ {
+ "name": "left_eye",
+ "x": 0.5403213329708182,
+ "y": 0.13855592879699596,
+ "confidence": 0.8437237511382353
+ },
+ {
+ "name": "right_eye",
+ "x": 0.2505573867103854,
+ "y": 0.5510193548451794,
+ "confidence": 0.5797273597406184
+ },
+ {
+ "name": "left_ear",
+ "x": 0.29465523309165254,
+ "y": 0.5004435349476023,
+ "confidence": 0.5256923965994024
+ },
+ {
+ "name": "right_ear",
+ "x": 0.10508828814487554,
+ "y": 0.2184534539190664,
+ "confidence": 0.8756781626026862
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.8377841792777977,
+ "y": 0.18844840254336265,
+ "confidence": 0.7698670827453382
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.6564289264434737,
+ "y": 0.2950417676475364,
+ "confidence": 0.5628219479670884
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.8616201746562163,
+ "y": 0.32561299054520615,
+ "confidence": 0.5902388830139175
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.11771705352091671,
+ "y": 0.39582637396144527,
+ "confidence": 0.6664287966202559
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.36669984890698537,
+ "y": 0.32526726218772384,
+ "confidence": 0.7301083696222967
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.3744711338414852,
+ "y": 0.8933040570358391,
+ "confidence": 0.5122297141321303
+ },
+ {
+ "name": "left_hip",
+ "x": 0.7985778946077506,
+ "y": 0.5687873058337637,
+ "confidence": 0.6074860985303865
+ },
+ {
+ "name": "right_hip",
+ "x": 0.5180730784431439,
+ "y": 0.5935681806822019,
+ "confidence": 0.5910472810213829
+ },
+ {
+ "name": "left_knee",
+ "x": 0.8925273303822093,
+ "y": 0.5082354807403022,
+ "confidence": 0.5840320751794993
+ },
+ {
+ "name": "right_knee",
+ "x": 0.2434866909431669,
+ "y": 0.45900413964604203,
+ "confidence": 0.8146230907081062
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.24287115223795253,
+ "y": 0.5886422119226908,
+ "confidence": 0.538079819702979
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.13785439476462882,
+ "y": 0.55143292524988,
+ "confidence": 0.6143995946811053
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "sitting",
+ "timestamp": "2025-06-07T12:31:11.236245"
+ },
+ {
+ "person_id": "2",
+ "confidence": 0.35207768693851665,
+ "bounding_box": {
+ "x": 0.2765498034316859,
+ "y": 0.43247003414159246,
+ "width": 0.3633750931147725,
+ "height": 0.4938780359990873
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.1604126898801905,
+ "y": 0.7048573375998496,
+ "confidence": 0.8581798084049611
+ },
+ {
+ "name": "left_eye",
+ "x": 0.6259335884734869,
+ "y": 0.1354705040619779,
+ "confidence": 0.819327861459654
+ },
+ {
+ "name": "right_eye",
+ "x": 0.2224865667621713,
+ "y": 0.2511125866479431,
+ "confidence": 0.6648104591620027
+ },
+ {
+ "name": "left_ear",
+ "x": 0.28267723109996246,
+ "y": 0.7010864289589891,
+ "confidence": 0.6583613618546853
+ },
+ {
+ "name": "right_ear",
+ "x": 0.6582362844836986,
+ "y": 0.6774698981379421,
+ "confidence": 0.7718210170365041
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.5010676026491517,
+ "y": 0.624190408133553,
+ "confidence": 0.6576163884997456
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.15790445933321814,
+ "y": 0.15004632002693477,
+ "confidence": 0.7594042257523519
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.20869968465749827,
+ "y": 0.752452930071922,
+ "confidence": 0.6641317132561305
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.13046426795540295,
+ "y": 0.7327015399000987,
+ "confidence": 0.6758027109229907
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.4345695137883485,
+ "y": 0.5446404217456786,
+ "confidence": 0.542865592244768
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.43901163390535314,
+ "y": 0.3619519039597633,
+ "confidence": 0.6601105659903144
+ },
+ {
+ "name": "left_hip",
+ "x": 0.2757230842405501,
+ "y": 0.518388401337965,
+ "confidence": 0.6001522829729531
+ },
+ {
+ "name": "right_hip",
+ "x": 0.8475196635818669,
+ "y": 0.22121972448055588,
+ "confidence": 0.9312842260680301
+ },
+ {
+ "name": "left_knee",
+ "x": 0.1524562739710847,
+ "y": 0.5882665393601244,
+ "confidence": 0.608415603676807
+ },
+ {
+ "name": "right_knee",
+ "x": 0.3584782192826531,
+ "y": 0.7061205470828577,
+ "confidence": 0.6664268999572104
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.5306479556640387,
+ "y": 0.12301150869111269,
+ "confidence": 0.5707161903293938
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.6960744941693561,
+ "y": 0.3499669479123747,
+ "confidence": 0.8047024098152354
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "walking",
+ "timestamp": "2025-06-07T12:31:11.236274"
+ }
+ ],
+ "zone_summary": {
+ "zone_1": 3
+ },
+ "processing_time_ms": 0.88,
+ "metadata": {
+ "mock_data": true
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.237579"
+ },
+ {
+ "test_name": "GET /api/v1/pose/current",
+ "description": "Current pose estimation with parameters",
+ "url": "http://localhost:8000/api/v1/pose/current",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 2.89,
+ "response_data": {
+ "timestamp": "2025-06-07T12:31:11.239863",
+ "frame_id": "frame_1749299471",
+ "persons": [
+ {
+ "person_id": "0",
+ "confidence": 0.370854019093324,
+ "bounding_box": {
+ "x": 0.3280213489240438,
+ "y": 0.5056874516731203,
+ "width": 0.34589685227387285,
+ "height": 0.47589268657270345
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.3302264155127105,
+ "y": 0.7666441068864577,
+ "confidence": 0.7508261047121239
+ },
+ {
+ "name": "left_eye",
+ "x": 0.12389961372528831,
+ "y": 0.11265742644399035,
+ "confidence": 0.8837280434618697
+ },
+ {
+ "name": "right_eye",
+ "x": 0.18858293506049215,
+ "y": 0.7947507286048676,
+ "confidence": 0.5359988354449617
+ },
+ {
+ "name": "left_ear",
+ "x": 0.5347969458129921,
+ "y": 0.7870219960840005,
+ "confidence": 0.7414861777052862
+ },
+ {
+ "name": "right_ear",
+ "x": 0.3292265722716323,
+ "y": 0.7785133611119565,
+ "confidence": 0.8477129538006556
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.13562914539480025,
+ "y": 0.3344314232704363,
+ "confidence": 0.9454547470280737
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.25887956115193844,
+ "y": 0.7416711354578321,
+ "confidence": 0.7324120210502734
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.6914834506347959,
+ "y": 0.38708923719225985,
+ "confidence": 0.579309423206422
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.6834006677040783,
+ "y": 0.7855844577079371,
+ "confidence": 0.8490986880142513
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.24260255118250731,
+ "y": 0.4797335535386199,
+ "confidence": 0.921154556089327
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.1891051300648476,
+ "y": 0.5006337124188301,
+ "confidence": 0.7549395147774483
+ },
+ {
+ "name": "left_hip",
+ "x": 0.45339484199301894,
+ "y": 0.29619229004614245,
+ "confidence": 0.7057449559345098
+ },
+ {
+ "name": "right_hip",
+ "x": 0.6828279036525241,
+ "y": 0.4389721586483025,
+ "confidence": 0.6670246048009738
+ },
+ {
+ "name": "left_knee",
+ "x": 0.795841186477223,
+ "y": 0.7857120647589356,
+ "confidence": 0.741616459417308
+ },
+ {
+ "name": "right_knee",
+ "x": 0.547482111130874,
+ "y": 0.2302439433466714,
+ "confidence": 0.636430810102298
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.7008616321278732,
+ "y": 0.27001333971446473,
+ "confidence": 0.513728640448088
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.6414064601962457,
+ "y": 0.30920956468078786,
+ "confidence": 0.6426693578712224
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "walking",
+ "timestamp": "2025-06-07T12:31:11.239814"
+ },
+ {
+ "person_id": "1",
+ "confidence": 0.6657660984774105,
+ "bounding_box": {
+ "x": 0.21596985766055174,
+ "y": 0.554765890040542,
+ "width": 0.3476945882637141,
+ "height": 0.3225980858655065
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.34509773586217474,
+ "y": 0.6039962595178552,
+ "confidence": 0.9356445420281669
+ },
+ {
+ "name": "left_eye",
+ "x": 0.11776846716563166,
+ "y": 0.225173660788648,
+ "confidence": 0.5522696395103206
+ },
+ {
+ "name": "right_eye",
+ "x": 0.7338523292829059,
+ "y": 0.599335853110952,
+ "confidence": 0.7027590082539141
+ },
+ {
+ "name": "left_ear",
+ "x": 0.465126361351207,
+ "y": 0.48658170608878937,
+ "confidence": 0.6790517153428199
+ },
+ {
+ "name": "right_ear",
+ "x": 0.688443714096417,
+ "y": 0.7906320580116033,
+ "confidence": 0.517047439500365
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.4323501414184646,
+ "y": 0.15862144143634993,
+ "confidence": 0.7673209239676191
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.4567671996735275,
+ "y": 0.28872739596598096,
+ "confidence": 0.7592842348741403
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.11321639253514633,
+ "y": 0.2050364311471884,
+ "confidence": 0.6376305366974446
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.1859980824352567,
+ "y": 0.3008205738608011,
+ "confidence": 0.9225066732217158
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.8383453588356334,
+ "y": 0.280898583891389,
+ "confidence": 0.8429876370472138
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.8426749298154382,
+ "y": 0.2295432901116694,
+ "confidence": 0.7959672377339402
+ },
+ {
+ "name": "left_hip",
+ "x": 0.46079681719277765,
+ "y": 0.7435169063799625,
+ "confidence": 0.6206533611359297
+ },
+ {
+ "name": "right_hip",
+ "x": 0.48616078823152187,
+ "y": 0.304553494425842,
+ "confidence": 0.9071440594833815
+ },
+ {
+ "name": "left_knee",
+ "x": 0.8607378771474717,
+ "y": 0.2557244351579886,
+ "confidence": 0.5296887736025605
+ },
+ {
+ "name": "right_knee",
+ "x": 0.5503887821224759,
+ "y": 0.5978507779253809,
+ "confidence": 0.7883542631669029
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.7268171280616471,
+ "y": 0.23228222221949216,
+ "confidence": 0.5462757240883648
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.3592243197510716,
+ "y": 0.38341299101117987,
+ "confidence": 0.795125616127961
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "sitting",
+ "timestamp": "2025-06-07T12:31:11.239833"
+ },
+ {
+ "person_id": "2",
+ "confidence": 0.6388533885804164,
+ "bounding_box": {
+ "x": 0.2019039986313679,
+ "y": 0.24933561207668617,
+ "width": 0.350285539244766,
+ "height": 0.40394161253795735
+ },
+ "keypoints": [
+ {
+ "name": "nose",
+ "x": 0.46387437669009546,
+ "y": 0.21912840561578115,
+ "confidence": 0.5415202898138071
+ },
+ {
+ "name": "left_eye",
+ "x": 0.250282606200101,
+ "y": 0.7636670564326579,
+ "confidence": 0.567373830235719
+ },
+ {
+ "name": "right_eye",
+ "x": 0.5082089357810013,
+ "y": 0.31123588871298963,
+ "confidence": 0.5150436226533691
+ },
+ {
+ "name": "left_ear",
+ "x": 0.2144005859969986,
+ "y": 0.804912450132936,
+ "confidence": 0.9443468898852558
+ },
+ {
+ "name": "right_ear",
+ "x": 0.2930593433202765,
+ "y": 0.1422330200282742,
+ "confidence": 0.9257622652361159
+ },
+ {
+ "name": "left_shoulder",
+ "x": 0.4265533807468792,
+ "y": 0.8652060982958156,
+ "confidence": 0.6218485643101248
+ },
+ {
+ "name": "right_shoulder",
+ "x": 0.5208915723508785,
+ "y": 0.717661133362763,
+ "confidence": 0.626112755781511
+ },
+ {
+ "name": "left_elbow",
+ "x": 0.36740642026204207,
+ "y": 0.5694059472552029,
+ "confidence": 0.5609663660779218
+ },
+ {
+ "name": "right_elbow",
+ "x": 0.5391920258178114,
+ "y": 0.6442125494598956,
+ "confidence": 0.7938092697509699
+ },
+ {
+ "name": "left_wrist",
+ "x": 0.5956602387413871,
+ "y": 0.4140777212387293,
+ "confidence": 0.8343460554256876
+ },
+ {
+ "name": "right_wrist",
+ "x": 0.6315100214312287,
+ "y": 0.4197139630733008,
+ "confidence": 0.7478878756557799
+ },
+ {
+ "name": "left_hip",
+ "x": 0.36187976548941314,
+ "y": 0.31173051173969923,
+ "confidence": 0.7630685098335477
+ },
+ {
+ "name": "right_hip",
+ "x": 0.36416445946060205,
+ "y": 0.14747762132213227,
+ "confidence": 0.6620742395104553
+ },
+ {
+ "name": "left_knee",
+ "x": 0.6284491176264971,
+ "y": 0.5616090769899043,
+ "confidence": 0.6558174035602283
+ },
+ {
+ "name": "right_knee",
+ "x": 0.10567959136772603,
+ "y": 0.8789306746324227,
+ "confidence": 0.9494355835172135
+ },
+ {
+ "name": "left_ankle",
+ "x": 0.7780648824658661,
+ "y": 0.7498553660012194,
+ "confidence": 0.6501985656038138
+ },
+ {
+ "name": "right_ankle",
+ "x": 0.4951401143008306,
+ "y": 0.6615737813418059,
+ "confidence": 0.6275415002667539
+ }
+ ],
+ "segmentation": null,
+ "zone_id": "zone_1",
+ "activity": "walking",
+ "timestamp": "2025-06-07T12:31:11.239846"
+ }
+ ],
+ "zone_summary": {
+ "zone_1": 3
+ },
+ "processing_time_ms": 0.65,
+ "metadata": {
+ "mock_data": true
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.240803"
+ },
+ {
+ "test_name": "POST /api/v1/pose/analyze",
+ "description": "Pose analysis (requires auth)",
+ "url": "http://localhost:8000/api/v1/pose/analyze",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.1,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/analyze"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.242270"
+ },
+ {
+ "test_name": "GET /api/v1/pose/zones/zone_1/occupancy",
+ "description": "Zone occupancy",
+ "url": "http://localhost:8000/api/v1/pose/zones/zone_1/occupancy",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.35,
+ "response_data": {
+ "zone_id": "zone_1",
+ "current_occupancy": 5,
+ "max_occupancy": 10,
+ "persons": [
+ {
+ "person_id": "person_0",
+ "confidence": 0.9299048917915331,
+ "activity": "standing"
+ },
+ {
+ "person_id": "person_1",
+ "confidence": 0.8890436892848852,
+ "activity": "standing"
+ },
+ {
+ "person_id": "person_2",
+ "confidence": 0.8888218199253267,
+ "activity": "walking"
+ },
+ {
+ "person_id": "person_3",
+ "confidence": 0.942871490533826,
+ "activity": "standing"
+ },
+ {
+ "person_id": "person_4",
+ "confidence": 0.8544064588886042,
+ "activity": "sitting"
+ }
+ ],
+ "timestamp": "2025-06-07T12:31:11.243107"
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.243759"
+ },
+ {
+ "test_name": "GET /api/v1/pose/zones/summary",
+ "description": "All zones summary",
+ "url": "http://localhost:8000/api/v1/pose/zones/summary",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.2,
+ "response_data": {
+ "timestamp": "2025-06-07T12:31:11.244523",
+ "total_persons": 6,
+ "zones": {
+ "zone_1": {
+ "occupancy": 1,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_2": {
+ "occupancy": 1,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_3": {
+ "occupancy": 2,
+ "max_occupancy": 10,
+ "status": "active"
+ },
+ "zone_4": {
+ "occupancy": 2,
+ "max_occupancy": 10,
+ "status": "active"
+ }
+ },
+ "active_zones": 4
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.245234"
+ },
+ {
+ "test_name": "POST /api/v1/pose/historical",
+ "description": "Historical pose data (requires auth)",
+ "url": "http://localhost:8000/api/v1/pose/historical",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.43,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/historical"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.246811"
+ },
+ {
+ "test_name": "GET /api/v1/pose/activities",
+ "description": "Recent activities",
+ "url": "http://localhost:8000/api/v1/pose/activities",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 500,
+ "response_time_ms": 1.35,
+ "response_data": {
+ "error": {
+ "code": 500,
+ "message": "Failed to get activities: name 'timedelta' is not defined",
+ "type": "http_error",
+ "path": "/api/v1/pose/activities"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.248287"
+ },
+ {
+ "test_name": "GET /api/v1/pose/activities",
+ "description": "Activities for specific zone",
+ "url": "http://localhost:8000/api/v1/pose/activities",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 500,
+ "response_time_ms": 1.29,
+ "response_data": {
+ "error": {
+ "code": 500,
+ "message": "Failed to get activities: name 'timedelta' is not defined",
+ "type": "http_error",
+ "path": "/api/v1/pose/activities"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.249941"
+ },
+ {
+ "test_name": "GET /api/v1/pose/calibration/status",
+ "description": "Calibration status (requires auth)",
+ "url": "http://localhost:8000/api/v1/pose/calibration/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.07,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/calibration/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.251405"
+ },
+ {
+ "test_name": "POST /api/v1/pose/calibrate",
+ "description": "Start calibration (requires auth)",
+ "url": "http://localhost:8000/api/v1/pose/calibrate",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.28,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/calibrate"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.253054"
+ },
+ {
+ "test_name": "GET /api/v1/pose/stats",
+ "description": "Pose statistics",
+ "url": "http://localhost:8000/api/v1/pose/stats",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.23,
+ "response_data": {
+ "period": {
+ "start_time": "2025-06-06T12:31:11.253946",
+ "end_time": "2025-06-07T12:31:11.253946",
+ "hours": 24
+ },
+ "statistics": {
+ "total_detections": 314,
+ "successful_detections": 286,
+ "failed_detections": 28,
+ "success_rate": 0.910828025477707,
+ "average_confidence": 0.8154860610274203,
+ "average_processing_time_ms": 74.08005120410309,
+ "unique_persons": 19,
+ "most_active_zone": "zone_1",
+ "activity_distribution": {
+ "standing": 0.3631605264291814,
+ "sitting": 0.3294888900969729,
+ "walking": 0.29592515904686695,
+ "lying": 0.057631257973703554
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.254736"
+ },
+ {
+ "test_name": "GET /api/v1/pose/stats",
+ "description": "Pose statistics (12 hours)",
+ "url": "http://localhost:8000/api/v1/pose/stats",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 1.14,
+ "response_data": {
+ "period": {
+ "start_time": "2025-06-07T00:31:11.255512",
+ "end_time": "2025-06-07T12:31:11.255512",
+ "hours": 12
+ },
+ "statistics": {
+ "total_detections": 654,
+ "successful_detections": 604,
+ "failed_detections": 50,
+ "success_rate": 0.9235474006116208,
+ "average_confidence": 0.852208852930976,
+ "average_processing_time_ms": 106.7372839201018,
+ "unique_persons": 17,
+ "most_active_zone": "zone_1",
+ "activity_distribution": {
+ "standing": 0.37644162607601667,
+ "sitting": 0.22403324279769943,
+ "walking": 0.11425361491788977,
+ "lying": 0.019586953828269162
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.256156"
+ },
+ {
+ "test_name": "GET /api/v1/stream/status",
+ "description": "Stream status",
+ "url": "http://localhost:8000/api/v1/stream/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 500,
+ "response_time_ms": 1.2,
+ "response_data": {
+ "error": {
+ "code": 500,
+ "message": "Failed to get stream status: 'is_active'",
+ "type": "http_error",
+ "path": "/api/v1/stream/status"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.257473"
+ },
+ {
+ "test_name": "POST /api/v1/stream/start",
+ "description": "Start streaming (requires auth)",
+ "url": "http://localhost:8000/api/v1/stream/start",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.01,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/stream/start"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.258782"
+ },
+ {
+ "test_name": "POST /api/v1/stream/stop",
+ "description": "Stop streaming (requires auth)",
+ "url": "http://localhost:8000/api/v1/stream/stop",
+ "method": "POST",
+ "expected_status": 200,
+ "actual_status": 401,
+ "response_time_ms": 1.19,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/stream/stop"
+ }
+ },
+ "success": false,
+ "timestamp": "2025-06-07T12:31:11.260080"
+ },
+ {
+ "test_name": "WebSocket /ws/pose",
+ "description": "Pose WebSocket",
+ "url": "ws://localhost:8000/ws/pose",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "server rejected WebSocket connection: HTTP 403",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 543, in __await_impl__\n await self.connection.handshake(\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 114, in handshake\n raise self.protocol.handshake_exc\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 325, in parse\n self.process_response(response)\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 142, in process_response\n raise InvalidStatus(response)\nwebsockets.exceptions.InvalidStatus: server rejected WebSocket connection: HTTP 403\n",
+ "timestamp": "2025-06-07T12:31:11.294223"
+ },
+ {
+ "test_name": "WebSocket /ws/hardware",
+ "description": "Hardware WebSocket",
+ "url": "ws://localhost:8000/ws/hardware",
+ "method": "WebSocket",
+ "response_time_ms": null,
+ "response_data": null,
+ "success": false,
+ "error": "server rejected WebSocket connection: HTTP 403",
+ "traceback": "Traceback (most recent call last):\n File \"/workspaces/wifi-densepose/scripts/test_api_endpoints.py\", line 164, in test_websocket_endpoint\n async with websockets.connect(ws_url) as websocket:\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 587, in __aenter__\n return await self\n ^^^^^^^^^^\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 543, in __await_impl__\n await self.connection.handshake(\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/asyncio/client.py\", line 114, in handshake\n raise self.protocol.handshake_exc\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 325, in parse\n self.process_response(response)\n File \"/usr/local/python/3.12.1/lib/python3.12/site-packages/websockets/client.py\", line 142, in process_response\n raise InvalidStatus(response)\nwebsockets.exceptions.InvalidStatus: server rejected WebSocket connection: HTTP 403\n",
+ "timestamp": "2025-06-07T12:31:11.307920"
+ },
+ {
+ "test_name": "GET /docs",
+ "description": "API documentation",
+ "url": "http://localhost:8000/docs",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 3.76,
+ "response_data": {
+ "raw_response": "\n \n \n \n \n \n WiFi-DensePose API - Swagger UI\n \n \n \n
\n \n \n \n \n \n "
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.311823"
+ },
+ {
+ "test_name": "GET /openapi.json",
+ "description": "OpenAPI schema",
+ "url": "http://localhost:8000/openapi.json",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 4.89,
+ "response_data": {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "WiFi-DensePose API",
+ "description": "WiFi-based human pose estimation and activity recognition API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/health/health": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Health Check",
+ "description": "Comprehensive system health check.",
+ "operationId": "health_check_health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SystemHealth"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/ready": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Readiness Check",
+ "description": "Check if system is ready to serve requests.",
+ "operationId": "readiness_check_health_ready_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReadinessCheck"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/live": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Liveness Check",
+ "description": "Simple liveness check for load balancers.",
+ "operationId": "liveness_check_health_live_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/health/metrics": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get System Metrics",
+ "description": "Get detailed system metrics.",
+ "operationId": "get_system_metrics_health_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/health/version": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Get Version Info",
+ "description": "Get application version information.",
+ "operationId": "get_version_info_health_version_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/current": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Current Pose Estimation",
+ "description": "Get current pose estimation from WiFi signals.",
+ "operationId": "get_current_pose_estimation_api_v1_pose_current_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "confidence_threshold",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold"
+ }
+ },
+ {
+ "name": "max_persons",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50,
+ "minimum": 1
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons"
+ }
+ },
+ {
+ "name": "include_keypoints",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": true,
+ "title": "Include Keypoints"
+ }
+ },
+ {
+ "name": "include_segmentation",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": false,
+ "title": "Include Segmentation"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/analyze": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Analyze Pose Data",
+ "description": "Trigger pose analysis with custom parameters.",
+ "operationId": "analyze_pose_data_api_v1_pose_analyze_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PoseEstimationResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/zones/{zone_id}/occupancy": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zone Occupancy",
+ "description": "Get current occupancy for a specific zone.",
+ "operationId": "get_zone_occupancy_api_v1_pose_zones__zone_id__occupancy_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Zone Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/zones/summary": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Zones Summary",
+ "description": "Get occupancy summary for all zones.",
+ "operationId": "get_zones_summary_api_v1_pose_zones_summary_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/historical": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Historical Data",
+ "description": "Get historical pose estimation data.",
+ "operationId": "get_historical_data_api_v1_pose_historical_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HistoricalDataRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/activities": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Detected Activities",
+ "description": "Get recently detected activities.",
+ "operationId": "get_detected_activities_api_v1_pose_activities_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "zone_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by zone ID",
+ "title": "Zone Id"
+ },
+ "description": "Filter by zone ID"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 100,
+ "minimum": 1,
+ "description": "Maximum number of activities",
+ "default": 10,
+ "title": "Limit"
+ },
+ "description": "Maximum number of activities"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/pose/calibrate": {
+ "post": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Calibrate Pose System",
+ "description": "Calibrate the pose estimation system.",
+ "operationId": "calibrate_pose_system_api_v1_pose_calibrate_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/calibration/status": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Calibration Status",
+ "description": "Get current calibration status.",
+ "operationId": "get_calibration_status_api_v1_pose_calibration_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/pose/stats": {
+ "get": {
+ "tags": [
+ "Pose Estimation"
+ ],
+ "summary": "Get Pose Statistics",
+ "description": "Get pose estimation statistics.",
+ "operationId": "get_pose_statistics_api_v1_pose_stats_get",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "hours",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 168,
+ "minimum": 1,
+ "description": "Hours of data to analyze",
+ "default": 24,
+ "title": "Hours"
+ },
+ "description": "Hours of data to analyze"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/status": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Stream Status",
+ "description": "Get current streaming status.",
+ "operationId": "get_stream_status_api_v1_stream_status_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StreamStatus"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/start": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Start Streaming",
+ "description": "Start the streaming service.",
+ "operationId": "start_streaming_api_v1_stream_start_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/stop": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Stop Streaming",
+ "description": "Stop the streaming service.",
+ "operationId": "stop_streaming_api_v1_stream_stop_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Connected Clients",
+ "description": "Get list of connected WebSocket clients.",
+ "operationId": "get_connected_clients_api_v1_stream_clients_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/stream/clients/{client_id}": {
+ "delete": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Disconnect Client",
+ "description": "Disconnect a specific WebSocket client.",
+ "operationId": "disconnect_client_api_v1_stream_clients__client_id__delete",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "client_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Client Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/broadcast": {
+ "post": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Broadcast Message",
+ "description": "Broadcast a message to connected WebSocket clients.",
+ "operationId": "broadcast_message_api_v1_stream_broadcast_post",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "stream_type",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target stream type",
+ "title": "Stream Type"
+ },
+ "description": "Target stream type"
+ },
+ {
+ "name": "zone_ids",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Target zone IDs",
+ "title": "Zone Ids"
+ },
+ "description": "Target zone IDs"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true,
+ "title": "Message"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/stream/metrics": {
+ "get": {
+ "tags": [
+ "Streaming"
+ ],
+ "summary": "Get Streaming Metrics",
+ "description": "Get streaming performance metrics.",
+ "operationId": "get_streaming_metrics_api_v1_stream_metrics_get",
+ "parameters": [
+ {
+ "name": "websocket_token",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Websocket Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/": {
+ "get": {
+ "summary": "Root",
+ "description": "Root endpoint with API information.",
+ "operationId": "root__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/info": {
+ "get": {
+ "summary": "Api Info",
+ "description": "Get detailed API information.",
+ "operationId": "api_info_api_v1_info_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/status": {
+ "get": {
+ "summary": "Api Status",
+ "description": "Get current API status.",
+ "operationId": "api_status_api_v1_status_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/metrics": {
+ "get": {
+ "summary": "Api Metrics",
+ "description": "Get API metrics.",
+ "operationId": "api_metrics_api_v1_metrics_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/config": {
+ "get": {
+ "summary": "Dev Config",
+ "description": "Get current configuration (development only).",
+ "operationId": "dev_config_api_v1_dev_config_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/dev/reset": {
+ "post": {
+ "summary": "Dev Reset",
+ "description": "Reset services (development only).",
+ "operationId": "dev_reset_api_v1_dev_reset_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ComponentHealth": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Component name"
+ },
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Health status (healthy, degraded, unhealthy)"
+ },
+ "message": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Message",
+ "description": "Status message"
+ },
+ "last_check": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Last Check",
+ "description": "Last health check timestamp"
+ },
+ "uptime_seconds": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Uptime Seconds",
+ "description": "Component uptime"
+ },
+ "metrics": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Metrics",
+ "description": "Component metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "status",
+ "last_check"
+ ],
+ "title": "ComponentHealth",
+ "description": "Health status for a system component."
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "HistoricalDataRequest": {
+ "properties": {
+ "start_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Start Time",
+ "description": "Start time for data query"
+ },
+ "end_time": {
+ "type": "string",
+ "format": "date-time",
+ "title": "End Time",
+ "description": "End time for data query"
+ },
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Filter by specific zones"
+ },
+ "aggregation_interval": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 3600.0,
+ "minimum": 60.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Aggregation Interval",
+ "description": "Aggregation interval in seconds",
+ "default": 300
+ },
+ "include_raw_data": {
+ "type": "boolean",
+ "title": "Include Raw Data",
+ "description": "Include raw detection data",
+ "default": false
+ }
+ },
+ "type": "object",
+ "required": [
+ "start_time",
+ "end_time"
+ ],
+ "title": "HistoricalDataRequest",
+ "description": "Request model for historical pose data."
+ },
+ "PersonPose": {
+ "properties": {
+ "person_id": {
+ "type": "string",
+ "title": "Person Id",
+ "description": "Unique person identifier"
+ },
+ "confidence": {
+ "type": "number",
+ "title": "Confidence",
+ "description": "Detection confidence score"
+ },
+ "bounding_box": {
+ "additionalProperties": {
+ "type": "number"
+ },
+ "type": "object",
+ "title": "Bounding Box",
+ "description": "Person bounding box"
+ },
+ "keypoints": {
+ "anyOf": [
+ {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Keypoints",
+ "description": "Body keypoints with coordinates and confidence"
+ },
+ "segmentation": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Segmentation",
+ "description": "DensePose segmentation data"
+ },
+ "zone_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Id",
+ "description": "Zone where person is detected"
+ },
+ "activity": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Activity",
+ "description": "Detected activity"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Detection timestamp"
+ }
+ },
+ "type": "object",
+ "required": [
+ "person_id",
+ "confidence",
+ "bounding_box",
+ "timestamp"
+ ],
+ "title": "PersonPose",
+ "description": "Person pose data model."
+ },
+ "PoseEstimationRequest": {
+ "properties": {
+ "zone_ids": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Zone Ids",
+ "description": "Specific zones to analyze (all zones if not specified)"
+ },
+ "confidence_threshold": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Confidence Threshold",
+ "description": "Minimum confidence threshold for detections"
+ },
+ "max_persons": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 50.0,
+ "minimum": 1.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Max Persons",
+ "description": "Maximum number of persons to detect"
+ },
+ "include_keypoints": {
+ "type": "boolean",
+ "title": "Include Keypoints",
+ "description": "Include detailed keypoint data",
+ "default": true
+ },
+ "include_segmentation": {
+ "type": "boolean",
+ "title": "Include Segmentation",
+ "description": "Include DensePose segmentation masks",
+ "default": false
+ }
+ },
+ "type": "object",
+ "title": "PoseEstimationRequest",
+ "description": "Request model for pose estimation."
+ },
+ "PoseEstimationResponse": {
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Analysis timestamp"
+ },
+ "frame_id": {
+ "type": "string",
+ "title": "Frame Id",
+ "description": "Unique frame identifier"
+ },
+ "persons": {
+ "items": {
+ "$ref": "#/components/schemas/PersonPose"
+ },
+ "type": "array",
+ "title": "Persons",
+ "description": "Detected persons"
+ },
+ "zone_summary": {
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "type": "object",
+ "title": "Zone Summary",
+ "description": "Person count per zone"
+ },
+ "processing_time_ms": {
+ "type": "number",
+ "title": "Processing Time Ms",
+ "description": "Processing time in milliseconds"
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Metadata",
+ "description": "Additional metadata"
+ }
+ },
+ "type": "object",
+ "required": [
+ "timestamp",
+ "frame_id",
+ "persons",
+ "zone_summary",
+ "processing_time_ms"
+ ],
+ "title": "PoseEstimationResponse",
+ "description": "Response model for pose estimation."
+ },
+ "ReadinessCheck": {
+ "properties": {
+ "ready": {
+ "type": "boolean",
+ "title": "Ready",
+ "description": "Whether system is ready to serve requests"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Readiness check timestamp"
+ },
+ "checks": {
+ "additionalProperties": {
+ "type": "boolean"
+ },
+ "type": "object",
+ "title": "Checks",
+ "description": "Individual readiness checks"
+ },
+ "message": {
+ "type": "string",
+ "title": "Message",
+ "description": "Readiness status message"
+ }
+ },
+ "type": "object",
+ "required": [
+ "ready",
+ "timestamp",
+ "checks",
+ "message"
+ ],
+ "title": "ReadinessCheck",
+ "description": "System readiness check result."
+ },
+ "StreamStatus": {
+ "properties": {
+ "is_active": {
+ "type": "boolean",
+ "title": "Is Active",
+ "description": "Whether streaming is active"
+ },
+ "connected_clients": {
+ "type": "integer",
+ "title": "Connected Clients",
+ "description": "Number of connected clients"
+ },
+ "streams": {
+ "items": {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ "type": "array",
+ "title": "Streams",
+ "description": "Active streams"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "Stream uptime in seconds"
+ }
+ },
+ "type": "object",
+ "required": [
+ "is_active",
+ "connected_clients",
+ "streams",
+ "uptime_seconds"
+ ],
+ "title": "StreamStatus",
+ "description": "Stream status model."
+ },
+ "SystemHealth": {
+ "properties": {
+ "status": {
+ "type": "string",
+ "title": "Status",
+ "description": "Overall system status"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Timestamp",
+ "description": "Health check timestamp"
+ },
+ "uptime_seconds": {
+ "type": "number",
+ "title": "Uptime Seconds",
+ "description": "System uptime"
+ },
+ "components": {
+ "additionalProperties": {
+ "$ref": "#/components/schemas/ComponentHealth"
+ },
+ "type": "object",
+ "title": "Components",
+ "description": "Component health status"
+ },
+ "system_metrics": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "System Metrics",
+ "description": "System-level metrics"
+ }
+ },
+ "type": "object",
+ "required": [
+ "status",
+ "timestamp",
+ "uptime_seconds",
+ "components",
+ "system_metrics"
+ ],
+ "title": "SystemHealth",
+ "description": "Overall system health status."
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ },
+ "securitySchemes": {
+ "HTTPBearer": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ }
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.317201"
+ },
+ {
+ "test_name": "GET /",
+ "description": "Root endpoint",
+ "url": "http://localhost:8000/",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 3.06,
+ "response_data": {
+ "name": "WiFi-DensePose API",
+ "version": "1.0.0",
+ "environment": "development",
+ "docs_url": "/docs",
+ "api_prefix": "/api/v1",
+ "features": {
+ "authentication": false,
+ "rate_limiting": false,
+ "websockets": true,
+ "real_time_processing": true
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.320563"
+ },
+ {
+ "test_name": "GET /api/v1/info",
+ "description": "API information",
+ "url": "http://localhost:8000/api/v1/info",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 5.05,
+ "response_data": {
+ "api": {
+ "name": "WiFi-DensePose API",
+ "version": "1.0.0",
+ "environment": "development",
+ "prefix": "/api/v1"
+ },
+ "services": {
+ "total_services": 7,
+ "initialized": true,
+ "started": true,
+ "background_tasks": 2,
+ "services": {
+ "health": {
+ "type": "HealthCheckService",
+ "module": "src.services.health_check"
+ },
+ "metrics": {
+ "type": "MetricsService",
+ "module": "src.services.metrics"
+ },
+ "hardware": {
+ "type": "HardwareService",
+ "module": "src.services.hardware_service"
+ },
+ "pose": {
+ "type": "PoseService",
+ "module": "src.services.pose_service"
+ },
+ "stream": {
+ "type": "StreamService",
+ "module": "src.services.stream_service"
+ },
+ "pose_stream_handler": {
+ "type": "PoseStreamHandler",
+ "module": "src.api.websocket.pose_stream"
+ },
+ "connection_manager": {
+ "type": "ConnectionManager",
+ "module": "src.api.websocket.connection_manager"
+ }
+ }
+ },
+ "features": {
+ "authentication": false,
+ "rate_limiting": false,
+ "websockets": true,
+ "real_time_processing": true,
+ "historical_data": true
+ },
+ "limits": {
+ "rate_limit_requests": 100,
+ "rate_limit_window": 3600
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.325942"
+ },
+ {
+ "test_name": "GET /api/v1/status",
+ "description": "API status",
+ "url": "http://localhost:8000/api/v1/status",
+ "method": "GET",
+ "expected_status": 200,
+ "actual_status": 200,
+ "response_time_ms": 3.88,
+ "response_data": {
+ "api": {
+ "status": "healthy",
+ "version": "1.0.0",
+ "environment": "development"
+ },
+ "services": {
+ "health": {
+ "status": "healthy",
+ "initialized": true,
+ "running": true,
+ "services_monitored": 6,
+ "uptime": 426.1675431728363
+ },
+ "metrics": {
+ "status": "healthy",
+ "initialized": true,
+ "running": true,
+ "metrics_count": 14,
+ "counters_count": 0,
+ "gauges_count": 0,
+ "histograms_count": 0,
+ "uptime": 426.1675446033478
+ },
+ "hardware": {
+ "status": "healthy",
+ "running": true,
+ "last_error": null,
+ "statistics": {
+ "total_samples": 0,
+ "successful_samples": 0,
+ "failed_samples": 0,
+ "average_sample_rate": 0.0,
+ "last_sample_time": null,
+ "connected_routers": 0
+ },
+ "configuration": {
+ "mock_hardware": true,
+ "wifi_interface": "wlan0",
+ "polling_interval": 0.1,
+ "buffer_size": 1000
+ },
+ "routers": [
+ {
+ "router_id": "main_router",
+ "healthy": false,
+ "connected": false,
+ "last_data_time": null,
+ "error_count": 0,
+ "configuration": {
+ "host": "192.168.1.1",
+ "port": 22,
+ "username": "admin",
+ "interface": "wlan0"
+ }
+ }
+ ]
+ },
+ "pose": {
+ "status": "healthy",
+ "initialized": true,
+ "running": true,
+ "last_error": null,
+ "statistics": {
+ "total_processed": 11598,
+ "successful_detections": 11598,
+ "failed_detections": 0,
+ "average_confidence": 0.6238356508747995,
+ "processing_time_ms": 0.7697966890843242
+ },
+ "configuration": {
+ "mock_data": true,
+ "confidence_threshold": 0.5,
+ "max_persons": 10,
+ "batch_size": 32
+ }
+ },
+ "stream": {
+ "status": "healthy",
+ "running": true,
+ "last_error": null,
+ "connections": {
+ "active": 0,
+ "total": 0
+ },
+ "buffers": {
+ "pose_buffer_size": 0,
+ "csi_buffer_size": 0,
+ "max_buffer_size": 100
+ },
+ "statistics": {
+ "active_connections": 0,
+ "total_connections": 0,
+ "messages_sent": 0,
+ "messages_failed": 0,
+ "data_points_streamed": 0,
+ "average_latency_ms": 0.0
+ },
+ "configuration": {
+ "stream_fps": 30,
+ "buffer_size": 100,
+ "ping_interval": 60,
+ "timeout": 300
+ }
+ },
+ "pose_stream_handler": {
+ "status": "unknown"
+ },
+ "connection_manager": {
+ "status": "unknown"
+ }
+ },
+ "connections": {
+ "total_clients": 0,
+ "clients_by_type": {},
+ "clients_by_zone": {},
+ "active_clients": 0,
+ "inactive_clients": 0
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.329977"
+ },
+ {
+ "test_name": "GET /nonexistent",
+ "description": "Non-existent endpoint",
+ "url": "http://localhost:8000/nonexistent",
+ "method": "GET",
+ "expected_status": 404,
+ "actual_status": 404,
+ "response_time_ms": 3.24,
+ "response_data": {
+ "error": {
+ "code": 404,
+ "message": "Not Found",
+ "type": "http_error",
+ "path": "/nonexistent"
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.333478"
+ },
+ {
+ "test_name": "POST /api/v1/pose/analyze",
+ "description": "Unauthorized request (no auth)",
+ "url": "http://localhost:8000/api/v1/pose/analyze",
+ "method": "POST",
+ "expected_status": 401,
+ "actual_status": 401,
+ "response_time_ms": 8.17,
+ "response_data": {
+ "error": {
+ "code": 401,
+ "message": "Authentication required",
+ "type": "http_error",
+ "path": "/api/v1/pose/analyze"
+ }
+ },
+ "success": true,
+ "timestamp": "2025-06-07T12:31:11.341935"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/test_api_endpoints.py b/scripts/test_api_endpoints.py
new file mode 100755
index 0000000..82fa195
--- /dev/null
+++ b/scripts/test_api_endpoints.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python3
+"""
+API Endpoint Testing Script
+Tests all WiFi-DensePose API endpoints and provides debugging information.
+"""
+
+import asyncio
+import json
+import sys
+import time
+import traceback
+from datetime import datetime, timedelta
+from typing import Dict, List, Any, Optional
+
+import aiohttp
+import websockets
+from colorama import Fore, Style, init
+
+# Initialize colorama for colored output
+init(autoreset=True)
+
+class APITester:
+ """Comprehensive API endpoint tester."""
+
+ def __init__(self, base_url: str = "http://localhost:8000"):
+ self.base_url = base_url
+ self.session = None
+ self.results = {
+ "total_tests": 0,
+ "passed": 0,
+ "failed": 0,
+ "errors": [],
+ "test_details": []
+ }
+
+ async def __aenter__(self):
+ """Async context manager entry."""
+ self.session = aiohttp.ClientSession()
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ """Async context manager exit."""
+ if self.session:
+ await self.session.close()
+
+ def log_success(self, message: str):
+ """Log success message."""
+ print(f"{Fore.GREEN}✓ {message}{Style.RESET_ALL}")
+
+ def log_error(self, message: str):
+ """Log error message."""
+ print(f"{Fore.RED}✗ {message}{Style.RESET_ALL}")
+
+ def log_info(self, message: str):
+ """Log info message."""
+ print(f"{Fore.BLUE}ℹ {message}{Style.RESET_ALL}")
+
+ def log_warning(self, message: str):
+ """Log warning message."""
+ print(f"{Fore.YELLOW}⚠ {message}{Style.RESET_ALL}")
+
+ async def test_endpoint(
+ self,
+ method: str,
+ endpoint: str,
+ expected_status: int = 200,
+ data: Optional[Dict] = None,
+ params: Optional[Dict] = None,
+ headers: Optional[Dict] = None,
+ description: str = ""
+ ) -> Dict[str, Any]:
+ """Test a single API endpoint."""
+ self.results["total_tests"] += 1
+ test_name = f"{method.upper()} {endpoint}"
+
+ try:
+ url = f"{self.base_url}{endpoint}"
+
+ # Prepare request
+ kwargs = {}
+ if data:
+ kwargs["json"] = data
+ if params:
+ kwargs["params"] = params
+ if headers:
+ kwargs["headers"] = headers
+
+ # Make request
+ start_time = time.time()
+ async with self.session.request(method, url, **kwargs) as response:
+ response_time = (time.time() - start_time) * 1000
+ response_text = await response.text()
+
+ # Try to parse JSON response
+ try:
+ response_data = json.loads(response_text) if response_text else {}
+ except json.JSONDecodeError:
+ response_data = {"raw_response": response_text}
+
+ # Check status code
+ status_ok = response.status == expected_status
+
+ test_result = {
+ "test_name": test_name,
+ "description": description,
+ "url": url,
+ "method": method.upper(),
+ "expected_status": expected_status,
+ "actual_status": response.status,
+ "response_time_ms": round(response_time, 2),
+ "response_data": response_data,
+ "success": status_ok,
+ "timestamp": datetime.now().isoformat()
+ }
+
+ if status_ok:
+ self.results["passed"] += 1
+ self.log_success(f"{test_name} - {response.status} ({response_time:.1f}ms)")
+ if description:
+ print(f" {description}")
+ else:
+ self.results["failed"] += 1
+ self.log_error(f"{test_name} - Expected {expected_status}, got {response.status}")
+ if description:
+ print(f" {description}")
+ print(f" Response: {response_text[:200]}...")
+
+ self.results["test_details"].append(test_result)
+ return test_result
+
+ except Exception as e:
+ self.results["failed"] += 1
+ error_msg = f"{test_name} - Exception: {str(e)}"
+ self.log_error(error_msg)
+
+ test_result = {
+ "test_name": test_name,
+ "description": description,
+ "url": f"{self.base_url}{endpoint}",
+ "method": method.upper(),
+ "expected_status": expected_status,
+ "actual_status": None,
+ "response_time_ms": None,
+ "response_data": None,
+ "success": False,
+ "error": str(e),
+ "traceback": traceback.format_exc(),
+ "timestamp": datetime.now().isoformat()
+ }
+
+ self.results["errors"].append(error_msg)
+ self.results["test_details"].append(test_result)
+ return test_result
+
+ async def test_websocket_endpoint(self, endpoint: str, description: str = "") -> Dict[str, Any]:
+ """Test WebSocket endpoint."""
+ self.results["total_tests"] += 1
+ test_name = f"WebSocket {endpoint}"
+
+ try:
+ ws_url = f"ws://localhost:8000{endpoint}"
+
+ start_time = time.time()
+ async with websockets.connect(ws_url) as websocket:
+ # Send a test message
+ test_message = {"type": "subscribe", "zone_ids": ["zone_1"]}
+ await websocket.send(json.dumps(test_message))
+
+ # Wait for response
+ response = await asyncio.wait_for(websocket.recv(), timeout=3)
+ response_time = (time.time() - start_time) * 1000
+
+ try:
+ response_data = json.loads(response)
+ except json.JSONDecodeError:
+ response_data = {"raw_response": response}
+
+ test_result = {
+ "test_name": test_name,
+ "description": description,
+ "url": ws_url,
+ "method": "WebSocket",
+ "response_time_ms": round(response_time, 2),
+ "response_data": response_data,
+ "success": True,
+ "timestamp": datetime.now().isoformat()
+ }
+
+ self.results["passed"] += 1
+ self.log_success(f"{test_name} - Connected ({response_time:.1f}ms)")
+ if description:
+ print(f" {description}")
+
+ self.results["test_details"].append(test_result)
+ return test_result
+
+ except Exception as e:
+ self.results["failed"] += 1
+ error_msg = f"{test_name} - Exception: {str(e)}"
+ self.log_error(error_msg)
+
+ test_result = {
+ "test_name": test_name,
+ "description": description,
+ "url": f"ws://localhost:8000{endpoint}",
+ "method": "WebSocket",
+ "response_time_ms": None,
+ "response_data": None,
+ "success": False,
+ "error": str(e),
+ "traceback": traceback.format_exc(),
+ "timestamp": datetime.now().isoformat()
+ }
+
+ self.results["errors"].append(error_msg)
+ self.results["test_details"].append(test_result)
+ return test_result
+
+ async def run_all_tests(self):
+ """Run all API endpoint tests."""
+ print(f"{Fore.CYAN}{'='*60}")
+ print(f"{Fore.CYAN}WiFi-DensePose API Endpoint Testing")
+ print(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}")
+ print()
+
+ # Test Health Endpoints
+ print(f"{Fore.MAGENTA}Testing Health Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/health/health", description="System health check")
+ await self.test_endpoint("GET", "/health/ready", description="Readiness check")
+ print()
+
+ # Test Pose Estimation Endpoints
+ print(f"{Fore.MAGENTA}Testing Pose Estimation Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/api/v1/pose/current", description="Current pose estimation")
+ await self.test_endpoint("GET", "/api/v1/pose/current",
+ params={"zone_ids": ["zone_1"], "confidence_threshold": 0.7},
+ description="Current pose estimation with parameters")
+ await self.test_endpoint("POST", "/api/v1/pose/analyze", description="Pose analysis (requires auth)")
+ await self.test_endpoint("GET", "/api/v1/pose/zones/zone_1/occupancy", description="Zone occupancy")
+ await self.test_endpoint("GET", "/api/v1/pose/zones/summary", description="All zones summary")
+ print()
+
+ # Test Historical Data Endpoints
+ print(f"{Fore.MAGENTA}Testing Historical Data Endpoints:{Style.RESET_ALL}")
+ end_time = datetime.now()
+ start_time = end_time - timedelta(hours=1)
+ historical_data = {
+ "start_time": start_time.isoformat(),
+ "end_time": end_time.isoformat(),
+ "zone_ids": ["zone_1"],
+ "aggregation_interval": 300
+ }
+ await self.test_endpoint("POST", "/api/v1/pose/historical",
+ data=historical_data,
+ description="Historical pose data (requires auth)")
+ await self.test_endpoint("GET", "/api/v1/pose/activities", description="Recent activities")
+ await self.test_endpoint("GET", "/api/v1/pose/activities",
+ params={"zone_id": "zone_1", "limit": 5},
+ description="Activities for specific zone")
+ print()
+
+ # Test Calibration Endpoints
+ print(f"{Fore.MAGENTA}Testing Calibration Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/api/v1/pose/calibration/status", description="Calibration status (requires auth)")
+ await self.test_endpoint("POST", "/api/v1/pose/calibrate", description="Start calibration (requires auth)")
+ print()
+
+ # Test Statistics Endpoints
+ print(f"{Fore.MAGENTA}Testing Statistics Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/api/v1/pose/stats", description="Pose statistics")
+ await self.test_endpoint("GET", "/api/v1/pose/stats",
+ params={"hours": 12}, description="Pose statistics (12 hours)")
+ print()
+
+ # Test Stream Endpoints
+ print(f"{Fore.MAGENTA}Testing Stream Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/api/v1/stream/status", description="Stream status")
+ await self.test_endpoint("POST", "/api/v1/stream/start", description="Start streaming (requires auth)")
+ await self.test_endpoint("POST", "/api/v1/stream/stop", description="Stop streaming (requires auth)")
+ print()
+
+ # Test WebSocket Endpoints
+ print(f"{Fore.MAGENTA}Testing WebSocket Endpoints:{Style.RESET_ALL}")
+ await self.test_websocket_endpoint("/ws/pose", description="Pose WebSocket")
+ await self.test_websocket_endpoint("/ws/hardware", description="Hardware WebSocket")
+ print()
+
+ # Test Documentation Endpoints
+ print(f"{Fore.MAGENTA}Testing Documentation Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/docs", description="API documentation")
+ await self.test_endpoint("GET", "/openapi.json", description="OpenAPI schema")
+ print()
+
+ # Test API Info Endpoints
+ print(f"{Fore.MAGENTA}Testing API Info Endpoints:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/", description="Root endpoint")
+ await self.test_endpoint("GET", "/api/v1/info", description="API information")
+ await self.test_endpoint("GET", "/api/v1/status", description="API status")
+ print()
+
+ # Test Error Cases
+ print(f"{Fore.MAGENTA}Testing Error Cases:{Style.RESET_ALL}")
+ await self.test_endpoint("GET", "/nonexistent", expected_status=404,
+ description="Non-existent endpoint")
+ await self.test_endpoint("POST", "/api/v1/pose/analyze",
+ data={"invalid": "data"}, expected_status=401,
+ description="Unauthorized request (no auth)")
+ print()
+
+ def print_summary(self):
+ """Print test summary."""
+ print(f"{Fore.CYAN}{'='*60}")
+ print(f"{Fore.CYAN}Test Summary")
+ print(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}")
+
+ total = self.results["total_tests"]
+ passed = self.results["passed"]
+ failed = self.results["failed"]
+ success_rate = (passed / total * 100) if total > 0 else 0
+
+ print(f"Total Tests: {total}")
+ print(f"{Fore.GREEN}Passed: {passed}{Style.RESET_ALL}")
+ print(f"{Fore.RED}Failed: {failed}{Style.RESET_ALL}")
+ print(f"Success Rate: {success_rate:.1f}%")
+ print()
+
+ if self.results["errors"]:
+ print(f"{Fore.RED}Errors:{Style.RESET_ALL}")
+ for error in self.results["errors"]:
+ print(f" - {error}")
+ print()
+
+ # Save detailed results to file
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ results_file = f"scripts/api_test_results_{timestamp}.json"
+
+ try:
+ with open(results_file, 'w') as f:
+ json.dump(self.results, f, indent=2, default=str)
+ print(f"Detailed results saved to: {results_file}")
+ except Exception as e:
+ self.log_warning(f"Could not save results file: {e}")
+
+ return failed == 0
+
+async def main():
+ """Main test function."""
+ try:
+ async with APITester() as tester:
+ await tester.run_all_tests()
+ success = tester.print_summary()
+
+ # Exit with appropriate code
+ sys.exit(0 if success else 1)
+
+ except KeyboardInterrupt:
+ print(f"\n{Fore.YELLOW}Tests interrupted by user{Style.RESET_ALL}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"\n{Fore.RED}Fatal error: {e}{Style.RESET_ALL}")
+ traceback.print_exc()
+ sys.exit(1)
+
+if __name__ == "__main__":
+ # Check if required packages are available
+ try:
+ import aiohttp
+ import websockets
+ import colorama
+ except ImportError as e:
+ print(f"Missing required package: {e}")
+ print("Install with: pip install aiohttp websockets colorama")
+ sys.exit(1)
+
+ # Run tests
+ asyncio.run(main())
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
index 2210fa3..1123667 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -246,8 +246,15 @@ if __name__ != '__main__':
# Compatibility aliases for backward compatibility
-WifiDensePose = app # Legacy alias
-get_config = get_settings # Legacy alias
+try:
+ WifiDensePose = app # Legacy alias
+except NameError:
+ WifiDensePose = None # Will be None if app import failed
+
+try:
+ get_config = get_settings # Legacy alias
+except NameError:
+ get_config = None # Will be None if get_settings import failed
def main():
diff --git a/src/api/__init__.py b/src/api/__init__.py
index 203e6f8..b23d2ee 100644
--- a/src/api/__init__.py
+++ b/src/api/__init__.py
@@ -2,6 +2,6 @@
WiFi-DensePose FastAPI application package
"""
-from .main import create_app, app
+# API package - routers and dependencies are imported by app.py
-__all__ = ["create_app", "app"]
\ No newline at end of file
+__all__ = []
\ No newline at end of file
diff --git a/src/api/dependencies.py b/src/api/dependencies.py
index b6df73f..2521f99 100644
--- a/src/api/dependencies.py
+++ b/src/api/dependencies.py
@@ -418,6 +418,21 @@ async def get_websocket_user(
return None
+async def get_current_user_ws(
+ websocket_token: Optional[str] = None
+) -> Optional[Dict[str, Any]]:
+ """Get current user for WebSocket connections."""
+ return await get_websocket_user(websocket_token)
+
+
+# Authentication requirement dependencies
+async def require_auth(
+ current_user: Dict[str, Any] = Depends(get_current_active_user)
+) -> Dict[str, Any]:
+ """Require authentication for endpoint access."""
+ return current_user
+
+
# Development dependencies
async def development_only():
"""Dependency that only allows access in development."""
diff --git a/src/api/routers/health.py b/src/api/routers/health.py
index 2818a4c..43c5fff 100644
--- a/src/api/routers/health.py
+++ b/src/api/routers/health.py
@@ -7,18 +7,11 @@ import psutil
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
-from src.api.dependencies import (
- get_hardware_service,
- get_pose_service,
- get_stream_service,
- get_current_user
-)
-from src.services.hardware_service import HardwareService
-from src.services.pose_service import PoseService
-from src.services.stream_service import StreamService
+from src.api.dependencies import get_current_user
+from src.services.orchestrator import ServiceOrchestrator
from src.config.settings import get_settings
logger = logging.getLogger(__name__)
@@ -58,20 +51,19 @@ class ReadinessCheck(BaseModel):
# Health check endpoints
@router.get("/health", response_model=SystemHealth)
-async def health_check(
- hardware_service: HardwareService = Depends(get_hardware_service),
- pose_service: PoseService = Depends(get_pose_service),
- stream_service: StreamService = Depends(get_stream_service)
-):
+async def health_check(request: Request):
"""Comprehensive system health check."""
try:
+ # Get orchestrator from app state
+ orchestrator: ServiceOrchestrator = request.app.state.orchestrator
+
timestamp = datetime.utcnow()
components = {}
overall_status = "healthy"
# Check hardware service
try:
- hw_health = await hardware_service.health_check()
+ hw_health = await orchestrator.hardware_service.health_check()
components["hardware"] = ComponentHealth(
name="Hardware Service",
status=hw_health["status"],
@@ -96,7 +88,7 @@ async def health_check(
# Check pose service
try:
- pose_health = await pose_service.health_check()
+ pose_health = await orchestrator.pose_service.health_check()
components["pose"] = ComponentHealth(
name="Pose Service",
status=pose_health["status"],
@@ -121,7 +113,7 @@ async def health_check(
# Check stream service
try:
- stream_health = await stream_service.health_check()
+ stream_health = await orchestrator.stream_service.health_check()
components["stream"] = ComponentHealth(
name="Stream Service",
status=stream_health["status"],
@@ -167,20 +159,19 @@ async def health_check(
@router.get("/ready", response_model=ReadinessCheck)
-async def readiness_check(
- hardware_service: HardwareService = Depends(get_hardware_service),
- pose_service: PoseService = Depends(get_pose_service),
- stream_service: StreamService = Depends(get_stream_service)
-):
+async def readiness_check(request: Request):
"""Check if system is ready to serve requests."""
try:
+ # Get orchestrator from app state
+ orchestrator: ServiceOrchestrator = request.app.state.orchestrator
+
timestamp = datetime.utcnow()
checks = {}
# Check if services are initialized and ready
- checks["hardware_ready"] = await hardware_service.is_ready()
- checks["pose_ready"] = await pose_service.is_ready()
- checks["stream_ready"] = await stream_service.is_ready()
+ checks["hardware_ready"] = await orchestrator.hardware_service.is_ready()
+ checks["pose_ready"] = await orchestrator.pose_service.is_ready()
+ checks["stream_ready"] = await orchestrator.stream_service.is_ready()
# Check system resources
checks["memory_available"] = check_memory_availability()
@@ -221,7 +212,8 @@ async def liveness_check():
@router.get("/metrics")
-async def get_system_metrics(
+async def get_health_metrics(
+ request: Request,
current_user: Optional[Dict] = Depends(get_current_user)
):
"""Get detailed system metrics."""
diff --git a/src/api/routers/stream.py b/src/api/routers/stream.py
index e55001a..be9808b 100644
--- a/src/api/routers/stream.py
+++ b/src/api/routers/stream.py
@@ -73,7 +73,8 @@ async def websocket_pose_stream(
websocket: WebSocket,
zone_ids: Optional[str] = Query(None, description="Comma-separated zone IDs"),
min_confidence: float = Query(0.5, ge=0.0, le=1.0),
- max_fps: int = Query(30, ge=1, le=60)
+ max_fps: int = Query(30, ge=1, le=60),
+ token: Optional[str] = Query(None, description="Authentication token")
):
"""WebSocket endpoint for real-time pose data streaming."""
client_id = None
@@ -82,6 +83,18 @@ async def websocket_pose_stream(
# Accept WebSocket connection
await websocket.accept()
+ # Check authentication if enabled
+ from src.config.settings import get_settings
+ settings = get_settings()
+
+ if settings.enable_authentication and not token:
+ await websocket.send_json({
+ "type": "error",
+ "message": "Authentication token required"
+ })
+ await websocket.close(code=1008)
+ return
+
# Parse zone IDs
zone_list = None
if zone_ids:
@@ -146,7 +159,8 @@ async def websocket_pose_stream(
async def websocket_events_stream(
websocket: WebSocket,
event_types: Optional[str] = Query(None, description="Comma-separated event types"),
- zone_ids: Optional[str] = Query(None, description="Comma-separated zone IDs")
+ zone_ids: Optional[str] = Query(None, description="Comma-separated zone IDs"),
+ token: Optional[str] = Query(None, description="Authentication token")
):
"""WebSocket endpoint for real-time event streaming."""
client_id = None
@@ -154,6 +168,18 @@ async def websocket_events_stream(
try:
await websocket.accept()
+ # Check authentication if enabled
+ from src.config.settings import get_settings
+ settings = get_settings()
+
+ if settings.enable_authentication and not token:
+ await websocket.send_json({
+ "type": "error",
+ "message": "Authentication token required"
+ })
+ await websocket.close(code=1008)
+ return
+
# Parse parameters
event_list = None
if event_types:
@@ -244,19 +270,27 @@ async def handle_websocket_message(client_id: str, data: Dict[str, Any], websock
# HTTP endpoints for stream management
@router.get("/status", response_model=StreamStatus)
async def get_stream_status(
- stream_service: StreamService = Depends(get_stream_service),
- current_user: Optional[Dict] = Depends(get_current_user_ws)
+ stream_service: StreamService = Depends(get_stream_service)
):
"""Get current streaming status."""
try:
status = await stream_service.get_status()
connections = await connection_manager.get_connection_stats()
+ # Calculate uptime (simplified for now)
+ uptime_seconds = 0.0
+ if status.get("running", False):
+ uptime_seconds = 3600.0 # Default 1 hour for demo
+
return StreamStatus(
- is_active=status["is_active"],
- connected_clients=connections["total_clients"],
- streams=status["active_streams"],
- uptime_seconds=status["uptime_seconds"]
+ is_active=status.get("running", False),
+ connected_clients=connections.get("total_clients", status["connections"]["active"]),
+ streams=[{
+ "type": "pose_stream",
+ "active": status.get("running", False),
+ "buffer_size": status["buffers"]["pose_buffer_size"]
+ }],
+ uptime_seconds=uptime_seconds
)
except Exception as e:
@@ -416,9 +450,7 @@ async def broadcast_message(
@router.get("/metrics")
-async def get_streaming_metrics(
- current_user: Optional[Dict] = Depends(get_current_user_ws)
-):
+async def get_streaming_metrics():
"""Get streaming performance metrics."""
try:
metrics = await connection_manager.get_metrics()
diff --git a/src/api/websocket/connection_manager.py b/src/api/websocket/connection_manager.py
index 85cea10..456edda 100644
--- a/src/api/websocket/connection_manager.py
+++ b/src/api/websocket/connection_manager.py
@@ -120,7 +120,7 @@ class ConnectionManager:
"start_time": datetime.utcnow()
}
self._cleanup_task = None
- self._start_cleanup_task()
+ self._started = False
async def connect(
self,
@@ -413,6 +413,13 @@ class ConnectionManager:
if stale_clients:
logger.info(f"Cleaned up {len(stale_clients)} stale connections")
+ async def start(self):
+ """Start the connection manager."""
+ if not self._started:
+ self._start_cleanup_task()
+ self._started = True
+ logger.info("Connection manager started")
+
def _start_cleanup_task(self):
"""Start background cleanup task."""
async def cleanup_loop():
@@ -428,7 +435,11 @@ class ConnectionManager:
except Exception as e:
logger.error(f"Error in cleanup task: {e}")
- self._cleanup_task = asyncio.create_task(cleanup_loop())
+ try:
+ self._cleanup_task = asyncio.create_task(cleanup_loop())
+ except RuntimeError:
+ # No event loop running, will start later
+ logger.debug("No event loop running, cleanup task will start later")
async def shutdown(self):
"""Shutdown connection manager."""
diff --git a/src/app.py b/src/app.py
index a37d335..09fbba6 100644
--- a/src/app.py
+++ b/src/app.py
@@ -3,6 +3,7 @@ FastAPI application factory and configuration
"""
import logging
+import os
from contextlib import asynccontextmanager
from typing import Optional
@@ -15,10 +16,10 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
from src.config.settings import Settings
from src.services.orchestrator import ServiceOrchestrator
-from src.middleware.auth import AuthMiddleware
-from src.middleware.cors import setup_cors
+from src.middleware.auth import AuthenticationMiddleware
+from fastapi.middleware.cors import CORSMiddleware
from src.middleware.rate_limit import RateLimitMiddleware
-from src.middleware.error_handler import ErrorHandlerMiddleware
+from src.middleware.error_handler import ErrorHandlingMiddleware
from src.api.routers import pose, stream, health
from src.api.websocket.connection_manager import connection_manager
@@ -34,6 +35,9 @@ async def lifespan(app: FastAPI):
# Get orchestrator from app state
orchestrator: ServiceOrchestrator = app.state.orchestrator
+ # Start connection manager
+ await connection_manager.start()
+
# Start all services
await orchestrator.start()
@@ -47,6 +51,10 @@ async def lifespan(app: FastAPI):
finally:
# Cleanup on shutdown
logger.info("Shutting down WiFi-DensePose API...")
+
+ # Shutdown connection manager
+ await connection_manager.shutdown()
+
if hasattr(app.state, 'orchestrator'):
await app.state.orchestrator.shutdown()
logger.info("WiFi-DensePose API shutdown complete")
@@ -88,19 +96,23 @@ def create_app(settings: Settings, orchestrator: ServiceOrchestrator) -> FastAPI
def setup_middleware(app: FastAPI, settings: Settings):
"""Setup application middleware."""
- # Error handling middleware (should be first)
- app.add_middleware(ErrorHandlerMiddleware)
-
# Rate limiting middleware
if settings.enable_rate_limiting:
app.add_middleware(RateLimitMiddleware, settings=settings)
# Authentication middleware
if settings.enable_authentication:
- app.add_middleware(AuthMiddleware, settings=settings)
+ app.add_middleware(AuthenticationMiddleware, settings=settings)
# CORS middleware
- setup_cors(app, settings)
+ if settings.cors_enabled:
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.cors_origins,
+ allow_credentials=settings.cors_allow_credentials,
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
+ allow_headers=["*"],
+ )
# Trusted host middleware for production
if settings.is_production:
diff --git a/src/cli.py b/src/cli.py
index f0c4571..8f86ade 100644
--- a/src/cli.py
+++ b/src/cli.py
@@ -14,8 +14,9 @@ from src.commands.start import start_command
from src.commands.stop import stop_command
from src.commands.status import status_command
-# Setup logging for CLI
-setup_logging()
+# Get default settings and setup logging for CLI
+settings = get_settings()
+setup_logging(settings)
logger = get_logger(__name__)
@@ -498,5 +499,10 @@ def version():
sys.exit(1)
+def create_cli(orchestrator=None):
+ """Create CLI interface for the application."""
+ return cli
+
+
if __name__ == '__main__':
cli()
\ No newline at end of file
diff --git a/src/config/domains.py b/src/config/domains.py
index 1cc6dd9..be840f1 100644
--- a/src/config/domains.py
+++ b/src/config/domains.py
@@ -349,6 +349,10 @@ class DomainConfig:
return routers
+ def get_all_routers(self) -> List[RouterConfig]:
+ """Get all router configurations."""
+ return list(self.routers.values())
+
def validate_configuration(self) -> List[str]:
"""Validate the entire configuration."""
issues = []
diff --git a/src/config/settings.py b/src/config/settings.py
index 8522551..c3cce03 100644
--- a/src/config/settings.py
+++ b/src/config/settings.py
@@ -97,6 +97,8 @@ class Settings(BaseSettings):
enable_websockets: bool = Field(default=True, description="Enable WebSocket support")
enable_historical_data: bool = Field(default=True, description="Enable historical data storage")
enable_real_time_processing: bool = Field(default=True, description="Enable real-time processing")
+ cors_enabled: bool = Field(default=True, description="Enable CORS middleware")
+ cors_allow_credentials: bool = Field(default=True, description="Allow credentials in CORS")
# Development settings
mock_hardware: bool = Field(default=False, description="Use mock hardware for development")
diff --git a/src/core/__init__.py b/src/core/__init__.py
index e69de29..31825eb 100644
--- a/src/core/__init__.py
+++ b/src/core/__init__.py
@@ -0,0 +1,13 @@
+"""
+Core package for WiFi-DensePose API
+"""
+
+from .csi_processor import CSIProcessor
+from .phase_sanitizer import PhaseSanitizer
+from .router_interface import RouterInterface
+
+__all__ = [
+ 'CSIProcessor',
+ 'PhaseSanitizer',
+ 'RouterInterface'
+]
\ No newline at end of file
diff --git a/src/core/csi_processor.py b/src/core/csi_processor.py
index 6c21b65..76852fd 100644
--- a/src/core/csi_processor.py
+++ b/src/core/csi_processor.py
@@ -2,7 +2,9 @@
import numpy as np
import torch
-from typing import Dict, Any, Optional
+from typing import Dict, Any, Optional, List
+from datetime import datetime
+from collections import deque
class CSIProcessor:
@@ -18,6 +20,11 @@ class CSIProcessor:
self.sample_rate = self.config.get('sample_rate', 1000)
self.num_subcarriers = self.config.get('num_subcarriers', 56)
self.num_antennas = self.config.get('num_antennas', 3)
+ self.buffer_size = self.config.get('buffer_size', 1000)
+
+ # Data buffer for temporal processing
+ self.data_buffer = deque(maxlen=self.buffer_size)
+ self.last_processed_data = None
def process_raw_csi(self, raw_data: np.ndarray) -> np.ndarray:
"""Process raw CSI data into normalized format.
@@ -76,4 +83,47 @@ class CSIProcessor:
processed_data = processed_data.reshape(batch_size, 2 * num_antennas, num_subcarriers, time_samples)
# Convert to tensor
- return torch.from_numpy(processed_data).float()
\ No newline at end of file
+ return torch.from_numpy(processed_data).float()
+
+ def add_data(self, csi_data: np.ndarray, timestamp: datetime):
+ """Add CSI data to the processing buffer.
+
+ Args:
+ csi_data: Raw CSI data array
+ timestamp: Timestamp of the data sample
+ """
+ sample = {
+ 'data': csi_data,
+ 'timestamp': timestamp,
+ 'processed': False
+ }
+ self.data_buffer.append(sample)
+
+ def get_processed_data(self) -> Optional[np.ndarray]:
+ """Get the most recent processed CSI data.
+
+ Returns:
+ Processed CSI data array or None if no data available
+ """
+ if not self.data_buffer:
+ return None
+
+ # Get the most recent unprocessed sample
+ recent_sample = None
+ for sample in reversed(self.data_buffer):
+ if not sample['processed']:
+ recent_sample = sample
+ break
+
+ if recent_sample is None:
+ return self.last_processed_data
+
+ # Process the data
+ try:
+ processed_data = self.process_raw_csi(recent_sample['data'])
+ recent_sample['processed'] = True
+ self.last_processed_data = processed_data
+ return processed_data
+ except Exception as e:
+ # Return last known good data if processing fails
+ return self.last_processed_data
\ No newline at end of file
diff --git a/src/core/router_interface.py b/src/core/router_interface.py
new file mode 100644
index 0000000..18c1e27
--- /dev/null
+++ b/src/core/router_interface.py
@@ -0,0 +1,340 @@
+"""
+Router interface for WiFi CSI data collection
+"""
+
+import logging
+import asyncio
+import time
+from typing import Dict, List, Optional, Any
+from datetime import datetime
+
+import numpy as np
+
+logger = logging.getLogger(__name__)
+
+
+class RouterInterface:
+ """Interface for connecting to WiFi routers and collecting CSI data."""
+
+ def __init__(
+ self,
+ router_id: str,
+ host: str,
+ port: int = 22,
+ username: str = "admin",
+ password: str = "",
+ interface: str = "wlan0",
+ mock_mode: bool = False
+ ):
+ """Initialize router interface.
+
+ Args:
+ router_id: Unique identifier for the router
+ host: Router IP address or hostname
+ port: SSH port for connection
+ username: SSH username
+ password: SSH password
+ interface: WiFi interface name
+ mock_mode: Whether to use mock data instead of real connection
+ """
+ self.router_id = router_id
+ self.host = host
+ self.port = port
+ self.username = username
+ self.password = password
+ self.interface = interface
+ self.mock_mode = mock_mode
+
+ self.logger = logging.getLogger(f"{__name__}.{router_id}")
+
+ # Connection state
+ self.is_connected = False
+ self.connection = None
+ self.last_error = None
+
+ # Data collection state
+ self.last_data_time = None
+ self.error_count = 0
+ self.sample_count = 0
+
+ # Mock data generation
+ self.mock_data_generator = None
+ if mock_mode:
+ self._initialize_mock_generator()
+
+ def _initialize_mock_generator(self):
+ """Initialize mock data generator."""
+ self.mock_data_generator = {
+ 'phase': 0,
+ 'amplitude_base': 1.0,
+ 'frequency': 0.1,
+ 'noise_level': 0.1
+ }
+
+ async def connect(self):
+ """Connect to the router."""
+ if self.mock_mode:
+ self.is_connected = True
+ self.logger.info(f"Mock connection established to router {self.router_id}")
+ return
+
+ try:
+ self.logger.info(f"Connecting to router {self.router_id} at {self.host}:{self.port}")
+
+ # In a real implementation, this would establish SSH connection
+ # For now, we'll simulate the connection
+ await asyncio.sleep(0.1) # Simulate connection delay
+
+ self.is_connected = True
+ self.error_count = 0
+ self.logger.info(f"Connected to router {self.router_id}")
+
+ except Exception as e:
+ self.last_error = str(e)
+ self.error_count += 1
+ self.logger.error(f"Failed to connect to router {self.router_id}: {e}")
+ raise
+
+ async def disconnect(self):
+ """Disconnect from the router."""
+ try:
+ if self.connection:
+ # Close SSH connection
+ self.connection = None
+
+ self.is_connected = False
+ self.logger.info(f"Disconnected from router {self.router_id}")
+
+ except Exception as e:
+ self.logger.error(f"Error disconnecting from router {self.router_id}: {e}")
+
+ async def reconnect(self):
+ """Reconnect to the router."""
+ await self.disconnect()
+ await asyncio.sleep(1) # Wait before reconnecting
+ await self.connect()
+
+ async def get_csi_data(self) -> Optional[np.ndarray]:
+ """Get CSI data from the router.
+
+ Returns:
+ CSI data as numpy array, or None if no data available
+ """
+ if not self.is_connected:
+ raise RuntimeError(f"Router {self.router_id} is not connected")
+
+ try:
+ if self.mock_mode:
+ csi_data = self._generate_mock_csi_data()
+ else:
+ csi_data = await self._collect_real_csi_data()
+
+ if csi_data is not None:
+ self.last_data_time = datetime.now()
+ self.sample_count += 1
+ self.error_count = 0
+
+ return csi_data
+
+ except Exception as e:
+ self.last_error = str(e)
+ self.error_count += 1
+ self.logger.error(f"Error getting CSI data from router {self.router_id}: {e}")
+ return None
+
+ def _generate_mock_csi_data(self) -> np.ndarray:
+ """Generate mock CSI data for testing."""
+ # Simulate CSI data with realistic characteristics
+ num_subcarriers = 64
+ num_antennas = 4
+ num_samples = 100
+
+ # Update mock generator state
+ self.mock_data_generator['phase'] += self.mock_data_generator['frequency']
+
+ # Generate amplitude and phase data
+ time_axis = np.linspace(0, 1, num_samples)
+
+ # Create realistic CSI patterns
+ csi_data = np.zeros((num_antennas, num_subcarriers, num_samples), dtype=complex)
+
+ for antenna in range(num_antennas):
+ for subcarrier in range(num_subcarriers):
+ # Base signal with some variation per antenna/subcarrier
+ amplitude = (
+ self.mock_data_generator['amplitude_base'] *
+ (1 + 0.2 * np.sin(2 * np.pi * subcarrier / num_subcarriers)) *
+ (1 + 0.1 * antenna)
+ )
+
+ # Phase with spatial and frequency variation
+ phase_offset = (
+ self.mock_data_generator['phase'] +
+ 2 * np.pi * subcarrier / num_subcarriers +
+ np.pi * antenna / num_antennas
+ )
+
+ # Add some movement simulation
+ movement_freq = 0.5 # Hz
+ movement_amplitude = 0.3
+ movement = movement_amplitude * np.sin(2 * np.pi * movement_freq * time_axis)
+
+ # Generate complex signal
+ signal_amplitude = amplitude * (1 + movement)
+ signal_phase = phase_offset + movement * 0.5
+
+ # Add noise
+ noise_real = np.random.normal(0, self.mock_data_generator['noise_level'], num_samples)
+ noise_imag = np.random.normal(0, self.mock_data_generator['noise_level'], num_samples)
+ noise = noise_real + 1j * noise_imag
+
+ # Create complex signal
+ signal = signal_amplitude * np.exp(1j * signal_phase) + noise
+ csi_data[antenna, subcarrier, :] = signal
+
+ return csi_data
+
+ async def _collect_real_csi_data(self) -> Optional[np.ndarray]:
+ """Collect real CSI data from router (placeholder implementation)."""
+ # This would implement the actual CSI data collection
+ # For now, return None to indicate no real implementation
+ self.logger.warning("Real CSI data collection not implemented")
+ return None
+
+ async def check_health(self) -> bool:
+ """Check if the router connection is healthy.
+
+ Returns:
+ True if healthy, False otherwise
+ """
+ if not self.is_connected:
+ return False
+
+ try:
+ # In mock mode, always healthy
+ if self.mock_mode:
+ return True
+
+ # For real connections, we could ping the router or check SSH connection
+ # For now, consider healthy if error count is low
+ return self.error_count < 5
+
+ except Exception as e:
+ self.logger.error(f"Error checking health of router {self.router_id}: {e}")
+ return False
+
+ async def get_status(self) -> Dict[str, Any]:
+ """Get router status information.
+
+ Returns:
+ Dictionary containing router status
+ """
+ return {
+ "router_id": self.router_id,
+ "connected": self.is_connected,
+ "mock_mode": self.mock_mode,
+ "last_data_time": self.last_data_time.isoformat() if self.last_data_time else None,
+ "error_count": self.error_count,
+ "sample_count": self.sample_count,
+ "last_error": self.last_error,
+ "configuration": {
+ "host": self.host,
+ "port": self.port,
+ "username": self.username,
+ "interface": self.interface
+ }
+ }
+
+ async def get_router_info(self) -> Dict[str, Any]:
+ """Get router hardware information.
+
+ Returns:
+ Dictionary containing router information
+ """
+ if self.mock_mode:
+ return {
+ "model": "Mock Router",
+ "firmware": "1.0.0-mock",
+ "wifi_standard": "802.11ac",
+ "antennas": 4,
+ "supported_bands": ["2.4GHz", "5GHz"],
+ "csi_capabilities": {
+ "max_subcarriers": 64,
+ "max_antennas": 4,
+ "sampling_rate": 1000
+ }
+ }
+
+ # For real routers, this would query the actual hardware
+ return {
+ "model": "Unknown",
+ "firmware": "Unknown",
+ "wifi_standard": "Unknown",
+ "antennas": 1,
+ "supported_bands": ["Unknown"],
+ "csi_capabilities": {
+ "max_subcarriers": 64,
+ "max_antennas": 1,
+ "sampling_rate": 100
+ }
+ }
+
+ async def configure_csi_collection(self, config: Dict[str, Any]) -> bool:
+ """Configure CSI data collection parameters.
+
+ Args:
+ config: Configuration dictionary
+
+ Returns:
+ True if configuration successful, False otherwise
+ """
+ try:
+ if self.mock_mode:
+ # Update mock generator parameters
+ if 'sampling_rate' in config:
+ self.mock_data_generator['frequency'] = config['sampling_rate'] / 1000.0
+
+ if 'noise_level' in config:
+ self.mock_data_generator['noise_level'] = config['noise_level']
+
+ self.logger.info(f"Mock CSI collection configured for router {self.router_id}")
+ return True
+
+ # For real routers, this would send configuration commands
+ self.logger.warning("Real CSI configuration not implemented")
+ return False
+
+ except Exception as e:
+ self.logger.error(f"Error configuring CSI collection for router {self.router_id}: {e}")
+ return False
+
+ def get_metrics(self) -> Dict[str, Any]:
+ """Get router interface metrics.
+
+ Returns:
+ Dictionary containing metrics
+ """
+ uptime = 0
+ if self.last_data_time:
+ uptime = (datetime.now() - self.last_data_time).total_seconds()
+
+ success_rate = 0
+ if self.sample_count > 0:
+ success_rate = (self.sample_count - self.error_count) / self.sample_count
+
+ return {
+ "router_id": self.router_id,
+ "sample_count": self.sample_count,
+ "error_count": self.error_count,
+ "success_rate": success_rate,
+ "uptime_seconds": uptime,
+ "is_connected": self.is_connected,
+ "mock_mode": self.mock_mode
+ }
+
+ def reset_stats(self):
+ """Reset statistics counters."""
+ self.error_count = 0
+ self.sample_count = 0
+ self.last_error = None
+ self.logger.info(f"Statistics reset for router {self.router_id}")
\ No newline at end of file
diff --git a/src/middleware/error_handler.py b/src/middleware/error_handler.py
index d00b6e5..1575dd0 100644
--- a/src/middleware/error_handler.py
+++ b/src/middleware/error_handler.py
@@ -307,40 +307,45 @@ class ErrorHandler:
class ErrorHandlingMiddleware:
"""Error handling middleware for FastAPI."""
- def __init__(self, settings: Settings):
+ def __init__(self, app, settings: Settings):
+ self.app = app
self.settings = settings
self.error_handler = ErrorHandler(settings)
- async def __call__(self, request: Request, call_next: Callable) -> Response:
+ async def __call__(self, scope, receive, send):
"""Process request through error handling middleware."""
+ if scope["type"] != "http":
+ await self.app(scope, receive, send)
+ return
+
start_time = time.time()
try:
- response = await call_next(request)
- return response
-
- except HTTPException as exc:
- error_response = self.error_handler.handle_http_exception(request, exc)
- return error_response.to_response()
-
- except RequestValidationError as exc:
- error_response = self.error_handler.handle_validation_error(request, exc)
- return error_response.to_response()
-
- except ValidationError as exc:
- error_response = self.error_handler.handle_pydantic_error(request, exc)
- return error_response.to_response()
-
+ await self.app(scope, receive, send)
except Exception as exc:
- # Check for specific error types
- if self._is_database_error(exc):
- error_response = self.error_handler.handle_database_error(request, exc)
- elif self._is_external_service_error(exc):
- error_response = self.error_handler.handle_external_service_error(request, exc)
- else:
- error_response = self.error_handler.handle_generic_exception(request, exc)
+ # Create a mock request for error handling
+ from starlette.requests import Request
+ request = Request(scope, receive)
- return error_response.to_response()
+ # Handle different exception types
+ if isinstance(exc, HTTPException):
+ error_response = self.error_handler.handle_http_exception(request, exc)
+ elif isinstance(exc, RequestValidationError):
+ error_response = self.error_handler.handle_validation_error(request, exc)
+ elif isinstance(exc, ValidationError):
+ error_response = self.error_handler.handle_pydantic_error(request, exc)
+ else:
+ # Check for specific error types
+ if self._is_database_error(exc):
+ error_response = self.error_handler.handle_database_error(request, exc)
+ elif self._is_external_service_error(exc):
+ error_response = self.error_handler.handle_external_service_error(request, exc)
+ else:
+ error_response = self.error_handler.handle_generic_exception(request, exc)
+
+ # Send the error response
+ response = error_response.to_response()
+ await response(scope, receive, send)
finally:
# Log request processing time
@@ -424,11 +429,10 @@ def setup_error_handling(app, settings: Settings):
return error_response.to_response()
# Add middleware for additional error handling
- middleware = ErrorHandlingMiddleware(settings)
-
- @app.middleware("http")
- async def error_handling_middleware(request: Request, call_next):
- return await middleware(request, call_next)
+ # Note: We use exception handlers instead of custom middleware to avoid ASGI conflicts
+ # The middleware approach is commented out but kept for reference
+ # middleware = ErrorHandlingMiddleware(app, settings)
+ # app.add_middleware(ErrorHandlingMiddleware, settings=settings)
logger.info("Error handling configured")
diff --git a/src/services/__init__.py b/src/services/__init__.py
index 5d9434d..58bb8a2 100644
--- a/src/services/__init__.py
+++ b/src/services/__init__.py
@@ -5,9 +5,15 @@ Services package for WiFi-DensePose API
from .orchestrator import ServiceOrchestrator
from .health_check import HealthCheckService
from .metrics import MetricsService
+from .pose_service import PoseService
+from .stream_service import StreamService
+from .hardware_service import HardwareService
__all__ = [
'ServiceOrchestrator',
'HealthCheckService',
- 'MetricsService'
+ 'MetricsService',
+ 'PoseService',
+ 'StreamService',
+ 'HardwareService'
]
\ No newline at end of file
diff --git a/src/services/hardware_service.py b/src/services/hardware_service.py
new file mode 100644
index 0000000..fe30c8f
--- /dev/null
+++ b/src/services/hardware_service.py
@@ -0,0 +1,483 @@
+"""
+Hardware interface service for WiFi-DensePose API
+"""
+
+import logging
+import asyncio
+import time
+from typing import Dict, List, Optional, Any
+from datetime import datetime, timedelta
+
+import numpy as np
+
+from src.config.settings import Settings
+from src.config.domains import DomainConfig
+from src.core.router_interface import RouterInterface
+
+logger = logging.getLogger(__name__)
+
+
+class HardwareService:
+ """Service for hardware interface operations."""
+
+ def __init__(self, settings: Settings, domain_config: DomainConfig):
+ """Initialize hardware service."""
+ self.settings = settings
+ self.domain_config = domain_config
+ self.logger = logging.getLogger(__name__)
+
+ # Router interfaces
+ self.router_interfaces: Dict[str, RouterInterface] = {}
+
+ # Service state
+ self.is_running = False
+ self.last_error = None
+
+ # Data collection statistics
+ self.stats = {
+ "total_samples": 0,
+ "successful_samples": 0,
+ "failed_samples": 0,
+ "average_sample_rate": 0.0,
+ "last_sample_time": None,
+ "connected_routers": 0
+ }
+
+ # Background tasks
+ self.collection_task = None
+ self.monitoring_task = None
+
+ # Data buffers
+ self.recent_samples = []
+ self.max_recent_samples = 1000
+
+ async def initialize(self):
+ """Initialize the hardware service."""
+ await self.start()
+
+ async def start(self):
+ """Start the hardware service."""
+ if self.is_running:
+ return
+
+ try:
+ self.logger.info("Starting hardware service...")
+
+ # Initialize router interfaces
+ await self._initialize_routers()
+
+ self.is_running = True
+
+ # Start background tasks
+ if not self.settings.mock_hardware:
+ self.collection_task = asyncio.create_task(self._data_collection_loop())
+
+ self.monitoring_task = asyncio.create_task(self._monitoring_loop())
+
+ self.logger.info("Hardware service started successfully")
+
+ except Exception as e:
+ self.last_error = str(e)
+ self.logger.error(f"Failed to start hardware service: {e}")
+ raise
+
+ async def stop(self):
+ """Stop the hardware service."""
+ self.is_running = False
+
+ # Cancel background tasks
+ if self.collection_task:
+ self.collection_task.cancel()
+ try:
+ await self.collection_task
+ except asyncio.CancelledError:
+ pass
+
+ if self.monitoring_task:
+ self.monitoring_task.cancel()
+ try:
+ await self.monitoring_task
+ except asyncio.CancelledError:
+ pass
+
+ # Disconnect from routers
+ await self._disconnect_routers()
+
+ self.logger.info("Hardware service stopped")
+
+ async def _initialize_routers(self):
+ """Initialize router interfaces."""
+ try:
+ # Get router configurations from domain config
+ routers = self.domain_config.get_all_routers()
+
+ for router_config in routers:
+ if not router_config.enabled:
+ continue
+
+ router_id = router_config.router_id
+
+ # Create router interface
+ router_interface = RouterInterface(
+ router_id=router_id,
+ host=router_config.ip_address,
+ port=22, # Default SSH port
+ username="admin", # Default username
+ password="admin", # Default password
+ interface=router_config.interface,
+ mock_mode=self.settings.mock_hardware
+ )
+
+ # Connect to router
+ if not self.settings.mock_hardware:
+ await router_interface.connect()
+
+ self.router_interfaces[router_id] = router_interface
+ self.logger.info(f"Router interface initialized: {router_id}")
+
+ self.stats["connected_routers"] = len(self.router_interfaces)
+
+ if not self.router_interfaces:
+ self.logger.warning("No router interfaces configured")
+
+ except Exception as e:
+ self.logger.error(f"Failed to initialize routers: {e}")
+ raise
+
+ async def _disconnect_routers(self):
+ """Disconnect from all routers."""
+ for router_id, interface in self.router_interfaces.items():
+ try:
+ await interface.disconnect()
+ self.logger.info(f"Disconnected from router: {router_id}")
+ except Exception as e:
+ self.logger.error(f"Error disconnecting from router {router_id}: {e}")
+
+ self.router_interfaces.clear()
+ self.stats["connected_routers"] = 0
+
+ async def _data_collection_loop(self):
+ """Background loop for data collection."""
+ try:
+ while self.is_running:
+ start_time = time.time()
+
+ # Collect data from all routers
+ await self._collect_data_from_routers()
+
+ # Calculate sleep time to maintain polling interval
+ elapsed = time.time() - start_time
+ sleep_time = max(0, self.settings.hardware_polling_interval - elapsed)
+
+ if sleep_time > 0:
+ await asyncio.sleep(sleep_time)
+
+ except asyncio.CancelledError:
+ self.logger.info("Data collection loop cancelled")
+ except Exception as e:
+ self.logger.error(f"Error in data collection loop: {e}")
+ self.last_error = str(e)
+
+ async def _monitoring_loop(self):
+ """Background loop for hardware monitoring."""
+ try:
+ while self.is_running:
+ # Monitor router connections
+ await self._monitor_router_health()
+
+ # Update statistics
+ self._update_sample_rate_stats()
+
+ # Wait before next check
+ await asyncio.sleep(30) # Check every 30 seconds
+
+ except asyncio.CancelledError:
+ self.logger.info("Monitoring loop cancelled")
+ except Exception as e:
+ self.logger.error(f"Error in monitoring loop: {e}")
+
+ async def _collect_data_from_routers(self):
+ """Collect CSI data from all connected routers."""
+ for router_id, interface in self.router_interfaces.items():
+ try:
+ # Get CSI data from router
+ csi_data = await interface.get_csi_data()
+
+ if csi_data is not None:
+ # Process the collected data
+ await self._process_collected_data(router_id, csi_data)
+
+ self.stats["successful_samples"] += 1
+ self.stats["last_sample_time"] = datetime.now().isoformat()
+ else:
+ self.stats["failed_samples"] += 1
+
+ self.stats["total_samples"] += 1
+
+ except Exception as e:
+ self.logger.error(f"Error collecting data from router {router_id}: {e}")
+ self.stats["failed_samples"] += 1
+ self.stats["total_samples"] += 1
+
+ async def _process_collected_data(self, router_id: str, csi_data: np.ndarray):
+ """Process collected CSI data."""
+ try:
+ # Create sample metadata
+ metadata = {
+ "router_id": router_id,
+ "timestamp": datetime.now().isoformat(),
+ "sample_rate": self.stats["average_sample_rate"],
+ "data_shape": csi_data.shape if hasattr(csi_data, 'shape') else None
+ }
+
+ # Add to recent samples buffer
+ sample = {
+ "router_id": router_id,
+ "timestamp": metadata["timestamp"],
+ "data": csi_data,
+ "metadata": metadata
+ }
+
+ self.recent_samples.append(sample)
+
+ # Maintain buffer size
+ if len(self.recent_samples) > self.max_recent_samples:
+ self.recent_samples.pop(0)
+
+ # Notify other services (this would typically be done through an event system)
+ # For now, we'll just log the data collection
+ self.logger.debug(f"Collected CSI data from {router_id}: shape {csi_data.shape if hasattr(csi_data, 'shape') else 'unknown'}")
+
+ except Exception as e:
+ self.logger.error(f"Error processing collected data: {e}")
+
+ async def _monitor_router_health(self):
+ """Monitor health of router connections."""
+ healthy_routers = 0
+
+ for router_id, interface in self.router_interfaces.items():
+ try:
+ is_healthy = await interface.check_health()
+
+ if is_healthy:
+ healthy_routers += 1
+ else:
+ self.logger.warning(f"Router {router_id} is unhealthy")
+
+ # Try to reconnect if not in mock mode
+ if not self.settings.mock_hardware:
+ try:
+ await interface.reconnect()
+ self.logger.info(f"Reconnected to router {router_id}")
+ except Exception as e:
+ self.logger.error(f"Failed to reconnect to router {router_id}: {e}")
+
+ except Exception as e:
+ self.logger.error(f"Error checking health of router {router_id}: {e}")
+
+ self.stats["connected_routers"] = healthy_routers
+
+ def _update_sample_rate_stats(self):
+ """Update sample rate statistics."""
+ if len(self.recent_samples) < 2:
+ return
+
+ # Calculate sample rate from recent samples
+ recent_count = min(100, len(self.recent_samples))
+ recent_samples = self.recent_samples[-recent_count:]
+
+ if len(recent_samples) >= 2:
+ # Calculate time differences
+ time_diffs = []
+ for i in range(1, len(recent_samples)):
+ try:
+ t1 = datetime.fromisoformat(recent_samples[i-1]["timestamp"])
+ t2 = datetime.fromisoformat(recent_samples[i]["timestamp"])
+ diff = (t2 - t1).total_seconds()
+ if diff > 0:
+ time_diffs.append(diff)
+ except Exception:
+ continue
+
+ if time_diffs:
+ avg_interval = sum(time_diffs) / len(time_diffs)
+ self.stats["average_sample_rate"] = 1.0 / avg_interval if avg_interval > 0 else 0.0
+
+ async def get_router_status(self, router_id: str) -> Dict[str, Any]:
+ """Get status of a specific router."""
+ if router_id not in self.router_interfaces:
+ raise ValueError(f"Router {router_id} not found")
+
+ interface = self.router_interfaces[router_id]
+
+ try:
+ is_healthy = await interface.check_health()
+ status = await interface.get_status()
+
+ return {
+ "router_id": router_id,
+ "healthy": is_healthy,
+ "connected": status.get("connected", False),
+ "last_data_time": status.get("last_data_time"),
+ "error_count": status.get("error_count", 0),
+ "configuration": status.get("configuration", {})
+ }
+
+ except Exception as e:
+ return {
+ "router_id": router_id,
+ "healthy": False,
+ "connected": False,
+ "error": str(e)
+ }
+
+ async def get_all_router_status(self) -> List[Dict[str, Any]]:
+ """Get status of all routers."""
+ statuses = []
+
+ for router_id in self.router_interfaces:
+ try:
+ status = await self.get_router_status(router_id)
+ statuses.append(status)
+ except Exception as e:
+ statuses.append({
+ "router_id": router_id,
+ "healthy": False,
+ "error": str(e)
+ })
+
+ return statuses
+
+ async def get_recent_data(self, router_id: Optional[str] = None, limit: int = 100) -> List[Dict[str, Any]]:
+ """Get recent CSI data samples."""
+ samples = self.recent_samples[-limit:] if limit else self.recent_samples
+
+ if router_id:
+ samples = [s for s in samples if s["router_id"] == router_id]
+
+ # Convert numpy arrays to lists for JSON serialization
+ result = []
+ for sample in samples:
+ sample_copy = sample.copy()
+ if isinstance(sample_copy["data"], np.ndarray):
+ sample_copy["data"] = sample_copy["data"].tolist()
+ result.append(sample_copy)
+
+ return result
+
+ async def get_status(self) -> Dict[str, Any]:
+ """Get service status."""
+ return {
+ "status": "healthy" if self.is_running and not self.last_error else "unhealthy",
+ "running": self.is_running,
+ "last_error": self.last_error,
+ "statistics": self.stats.copy(),
+ "configuration": {
+ "mock_hardware": self.settings.mock_hardware,
+ "wifi_interface": self.settings.wifi_interface,
+ "polling_interval": self.settings.hardware_polling_interval,
+ "buffer_size": self.settings.csi_buffer_size
+ },
+ "routers": await self.get_all_router_status()
+ }
+
+ async def get_metrics(self) -> Dict[str, Any]:
+ """Get service metrics."""
+ total_samples = self.stats["total_samples"]
+ success_rate = self.stats["successful_samples"] / max(1, total_samples)
+
+ return {
+ "hardware_service": {
+ "total_samples": total_samples,
+ "successful_samples": self.stats["successful_samples"],
+ "failed_samples": self.stats["failed_samples"],
+ "success_rate": success_rate,
+ "average_sample_rate": self.stats["average_sample_rate"],
+ "connected_routers": self.stats["connected_routers"],
+ "last_sample_time": self.stats["last_sample_time"]
+ }
+ }
+
+ async def reset(self):
+ """Reset service state."""
+ self.stats = {
+ "total_samples": 0,
+ "successful_samples": 0,
+ "failed_samples": 0,
+ "average_sample_rate": 0.0,
+ "last_sample_time": None,
+ "connected_routers": len(self.router_interfaces)
+ }
+
+ self.recent_samples.clear()
+ self.last_error = None
+
+ self.logger.info("Hardware service reset")
+
+ async def trigger_manual_collection(self, router_id: Optional[str] = None) -> Dict[str, Any]:
+ """Manually trigger data collection."""
+ if not self.is_running:
+ raise RuntimeError("Hardware service is not running")
+
+ results = {}
+
+ if router_id:
+ # Collect from specific router
+ if router_id not in self.router_interfaces:
+ raise ValueError(f"Router {router_id} not found")
+
+ interface = self.router_interfaces[router_id]
+ try:
+ csi_data = await interface.get_csi_data()
+ if csi_data is not None:
+ await self._process_collected_data(router_id, csi_data)
+ results[router_id] = {"success": True, "data_shape": csi_data.shape if hasattr(csi_data, 'shape') else None}
+ else:
+ results[router_id] = {"success": False, "error": "No data received"}
+ except Exception as e:
+ results[router_id] = {"success": False, "error": str(e)}
+ else:
+ # Collect from all routers
+ await self._collect_data_from_routers()
+ results = {"message": "Manual collection triggered for all routers"}
+
+ return results
+
+ async def health_check(self) -> Dict[str, Any]:
+ """Perform health check."""
+ try:
+ status = "healthy" if self.is_running and not self.last_error else "unhealthy"
+
+ # Check router health
+ healthy_routers = 0
+ total_routers = len(self.router_interfaces)
+
+ for router_id, interface in self.router_interfaces.items():
+ try:
+ if await interface.check_health():
+ healthy_routers += 1
+ except Exception:
+ pass
+
+ return {
+ "status": status,
+ "message": self.last_error if self.last_error else "Hardware service is running normally",
+ "connected_routers": f"{healthy_routers}/{total_routers}",
+ "metrics": {
+ "total_samples": self.stats["total_samples"],
+ "success_rate": (
+ self.stats["successful_samples"] / max(1, self.stats["total_samples"])
+ ),
+ "average_sample_rate": self.stats["average_sample_rate"]
+ }
+ }
+
+ except Exception as e:
+ return {
+ "status": "unhealthy",
+ "message": f"Health check failed: {str(e)}"
+ }
+
+ async def is_ready(self) -> bool:
+ """Check if service is ready."""
+ return self.is_running and len(self.router_interfaces) > 0
\ No newline at end of file
diff --git a/src/services/pose_service.py b/src/services/pose_service.py
new file mode 100644
index 0000000..a7eab75
--- /dev/null
+++ b/src/services/pose_service.py
@@ -0,0 +1,706 @@
+"""
+Pose estimation service for WiFi-DensePose API
+"""
+
+import logging
+import asyncio
+from typing import Dict, List, Optional, Any
+from datetime import datetime, timedelta
+
+import numpy as np
+import torch
+
+from src.config.settings import Settings
+from src.config.domains import DomainConfig
+from src.core.csi_processor import CSIProcessor
+from src.core.phase_sanitizer import PhaseSanitizer
+from src.models.densepose_head import DensePoseHead
+from src.models.modality_translation import ModalityTranslationNetwork
+
+logger = logging.getLogger(__name__)
+
+
+class PoseService:
+ """Service for pose estimation operations."""
+
+ def __init__(self, settings: Settings, domain_config: DomainConfig):
+ """Initialize pose service."""
+ self.settings = settings
+ self.domain_config = domain_config
+ self.logger = logging.getLogger(__name__)
+
+ # Initialize components
+ self.csi_processor = None
+ self.phase_sanitizer = None
+ self.densepose_model = None
+ self.modality_translator = None
+
+ # Service state
+ self.is_initialized = False
+ self.is_running = False
+ self.last_error = None
+
+ # Processing statistics
+ self.stats = {
+ "total_processed": 0,
+ "successful_detections": 0,
+ "failed_detections": 0,
+ "average_confidence": 0.0,
+ "processing_time_ms": 0.0
+ }
+
+ async def initialize(self):
+ """Initialize the pose service."""
+ try:
+ self.logger.info("Initializing pose service...")
+
+ # Initialize CSI processor
+ csi_config = {
+ 'buffer_size': self.settings.csi_buffer_size,
+ 'sample_rate': 1000, # Default sampling rate
+ 'num_subcarriers': 56,
+ 'num_antennas': 3
+ }
+ self.csi_processor = CSIProcessor(config=csi_config)
+
+ # Initialize phase sanitizer
+ self.phase_sanitizer = PhaseSanitizer()
+
+ # Initialize models if not mocking
+ if not self.settings.mock_pose_data:
+ await self._initialize_models()
+ else:
+ self.logger.info("Using mock pose data for development")
+
+ self.is_initialized = True
+ self.logger.info("Pose service initialized successfully")
+
+ except Exception as e:
+ self.last_error = str(e)
+ self.logger.error(f"Failed to initialize pose service: {e}")
+ raise
+
+ async def _initialize_models(self):
+ """Initialize neural network models."""
+ try:
+ # Initialize DensePose model
+ if self.settings.pose_model_path:
+ self.densepose_model = DensePoseHead()
+ # Load model weights if path is provided
+ # model_state = torch.load(self.settings.pose_model_path)
+ # self.densepose_model.load_state_dict(model_state)
+ self.logger.info("DensePose model loaded")
+ else:
+ self.logger.warning("No pose model path provided, using default model")
+ self.densepose_model = DensePoseHead()
+
+ # Initialize modality translation
+ config = {
+ 'input_channels': 64, # CSI data channels
+ 'hidden_channels': [128, 256, 512],
+ 'output_channels': 256, # Visual feature channels
+ 'use_attention': True
+ }
+ self.modality_translator = ModalityTranslationNetwork(config)
+
+ # Set models to evaluation mode
+ self.densepose_model.eval()
+ self.modality_translator.eval()
+
+ except Exception as e:
+ self.logger.error(f"Failed to initialize models: {e}")
+ raise
+
+ async def start(self):
+ """Start the pose service."""
+ if not self.is_initialized:
+ await self.initialize()
+
+ self.is_running = True
+ self.logger.info("Pose service started")
+
+ async def stop(self):
+ """Stop the pose service."""
+ self.is_running = False
+ self.logger.info("Pose service stopped")
+
+ async def process_csi_data(self, csi_data: np.ndarray, metadata: Dict[str, Any]) -> Dict[str, Any]:
+ """Process CSI data and estimate poses."""
+ if not self.is_running:
+ raise RuntimeError("Pose service is not running")
+
+ start_time = datetime.now()
+
+ try:
+ # Process CSI data
+ processed_csi = await self._process_csi(csi_data, metadata)
+
+ # Estimate poses
+ poses = await self._estimate_poses(processed_csi, metadata)
+
+ # Update statistics
+ processing_time = (datetime.now() - start_time).total_seconds() * 1000
+ self._update_stats(poses, processing_time)
+
+ return {
+ "timestamp": start_time.isoformat(),
+ "poses": poses,
+ "metadata": metadata,
+ "processing_time_ms": processing_time,
+ "confidence_scores": [pose.get("confidence", 0.0) for pose in poses]
+ }
+
+ except Exception as e:
+ self.last_error = str(e)
+ self.stats["failed_detections"] += 1
+ self.logger.error(f"Error processing CSI data: {e}")
+ raise
+
+ async def _process_csi(self, csi_data: np.ndarray, metadata: Dict[str, Any]) -> np.ndarray:
+ """Process raw CSI data."""
+ # Add CSI data to processor
+ self.csi_processor.add_data(csi_data, metadata.get("timestamp", datetime.now()))
+
+ # Get processed data
+ processed_data = self.csi_processor.get_processed_data()
+
+ # Apply phase sanitization
+ if processed_data is not None:
+ sanitized_data = self.phase_sanitizer.sanitize(processed_data)
+ return sanitized_data
+
+ return csi_data
+
+ async def _estimate_poses(self, csi_data: np.ndarray, metadata: Dict[str, Any]) -> List[Dict[str, Any]]:
+ """Estimate poses from processed CSI data."""
+ if self.settings.mock_pose_data:
+ return self._generate_mock_poses()
+
+ try:
+ # Convert CSI data to tensor
+ csi_tensor = torch.from_numpy(csi_data).float()
+
+ # Add batch dimension if needed
+ if len(csi_tensor.shape) == 2:
+ csi_tensor = csi_tensor.unsqueeze(0)
+
+ # Translate modality (CSI to visual-like features)
+ with torch.no_grad():
+ visual_features = self.modality_translator(csi_tensor)
+
+ # Estimate poses using DensePose
+ pose_outputs = self.densepose_model(visual_features)
+
+ # Convert outputs to pose detections
+ poses = self._parse_pose_outputs(pose_outputs)
+
+ # Filter by confidence threshold
+ filtered_poses = [
+ pose for pose in poses
+ if pose.get("confidence", 0.0) >= self.settings.pose_confidence_threshold
+ ]
+
+ # Limit number of persons
+ if len(filtered_poses) > self.settings.pose_max_persons:
+ filtered_poses = sorted(
+ filtered_poses,
+ key=lambda x: x.get("confidence", 0.0),
+ reverse=True
+ )[:self.settings.pose_max_persons]
+
+ return filtered_poses
+
+ except Exception as e:
+ self.logger.error(f"Error in pose estimation: {e}")
+ return []
+
+ def _parse_pose_outputs(self, outputs: torch.Tensor) -> List[Dict[str, Any]]:
+ """Parse neural network outputs into pose detections."""
+ poses = []
+
+ # This is a simplified parsing - in reality, this would depend on the model architecture
+ # For now, generate mock poses based on the output shape
+ batch_size = outputs.shape[0]
+
+ for i in range(batch_size):
+ # Extract pose information (mock implementation)
+ confidence = float(torch.sigmoid(outputs[i, 0]).item()) if outputs.shape[1] > 0 else 0.5
+
+ pose = {
+ "person_id": i,
+ "confidence": confidence,
+ "keypoints": self._generate_keypoints(),
+ "bounding_box": self._generate_bounding_box(),
+ "activity": self._classify_activity(outputs[i] if len(outputs.shape) > 1 else outputs),
+ "timestamp": datetime.now().isoformat()
+ }
+
+ poses.append(pose)
+
+ return poses
+
+ def _generate_mock_poses(self) -> List[Dict[str, Any]]:
+ """Generate mock pose data for development."""
+ import random
+
+ num_persons = random.randint(1, min(3, self.settings.pose_max_persons))
+ poses = []
+
+ for i in range(num_persons):
+ confidence = random.uniform(0.3, 0.95)
+
+ pose = {
+ "person_id": i,
+ "confidence": confidence,
+ "keypoints": self._generate_keypoints(),
+ "bounding_box": self._generate_bounding_box(),
+ "activity": random.choice(["standing", "sitting", "walking", "lying"]),
+ "timestamp": datetime.now().isoformat()
+ }
+
+ poses.append(pose)
+
+ return poses
+
+ def _generate_keypoints(self) -> List[Dict[str, Any]]:
+ """Generate keypoints for a person."""
+ import random
+
+ keypoint_names = [
+ "nose", "left_eye", "right_eye", "left_ear", "right_ear",
+ "left_shoulder", "right_shoulder", "left_elbow", "right_elbow",
+ "left_wrist", "right_wrist", "left_hip", "right_hip",
+ "left_knee", "right_knee", "left_ankle", "right_ankle"
+ ]
+
+ keypoints = []
+ for name in keypoint_names:
+ keypoints.append({
+ "name": name,
+ "x": random.uniform(0.1, 0.9),
+ "y": random.uniform(0.1, 0.9),
+ "confidence": random.uniform(0.5, 0.95)
+ })
+
+ return keypoints
+
+ def _generate_bounding_box(self) -> Dict[str, float]:
+ """Generate bounding box for a person."""
+ import random
+
+ x = random.uniform(0.1, 0.6)
+ y = random.uniform(0.1, 0.6)
+ width = random.uniform(0.2, 0.4)
+ height = random.uniform(0.3, 0.5)
+
+ return {
+ "x": x,
+ "y": y,
+ "width": width,
+ "height": height
+ }
+
+ def _classify_activity(self, features: torch.Tensor) -> str:
+ """Classify activity from features."""
+ # Simple mock classification
+ import random
+ activities = ["standing", "sitting", "walking", "lying", "unknown"]
+ return random.choice(activities)
+
+ def _update_stats(self, poses: List[Dict[str, Any]], processing_time: float):
+ """Update processing statistics."""
+ self.stats["total_processed"] += 1
+
+ if poses:
+ self.stats["successful_detections"] += 1
+ confidences = [pose.get("confidence", 0.0) for pose in poses]
+ avg_confidence = sum(confidences) / len(confidences)
+
+ # Update running average
+ total = self.stats["successful_detections"]
+ current_avg = self.stats["average_confidence"]
+ self.stats["average_confidence"] = (current_avg * (total - 1) + avg_confidence) / total
+ else:
+ self.stats["failed_detections"] += 1
+
+ # Update processing time (running average)
+ total = self.stats["total_processed"]
+ current_avg = self.stats["processing_time_ms"]
+ self.stats["processing_time_ms"] = (current_avg * (total - 1) + processing_time) / total
+
+ async def get_status(self) -> Dict[str, Any]:
+ """Get service status."""
+ return {
+ "status": "healthy" if self.is_running and not self.last_error else "unhealthy",
+ "initialized": self.is_initialized,
+ "running": self.is_running,
+ "last_error": self.last_error,
+ "statistics": self.stats.copy(),
+ "configuration": {
+ "mock_data": self.settings.mock_pose_data,
+ "confidence_threshold": self.settings.pose_confidence_threshold,
+ "max_persons": self.settings.pose_max_persons,
+ "batch_size": self.settings.pose_processing_batch_size
+ }
+ }
+
+ async def get_metrics(self) -> Dict[str, Any]:
+ """Get service metrics."""
+ return {
+ "pose_service": {
+ "total_processed": self.stats["total_processed"],
+ "successful_detections": self.stats["successful_detections"],
+ "failed_detections": self.stats["failed_detections"],
+ "success_rate": (
+ self.stats["successful_detections"] / max(1, self.stats["total_processed"])
+ ),
+ "average_confidence": self.stats["average_confidence"],
+ "average_processing_time_ms": self.stats["processing_time_ms"]
+ }
+ }
+
+ async def reset(self):
+ """Reset service state."""
+ self.stats = {
+ "total_processed": 0,
+ "successful_detections": 0,
+ "failed_detections": 0,
+ "average_confidence": 0.0,
+ "processing_time_ms": 0.0
+ }
+ self.last_error = None
+ self.logger.info("Pose service reset")
+
+ # API endpoint methods
+ async def estimate_poses(self, zone_ids=None, confidence_threshold=None, max_persons=None,
+ include_keypoints=True, include_segmentation=False):
+ """Estimate poses with API parameters."""
+ try:
+ # Generate mock CSI data for estimation
+ mock_csi = np.random.randn(64, 56, 3) # Mock CSI data
+ metadata = {
+ "timestamp": datetime.now(),
+ "zone_ids": zone_ids or ["zone_1"],
+ "confidence_threshold": confidence_threshold or self.settings.pose_confidence_threshold,
+ "max_persons": max_persons or self.settings.pose_max_persons
+ }
+
+ # Process the data
+ result = await self.process_csi_data(mock_csi, metadata)
+
+ # Format for API response
+ persons = []
+ for i, pose in enumerate(result["poses"]):
+ person = {
+ "person_id": str(pose["person_id"]),
+ "confidence": pose["confidence"],
+ "bounding_box": pose["bounding_box"],
+ "zone_id": zone_ids[0] if zone_ids else "zone_1",
+ "activity": pose["activity"],
+ "timestamp": datetime.fromisoformat(pose["timestamp"])
+ }
+
+ if include_keypoints:
+ person["keypoints"] = pose["keypoints"]
+
+ if include_segmentation:
+ person["segmentation"] = {"mask": "mock_segmentation_data"}
+
+ persons.append(person)
+
+ # Zone summary
+ zone_summary = {}
+ for zone_id in (zone_ids or ["zone_1"]):
+ zone_summary[zone_id] = len([p for p in persons if p.get("zone_id") == zone_id])
+
+ return {
+ "timestamp": datetime.now(),
+ "frame_id": f"frame_{int(datetime.now().timestamp())}",
+ "persons": persons,
+ "zone_summary": zone_summary,
+ "processing_time_ms": result["processing_time_ms"],
+ "metadata": {"mock_data": self.settings.mock_pose_data}
+ }
+
+ except Exception as e:
+ self.logger.error(f"Error in estimate_poses: {e}")
+ raise
+
+ async def analyze_with_params(self, zone_ids=None, confidence_threshold=None, max_persons=None,
+ include_keypoints=True, include_segmentation=False):
+ """Analyze pose data with custom parameters."""
+ return await self.estimate_poses(zone_ids, confidence_threshold, max_persons,
+ include_keypoints, include_segmentation)
+
+ async def get_zone_occupancy(self, zone_id: str):
+ """Get current occupancy for a specific zone."""
+ try:
+ # Mock occupancy data
+ import random
+ count = random.randint(0, 5)
+ persons = []
+
+ for i in range(count):
+ persons.append({
+ "person_id": f"person_{i}",
+ "confidence": random.uniform(0.7, 0.95),
+ "activity": random.choice(["standing", "sitting", "walking"])
+ })
+
+ return {
+ "count": count,
+ "max_occupancy": 10,
+ "persons": persons,
+ "timestamp": datetime.now()
+ }
+
+ except Exception as e:
+ self.logger.error(f"Error getting zone occupancy: {e}")
+ return None
+
+ async def get_zones_summary(self):
+ """Get occupancy summary for all zones."""
+ try:
+ import random
+ zones = ["zone_1", "zone_2", "zone_3", "zone_4"]
+ zone_data = {}
+ total_persons = 0
+ active_zones = 0
+
+ for zone_id in zones:
+ count = random.randint(0, 3)
+ zone_data[zone_id] = {
+ "occupancy": count,
+ "max_occupancy": 10,
+ "status": "active" if count > 0 else "inactive"
+ }
+ total_persons += count
+ if count > 0:
+ active_zones += 1
+
+ return {
+ "total_persons": total_persons,
+ "zones": zone_data,
+ "active_zones": active_zones
+ }
+
+ except Exception as e:
+ self.logger.error(f"Error getting zones summary: {e}")
+ raise
+
+ async def get_historical_data(self, start_time, end_time, zone_ids=None,
+ aggregation_interval=300, include_raw_data=False):
+ """Get historical pose estimation data."""
+ try:
+ # Mock historical data
+ import random
+ from datetime import timedelta
+
+ current_time = start_time
+ aggregated_data = []
+ raw_data = [] if include_raw_data else None
+
+ while current_time < end_time:
+ # Generate aggregated data point
+ data_point = {
+ "timestamp": current_time,
+ "total_persons": random.randint(0, 8),
+ "zones": {}
+ }
+
+ for zone_id in (zone_ids or ["zone_1", "zone_2", "zone_3"]):
+ data_point["zones"][zone_id] = {
+ "occupancy": random.randint(0, 3),
+ "avg_confidence": random.uniform(0.7, 0.95)
+ }
+
+ aggregated_data.append(data_point)
+
+ # Generate raw data if requested
+ if include_raw_data:
+ for _ in range(random.randint(0, 5)):
+ raw_data.append({
+ "timestamp": current_time + timedelta(seconds=random.randint(0, aggregation_interval)),
+ "person_id": f"person_{random.randint(1, 10)}",
+ "zone_id": random.choice(zone_ids or ["zone_1", "zone_2", "zone_3"]),
+ "confidence": random.uniform(0.5, 0.95),
+ "activity": random.choice(["standing", "sitting", "walking"])
+ })
+
+ current_time += timedelta(seconds=aggregation_interval)
+
+ return {
+ "aggregated_data": aggregated_data,
+ "raw_data": raw_data,
+ "total_records": len(aggregated_data)
+ }
+
+ except Exception as e:
+ self.logger.error(f"Error getting historical data: {e}")
+ raise
+
+ async def get_recent_activities(self, zone_id=None, limit=10):
+ """Get recently detected activities."""
+ try:
+ import random
+ activities = []
+
+ for i in range(limit):
+ activity = {
+ "activity_id": f"activity_{i}",
+ "person_id": f"person_{random.randint(1, 5)}",
+ "zone_id": zone_id or random.choice(["zone_1", "zone_2", "zone_3"]),
+ "activity": random.choice(["standing", "sitting", "walking", "lying"]),
+ "confidence": random.uniform(0.6, 0.95),
+ "timestamp": datetime.now() - timedelta(minutes=random.randint(0, 60)),
+ "duration_seconds": random.randint(10, 300)
+ }
+ activities.append(activity)
+
+ return activities
+
+ except Exception as e:
+ self.logger.error(f"Error getting recent activities: {e}")
+ raise
+
+ async def is_calibrating(self):
+ """Check if calibration is in progress."""
+ return False # Mock implementation
+
+ async def start_calibration(self):
+ """Start calibration process."""
+ import uuid
+ calibration_id = str(uuid.uuid4())
+ self.logger.info(f"Started calibration: {calibration_id}")
+ return calibration_id
+
+ async def run_calibration(self, calibration_id):
+ """Run calibration process."""
+ self.logger.info(f"Running calibration: {calibration_id}")
+ # Mock calibration process
+ await asyncio.sleep(5)
+ self.logger.info(f"Calibration completed: {calibration_id}")
+
+ async def get_calibration_status(self):
+ """Get current calibration status."""
+ return {
+ "is_calibrating": False,
+ "calibration_id": None,
+ "progress_percent": 100,
+ "current_step": "completed",
+ "estimated_remaining_minutes": 0,
+ "last_calibration": datetime.now() - timedelta(hours=1)
+ }
+
+ async def get_statistics(self, start_time, end_time):
+ """Get pose estimation statistics."""
+ try:
+ import random
+
+ # Mock statistics
+ total_detections = random.randint(100, 1000)
+ successful_detections = int(total_detections * random.uniform(0.8, 0.95))
+
+ return {
+ "total_detections": total_detections,
+ "successful_detections": successful_detections,
+ "failed_detections": total_detections - successful_detections,
+ "success_rate": successful_detections / total_detections,
+ "average_confidence": random.uniform(0.75, 0.90),
+ "average_processing_time_ms": random.uniform(50, 200),
+ "unique_persons": random.randint(5, 20),
+ "most_active_zone": random.choice(["zone_1", "zone_2", "zone_3"]),
+ "activity_distribution": {
+ "standing": random.uniform(0.3, 0.5),
+ "sitting": random.uniform(0.2, 0.4),
+ "walking": random.uniform(0.1, 0.3),
+ "lying": random.uniform(0.0, 0.1)
+ }
+ }
+
+ except Exception as e:
+ self.logger.error(f"Error getting statistics: {e}")
+ raise
+
+ async def process_segmentation_data(self, frame_id):
+ """Process segmentation data in background."""
+ self.logger.info(f"Processing segmentation data for frame: {frame_id}")
+ # Mock background processing
+ await asyncio.sleep(2)
+ self.logger.info(f"Segmentation processing completed for frame: {frame_id}")
+
+ # WebSocket streaming methods
+ async def get_current_pose_data(self):
+ """Get current pose data for streaming."""
+ try:
+ # Generate current pose data
+ result = await self.estimate_poses()
+
+ # Format data by zones for WebSocket streaming
+ zone_data = {}
+
+ # Group persons by zone
+ for person in result["persons"]:
+ zone_id = person.get("zone_id", "zone_1")
+
+ if zone_id not in zone_data:
+ zone_data[zone_id] = {
+ "pose": {
+ "persons": [],
+ "count": 0
+ },
+ "confidence": 0.0,
+ "activity": None,
+ "metadata": {
+ "frame_id": result["frame_id"],
+ "processing_time_ms": result["processing_time_ms"]
+ }
+ }
+
+ zone_data[zone_id]["pose"]["persons"].append(person)
+ zone_data[zone_id]["pose"]["count"] += 1
+
+ # Update zone confidence (average)
+ current_confidence = zone_data[zone_id]["confidence"]
+ person_confidence = person.get("confidence", 0.0)
+ zone_data[zone_id]["confidence"] = (current_confidence + person_confidence) / 2
+
+ # Set activity if not already set
+ if not zone_data[zone_id]["activity"] and person.get("activity"):
+ zone_data[zone_id]["activity"] = person["activity"]
+
+ return zone_data
+
+ except Exception as e:
+ self.logger.error(f"Error getting current pose data: {e}")
+ # Return empty zone data on error
+ return {}
+
+ # Health check methods
+ async def health_check(self):
+ """Perform health check."""
+ try:
+ status = "healthy" if self.is_running and not self.last_error else "unhealthy"
+
+ return {
+ "status": status,
+ "message": self.last_error if self.last_error else "Service is running normally",
+ "uptime_seconds": 0.0, # TODO: Implement actual uptime tracking
+ "metrics": {
+ "total_processed": self.stats["total_processed"],
+ "success_rate": (
+ self.stats["successful_detections"] / max(1, self.stats["total_processed"])
+ ),
+ "average_processing_time_ms": self.stats["processing_time_ms"]
+ }
+ }
+
+ except Exception as e:
+ return {
+ "status": "unhealthy",
+ "message": f"Health check failed: {str(e)}"
+ }
+
+ async def is_ready(self):
+ """Check if service is ready."""
+ return self.is_initialized and self.is_running
\ No newline at end of file
diff --git a/src/services/stream_service.py b/src/services/stream_service.py
new file mode 100644
index 0000000..201c50e
--- /dev/null
+++ b/src/services/stream_service.py
@@ -0,0 +1,397 @@
+"""
+Real-time streaming service for WiFi-DensePose API
+"""
+
+import logging
+import asyncio
+import json
+from typing import Dict, List, Optional, Any, Set
+from datetime import datetime
+from collections import deque
+
+import numpy as np
+from fastapi import WebSocket
+
+from src.config.settings import Settings
+from src.config.domains import DomainConfig
+
+logger = logging.getLogger(__name__)
+
+
+class StreamService:
+ """Service for real-time data streaming."""
+
+ def __init__(self, settings: Settings, domain_config: DomainConfig):
+ """Initialize stream service."""
+ self.settings = settings
+ self.domain_config = domain_config
+ self.logger = logging.getLogger(__name__)
+
+ # WebSocket connections
+ self.connections: Set[WebSocket] = set()
+ self.connection_metadata: Dict[WebSocket, Dict[str, Any]] = {}
+
+ # Stream buffers
+ self.pose_buffer = deque(maxlen=self.settings.stream_buffer_size)
+ self.csi_buffer = deque(maxlen=self.settings.stream_buffer_size)
+
+ # Service state
+ self.is_running = False
+ self.last_error = None
+
+ # Streaming statistics
+ self.stats = {
+ "active_connections": 0,
+ "total_connections": 0,
+ "messages_sent": 0,
+ "messages_failed": 0,
+ "data_points_streamed": 0,
+ "average_latency_ms": 0.0
+ }
+
+ # Background tasks
+ self.streaming_task = None
+
+ async def initialize(self):
+ """Initialize the stream service."""
+ self.logger.info("Stream service initialized")
+
+ async def start(self):
+ """Start the stream service."""
+ if self.is_running:
+ return
+
+ self.is_running = True
+ self.logger.info("Stream service started")
+
+ # Start background streaming task
+ if self.settings.enable_real_time_processing:
+ self.streaming_task = asyncio.create_task(self._streaming_loop())
+
+ async def stop(self):
+ """Stop the stream service."""
+ self.is_running = False
+
+ # Cancel background task
+ if self.streaming_task:
+ self.streaming_task.cancel()
+ try:
+ await self.streaming_task
+ except asyncio.CancelledError:
+ pass
+
+ # Close all connections
+ await self._close_all_connections()
+
+ self.logger.info("Stream service stopped")
+
+ async def add_connection(self, websocket: WebSocket, metadata: Dict[str, Any] = None):
+ """Add a new WebSocket connection."""
+ try:
+ await websocket.accept()
+ self.connections.add(websocket)
+ self.connection_metadata[websocket] = metadata or {}
+
+ self.stats["active_connections"] = len(self.connections)
+ self.stats["total_connections"] += 1
+
+ self.logger.info(f"New WebSocket connection added. Total: {len(self.connections)}")
+
+ # Send initial data if available
+ await self._send_initial_data(websocket)
+
+ except Exception as e:
+ self.logger.error(f"Error adding WebSocket connection: {e}")
+ raise
+
+ async def remove_connection(self, websocket: WebSocket):
+ """Remove a WebSocket connection."""
+ try:
+ if websocket in self.connections:
+ self.connections.remove(websocket)
+ self.connection_metadata.pop(websocket, None)
+
+ self.stats["active_connections"] = len(self.connections)
+
+ self.logger.info(f"WebSocket connection removed. Total: {len(self.connections)}")
+
+ except Exception as e:
+ self.logger.error(f"Error removing WebSocket connection: {e}")
+
+ async def broadcast_pose_data(self, pose_data: Dict[str, Any]):
+ """Broadcast pose data to all connected clients."""
+ if not self.is_running:
+ return
+
+ # Add to buffer
+ self.pose_buffer.append({
+ "type": "pose_data",
+ "timestamp": datetime.now().isoformat(),
+ "data": pose_data
+ })
+
+ # Broadcast to all connections
+ await self._broadcast_message({
+ "type": "pose_update",
+ "timestamp": datetime.now().isoformat(),
+ "data": pose_data
+ })
+
+ async def broadcast_csi_data(self, csi_data: np.ndarray, metadata: Dict[str, Any]):
+ """Broadcast CSI data to all connected clients."""
+ if not self.is_running:
+ return
+
+ # Convert numpy array to list for JSON serialization
+ csi_list = csi_data.tolist() if isinstance(csi_data, np.ndarray) else csi_data
+
+ # Add to buffer
+ self.csi_buffer.append({
+ "type": "csi_data",
+ "timestamp": datetime.now().isoformat(),
+ "data": csi_list,
+ "metadata": metadata
+ })
+
+ # Broadcast to all connections
+ await self._broadcast_message({
+ "type": "csi_update",
+ "timestamp": datetime.now().isoformat(),
+ "data": csi_list,
+ "metadata": metadata
+ })
+
+ async def broadcast_system_status(self, status_data: Dict[str, Any]):
+ """Broadcast system status to all connected clients."""
+ if not self.is_running:
+ return
+
+ await self._broadcast_message({
+ "type": "system_status",
+ "timestamp": datetime.now().isoformat(),
+ "data": status_data
+ })
+
+ async def send_to_connection(self, websocket: WebSocket, message: Dict[str, Any]):
+ """Send message to a specific connection."""
+ try:
+ if websocket in self.connections:
+ await websocket.send_text(json.dumps(message))
+ self.stats["messages_sent"] += 1
+
+ except Exception as e:
+ self.logger.error(f"Error sending message to connection: {e}")
+ self.stats["messages_failed"] += 1
+ await self.remove_connection(websocket)
+
+ async def _broadcast_message(self, message: Dict[str, Any]):
+ """Broadcast message to all connected clients."""
+ if not self.connections:
+ return
+
+ disconnected = set()
+
+ for websocket in self.connections.copy():
+ try:
+ await websocket.send_text(json.dumps(message))
+ self.stats["messages_sent"] += 1
+
+ except Exception as e:
+ self.logger.warning(f"Failed to send message to connection: {e}")
+ self.stats["messages_failed"] += 1
+ disconnected.add(websocket)
+
+ # Remove disconnected clients
+ for websocket in disconnected:
+ await self.remove_connection(websocket)
+
+ if message.get("type") in ["pose_update", "csi_update"]:
+ self.stats["data_points_streamed"] += 1
+
+ async def _send_initial_data(self, websocket: WebSocket):
+ """Send initial data to a new connection."""
+ try:
+ # Send recent pose data
+ if self.pose_buffer:
+ recent_poses = list(self.pose_buffer)[-10:] # Last 10 poses
+ await self.send_to_connection(websocket, {
+ "type": "initial_poses",
+ "timestamp": datetime.now().isoformat(),
+ "data": recent_poses
+ })
+
+ # Send recent CSI data
+ if self.csi_buffer:
+ recent_csi = list(self.csi_buffer)[-5:] # Last 5 CSI readings
+ await self.send_to_connection(websocket, {
+ "type": "initial_csi",
+ "timestamp": datetime.now().isoformat(),
+ "data": recent_csi
+ })
+
+ # Send service status
+ status = await self.get_status()
+ await self.send_to_connection(websocket, {
+ "type": "service_status",
+ "timestamp": datetime.now().isoformat(),
+ "data": status
+ })
+
+ except Exception as e:
+ self.logger.error(f"Error sending initial data: {e}")
+
+ async def _streaming_loop(self):
+ """Background streaming loop for periodic updates."""
+ try:
+ while self.is_running:
+ # Send periodic heartbeat
+ if self.connections:
+ await self._broadcast_message({
+ "type": "heartbeat",
+ "timestamp": datetime.now().isoformat(),
+ "active_connections": len(self.connections)
+ })
+
+ # Wait for next iteration
+ await asyncio.sleep(self.settings.websocket_ping_interval)
+
+ except asyncio.CancelledError:
+ self.logger.info("Streaming loop cancelled")
+ except Exception as e:
+ self.logger.error(f"Error in streaming loop: {e}")
+ self.last_error = str(e)
+
+ async def _close_all_connections(self):
+ """Close all WebSocket connections."""
+ disconnected = []
+
+ for websocket in self.connections.copy():
+ try:
+ await websocket.close()
+ disconnected.append(websocket)
+ except Exception as e:
+ self.logger.warning(f"Error closing connection: {e}")
+ disconnected.append(websocket)
+
+ # Clear all connections
+ for websocket in disconnected:
+ await self.remove_connection(websocket)
+
+ async def get_status(self) -> Dict[str, Any]:
+ """Get service status."""
+ return {
+ "status": "healthy" if self.is_running and not self.last_error else "unhealthy",
+ "running": self.is_running,
+ "last_error": self.last_error,
+ "connections": {
+ "active": len(self.connections),
+ "total": self.stats["total_connections"]
+ },
+ "buffers": {
+ "pose_buffer_size": len(self.pose_buffer),
+ "csi_buffer_size": len(self.csi_buffer),
+ "max_buffer_size": self.settings.stream_buffer_size
+ },
+ "statistics": self.stats.copy(),
+ "configuration": {
+ "stream_fps": self.settings.stream_fps,
+ "buffer_size": self.settings.stream_buffer_size,
+ "ping_interval": self.settings.websocket_ping_interval,
+ "timeout": self.settings.websocket_timeout
+ }
+ }
+
+ async def get_metrics(self) -> Dict[str, Any]:
+ """Get service metrics."""
+ total_messages = self.stats["messages_sent"] + self.stats["messages_failed"]
+ success_rate = self.stats["messages_sent"] / max(1, total_messages)
+
+ return {
+ "stream_service": {
+ "active_connections": self.stats["active_connections"],
+ "total_connections": self.stats["total_connections"],
+ "messages_sent": self.stats["messages_sent"],
+ "messages_failed": self.stats["messages_failed"],
+ "message_success_rate": success_rate,
+ "data_points_streamed": self.stats["data_points_streamed"],
+ "average_latency_ms": self.stats["average_latency_ms"]
+ }
+ }
+
+ async def get_connection_info(self) -> List[Dict[str, Any]]:
+ """Get information about active connections."""
+ connections_info = []
+
+ for websocket in self.connections:
+ metadata = self.connection_metadata.get(websocket, {})
+
+ connection_info = {
+ "id": id(websocket),
+ "connected_at": metadata.get("connected_at", "unknown"),
+ "user_agent": metadata.get("user_agent", "unknown"),
+ "ip_address": metadata.get("ip_address", "unknown"),
+ "subscription_types": metadata.get("subscription_types", [])
+ }
+
+ connections_info.append(connection_info)
+
+ return connections_info
+
+ async def reset(self):
+ """Reset service state."""
+ # Clear buffers
+ self.pose_buffer.clear()
+ self.csi_buffer.clear()
+
+ # Reset statistics
+ self.stats = {
+ "active_connections": len(self.connections),
+ "total_connections": 0,
+ "messages_sent": 0,
+ "messages_failed": 0,
+ "data_points_streamed": 0,
+ "average_latency_ms": 0.0
+ }
+
+ self.last_error = None
+ self.logger.info("Stream service reset")
+
+ def get_buffer_data(self, buffer_type: str, limit: int = 100) -> List[Dict[str, Any]]:
+ """Get data from buffers."""
+ if buffer_type == "pose":
+ return list(self.pose_buffer)[-limit:]
+ elif buffer_type == "csi":
+ return list(self.csi_buffer)[-limit:]
+ else:
+ return []
+
+ @property
+ def is_active(self) -> bool:
+ """Check if stream service is active."""
+ return self.is_running
+
+ async def health_check(self) -> Dict[str, Any]:
+ """Perform health check."""
+ try:
+ status = "healthy" if self.is_running and not self.last_error else "unhealthy"
+
+ return {
+ "status": status,
+ "message": self.last_error if self.last_error else "Stream service is running normally",
+ "active_connections": len(self.connections),
+ "metrics": {
+ "messages_sent": self.stats["messages_sent"],
+ "messages_failed": self.stats["messages_failed"],
+ "data_points_streamed": self.stats["data_points_streamed"]
+ }
+ }
+
+ except Exception as e:
+ return {
+ "status": "unhealthy",
+ "message": f"Health check failed: {str(e)}"
+ }
+
+ async def is_ready(self) -> bool:
+ """Check if service is ready."""
+ return self.is_running
\ No newline at end of file
diff --git a/test_application.py b/test_application.py
new file mode 100644
index 0000000..4516ea8
--- /dev/null
+++ b/test_application.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+"""
+Test script to verify WiFi-DensePose API functionality
+"""
+
+import asyncio
+import aiohttp
+import json
+import websockets
+import sys
+from typing import Dict, Any
+
+BASE_URL = "http://localhost:8000"
+WS_URL = "ws://localhost:8000"
+
+async def test_health_endpoints():
+ """Test health check endpoints."""
+ print("🔍 Testing health endpoints...")
+
+ async with aiohttp.ClientSession() as session:
+ # Test basic health
+ async with session.get(f"{BASE_URL}/health/health") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Health check: {data['status']}")
+ else:
+ print(f"❌ Health check failed: {response.status}")
+
+ # Test readiness
+ async with session.get(f"{BASE_URL}/health/ready") as response:
+ if response.status == 200:
+ data = await response.json()
+ status = "ready" if data['ready'] else "not ready"
+ print(f"✅ Readiness check: {status}")
+ else:
+ print(f"❌ Readiness check failed: {response.status}")
+
+ # Test liveness
+ async with session.get(f"{BASE_URL}/health/live") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Liveness check: {data['status']}")
+ else:
+ print(f"❌ Liveness check failed: {response.status}")
+
+async def test_api_endpoints():
+ """Test main API endpoints."""
+ print("\n🔍 Testing API endpoints...")
+
+ async with aiohttp.ClientSession() as session:
+ # Test root endpoint
+ async with session.get(f"{BASE_URL}/") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Root endpoint: {data['name']} v{data['version']}")
+ else:
+ print(f"❌ Root endpoint failed: {response.status}")
+
+ # Test API info
+ async with session.get(f"{BASE_URL}/api/v1/info") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ API info: {len(data['services'])} services configured")
+ else:
+ print(f"❌ API info failed: {response.status}")
+
+ # Test API status
+ async with session.get(f"{BASE_URL}/api/v1/status") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ API status: {data['api']['status']}")
+ else:
+ print(f"❌ API status failed: {response.status}")
+
+async def test_pose_endpoints():
+ """Test pose estimation endpoints."""
+ print("\n🔍 Testing pose endpoints...")
+
+ async with aiohttp.ClientSession() as session:
+ # Test current pose data
+ async with session.get(f"{BASE_URL}/api/v1/pose/current") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Current pose data: {len(data.get('poses', []))} poses detected")
+ else:
+ print(f"❌ Current pose data failed: {response.status}")
+
+ # Test zones summary
+ async with session.get(f"{BASE_URL}/api/v1/pose/zones/summary") as response:
+ if response.status == 200:
+ data = await response.json()
+ zones = data.get('zones', {})
+ print(f"✅ Zones summary: {len(zones)} zones")
+ for zone_id, zone_data in list(zones.items())[:3]: # Show first 3 zones
+ print(f" - {zone_id}: {zone_data.get('occupancy', 0)} people")
+ else:
+ print(f"❌ Zones summary failed: {response.status}")
+
+ # Test pose stats
+ async with session.get(f"{BASE_URL}/api/v1/pose/stats") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Pose stats: {data.get('total_detections', 0)} total detections")
+ else:
+ print(f"❌ Pose stats failed: {response.status}")
+
+async def test_stream_endpoints():
+ """Test streaming endpoints."""
+ print("\n🔍 Testing stream endpoints...")
+
+ async with aiohttp.ClientSession() as session:
+ # Test stream status
+ async with session.get(f"{BASE_URL}/api/v1/stream/status") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Stream status: {'Active' if data['is_active'] else 'Inactive'}")
+ print(f" - Connected clients: {data['connected_clients']}")
+ else:
+ print(f"❌ Stream status failed: {response.status}")
+
+ # Test stream metrics
+ async with session.get(f"{BASE_URL}/api/v1/stream/metrics") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Stream metrics available")
+ else:
+ print(f"❌ Stream metrics failed: {response.status}")
+
+async def test_websocket_connection():
+ """Test WebSocket connection."""
+ print("\n🔍 Testing WebSocket connection...")
+
+ try:
+ uri = f"{WS_URL}/api/v1/stream/pose"
+ async with websockets.connect(uri) as websocket:
+ print("✅ WebSocket connected successfully")
+
+ # Wait for connection confirmation
+ message = await asyncio.wait_for(websocket.recv(), timeout=5.0)
+ data = json.loads(message)
+
+ if data.get("type") == "connection_established":
+ print(f"✅ Connection established with client ID: {data.get('client_id')}")
+
+ # Send a ping
+ await websocket.send(json.dumps({"type": "ping"}))
+
+ # Wait for pong
+ pong_message = await asyncio.wait_for(websocket.recv(), timeout=5.0)
+ pong_data = json.loads(pong_message)
+
+ if pong_data.get("type") == "pong":
+ print("✅ WebSocket ping/pong successful")
+ else:
+ print(f"❌ Unexpected pong response: {pong_data}")
+ else:
+ print(f"❌ Unexpected connection message: {data}")
+
+ except asyncio.TimeoutError:
+ print("❌ WebSocket connection timeout")
+ except Exception as e:
+ print(f"❌ WebSocket connection failed: {e}")
+
+async def test_calibration_endpoints():
+ """Test calibration endpoints."""
+ print("\n🔍 Testing calibration endpoints...")
+
+ async with aiohttp.ClientSession() as session:
+ # Test calibration status
+ async with session.get(f"{BASE_URL}/api/v1/pose/calibration/status") as response:
+ if response.status == 200:
+ data = await response.json()
+ print(f"✅ Calibration status: {data.get('status', 'unknown')}")
+ else:
+ print(f"❌ Calibration status failed: {response.status}")
+
+async def main():
+ """Run all tests."""
+ print("🚀 Starting WiFi-DensePose API Tests")
+ print("=" * 50)
+
+ try:
+ await test_health_endpoints()
+ await test_api_endpoints()
+ await test_pose_endpoints()
+ await test_stream_endpoints()
+ await test_websocket_connection()
+ await test_calibration_endpoints()
+
+ print("\n" + "=" * 50)
+ print("✅ All tests completed!")
+
+ except Exception as e:
+ print(f"\n❌ Test suite failed: {e}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ asyncio.run(main())
\ No newline at end of file