Add API and Deployment documentation for WiFi-DensePose
- Created comprehensive API reference documentation covering authentication, request/response formats, error handling, and various API endpoints for pose estimation, system management, health checks, and WebSocket interactions. - Developed a detailed deployment guide outlining prerequisites, Docker and Kubernetes deployment steps, cloud deployment options for AWS, GCP, and Azure, and configuration for production environments.
This commit is contained in:
931
docs/api_reference.md
Normal file
931
docs/api_reference.md
Normal file
@@ -0,0 +1,931 @@
|
||||
# WiFi-DensePose API Reference
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Authentication](#authentication)
|
||||
3. [Base URL and Versioning](#base-url-and-versioning)
|
||||
4. [Request/Response Format](#requestresponse-format)
|
||||
5. [Error Handling](#error-handling)
|
||||
6. [Rate Limiting](#rate-limiting)
|
||||
7. [Pose Estimation API](#pose-estimation-api)
|
||||
8. [System Management API](#system-management-api)
|
||||
9. [Health Check API](#health-check-api)
|
||||
10. [WebSocket API](#websocket-api)
|
||||
11. [Data Models](#data-models)
|
||||
12. [SDK Examples](#sdk-examples)
|
||||
|
||||
## Overview
|
||||
|
||||
The WiFi-DensePose API provides comprehensive access to WiFi-based human pose estimation capabilities. The API follows REST principles and supports both synchronous HTTP requests and real-time WebSocket connections.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **RESTful Design**: Standard HTTP methods and status codes
|
||||
- **Real-time Streaming**: WebSocket support for live pose data
|
||||
- **Authentication**: JWT-based authentication with role-based access
|
||||
- **Rate Limiting**: Configurable rate limits to prevent abuse
|
||||
- **Comprehensive Documentation**: OpenAPI/Swagger documentation
|
||||
- **Error Handling**: Detailed error responses with actionable messages
|
||||
|
||||
### API Capabilities
|
||||
|
||||
- Real-time pose estimation from WiFi CSI data
|
||||
- Historical pose data retrieval and analysis
|
||||
- System health monitoring and diagnostics
|
||||
- Multi-zone occupancy tracking
|
||||
- Activity recognition and analytics
|
||||
- System configuration and calibration
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
The API uses JSON Web Tokens (JWT) for authentication. Include the token in the `Authorization` header:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
### Obtaining a Token
|
||||
|
||||
```bash
|
||||
# Login to get JWT token
|
||||
curl -X POST http://localhost:8000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "your-username",
|
||||
"password": "your-password"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 86400
|
||||
}
|
||||
```
|
||||
|
||||
### Token Refresh
|
||||
|
||||
```bash
|
||||
# Refresh expired token
|
||||
curl -X POST http://localhost:8000/api/v1/auth/refresh \
|
||||
-H "Authorization: Bearer <your-refresh-token>"
|
||||
```
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
Some endpoints are publicly accessible without authentication:
|
||||
- `GET /api/v1/health/*` - Health check endpoints
|
||||
- `GET /api/v1/version` - Version information
|
||||
- `GET /docs` - API documentation
|
||||
|
||||
## Base URL and Versioning
|
||||
|
||||
### Base URL
|
||||
```
|
||||
http://localhost:8000/api/v1
|
||||
```
|
||||
|
||||
### API Versioning
|
||||
The API uses URL path versioning. Current version is `v1`.
|
||||
|
||||
### Content Types
|
||||
- **Request**: `application/json`
|
||||
- **Response**: `application/json`
|
||||
- **WebSocket**: `application/json` messages
|
||||
|
||||
## Request/Response Format
|
||||
|
||||
### Standard Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid request parameters",
|
||||
"details": {
|
||||
"field": "confidence_threshold",
|
||||
"issue": "Value must be between 0.0 and 1.0"
|
||||
}
|
||||
},
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"status": "error"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 200 | Success |
|
||||
| 201 | Created |
|
||||
| 400 | Bad Request |
|
||||
| 401 | Unauthorized |
|
||||
| 403 | Forbidden |
|
||||
| 404 | Not Found |
|
||||
| 409 | Conflict |
|
||||
| 422 | Validation Error |
|
||||
| 429 | Rate Limited |
|
||||
| 500 | Internal Server Error |
|
||||
| 503 | Service Unavailable |
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `VALIDATION_ERROR` | Request validation failed |
|
||||
| `AUTHENTICATION_ERROR` | Authentication failed |
|
||||
| `AUTHORIZATION_ERROR` | Insufficient permissions |
|
||||
| `RESOURCE_NOT_FOUND` | Requested resource not found |
|
||||
| `RATE_LIMIT_EXCEEDED` | Rate limit exceeded |
|
||||
| `HARDWARE_ERROR` | Hardware communication error |
|
||||
| `PROCESSING_ERROR` | Pose processing error |
|
||||
| `CALIBRATION_ERROR` | System calibration error |
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Default Limits
|
||||
- **Authenticated users**: 1000 requests per hour
|
||||
- **Anonymous users**: 100 requests per hour
|
||||
- **WebSocket connections**: 10 concurrent per user
|
||||
|
||||
### Rate Limit Headers
|
||||
```http
|
||||
X-RateLimit-Limit: 1000
|
||||
X-RateLimit-Remaining: 999
|
||||
X-RateLimit-Reset: 1641556800
|
||||
```
|
||||
|
||||
### Rate Limit Response
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"message": "Rate limit exceeded. Try again in 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pose Estimation API
|
||||
|
||||
### Get Current Pose Estimation
|
||||
|
||||
Get real-time pose estimation from WiFi signals.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/current
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `zone_ids` (array, optional): Specific zones to analyze
|
||||
- `confidence_threshold` (float, optional): Minimum confidence (0.0-1.0)
|
||||
- `max_persons` (integer, optional): Maximum persons to detect (1-50)
|
||||
- `include_keypoints` (boolean, optional): Include keypoint data (default: true)
|
||||
- `include_segmentation` (boolean, optional): Include segmentation masks (default: false)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl "http://localhost:8000/api/v1/pose/current?confidence_threshold=0.7&max_persons=5" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"frame_id": "frame_12345",
|
||||
"persons": [
|
||||
{
|
||||
"person_id": "person_001",
|
||||
"confidence": 0.85,
|
||||
"bounding_box": {
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"width": 80,
|
||||
"height": 180
|
||||
},
|
||||
"keypoints": [
|
||||
{
|
||||
"name": "nose",
|
||||
"x": 140,
|
||||
"y": 160,
|
||||
"confidence": 0.9
|
||||
}
|
||||
],
|
||||
"zone_id": "zone_001",
|
||||
"activity": "standing",
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"zone_summary": {
|
||||
"zone_001": 1,
|
||||
"zone_002": 0
|
||||
},
|
||||
"processing_time_ms": 45.2
|
||||
}
|
||||
```
|
||||
|
||||
### Analyze Pose Data
|
||||
|
||||
Trigger pose analysis with custom parameters.
|
||||
|
||||
```http
|
||||
POST /api/v1/pose/analyze
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"zone_ids": ["zone_001", "zone_002"],
|
||||
"confidence_threshold": 0.8,
|
||||
"max_persons": 10,
|
||||
"include_keypoints": true,
|
||||
"include_segmentation": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Same format as current pose estimation.
|
||||
|
||||
### Get Zone Occupancy
|
||||
|
||||
Get current occupancy for a specific zone.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/zones/{zone_id}/occupancy
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
- `zone_id` (string): Zone identifier
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl "http://localhost:8000/api/v1/pose/zones/zone_001/occupancy" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"zone_id": "zone_001",
|
||||
"current_occupancy": 3,
|
||||
"max_occupancy": 10,
|
||||
"persons": [
|
||||
{
|
||||
"person_id": "person_001",
|
||||
"confidence": 0.85,
|
||||
"activity": "standing"
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Zones Summary
|
||||
|
||||
Get occupancy summary for all zones.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/zones/summary
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"total_persons": 5,
|
||||
"zones": {
|
||||
"zone_001": {
|
||||
"occupancy": 3,
|
||||
"max_occupancy": 10,
|
||||
"status": "normal"
|
||||
},
|
||||
"zone_002": {
|
||||
"occupancy": 2,
|
||||
"max_occupancy": 8,
|
||||
"status": "normal"
|
||||
}
|
||||
},
|
||||
"active_zones": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Get Historical Data
|
||||
|
||||
Retrieve historical pose estimation data.
|
||||
|
||||
```http
|
||||
POST /api/v1/pose/historical
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"start_time": "2025-01-07T00:00:00Z",
|
||||
"end_time": "2025-01-07T23:59:59Z",
|
||||
"zone_ids": ["zone_001"],
|
||||
"aggregation_interval": 300,
|
||||
"include_raw_data": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"query": {
|
||||
"start_time": "2025-01-07T00:00:00Z",
|
||||
"end_time": "2025-01-07T23:59:59Z",
|
||||
"zone_ids": ["zone_001"],
|
||||
"aggregation_interval": 300
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"timestamp": "2025-01-07T00:00:00Z",
|
||||
"average_occupancy": 2.5,
|
||||
"max_occupancy": 5,
|
||||
"total_detections": 150
|
||||
}
|
||||
],
|
||||
"total_records": 288
|
||||
}
|
||||
```
|
||||
|
||||
### Get Detected Activities
|
||||
|
||||
Get recently detected activities.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/activities
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `zone_id` (string, optional): Filter by zone
|
||||
- `limit` (integer, optional): Maximum activities (1-100, default: 10)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"activities": [
|
||||
{
|
||||
"activity": "walking",
|
||||
"person_id": "person_001",
|
||||
"zone_id": "zone_001",
|
||||
"confidence": 0.9,
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"duration_seconds": 15.5
|
||||
}
|
||||
],
|
||||
"total_count": 1,
|
||||
"zone_id": "zone_001"
|
||||
}
|
||||
```
|
||||
|
||||
### Calibrate System
|
||||
|
||||
Start system calibration process.
|
||||
|
||||
```http
|
||||
POST /api/v1/pose/calibrate
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"calibration_id": "cal_12345",
|
||||
"status": "started",
|
||||
"estimated_duration_minutes": 5,
|
||||
"message": "Calibration process started"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Calibration Status
|
||||
|
||||
Check calibration progress.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/calibration/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"is_calibrating": true,
|
||||
"calibration_id": "cal_12345",
|
||||
"progress_percent": 60,
|
||||
"current_step": "phase_sanitization",
|
||||
"estimated_remaining_minutes": 2,
|
||||
"last_calibration": "2025-01-06T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Pose Statistics
|
||||
|
||||
Get pose estimation statistics.
|
||||
|
||||
```http
|
||||
GET /api/v1/pose/stats
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `hours` (integer, optional): Hours of data to analyze (1-168, default: 24)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"period": {
|
||||
"start_time": "2025-01-06T10:00:00Z",
|
||||
"end_time": "2025-01-07T10:00:00Z",
|
||||
"hours": 24
|
||||
},
|
||||
"statistics": {
|
||||
"total_detections": 1500,
|
||||
"average_confidence": 0.82,
|
||||
"unique_persons": 25,
|
||||
"average_processing_time_ms": 47.3,
|
||||
"zones": {
|
||||
"zone_001": {
|
||||
"detections": 800,
|
||||
"average_occupancy": 3.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management API
|
||||
|
||||
### System Status
|
||||
|
||||
Get current system status.
|
||||
|
||||
```http
|
||||
GET /api/v1/system/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"uptime_seconds": 86400,
|
||||
"services": {
|
||||
"hardware": "healthy",
|
||||
"pose_estimation": "healthy",
|
||||
"streaming": "healthy"
|
||||
},
|
||||
"configuration": {
|
||||
"domain": "healthcare",
|
||||
"max_persons": 10,
|
||||
"confidence_threshold": 0.7
|
||||
},
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Start System
|
||||
|
||||
Start the pose estimation system.
|
||||
|
||||
```http
|
||||
POST /api/v1/system/start
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"configuration": {
|
||||
"domain": "healthcare",
|
||||
"environment_id": "room_001",
|
||||
"calibration_required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stop System
|
||||
|
||||
Stop the pose estimation system.
|
||||
|
||||
```http
|
||||
POST /api/v1/system/stop
|
||||
```
|
||||
|
||||
### Restart System
|
||||
|
||||
Restart the system with new configuration.
|
||||
|
||||
```http
|
||||
POST /api/v1/system/restart
|
||||
```
|
||||
|
||||
### Get Configuration
|
||||
|
||||
Get current system configuration.
|
||||
|
||||
```http
|
||||
GET /api/v1/config
|
||||
```
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Update system configuration.
|
||||
|
||||
```http
|
||||
PUT /api/v1/config
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"detection": {
|
||||
"confidence_threshold": 0.8,
|
||||
"max_persons": 8
|
||||
},
|
||||
"analytics": {
|
||||
"enable_fall_detection": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Health Check API
|
||||
|
||||
### Comprehensive Health Check
|
||||
|
||||
Get detailed system health information.
|
||||
|
||||
```http
|
||||
GET /api/v1/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"uptime_seconds": 86400,
|
||||
"components": {
|
||||
"hardware": {
|
||||
"name": "Hardware Service",
|
||||
"status": "healthy",
|
||||
"message": "All routers connected",
|
||||
"last_check": "2025-01-07T10:00:00Z",
|
||||
"uptime_seconds": 86400,
|
||||
"metrics": {
|
||||
"connected_routers": 3,
|
||||
"csi_data_rate": 30.5
|
||||
}
|
||||
},
|
||||
"pose": {
|
||||
"name": "Pose Service",
|
||||
"status": "healthy",
|
||||
"message": "Processing normally",
|
||||
"last_check": "2025-01-07T10:00:00Z",
|
||||
"metrics": {
|
||||
"processing_rate": 29.8,
|
||||
"average_latency_ms": 45.2
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_metrics": {
|
||||
"cpu": {
|
||||
"percent": 65.2,
|
||||
"count": 8
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": 16.0,
|
||||
"available_gb": 8.5,
|
||||
"percent": 46.9
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": 500.0,
|
||||
"free_gb": 350.0,
|
||||
"percent": 30.0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Readiness Check
|
||||
|
||||
Check if system is ready to serve requests.
|
||||
|
||||
```http
|
||||
GET /api/v1/ready
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"ready": true,
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"checks": {
|
||||
"hardware_ready": true,
|
||||
"pose_ready": true,
|
||||
"stream_ready": true,
|
||||
"memory_available": true,
|
||||
"disk_space_available": true
|
||||
},
|
||||
"message": "System is ready"
|
||||
}
|
||||
```
|
||||
|
||||
### Liveness Check
|
||||
|
||||
Simple liveness check for load balancers.
|
||||
|
||||
```http
|
||||
GET /api/v1/live
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "alive",
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### System Metrics
|
||||
|
||||
Get detailed system metrics.
|
||||
|
||||
```http
|
||||
GET /api/v1/metrics
|
||||
```
|
||||
|
||||
### Version Information
|
||||
|
||||
Get application version information.
|
||||
|
||||
```http
|
||||
GET /api/v1/version
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"name": "WiFi-DensePose API",
|
||||
"version": "1.0.0",
|
||||
"environment": "production",
|
||||
"debug": false,
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
### Connection
|
||||
|
||||
Connect to WebSocket endpoint:
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/pose/stream');
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
Send authentication message after connection:
|
||||
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: 'your-jwt-token'
|
||||
}));
|
||||
```
|
||||
|
||||
### Subscribe to Pose Updates
|
||||
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'pose_updates',
|
||||
filters: {
|
||||
zone_ids: ['zone_001'],
|
||||
min_confidence: 0.7
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Pose Data Message
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "pose_data",
|
||||
"channel": "pose_updates",
|
||||
"data": {
|
||||
"timestamp": "2025-01-07T10:00:00Z",
|
||||
"frame_id": "frame_12345",
|
||||
"persons": [
|
||||
{
|
||||
"person_id": "person_001",
|
||||
"confidence": 0.85,
|
||||
"bounding_box": {
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"width": 80,
|
||||
"height": 180
|
||||
},
|
||||
"zone_id": "zone_001"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Events
|
||||
|
||||
Subscribe to system events:
|
||||
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'system_events'
|
||||
}));
|
||||
```
|
||||
|
||||
### Event Message
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "system_event",
|
||||
"channel": "system_events",
|
||||
"data": {
|
||||
"event_type": "fall_detected",
|
||||
"person_id": "person_001",
|
||||
"zone_id": "zone_001",
|
||||
"confidence": 0.95,
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### PersonPose
|
||||
|
||||
```json
|
||||
{
|
||||
"person_id": "string",
|
||||
"confidence": 0.85,
|
||||
"bounding_box": {
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"width": 80,
|
||||
"height": 180
|
||||
},
|
||||
"keypoints": [
|
||||
{
|
||||
"name": "nose",
|
||||
"x": 140,
|
||||
"y": 160,
|
||||
"confidence": 0.9,
|
||||
"visible": true
|
||||
}
|
||||
],
|
||||
"segmentation": {
|
||||
"mask": "base64-encoded-mask",
|
||||
"body_parts": ["torso", "left_arm", "right_arm"]
|
||||
},
|
||||
"zone_id": "zone_001",
|
||||
"activity": "standing",
|
||||
"timestamp": "2025-01-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Keypoint Names
|
||||
|
||||
Standard keypoint names following COCO format:
|
||||
- `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`
|
||||
|
||||
### Activity Types
|
||||
|
||||
Supported activity classifications:
|
||||
- `standing`, `sitting`, `walking`, `running`, `lying_down`
|
||||
- `falling`, `jumping`, `bending`, `reaching`, `waving`
|
||||
|
||||
### Zone Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"zone_id": "zone_001",
|
||||
"name": "Living Room",
|
||||
"coordinates": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 500,
|
||||
"height": 300
|
||||
},
|
||||
"max_occupancy": 10,
|
||||
"alerts_enabled": true,
|
||||
"privacy_level": "high"
|
||||
}
|
||||
```
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### Python SDK
|
||||
|
||||
```python
|
||||
from wifi_densepose import WiFiDensePoseClient
|
||||
|
||||
# Initialize client
|
||||
client = WiFiDensePoseClient(
|
||||
base_url="http://localhost:8000",
|
||||
api_key="your-api-key"
|
||||
)
|
||||
|
||||
# Get current poses
|
||||
poses = client.get_current_poses(
|
||||
confidence_threshold=0.7,
|
||||
max_persons=5
|
||||
)
|
||||
|
||||
# Get historical data
|
||||
history = client.get_historical_data(
|
||||
start_time="2025-01-07T00:00:00Z",
|
||||
end_time="2025-01-07T23:59:59Z",
|
||||
zone_ids=["zone_001"]
|
||||
)
|
||||
|
||||
# Subscribe to real-time updates
|
||||
def pose_callback(poses):
|
||||
print(f"Received {len(poses)} poses")
|
||||
|
||||
client.subscribe_to_poses(callback=pose_callback)
|
||||
```
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```javascript
|
||||
import { WiFiDensePoseClient } from 'wifi-densepose-js';
|
||||
|
||||
// Initialize client
|
||||
const client = new WiFiDensePoseClient({
|
||||
baseUrl: 'http://localhost:8000',
|
||||
apiKey: 'your-api-key'
|
||||
});
|
||||
|
||||
// Get current poses
|
||||
const poses = await client.getCurrentPoses({
|
||||
confidenceThreshold: 0.7,
|
||||
maxPersons: 5
|
||||
});
|
||||
|
||||
// Subscribe to WebSocket updates
|
||||
client.subscribeToPoses({
|
||||
onData: (poses) => {
|
||||
console.log(`Received ${poses.length} poses`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### cURL Examples
|
||||
|
||||
```bash
|
||||
# Get current poses
|
||||
curl -X GET "http://localhost:8000/api/v1/pose/current?confidence_threshold=0.7" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json"
|
||||
|
||||
# Start system
|
||||
curl -X POST "http://localhost:8000/api/v1/system/start" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"configuration": {
|
||||
"domain": "healthcare",
|
||||
"environment_id": "room_001"
|
||||
}
|
||||
}'
|
||||
|
||||
# Get zone occupancy
|
||||
curl -X GET "http://localhost:8000/api/v1/pose/zones/zone_001/occupancy" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information, see:
|
||||
- [User Guide](user_guide.md)
|
||||
- [Deployment Guide](deployment.md)
|
||||
- [Troubleshooting Guide](troubleshooting.md)
|
||||
- [Interactive API Documentation](http://localhost:8000/docs)
|
||||
1103
docs/deployment.md
Normal file
1103
docs/deployment.md
Normal file
File diff suppressed because it is too large
Load Diff
1058
docs/troubleshooting.md
Normal file
1058
docs/troubleshooting.md
Normal file
File diff suppressed because it is too large
Load Diff
315
ui/style.css
315
ui/style.css
@@ -1304,4 +1304,317 @@ canvas {
|
||||
.implementation-note p {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
/* Additional styles for modular UI components */
|
||||
|
||||
/* Header info bar */
|
||||
.header-info {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
justify-content: center;
|
||||
margin-top: var(--space-16);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.api-version,
|
||||
.api-environment,
|
||||
.overall-health {
|
||||
padding: var(--space-4) var(--space-12);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.api-environment.env-development {
|
||||
background: rgba(var(--color-info-rgb), 0.1);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.api-environment.env-production {
|
||||
background: rgba(var(--color-success-rgb), 0.1);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.overall-health.status-healthy {
|
||||
background: rgba(var(--color-success-rgb), 0.1);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.overall-health.status-degraded {
|
||||
background: rgba(var(--color-warning-rgb), 0.1);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.overall-health.status-unhealthy {
|
||||
background: rgba(var(--color-error-rgb), 0.1);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Dashboard panels */
|
||||
.live-status-panel,
|
||||
.system-metrics-panel,
|
||||
.features-panel,
|
||||
.live-stats-panel {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-24);
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-16);
|
||||
margin-top: var(--space-16);
|
||||
}
|
||||
|
||||
.component-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-base);
|
||||
}
|
||||
|
||||
.component-status.status-healthy {
|
||||
border-color: var(--color-success);
|
||||
background: rgba(var(--color-success-rgb), 0.05);
|
||||
}
|
||||
|
||||
.component-status.status-degraded {
|
||||
border-color: var(--color-warning);
|
||||
background: rgba(var(--color-warning-rgb), 0.05);
|
||||
}
|
||||
|
||||
.component-status.status-unhealthy {
|
||||
border-color: var(--color-error);
|
||||
background: rgba(var(--color-error-rgb), 0.05);
|
||||
}
|
||||
|
||||
.component-name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: var(--font-size-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Metrics display */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--space-16);
|
||||
margin-top: var(--space-16);
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--color-secondary);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-fill.normal {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.progress-fill.warning {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
.progress-fill.critical {
|
||||
background: var(--color-error);
|
||||
}
|
||||
|
||||
/* Features status */
|
||||
.features-status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--space-12);
|
||||
margin-top: var(--space-16);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-8) var(--space-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-base);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.feature-item.enabled {
|
||||
background: rgba(var(--color-success-rgb), 0.05);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.feature-item.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.feature-status {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Live statistics */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--space-16);
|
||||
margin-top: var(--space-16);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item .stat-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.person-count,
|
||||
.avg-confidence,
|
||||
.detection-count {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Zones display */
|
||||
.zones-panel {
|
||||
margin-top: var(--space-24);
|
||||
}
|
||||
|
||||
.zones-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: var(--space-8);
|
||||
margin-top: var(--space-12);
|
||||
}
|
||||
|
||||
.zone-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-8) var(--space-12);
|
||||
background: var(--color-secondary);
|
||||
border-radius: var(--radius-base);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.zone-name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.zone-count {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-8);
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Error container */
|
||||
.error-container {
|
||||
background: rgba(var(--color-error-rgb), 0.1);
|
||||
border: 1px solid var(--color-error);
|
||||
color: var(--color-error);
|
||||
padding: var(--space-12) var(--space-16);
|
||||
border-radius: var(--radius-base);
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
/* Global error toast */
|
||||
.error-toast {
|
||||
position: fixed;
|
||||
bottom: var(--space-24);
|
||||
right: var(--space-24);
|
||||
background: var(--color-error);
|
||||
color: white;
|
||||
padding: var(--space-12) var(--space-20);
|
||||
border-radius: var(--radius-base);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.error-toast.show {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Tab badge */
|
||||
.tab-badge {
|
||||
display: inline-block;
|
||||
margin-left: var(--space-8);
|
||||
padding: var(--space-2) var(--space-6);
|
||||
background: var(--color-error);
|
||||
color: white;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Help text */
|
||||
.help-text {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-style: italic;
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
/* Array status */
|
||||
.array-status {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
margin-top: var(--space-16);
|
||||
padding: var(--space-12);
|
||||
background: var(--color-secondary);
|
||||
border-radius: var(--radius-base);
|
||||
}
|
||||
|
||||
.array-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
228
ui/tests/test-runner.html
Normal file
228
ui/tests/test-runner.html
Normal file
@@ -0,0 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WiFi DensePose UI Tests</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.test-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.test-suite {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.test-suite-header {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.test-case {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.test-case:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.test-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.test-status {
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.test-status.pass {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.test-status.fail {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.test-status.pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.test-summary {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.run-tests-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 25px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.run-tests-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.run-tests-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.test-output {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-header">
|
||||
<h1>WiFi DensePose UI Test Suite</h1>
|
||||
<p>Comprehensive testing for the modular UI components and API integration</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="runAllTests" class="run-tests-btn">Run All Tests</button>
|
||||
<button id="runUnitTests" class="run-tests-btn">Run Unit Tests</button>
|
||||
<button id="runIntegrationTests" class="run-tests-btn">Run Integration Tests</button>
|
||||
<button id="clearResults" class="run-tests-btn" style="background: #dc3545;">Clear Results</button>
|
||||
</div>
|
||||
|
||||
<div class="test-summary">
|
||||
<h3>Test Summary</h3>
|
||||
<div class="summary-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalTests">0</div>
|
||||
<div class="stat-label">Total Tests</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="passedTests" style="color: #28a745;">0</div>
|
||||
<div class="stat-label">Passed</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="failedTests" style="color: #dc3545;">0</div>
|
||||
<div class="stat-label">Failed</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="pendingTests" style="color: #ffc107;">0</div>
|
||||
<div class="stat-label">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">API Configuration Tests</div>
|
||||
<div id="apiConfigTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">API Service Tests</div>
|
||||
<div id="apiServiceTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">WebSocket Service Tests</div>
|
||||
<div id="websocketServiceTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">Pose Service Tests</div>
|
||||
<div id="poseServiceTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">Health Service Tests</div>
|
||||
<div id="healthServiceTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">UI Component Tests</div>
|
||||
<div id="uiComponentTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-suite">
|
||||
<div class="test-suite-header">Integration Tests</div>
|
||||
<div id="integrationTests"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-output" id="testOutput" style="display: none;"></div>
|
||||
|
||||
<script type="module" src="test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
476
ui/tests/test-runner.js
Normal file
476
ui/tests/test-runner.js
Normal file
@@ -0,0 +1,476 @@
|
||||
// Test Runner for WiFi DensePose UI
|
||||
|
||||
import { API_CONFIG, buildApiUrl, buildWsUrl } from '../config/api.config.js';
|
||||
import { apiService } from '../services/api.service.js';
|
||||
import { wsService } from '../services/websocket.service.js';
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
import { healthService } from '../services/health.service.js';
|
||||
import { TabManager } from '../components/TabManager.js';
|
||||
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.results = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
pending: 0
|
||||
};
|
||||
this.output = [];
|
||||
}
|
||||
|
||||
// Add a test
|
||||
test(name, category, testFn) {
|
||||
this.tests.push({
|
||||
name,
|
||||
category,
|
||||
fn: testFn,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async runAllTests() {
|
||||
this.clearResults();
|
||||
this.log('Starting test suite...\n');
|
||||
|
||||
for (const test of this.tests) {
|
||||
await this.runSingleTest(test);
|
||||
}
|
||||
|
||||
this.updateSummary();
|
||||
this.log(`\nTest suite completed. ${this.results.passed}/${this.results.total} tests passed.`);
|
||||
}
|
||||
|
||||
// Run tests by category
|
||||
async runTestsByCategory(category) {
|
||||
this.clearResults();
|
||||
const categoryTests = this.tests.filter(test => test.category === category);
|
||||
|
||||
this.log(`Starting ${category} tests...\n`);
|
||||
|
||||
for (const test of categoryTests) {
|
||||
await this.runSingleTest(test);
|
||||
}
|
||||
|
||||
this.updateSummary();
|
||||
this.log(`\n${category} tests completed. ${this.results.passed}/${this.results.total} tests passed.`);
|
||||
}
|
||||
|
||||
// Run a single test
|
||||
async runSingleTest(test) {
|
||||
this.log(`Running: ${test.name}...`);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
await test.fn();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
test.status = 'pass';
|
||||
this.results.passed++;
|
||||
this.log(` ✓ PASS (${duration}ms)`);
|
||||
|
||||
} catch (error) {
|
||||
test.status = 'fail';
|
||||
test.error = error.message;
|
||||
this.results.failed++;
|
||||
this.log(` ✗ FAIL: ${error.message}`);
|
||||
|
||||
} finally {
|
||||
this.results.total++;
|
||||
this.updateTestDisplay(test);
|
||||
}
|
||||
}
|
||||
|
||||
// Assertion helpers
|
||||
assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
}
|
||||
|
||||
assertEqual(actual, expected, message) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
assertNotEqual(actual, unexpected, message) {
|
||||
if (actual === unexpected) {
|
||||
throw new Error(message || `Expected not to equal ${unexpected}`);
|
||||
}
|
||||
}
|
||||
|
||||
assertThrows(fn, message) {
|
||||
try {
|
||||
fn();
|
||||
throw new Error(message || 'Expected function to throw');
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
async assertRejects(promise, message) {
|
||||
try {
|
||||
await promise;
|
||||
throw new Error(message || 'Expected promise to reject');
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Logging
|
||||
log(message) {
|
||||
this.output.push(message);
|
||||
const outputElement = document.getElementById('testOutput');
|
||||
if (outputElement) {
|
||||
outputElement.style.display = 'block';
|
||||
outputElement.textContent = this.output.join('\n');
|
||||
outputElement.scrollTop = outputElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear results
|
||||
clearResults() {
|
||||
this.results = { total: 0, passed: 0, failed: 0, pending: 0 };
|
||||
this.output = [];
|
||||
|
||||
// Reset test statuses
|
||||
this.tests.forEach(test => {
|
||||
test.status = 'pending';
|
||||
delete test.error;
|
||||
});
|
||||
|
||||
// Clear UI
|
||||
this.updateSummary();
|
||||
this.tests.forEach(test => this.updateTestDisplay(test));
|
||||
|
||||
const outputElement = document.getElementById('testOutput');
|
||||
if (outputElement) {
|
||||
outputElement.style.display = 'none';
|
||||
outputElement.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update test display
|
||||
updateTestDisplay(test) {
|
||||
const container = document.getElementById(`${test.category}Tests`);
|
||||
if (!container) return;
|
||||
|
||||
let testElement = container.querySelector(`[data-test="${test.name}"]`);
|
||||
if (!testElement) {
|
||||
testElement = document.createElement('div');
|
||||
testElement.className = 'test-case';
|
||||
testElement.setAttribute('data-test', test.name);
|
||||
testElement.innerHTML = `
|
||||
<div class="test-name">${test.name}</div>
|
||||
<div class="test-status pending">pending</div>
|
||||
`;
|
||||
container.appendChild(testElement);
|
||||
}
|
||||
|
||||
const statusElement = testElement.querySelector('.test-status');
|
||||
statusElement.className = `test-status ${test.status}`;
|
||||
statusElement.textContent = test.status;
|
||||
|
||||
if (test.error) {
|
||||
statusElement.title = test.error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update summary
|
||||
updateSummary() {
|
||||
document.getElementById('totalTests').textContent = this.results.total;
|
||||
document.getElementById('passedTests').textContent = this.results.passed;
|
||||
document.getElementById('failedTests').textContent = this.results.failed;
|
||||
document.getElementById('pendingTests').textContent = this.tests.length - this.results.total;
|
||||
}
|
||||
}
|
||||
|
||||
// Create test runner instance
|
||||
const testRunner = new TestRunner();
|
||||
|
||||
// Mock DOM elements for testing
|
||||
function createMockContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = `
|
||||
<nav class="nav-tabs">
|
||||
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
|
||||
<button class="nav-tab" data-tab="hardware">Hardware</button>
|
||||
</nav>
|
||||
<div id="dashboard" class="tab-content active"></div>
|
||||
<div id="hardware" class="tab-content"></div>
|
||||
`;
|
||||
return container;
|
||||
}
|
||||
|
||||
// API Configuration Tests
|
||||
testRunner.test('API_CONFIG contains required endpoints', 'apiConfig', () => {
|
||||
testRunner.assert(API_CONFIG.ENDPOINTS, 'ENDPOINTS should exist');
|
||||
testRunner.assert(API_CONFIG.ENDPOINTS.POSE, 'POSE endpoints should exist');
|
||||
testRunner.assert(API_CONFIG.ENDPOINTS.HEALTH, 'HEALTH endpoints should exist');
|
||||
testRunner.assert(API_CONFIG.ENDPOINTS.STREAM, 'STREAM endpoints should exist');
|
||||
});
|
||||
|
||||
testRunner.test('buildApiUrl constructs correct URLs', 'apiConfig', () => {
|
||||
const url = buildApiUrl('/api/v1/pose/current', { zone_id: 'zone1', limit: 10 });
|
||||
testRunner.assert(url.includes('/api/v1/pose/current'), 'URL should contain endpoint');
|
||||
testRunner.assert(url.includes('zone_id=zone1'), 'URL should contain zone_id parameter');
|
||||
testRunner.assert(url.includes('limit=10'), 'URL should contain limit parameter');
|
||||
});
|
||||
|
||||
testRunner.test('buildApiUrl handles path parameters', 'apiConfig', () => {
|
||||
const url = buildApiUrl('/api/v1/pose/zones/{zone_id}/occupancy', { zone_id: 'zone1' });
|
||||
testRunner.assert(url.includes('/api/v1/pose/zones/zone1/occupancy'), 'URL should replace path parameter');
|
||||
testRunner.assert(!url.includes('{zone_id}'), 'URL should not contain placeholder');
|
||||
});
|
||||
|
||||
testRunner.test('buildWsUrl constructs WebSocket URLs', 'apiConfig', () => {
|
||||
const url = buildWsUrl('/api/v1/stream/pose', { token: 'test-token' });
|
||||
testRunner.assert(url.startsWith('ws://') || url.startsWith('wss://'), 'URL should be WebSocket protocol');
|
||||
testRunner.assert(url.includes('/api/v1/stream/pose'), 'URL should contain endpoint');
|
||||
testRunner.assert(url.includes('token=test-token'), 'URL should contain token parameter');
|
||||
});
|
||||
|
||||
// API Service Tests
|
||||
testRunner.test('apiService has required methods', 'apiService', () => {
|
||||
testRunner.assert(typeof apiService.get === 'function', 'get method should exist');
|
||||
testRunner.assert(typeof apiService.post === 'function', 'post method should exist');
|
||||
testRunner.assert(typeof apiService.put === 'function', 'put method should exist');
|
||||
testRunner.assert(typeof apiService.delete === 'function', 'delete method should exist');
|
||||
});
|
||||
|
||||
testRunner.test('apiService can set auth token', 'apiService', () => {
|
||||
const token = 'test-token-123';
|
||||
apiService.setAuthToken(token);
|
||||
testRunner.assertEqual(apiService.authToken, token, 'Auth token should be set');
|
||||
});
|
||||
|
||||
testRunner.test('apiService builds correct headers', 'apiService', () => {
|
||||
apiService.setAuthToken('test-token');
|
||||
const headers = apiService.getHeaders();
|
||||
testRunner.assert(headers['Content-Type'], 'Content-Type header should exist');
|
||||
testRunner.assert(headers['Authorization'], 'Authorization header should exist');
|
||||
testRunner.assertEqual(headers['Authorization'], 'Bearer test-token', 'Authorization header should be correct');
|
||||
});
|
||||
|
||||
testRunner.test('apiService handles interceptors', 'apiService', () => {
|
||||
let requestIntercepted = false;
|
||||
let responseIntercepted = false;
|
||||
|
||||
apiService.addRequestInterceptor(() => {
|
||||
requestIntercepted = true;
|
||||
return { url: 'test', options: {} };
|
||||
});
|
||||
|
||||
apiService.addResponseInterceptor(() => {
|
||||
responseIntercepted = true;
|
||||
return new Response('{}');
|
||||
});
|
||||
|
||||
testRunner.assert(apiService.requestInterceptors.length > 0, 'Request interceptor should be added');
|
||||
testRunner.assert(apiService.responseInterceptors.length > 0, 'Response interceptor should be added');
|
||||
});
|
||||
|
||||
// WebSocket Service Tests
|
||||
testRunner.test('wsService has required methods', 'websocketService', () => {
|
||||
testRunner.assert(typeof wsService.connect === 'function', 'connect method should exist');
|
||||
testRunner.assert(typeof wsService.disconnect === 'function', 'disconnect method should exist');
|
||||
testRunner.assert(typeof wsService.send === 'function', 'send method should exist');
|
||||
testRunner.assert(typeof wsService.onMessage === 'function', 'onMessage method should exist');
|
||||
});
|
||||
|
||||
testRunner.test('wsService generates unique connection IDs', 'websocketService', () => {
|
||||
const id1 = wsService.generateId();
|
||||
const id2 = wsService.generateId();
|
||||
testRunner.assertNotEqual(id1, id2, 'Connection IDs should be unique');
|
||||
testRunner.assert(id1.startsWith('ws_'), 'Connection ID should have correct prefix');
|
||||
});
|
||||
|
||||
testRunner.test('wsService manages connection state', 'websocketService', () => {
|
||||
const initialConnections = wsService.getActiveConnections();
|
||||
testRunner.assert(Array.isArray(initialConnections), 'Active connections should be an array');
|
||||
});
|
||||
|
||||
// Pose Service Tests
|
||||
testRunner.test('poseService has required methods', 'poseService', () => {
|
||||
testRunner.assert(typeof poseService.getCurrentPose === 'function', 'getCurrentPose method should exist');
|
||||
testRunner.assert(typeof poseService.getZoneOccupancy === 'function', 'getZoneOccupancy method should exist');
|
||||
testRunner.assert(typeof poseService.startPoseStream === 'function', 'startPoseStream method should exist');
|
||||
testRunner.assert(typeof poseService.subscribeToPoseUpdates === 'function', 'subscribeToPoseUpdates method should exist');
|
||||
});
|
||||
|
||||
testRunner.test('poseService subscription management', 'poseService', () => {
|
||||
let callbackCalled = false;
|
||||
const unsubscribe = poseService.subscribeToPoseUpdates(() => {
|
||||
callbackCalled = true;
|
||||
});
|
||||
|
||||
testRunner.assert(typeof unsubscribe === 'function', 'Subscribe should return unsubscribe function');
|
||||
testRunner.assert(poseService.poseSubscribers.length > 0, 'Subscriber should be added');
|
||||
|
||||
unsubscribe();
|
||||
testRunner.assertEqual(poseService.poseSubscribers.length, 0, 'Subscriber should be removed');
|
||||
});
|
||||
|
||||
testRunner.test('poseService handles pose updates', 'poseService', () => {
|
||||
let receivedUpdate = null;
|
||||
|
||||
poseService.subscribeToPoseUpdates(update => {
|
||||
receivedUpdate = update;
|
||||
});
|
||||
|
||||
const testUpdate = { type: 'pose_update', data: { persons: [] } };
|
||||
poseService.notifyPoseSubscribers(testUpdate);
|
||||
|
||||
testRunner.assertEqual(receivedUpdate, testUpdate, 'Update should be received by subscriber');
|
||||
});
|
||||
|
||||
// Health Service Tests
|
||||
testRunner.test('healthService has required methods', 'healthService', () => {
|
||||
testRunner.assert(typeof healthService.getSystemHealth === 'function', 'getSystemHealth method should exist');
|
||||
testRunner.assert(typeof healthService.checkReadiness === 'function', 'checkReadiness method should exist');
|
||||
testRunner.assert(typeof healthService.startHealthMonitoring === 'function', 'startHealthMonitoring method should exist');
|
||||
testRunner.assert(typeof healthService.subscribeToHealth === 'function', 'subscribeToHealth method should exist');
|
||||
});
|
||||
|
||||
testRunner.test('healthService subscription management', 'healthService', () => {
|
||||
let callbackCalled = false;
|
||||
const unsubscribe = healthService.subscribeToHealth(() => {
|
||||
callbackCalled = true;
|
||||
});
|
||||
|
||||
testRunner.assert(typeof unsubscribe === 'function', 'Subscribe should return unsubscribe function');
|
||||
testRunner.assert(healthService.healthSubscribers.length > 0, 'Subscriber should be added');
|
||||
|
||||
unsubscribe();
|
||||
testRunner.assertEqual(healthService.healthSubscribers.length, 0, 'Subscriber should be removed');
|
||||
});
|
||||
|
||||
testRunner.test('healthService status checking', 'healthService', () => {
|
||||
// Set mock health status
|
||||
healthService.lastHealthStatus = { status: 'healthy' };
|
||||
testRunner.assert(healthService.isSystemHealthy(), 'System should be healthy');
|
||||
|
||||
healthService.lastHealthStatus = { status: 'unhealthy' };
|
||||
testRunner.assert(!healthService.isSystemHealthy(), 'System should not be healthy');
|
||||
|
||||
healthService.lastHealthStatus = null;
|
||||
testRunner.assertEqual(healthService.isSystemHealthy(), null, 'System health should be null when no status');
|
||||
});
|
||||
|
||||
// UI Component Tests
|
||||
testRunner.test('TabManager can be instantiated', 'uiComponent', () => {
|
||||
const container = createMockContainer();
|
||||
const tabManager = new TabManager(container);
|
||||
testRunner.assert(tabManager instanceof TabManager, 'TabManager should be instantiated');
|
||||
});
|
||||
|
||||
testRunner.test('TabManager initializes tabs', 'uiComponent', () => {
|
||||
const container = createMockContainer();
|
||||
const tabManager = new TabManager(container);
|
||||
tabManager.init();
|
||||
|
||||
testRunner.assert(tabManager.tabs.length > 0, 'Tabs should be found');
|
||||
testRunner.assert(tabManager.tabContents.length > 0, 'Tab contents should be found');
|
||||
});
|
||||
|
||||
testRunner.test('TabManager handles tab switching', 'uiComponent', () => {
|
||||
const container = createMockContainer();
|
||||
const tabManager = new TabManager(container);
|
||||
tabManager.init();
|
||||
|
||||
let tabChangeEvent = null;
|
||||
tabManager.onTabChange((newTab, oldTab) => {
|
||||
tabChangeEvent = { newTab, oldTab };
|
||||
});
|
||||
|
||||
// Switch to hardware tab
|
||||
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
||||
tabManager.switchTab(hardwareTab);
|
||||
|
||||
testRunner.assertEqual(tabManager.getActiveTab(), 'hardware', 'Active tab should be updated');
|
||||
testRunner.assert(tabChangeEvent, 'Tab change event should be fired');
|
||||
testRunner.assertEqual(tabChangeEvent.newTab, 'hardware', 'New tab should be correct');
|
||||
});
|
||||
|
||||
testRunner.test('TabManager can enable/disable tabs', 'uiComponent', () => {
|
||||
const container = createMockContainer();
|
||||
const tabManager = new TabManager(container);
|
||||
tabManager.init();
|
||||
|
||||
tabManager.setTabEnabled('hardware', false);
|
||||
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
||||
testRunner.assert(hardwareTab.disabled, 'Tab should be disabled');
|
||||
testRunner.assert(hardwareTab.classList.contains('disabled'), 'Tab should have disabled class');
|
||||
});
|
||||
|
||||
testRunner.test('TabManager can show/hide tabs', 'uiComponent', () => {
|
||||
const container = createMockContainer();
|
||||
const tabManager = new TabManager(container);
|
||||
tabManager.init();
|
||||
|
||||
tabManager.setTabVisible('hardware', false);
|
||||
const hardwareTab = container.querySelector('[data-tab="hardware"]');
|
||||
testRunner.assertEqual(hardwareTab.style.display, 'none', 'Tab should be hidden');
|
||||
});
|
||||
|
||||
// Integration Tests
|
||||
testRunner.test('Services can be imported together', 'integration', () => {
|
||||
testRunner.assert(apiService, 'API service should be available');
|
||||
testRunner.assert(wsService, 'WebSocket service should be available');
|
||||
testRunner.assert(poseService, 'Pose service should be available');
|
||||
testRunner.assert(healthService, 'Health service should be available');
|
||||
});
|
||||
|
||||
testRunner.test('Services maintain separate state', 'integration', () => {
|
||||
// Set different states
|
||||
apiService.setAuthToken('api-token');
|
||||
poseService.subscribeToPoseUpdates(() => {});
|
||||
healthService.subscribeToHealth(() => {});
|
||||
|
||||
// Verify independence
|
||||
testRunner.assertEqual(apiService.authToken, 'api-token', 'API service should maintain its token');
|
||||
testRunner.assert(poseService.poseSubscribers.length > 0, 'Pose service should have subscribers');
|
||||
testRunner.assert(healthService.healthSubscribers.length > 0, 'Health service should have subscribers');
|
||||
});
|
||||
|
||||
testRunner.test('Configuration is consistent across services', 'integration', () => {
|
||||
// All services should use the same configuration
|
||||
testRunner.assert(API_CONFIG.BASE_URL, 'Base URL should be configured');
|
||||
testRunner.assert(API_CONFIG.ENDPOINTS, 'Endpoints should be configured');
|
||||
testRunner.assert(API_CONFIG.WS_CONFIG, 'WebSocket config should be available');
|
||||
});
|
||||
|
||||
// Event listeners for UI
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('runAllTests').addEventListener('click', () => {
|
||||
testRunner.runAllTests();
|
||||
});
|
||||
|
||||
document.getElementById('runUnitTests').addEventListener('click', () => {
|
||||
const unitCategories = ['apiConfig', 'apiService', 'websocketService', 'poseService', 'healthService', 'uiComponent'];
|
||||
testRunner.clearResults();
|
||||
|
||||
(async () => {
|
||||
for (const category of unitCategories) {
|
||||
await testRunner.runTestsByCategory(category);
|
||||
}
|
||||
testRunner.updateSummary();
|
||||
})();
|
||||
});
|
||||
|
||||
document.getElementById('runIntegrationTests').addEventListener('click', () => {
|
||||
testRunner.runTestsByCategory('integration');
|
||||
});
|
||||
|
||||
document.getElementById('clearResults').addEventListener('click', () => {
|
||||
testRunner.clearResults();
|
||||
});
|
||||
|
||||
// Initialize test display
|
||||
testRunner.tests.forEach(test => testRunner.updateTestDisplay(test));
|
||||
testRunner.updateSummary();
|
||||
});
|
||||
|
||||
export { testRunner };
|
||||
Reference in New Issue
Block a user