feat: Implement hardware, pose, and stream services for WiFi-DensePose API

- Added HardwareService for managing router interfaces, data collection, and monitoring.
- Introduced PoseService for processing CSI data and estimating poses using neural networks.
- Created StreamService for real-time data streaming via WebSocket connections.
- Implemented initialization, start, stop, and status retrieval methods for each service.
- Added data processing, error handling, and statistics tracking across services.
- Integrated mock data generation for development and testing purposes.
This commit is contained in:
rUv
2025-06-07 12:47:54 +00:00
parent c378b705ca
commit 90f03bac7d
26 changed files with 9846 additions and 105 deletions
+7 -1
View File
@@ -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'
]
+483
View File
@@ -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
+706
View File
@@ -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
+397
View File
@@ -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