Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
252
ui/app.js
Normal file
252
ui/app.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// WiFi DensePose Application - Main Entry Point
|
||||
|
||||
import { TabManager } from './components/TabManager.js';
|
||||
import { DashboardTab } from './components/DashboardTab.js';
|
||||
import { HardwareTab } from './components/HardwareTab.js';
|
||||
import { LiveDemoTab } from './components/LiveDemoTab.js';
|
||||
import { apiService } from './services/api.service.js';
|
||||
import { wsService } from './services/websocket.service.js';
|
||||
import { healthService } from './services/health.service.js';
|
||||
|
||||
class WiFiDensePoseApp {
|
||||
constructor() {
|
||||
this.components = {};
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Initialize application
|
||||
async init() {
|
||||
try {
|
||||
console.log('Initializing WiFi DensePose UI...');
|
||||
|
||||
// Set up error handling
|
||||
this.setupErrorHandling();
|
||||
|
||||
// Initialize services
|
||||
await this.initializeServices();
|
||||
|
||||
// Initialize UI components
|
||||
this.initializeComponents();
|
||||
|
||||
// Set up global event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('WiFi DensePose UI initialized successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize application:', error);
|
||||
this.showGlobalError('Failed to initialize application. Please refresh the page.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
async initializeServices() {
|
||||
// Add request interceptor for error handling
|
||||
apiService.addResponseInterceptor(async (response, url) => {
|
||||
if (!response.ok && response.status === 401) {
|
||||
console.warn('Authentication required for:', url);
|
||||
// Handle authentication if needed
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
// Check API availability
|
||||
try {
|
||||
const health = await healthService.checkLiveness();
|
||||
console.log('API is available:', health);
|
||||
} catch (error) {
|
||||
console.error('API is not available:', error);
|
||||
throw new Error('API is not available. Please ensure the backend is running.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize UI components
|
||||
initializeComponents() {
|
||||
const container = document.querySelector('.container');
|
||||
if (!container) {
|
||||
throw new Error('Main container not found');
|
||||
}
|
||||
|
||||
// Initialize tab manager
|
||||
this.components.tabManager = new TabManager(container);
|
||||
this.components.tabManager.init();
|
||||
|
||||
// Initialize tab components
|
||||
this.initializeTabComponents();
|
||||
|
||||
// Set up tab change handling
|
||||
this.components.tabManager.onTabChange((newTab, oldTab) => {
|
||||
this.handleTabChange(newTab, oldTab);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize individual tab components
|
||||
initializeTabComponents() {
|
||||
// Dashboard tab
|
||||
const dashboardContainer = document.getElementById('dashboard');
|
||||
if (dashboardContainer) {
|
||||
this.components.dashboard = new DashboardTab(dashboardContainer);
|
||||
this.components.dashboard.init().catch(error => {
|
||||
console.error('Failed to initialize dashboard:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Hardware tab
|
||||
const hardwareContainer = document.getElementById('hardware');
|
||||
if (hardwareContainer) {
|
||||
this.components.hardware = new HardwareTab(hardwareContainer);
|
||||
this.components.hardware.init();
|
||||
}
|
||||
|
||||
// Live demo tab
|
||||
const demoContainer = document.getElementById('demo');
|
||||
if (demoContainer) {
|
||||
this.components.demo = new LiveDemoTab(demoContainer);
|
||||
this.components.demo.init();
|
||||
}
|
||||
|
||||
// Architecture tab - static content, no component needed
|
||||
|
||||
// Performance tab - static content, no component needed
|
||||
|
||||
// Applications tab - static content, no component needed
|
||||
}
|
||||
|
||||
// Handle tab changes
|
||||
handleTabChange(newTab, oldTab) {
|
||||
console.log(`Tab changed from ${oldTab} to ${newTab}`);
|
||||
|
||||
// Stop demo if leaving demo tab
|
||||
if (oldTab === 'demo' && this.components.demo) {
|
||||
this.components.demo.stopDemo();
|
||||
}
|
||||
|
||||
// Update components based on active tab
|
||||
switch (newTab) {
|
||||
case 'dashboard':
|
||||
// Dashboard auto-updates when visible
|
||||
break;
|
||||
|
||||
case 'hardware':
|
||||
// Hardware visualization is always active
|
||||
break;
|
||||
|
||||
case 'demo':
|
||||
// Demo starts manually
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up global event listeners
|
||||
setupEventListeners() {
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.handleResize();
|
||||
});
|
||||
|
||||
// Handle visibility change
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.handleVisibilityChange();
|
||||
});
|
||||
|
||||
// Handle before unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
handleResize() {
|
||||
// Update canvas sizes if needed
|
||||
const canvases = document.querySelectorAll('canvas');
|
||||
canvases.forEach(canvas => {
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
if (canvas.width !== rect.width || canvas.height !== rect.height) {
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle visibility change
|
||||
handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// Pause updates when page is hidden
|
||||
console.log('Page hidden, pausing updates');
|
||||
healthService.stopHealthMonitoring();
|
||||
} else {
|
||||
// Resume updates when page is visible
|
||||
console.log('Page visible, resuming updates');
|
||||
healthService.startHealthMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
// Set up error handling
|
||||
setupErrorHandling() {
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('Global error:', event.error);
|
||||
this.showGlobalError('An unexpected error occurred');
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
this.showGlobalError('An unexpected error occurred');
|
||||
});
|
||||
}
|
||||
|
||||
// Show global error message
|
||||
showGlobalError(message) {
|
||||
// Create error toast if it doesn't exist
|
||||
let errorToast = document.getElementById('globalErrorToast');
|
||||
if (!errorToast) {
|
||||
errorToast = document.createElement('div');
|
||||
errorToast.id = 'globalErrorToast';
|
||||
errorToast.className = 'error-toast';
|
||||
document.body.appendChild(errorToast);
|
||||
}
|
||||
|
||||
errorToast.textContent = message;
|
||||
errorToast.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
errorToast.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
cleanup() {
|
||||
console.log('Cleaning up application resources...');
|
||||
|
||||
// Dispose all components
|
||||
Object.values(this.components).forEach(component => {
|
||||
if (component && typeof component.dispose === 'function') {
|
||||
component.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect all WebSocket connections
|
||||
wsService.disconnectAll();
|
||||
|
||||
// Stop health monitoring
|
||||
healthService.dispose();
|
||||
}
|
||||
|
||||
// Public API
|
||||
getComponent(name) {
|
||||
return this.components[name];
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.wifiDensePoseApp = new WiFiDensePoseApp();
|
||||
window.wifiDensePoseApp.init();
|
||||
});
|
||||
|
||||
// Export for testing
|
||||
export { WiFiDensePoseApp };
|
||||
309
ui/components/DashboardTab.js
Normal file
309
ui/components/DashboardTab.js
Normal file
@@ -0,0 +1,309 @@
|
||||
// Dashboard Tab Component
|
||||
|
||||
import { healthService } from '../services/health.service.js';
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
|
||||
export class DashboardTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.statsElements = {};
|
||||
this.healthSubscription = null;
|
||||
this.statsInterval = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
async init() {
|
||||
this.cacheElements();
|
||||
await this.loadInitialData();
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
// Cache DOM elements
|
||||
cacheElements() {
|
||||
// System stats
|
||||
const statsContainer = this.container.querySelector('.system-stats');
|
||||
if (statsContainer) {
|
||||
this.statsElements = {
|
||||
bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'),
|
||||
samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'),
|
||||
accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'),
|
||||
hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value')
|
||||
};
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
this.statusElements = {
|
||||
apiStatus: this.container.querySelector('.api-status'),
|
||||
streamStatus: this.container.querySelector('.stream-status'),
|
||||
hardwareStatus: this.container.querySelector('.hardware-status')
|
||||
};
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// Get API info
|
||||
const info = await healthService.getApiInfo();
|
||||
this.updateApiInfo(info);
|
||||
|
||||
// Get current stats
|
||||
const stats = await poseService.getStats(1);
|
||||
this.updateStats(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
this.showError('Failed to load dashboard data');
|
||||
}
|
||||
}
|
||||
|
||||
// Start monitoring
|
||||
startMonitoring() {
|
||||
// Subscribe to health updates
|
||||
this.healthSubscription = healthService.subscribeToHealth(health => {
|
||||
this.updateHealthStatus(health);
|
||||
});
|
||||
|
||||
// Start periodic stats updates
|
||||
this.statsInterval = setInterval(() => {
|
||||
this.updateLiveStats();
|
||||
}, 5000);
|
||||
|
||||
// Start health monitoring
|
||||
healthService.startHealthMonitoring(30000);
|
||||
}
|
||||
|
||||
// Update API info display
|
||||
updateApiInfo(info) {
|
||||
// Update version
|
||||
const versionElement = this.container.querySelector('.api-version');
|
||||
if (versionElement && info.version) {
|
||||
versionElement.textContent = `v${info.version}`;
|
||||
}
|
||||
|
||||
// Update environment
|
||||
const envElement = this.container.querySelector('.api-environment');
|
||||
if (envElement && info.environment) {
|
||||
envElement.textContent = info.environment;
|
||||
envElement.className = `api-environment env-${info.environment}`;
|
||||
}
|
||||
|
||||
// Update features status
|
||||
if (info.features) {
|
||||
this.updateFeatures(info.features);
|
||||
}
|
||||
}
|
||||
|
||||
// Update features display
|
||||
updateFeatures(features) {
|
||||
const featuresContainer = this.container.querySelector('.features-status');
|
||||
if (!featuresContainer) return;
|
||||
|
||||
featuresContainer.innerHTML = '';
|
||||
|
||||
Object.entries(features).forEach(([feature, enabled]) => {
|
||||
const featureElement = document.createElement('div');
|
||||
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
|
||||
featureElement.innerHTML = `
|
||||
<span class="feature-name">${this.formatFeatureName(feature)}</span>
|
||||
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
|
||||
`;
|
||||
featuresContainer.appendChild(featureElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update health status
|
||||
updateHealthStatus(health) {
|
||||
if (!health) return;
|
||||
|
||||
// Update overall status
|
||||
const overallStatus = this.container.querySelector('.overall-health');
|
||||
if (overallStatus) {
|
||||
overallStatus.className = `overall-health status-${health.status}`;
|
||||
overallStatus.textContent = health.status.toUpperCase();
|
||||
}
|
||||
|
||||
// Update component statuses
|
||||
if (health.components) {
|
||||
Object.entries(health.components).forEach(([component, status]) => {
|
||||
this.updateComponentStatus(component, status);
|
||||
});
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
if (health.metrics) {
|
||||
this.updateSystemMetrics(health.metrics);
|
||||
}
|
||||
}
|
||||
|
||||
// Update component status
|
||||
updateComponentStatus(component, status) {
|
||||
const element = this.container.querySelector(`[data-component="${component}"]`);
|
||||
if (element) {
|
||||
element.className = `component-status status-${status.status}`;
|
||||
element.querySelector('.status-text').textContent = status.status;
|
||||
|
||||
if (status.message) {
|
||||
element.querySelector('.status-message').textContent = status.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update system metrics
|
||||
updateSystemMetrics(metrics) {
|
||||
// CPU usage
|
||||
const cpuElement = this.container.querySelector('.cpu-usage');
|
||||
if (cpuElement && metrics.cpu_percent !== undefined) {
|
||||
cpuElement.textContent = `${metrics.cpu_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('cpu', metrics.cpu_percent);
|
||||
}
|
||||
|
||||
// Memory usage
|
||||
const memoryElement = this.container.querySelector('.memory-usage');
|
||||
if (memoryElement && metrics.memory_percent !== undefined) {
|
||||
memoryElement.textContent = `${metrics.memory_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('memory', metrics.memory_percent);
|
||||
}
|
||||
|
||||
// Disk usage
|
||||
const diskElement = this.container.querySelector('.disk-usage');
|
||||
if (diskElement && metrics.disk_percent !== undefined) {
|
||||
diskElement.textContent = `${metrics.disk_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('disk', metrics.disk_percent);
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
updateProgressBar(type, percent) {
|
||||
const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`);
|
||||
if (progressBar) {
|
||||
const fill = progressBar.querySelector('.progress-fill');
|
||||
if (fill) {
|
||||
fill.style.width = `${percent}%`;
|
||||
fill.className = `progress-fill ${this.getProgressClass(percent)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get progress class based on percentage
|
||||
getProgressClass(percent) {
|
||||
if (percent >= 90) return 'critical';
|
||||
if (percent >= 75) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Update live statistics
|
||||
async updateLiveStats() {
|
||||
try {
|
||||
// Get current pose data
|
||||
const currentPose = await poseService.getCurrentPose();
|
||||
this.updatePoseStats(currentPose);
|
||||
|
||||
// Get zones summary
|
||||
const zonesSummary = await poseService.getZonesSummary();
|
||||
this.updateZonesDisplay(zonesSummary);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update live stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update pose statistics
|
||||
updatePoseStats(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('.person-count');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.total_persons || 0;
|
||||
}
|
||||
|
||||
// Update average confidence
|
||||
const avgConfidence = this.container.querySelector('.avg-confidence');
|
||||
if (avgConfidence && poseData.persons) {
|
||||
const confidences = poseData.persons.map(p => p.confidence);
|
||||
const avg = confidences.length > 0
|
||||
? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1)
|
||||
: 0;
|
||||
avgConfidence.textContent = `${avg}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update zones display
|
||||
updateZonesDisplay(zonesSummary) {
|
||||
const zonesContainer = this.container.querySelector('.zones-summary');
|
||||
if (!zonesContainer || !zonesSummary) return;
|
||||
|
||||
zonesContainer.innerHTML = '';
|
||||
|
||||
Object.entries(zonesSummary.zones).forEach(([zoneId, data]) => {
|
||||
const zoneElement = document.createElement('div');
|
||||
zoneElement.className = 'zone-item';
|
||||
zoneElement.innerHTML = `
|
||||
<span class="zone-name">${data.name || zoneId}</span>
|
||||
<span class="zone-count">${data.person_count}</span>
|
||||
`;
|
||||
zonesContainer.appendChild(zoneElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
updateStats(stats) {
|
||||
if (!stats) return;
|
||||
|
||||
// Update detection count
|
||||
const detectionCount = this.container.querySelector('.detection-count');
|
||||
if (detectionCount && stats.total_detections !== undefined) {
|
||||
detectionCount.textContent = this.formatNumber(stats.total_detections);
|
||||
}
|
||||
|
||||
// Update accuracy if available
|
||||
if (this.statsElements.accuracy && stats.average_confidence !== undefined) {
|
||||
this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Format feature name
|
||||
formatFeatureName(name) {
|
||||
return name.replace(/_/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Format large numbers
|
||||
formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
showError(message) {
|
||||
const errorContainer = this.container.querySelector('.error-container');
|
||||
if (errorContainer) {
|
||||
errorContainer.textContent = message;
|
||||
errorContainer.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
errorContainer.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
if (this.healthSubscription) {
|
||||
this.healthSubscription();
|
||||
}
|
||||
|
||||
if (this.statsInterval) {
|
||||
clearInterval(this.statsInterval);
|
||||
}
|
||||
|
||||
healthService.stopHealthMonitoring();
|
||||
}
|
||||
}
|
||||
165
ui/components/HardwareTab.js
Normal file
165
ui/components/HardwareTab.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// Hardware Tab Component
|
||||
|
||||
export class HardwareTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.antennas = [];
|
||||
this.csiUpdateInterval = null;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
init() {
|
||||
this.setupAntennas();
|
||||
this.startCSISimulation();
|
||||
}
|
||||
|
||||
// Set up antenna interactions
|
||||
setupAntennas() {
|
||||
this.antennas = Array.from(this.container.querySelectorAll('.antenna'));
|
||||
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.addEventListener('click', () => {
|
||||
antenna.classList.toggle('active');
|
||||
this.updateCSIDisplay();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start CSI simulation
|
||||
startCSISimulation() {
|
||||
// Initial update
|
||||
this.updateCSIDisplay();
|
||||
|
||||
// Set up periodic updates
|
||||
this.csiUpdateInterval = setInterval(() => {
|
||||
if (this.hasActiveAntennas()) {
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Check if any antennas are active
|
||||
hasActiveAntennas() {
|
||||
return this.antennas.some(antenna => antenna.classList.contains('active'));
|
||||
}
|
||||
|
||||
// Update CSI display
|
||||
updateCSIDisplay() {
|
||||
const activeAntennas = this.antennas.filter(a => a.classList.contains('active'));
|
||||
const isActive = activeAntennas.length > 0;
|
||||
|
||||
// Get display elements
|
||||
const amplitudeFill = this.container.querySelector('.csi-fill.amplitude');
|
||||
const phaseFill = this.container.querySelector('.csi-fill.phase');
|
||||
const amplitudeValue = this.container.querySelector('.csi-row:first-child .csi-value');
|
||||
const phaseValue = this.container.querySelector('.csi-row:last-child .csi-value');
|
||||
|
||||
if (!isActive) {
|
||||
// Set to zero when no antennas active
|
||||
if (amplitudeFill) amplitudeFill.style.width = '0%';
|
||||
if (phaseFill) phaseFill.style.width = '0%';
|
||||
if (amplitudeValue) amplitudeValue.textContent = '0.00';
|
||||
if (phaseValue) phaseValue.textContent = '0.0π';
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate realistic CSI values based on active antennas
|
||||
const txCount = activeAntennas.filter(a => a.classList.contains('tx')).length;
|
||||
const rxCount = activeAntennas.filter(a => a.classList.contains('rx')).length;
|
||||
|
||||
// Amplitude increases with more active antennas
|
||||
const baseAmplitude = 0.3 + (txCount * 0.1) + (rxCount * 0.05);
|
||||
const amplitude = Math.min(0.95, baseAmplitude + (Math.random() * 0.1 - 0.05));
|
||||
|
||||
// Phase varies more with multiple antennas
|
||||
const phaseVariation = 0.5 + (activeAntennas.length * 0.1);
|
||||
const phase = 0.5 + Math.random() * phaseVariation;
|
||||
|
||||
// Update display
|
||||
if (amplitudeFill) {
|
||||
amplitudeFill.style.width = `${amplitude * 100}%`;
|
||||
amplitudeFill.style.transition = 'width 0.5s ease';
|
||||
}
|
||||
|
||||
if (phaseFill) {
|
||||
phaseFill.style.width = `${phase * 50}%`;
|
||||
phaseFill.style.transition = 'width 0.5s ease';
|
||||
}
|
||||
|
||||
if (amplitudeValue) {
|
||||
amplitudeValue.textContent = amplitude.toFixed(2);
|
||||
}
|
||||
|
||||
if (phaseValue) {
|
||||
phaseValue.textContent = `${phase.toFixed(1)}π`;
|
||||
}
|
||||
|
||||
// Update antenna array visualization
|
||||
this.updateAntennaArray(activeAntennas);
|
||||
}
|
||||
|
||||
// Update antenna array visualization
|
||||
updateAntennaArray(activeAntennas) {
|
||||
const arrayStatus = this.container.querySelector('.array-status');
|
||||
if (!arrayStatus) return;
|
||||
|
||||
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
|
||||
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
|
||||
|
||||
arrayStatus.innerHTML = `
|
||||
<div class="array-info">
|
||||
<span class="info-label">Active TX:</span>
|
||||
<span class="info-value">${txActive}/3</span>
|
||||
</div>
|
||||
<div class="array-info">
|
||||
<span class="info-label">Active RX:</span>
|
||||
<span class="info-value">${rxActive}/6</span>
|
||||
</div>
|
||||
<div class="array-info">
|
||||
<span class="info-label">Signal Quality:</span>
|
||||
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Calculate signal quality based on active antennas
|
||||
calculateSignalQuality(txCount, rxCount) {
|
||||
if (txCount === 0 || rxCount === 0) return 0;
|
||||
|
||||
const txRatio = txCount / 3;
|
||||
const rxRatio = rxCount / 6;
|
||||
const quality = (txRatio * 0.4 + rxRatio * 0.6) * 100;
|
||||
|
||||
return Math.round(quality);
|
||||
}
|
||||
|
||||
// Toggle all antennas
|
||||
toggleAllAntennas(active) {
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.classList.toggle('active', active);
|
||||
});
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
|
||||
// Reset antenna configuration
|
||||
resetAntennas() {
|
||||
// Set default configuration (all active)
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.classList.add('active');
|
||||
});
|
||||
this.updateCSIDisplay();
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
if (this.csiUpdateInterval) {
|
||||
clearInterval(this.csiUpdateInterval);
|
||||
this.csiUpdateInterval = null;
|
||||
}
|
||||
|
||||
this.antennas.forEach(antenna => {
|
||||
antenna.removeEventListener('click', this.toggleAntenna);
|
||||
});
|
||||
}
|
||||
}
|
||||
391
ui/components/LiveDemoTab.js
Normal file
391
ui/components/LiveDemoTab.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// Live Demo Tab Component
|
||||
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
import { streamService } from '../services/stream.service.js';
|
||||
|
||||
export class LiveDemoTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.isRunning = false;
|
||||
this.streamConnection = null;
|
||||
this.poseSubscription = null;
|
||||
this.signalCanvas = null;
|
||||
this.poseCanvas = null;
|
||||
this.signalCtx = null;
|
||||
this.poseCtx = null;
|
||||
this.animationFrame = null;
|
||||
this.signalTime = 0;
|
||||
this.poseData = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
init() {
|
||||
this.setupCanvases();
|
||||
this.setupControls();
|
||||
this.initializeDisplays();
|
||||
}
|
||||
|
||||
// Set up canvases
|
||||
setupCanvases() {
|
||||
this.signalCanvas = this.container.querySelector('#signalCanvas');
|
||||
this.poseCanvas = this.container.querySelector('#poseCanvas');
|
||||
|
||||
if (this.signalCanvas) {
|
||||
this.signalCtx = this.signalCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
if (this.poseCanvas) {
|
||||
this.poseCtx = this.poseCanvas.getContext('2d');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up control buttons
|
||||
setupControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.addEventListener('click', () => this.startDemo());
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.addEventListener('click', () => this.stopDemo());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize displays
|
||||
initializeDisplays() {
|
||||
// Initialize signal canvas
|
||||
if (this.signalCtx) {
|
||||
this.signalCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.signalCtx.fillRect(0, 0, this.signalCanvas.width, this.signalCanvas.height);
|
||||
}
|
||||
|
||||
// Initialize pose canvas
|
||||
if (this.poseCtx) {
|
||||
this.poseCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.poseCtx.fillRect(0, 0, this.poseCanvas.width, this.poseCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Start demo
|
||||
async startDemo() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
try {
|
||||
// Update UI
|
||||
this.isRunning = true;
|
||||
this.updateControls();
|
||||
this.updateStatus('Starting...', 'info');
|
||||
|
||||
// Check stream status
|
||||
const streamStatus = await streamService.getStatus();
|
||||
|
||||
if (!streamStatus.is_active) {
|
||||
// Try to start streaming
|
||||
await streamService.start();
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
this.streamConnection = poseService.startPoseStream({
|
||||
minConfidence: 0.5,
|
||||
maxFps: 30
|
||||
});
|
||||
|
||||
// Subscribe to pose updates
|
||||
this.poseSubscription = poseService.subscribeToPoseUpdates(update => {
|
||||
this.handlePoseUpdate(update);
|
||||
});
|
||||
|
||||
// Start animations
|
||||
this.startAnimations();
|
||||
|
||||
// Update status
|
||||
this.updateStatus('Running', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start demo:', error);
|
||||
this.updateStatus('Failed to start', 'error');
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop demo
|
||||
stopDemo() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update UI
|
||||
this.isRunning = false;
|
||||
this.updateControls();
|
||||
this.updateStatus('Stopped', 'info');
|
||||
|
||||
// Stop pose stream
|
||||
if (this.poseSubscription) {
|
||||
this.poseSubscription();
|
||||
this.poseSubscription = null;
|
||||
}
|
||||
|
||||
poseService.stopPoseStream();
|
||||
this.streamConnection = null;
|
||||
|
||||
// Stop animations
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update controls
|
||||
updateControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.disabled = this.isRunning;
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.disabled = !this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status display
|
||||
updateStatus(text, type) {
|
||||
const statusElement = this.container.querySelector('#demoStatus');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = text;
|
||||
statusElement.className = `status status--${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pose updates
|
||||
handlePoseUpdate(update) {
|
||||
switch (update.type) {
|
||||
case 'connected':
|
||||
console.log('Pose stream connected');
|
||||
break;
|
||||
|
||||
case 'pose_update':
|
||||
this.poseData = update.data;
|
||||
this.updateMetrics(update.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Pose stream error:', update.error);
|
||||
this.updateStatus('Stream error', 'error');
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log('Pose stream disconnected');
|
||||
if (this.isRunning) {
|
||||
this.updateStatus('Disconnected', 'warning');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update metrics display
|
||||
updateMetrics(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update signal strength (simulated based on detection confidence)
|
||||
const signalStrength = this.container.querySelector('#signalStrength');
|
||||
if (signalStrength) {
|
||||
const strength = poseData.persons?.length > 0
|
||||
? -45 - Math.random() * 10
|
||||
: -55 - Math.random() * 10;
|
||||
signalStrength.textContent = `${strength.toFixed(0)} dBm`;
|
||||
}
|
||||
|
||||
// Update latency
|
||||
const latency = this.container.querySelector('#latency');
|
||||
if (latency && poseData.processing_time) {
|
||||
latency.textContent = `${poseData.processing_time.toFixed(0)} ms`;
|
||||
}
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('#personCount');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.persons?.length || 0;
|
||||
}
|
||||
|
||||
// Update confidence
|
||||
const confidence = this.container.querySelector('#confidence');
|
||||
if (confidence && poseData.persons?.length > 0) {
|
||||
const avgConfidence = poseData.persons.reduce((sum, p) => sum + p.confidence, 0)
|
||||
/ poseData.persons.length * 100;
|
||||
confidence.textContent = `${avgConfidence.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// Update keypoints
|
||||
const keypoints = this.container.querySelector('#keypoints');
|
||||
if (keypoints && poseData.persons?.length > 0) {
|
||||
const totalKeypoints = poseData.persons[0].keypoints?.length || 0;
|
||||
const detectedKeypoints = poseData.persons[0].keypoints?.filter(kp => kp.confidence > 0.5).length || 0;
|
||||
keypoints.textContent = `${detectedKeypoints}/${totalKeypoints}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Start animations
|
||||
startAnimations() {
|
||||
const animate = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update signal visualization
|
||||
this.updateSignalVisualization();
|
||||
|
||||
// Update pose visualization
|
||||
this.updatePoseVisualization();
|
||||
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
// Update signal visualization
|
||||
updateSignalVisualization() {
|
||||
if (!this.signalCtx) return;
|
||||
|
||||
const ctx = this.signalCtx;
|
||||
const width = this.signalCanvas.width;
|
||||
const height = this.signalCanvas.height;
|
||||
|
||||
// Clear canvas with fade effect
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw amplitude signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#1FB8CD';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 30 : 10;
|
||||
const frequency = hasData ? 0.05 : 0.02;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.sin(x * frequency + this.signalTime) * amplitude +
|
||||
Math.sin(x * 0.02 + this.signalTime * 1.5) * 15;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw phase signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#FFC185';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 25 : 15;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.cos(x * 0.03 + this.signalTime * 0.8) * amplitude +
|
||||
Math.cos(x * 0.01 + this.signalTime * 0.5) * 20;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
this.signalTime += 0.05;
|
||||
}
|
||||
|
||||
// Update pose visualization
|
||||
updatePoseVisualization() {
|
||||
if (!this.poseCtx || !this.poseData) return;
|
||||
|
||||
const ctx = this.poseCtx;
|
||||
const width = this.poseCanvas.width;
|
||||
const height = this.poseCanvas.height;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw each detected person
|
||||
if (this.poseData.persons) {
|
||||
this.poseData.persons.forEach((person, index) => {
|
||||
this.drawPerson(ctx, person, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a person's pose
|
||||
drawPerson(ctx, person, index) {
|
||||
if (!person.keypoints) return;
|
||||
|
||||
// Define COCO keypoint connections
|
||||
const connections = [
|
||||
[0, 1], [0, 2], [1, 3], [2, 4], // Head
|
||||
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10], // Arms
|
||||
[5, 11], [6, 12], [11, 12], // Body
|
||||
[11, 13], [13, 15], [12, 14], [14, 16] // Legs
|
||||
];
|
||||
|
||||
// Scale keypoints to canvas
|
||||
const scale = Math.min(this.poseCanvas.width, this.poseCanvas.height) / 2;
|
||||
const offsetX = this.poseCanvas.width / 2;
|
||||
const offsetY = this.poseCanvas.height / 2;
|
||||
|
||||
// Draw skeleton connections
|
||||
ctx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`;
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
connections.forEach(([i, j]) => {
|
||||
const kp1 = person.keypoints[i];
|
||||
const kp2 = person.keypoints[j];
|
||||
|
||||
if (kp1 && kp2 && kp1.confidence > 0.3 && kp2.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(kp1.x * scale + offsetX, kp1.y * scale + offsetY);
|
||||
ctx.lineTo(kp2.x * scale + offsetX, kp2.y * scale + offsetY);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw keypoints
|
||||
ctx.fillStyle = `hsl(${index * 60}, 70%, 60%)`;
|
||||
|
||||
person.keypoints.forEach(kp => {
|
||||
if (kp.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
kp.x * scale + offsetX,
|
||||
kp.y * scale + offsetY,
|
||||
5,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw confidence label
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText(
|
||||
`Person ${index + 1}: ${(person.confidence * 100).toFixed(1)}%`,
|
||||
10,
|
||||
20 + index * 20
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
138
ui/components/TabManager.js
Normal file
138
ui/components/TabManager.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// Tab Manager Component
|
||||
|
||||
export class TabManager {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.tabs = [];
|
||||
this.activeTab = null;
|
||||
this.tabChangeCallbacks = [];
|
||||
}
|
||||
|
||||
// Initialize tabs
|
||||
init() {
|
||||
// Find all tabs and contents
|
||||
this.tabs = Array.from(this.container.querySelectorAll('.nav-tab'));
|
||||
this.tabContents = Array.from(this.container.querySelectorAll('.tab-content'));
|
||||
|
||||
// Set up event listeners
|
||||
this.tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => this.switchTab(tab));
|
||||
});
|
||||
|
||||
// Activate first tab if none active
|
||||
const activeTab = this.tabs.find(tab => tab.classList.contains('active'));
|
||||
if (activeTab) {
|
||||
this.activeTab = activeTab.getAttribute('data-tab');
|
||||
} else if (this.tabs.length > 0) {
|
||||
this.switchTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to a tab
|
||||
switchTab(tabElement) {
|
||||
const tabId = tabElement.getAttribute('data-tab');
|
||||
|
||||
if (tabId === this.activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tab states
|
||||
this.tabs.forEach(tab => {
|
||||
tab.classList.toggle('active', tab === tabElement);
|
||||
});
|
||||
|
||||
// Update content visibility
|
||||
this.tabContents.forEach(content => {
|
||||
content.classList.toggle('active', content.id === tabId);
|
||||
});
|
||||
|
||||
// Update active tab
|
||||
const previousTab = this.activeTab;
|
||||
this.activeTab = tabId;
|
||||
|
||||
// Notify callbacks
|
||||
this.notifyTabChange(tabId, previousTab);
|
||||
}
|
||||
|
||||
// Switch to tab by ID
|
||||
switchToTab(tabId) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
this.switchTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// Register tab change callback
|
||||
onTabChange(callback) {
|
||||
this.tabChangeCallbacks.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.tabChangeCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.tabChangeCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify tab change callbacks
|
||||
notifyTabChange(newTab, previousTab) {
|
||||
this.tabChangeCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(newTab, previousTab);
|
||||
} catch (error) {
|
||||
console.error('Error in tab change callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get active tab
|
||||
getActiveTab() {
|
||||
return this.activeTab;
|
||||
}
|
||||
|
||||
// Enable/disable tab
|
||||
setTabEnabled(tabId, enabled) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
tab.disabled = !enabled;
|
||||
tab.classList.toggle('disabled', !enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide tab
|
||||
setTabVisible(tabId, visible) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (tab) {
|
||||
tab.style.display = visible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Add badge to tab
|
||||
setTabBadge(tabId, badge) {
|
||||
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
|
||||
if (!tab) return;
|
||||
|
||||
// Remove existing badge
|
||||
const existingBadge = tab.querySelector('.tab-badge');
|
||||
if (existingBadge) {
|
||||
existingBadge.remove();
|
||||
}
|
||||
|
||||
// Add new badge if provided
|
||||
if (badge) {
|
||||
const badgeElement = document.createElement('span');
|
||||
badgeElement.className = 'tab-badge';
|
||||
badgeElement.textContent = badge;
|
||||
tab.appendChild(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.tabs.forEach(tab => {
|
||||
tab.removeEventListener('click', this.switchTab);
|
||||
});
|
||||
this.tabChangeCallbacks = [];
|
||||
}
|
||||
}
|
||||
118
ui/config/api.config.js
Normal file
118
ui/config/api.config.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// API Configuration for WiFi-DensePose UI
|
||||
|
||||
export const API_CONFIG = {
|
||||
BASE_URL: window.location.origin,
|
||||
API_VERSION: '/api/v1',
|
||||
WS_PREFIX: 'ws://',
|
||||
WSS_PREFIX: 'wss://',
|
||||
|
||||
// API Endpoints
|
||||
ENDPOINTS: {
|
||||
// Root & Info
|
||||
ROOT: '/',
|
||||
INFO: '/api/v1/info',
|
||||
STATUS: '/api/v1/status',
|
||||
METRICS: '/api/v1/metrics',
|
||||
|
||||
// Health
|
||||
HEALTH: {
|
||||
SYSTEM: '/health/health',
|
||||
READY: '/health/ready',
|
||||
LIVE: '/health/live',
|
||||
METRICS: '/health/metrics',
|
||||
VERSION: '/health/version'
|
||||
},
|
||||
|
||||
// Pose
|
||||
POSE: {
|
||||
CURRENT: '/api/v1/pose/current',
|
||||
ANALYZE: '/api/v1/pose/analyze',
|
||||
ZONE_OCCUPANCY: '/api/v1/pose/zones/{zone_id}/occupancy',
|
||||
ZONES_SUMMARY: '/api/v1/pose/zones/summary',
|
||||
HISTORICAL: '/api/v1/pose/historical',
|
||||
ACTIVITIES: '/api/v1/pose/activities',
|
||||
CALIBRATE: '/api/v1/pose/calibrate',
|
||||
CALIBRATION_STATUS: '/api/v1/pose/calibration/status',
|
||||
STATS: '/api/v1/pose/stats'
|
||||
},
|
||||
|
||||
// Streaming
|
||||
STREAM: {
|
||||
STATUS: '/api/v1/stream/status',
|
||||
START: '/api/v1/stream/start',
|
||||
STOP: '/api/v1/stream/stop',
|
||||
CLIENTS: '/api/v1/stream/clients',
|
||||
DISCONNECT_CLIENT: '/api/v1/stream/clients/{client_id}',
|
||||
BROADCAST: '/api/v1/stream/broadcast',
|
||||
METRICS: '/api/v1/stream/metrics',
|
||||
// WebSocket endpoints
|
||||
WS_POSE: '/api/v1/stream/pose',
|
||||
WS_EVENTS: '/api/v1/stream/events'
|
||||
},
|
||||
|
||||
// Development (only in dev mode)
|
||||
DEV: {
|
||||
CONFIG: '/api/v1/dev/config',
|
||||
RESET: '/api/v1/dev/reset'
|
||||
}
|
||||
},
|
||||
|
||||
// Default request options
|
||||
DEFAULT_HEADERS: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
RATE_LIMITS: {
|
||||
REQUESTS_PER_MINUTE: 60,
|
||||
BURST_LIMIT: 10
|
||||
},
|
||||
|
||||
// WebSocket configuration
|
||||
WS_CONFIG: {
|
||||
RECONNECT_DELAY: 5000,
|
||||
MAX_RECONNECT_ATTEMPTS: 5,
|
||||
PING_INTERVAL: 30000,
|
||||
MESSAGE_TIMEOUT: 10000
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to build API URLs
|
||||
export function buildApiUrl(endpoint, params = {}) {
|
||||
let url = `${API_CONFIG.BASE_URL}${endpoint}`;
|
||||
|
||||
// Replace path parameters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (url.includes(`{${key}}`)) {
|
||||
url = url.replace(`{${key}}`, params[key]);
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Add query parameters
|
||||
const queryParams = new URLSearchParams(params);
|
||||
if (queryParams.toString()) {
|
||||
url += `?${queryParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper function to build WebSocket URLs
|
||||
export function buildWsUrl(endpoint, params = {}) {
|
||||
const protocol = window.location.protocol === 'https:'
|
||||
? API_CONFIG.WSS_PREFIX
|
||||
: API_CONFIG.WS_PREFIX;
|
||||
|
||||
const host = window.location.host;
|
||||
let url = `${protocol}${host}${endpoint}`;
|
||||
|
||||
// Add query parameters
|
||||
const queryParams = new URLSearchParams(params);
|
||||
if (queryParams.toString()) {
|
||||
url += `?${queryParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
489
ui/index.html
Normal file
489
ui/index.html
Normal file
@@ -0,0 +1,489 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WiFi DensePose: Human Tracking Through Walls</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>WiFi DensePose</h1>
|
||||
<p class="subtitle">Human Tracking Through Walls Using WiFi Signals</p>
|
||||
<div class="header-info">
|
||||
<span class="api-version"></span>
|
||||
<span class="api-environment"></span>
|
||||
<span class="overall-health"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="nav-tabs">
|
||||
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
|
||||
<button class="nav-tab" data-tab="hardware">Hardware</button>
|
||||
<button class="nav-tab" data-tab="demo">Live Demo</button>
|
||||
<button class="nav-tab" data-tab="architecture">Architecture</button>
|
||||
<button class="nav-tab" data-tab="performance">Performance</button>
|
||||
<button class="nav-tab" data-tab="applications">Applications</button>
|
||||
</nav>
|
||||
|
||||
<!-- Dashboard Tab -->
|
||||
<section id="dashboard" class="tab-content active">
|
||||
<div class="hero-section">
|
||||
<h2>Revolutionary WiFi-Based Human Pose Detection</h2>
|
||||
<p class="hero-description">
|
||||
AI can track your full-body movement through walls using just WiFi signals.
|
||||
Researchers at Carnegie Mellon have trained a neural network to turn basic WiFi
|
||||
signals into detailed wireframe models of human bodies.
|
||||
</p>
|
||||
|
||||
<!-- Error container -->
|
||||
<div class="error-container" style="display: none;"></div>
|
||||
|
||||
<!-- Live Status Panel -->
|
||||
<div class="live-status-panel">
|
||||
<h3>System Status</h3>
|
||||
<div class="status-grid">
|
||||
<div class="component-status" data-component="api">
|
||||
<span class="component-name">API Server</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="hardware">
|
||||
<span class="component-name">Hardware</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="inference">
|
||||
<span class="component-name">Inference</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
<div class="component-status" data-component="streaming">
|
||||
<span class="component-name">Streaming</span>
|
||||
<span class="status-text">-</span>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Metrics -->
|
||||
<div class="system-metrics-panel">
|
||||
<h3>System Metrics</h3>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">CPU Usage</span>
|
||||
<div class="progress-bar" data-type="cpu">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="cpu-usage">0%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Memory Usage</span>
|
||||
<div class="progress-bar" data-type="memory">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="memory-usage">0%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Disk Usage</span>
|
||||
<div class="progress-bar" data-type="disk">
|
||||
<div class="progress-fill normal" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="disk-usage">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Status -->
|
||||
<div class="features-panel">
|
||||
<h3>Features</h3>
|
||||
<div class="features-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Live Statistics -->
|
||||
<div class="live-stats-panel">
|
||||
<h3>Live Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Active Persons</span>
|
||||
<span class="person-count">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Avg Confidence</span>
|
||||
<span class="avg-confidence">0%</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Detections</span>
|
||||
<span class="detection-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zones-panel">
|
||||
<h4>Zone Occupancy</h4>
|
||||
<div class="zones-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-benefits">
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🏠</div>
|
||||
<h3>Through Walls</h3>
|
||||
<p>Works through solid barriers with no line of sight required</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🔒</div>
|
||||
<h3>Privacy-Preserving</h3>
|
||||
<p>No cameras or visual recording - just WiFi signal analysis</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">⚡</div>
|
||||
<h3>Real-Time</h3>
|
||||
<p>Maps 24 body regions in real-time at 100Hz sampling rate</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">💰</div>
|
||||
<h3>Low Cost</h3>
|
||||
<p>Built using $30 commercial WiFi hardware</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="system-stats">
|
||||
<div class="stat" data-stat="body-regions">
|
||||
<span class="stat-value">24</span>
|
||||
<span class="stat-label">Body Regions</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="sampling-rate">
|
||||
<span class="stat-value">100Hz</span>
|
||||
<span class="stat-label">Sampling Rate</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="accuracy">
|
||||
<span class="stat-value">87.2%</span>
|
||||
<span class="stat-label">Accuracy (AP@50)</span>
|
||||
</div>
|
||||
<div class="stat" data-stat="hardware-cost">
|
||||
<span class="stat-value">$30</span>
|
||||
<span class="stat-label">Hardware Cost</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hardware Tab -->
|
||||
<section id="hardware" class="tab-content">
|
||||
<h2>Hardware Configuration</h2>
|
||||
|
||||
<div class="hardware-grid">
|
||||
<div class="antenna-section">
|
||||
<h3>3×3 Antenna Array</h3>
|
||||
<p class="help-text">Click antennas to toggle their state</p>
|
||||
<div class="antenna-array">
|
||||
<div class="antenna-grid">
|
||||
<div class="antenna tx active" data-type="TX1"></div>
|
||||
<div class="antenna tx active" data-type="TX2"></div>
|
||||
<div class="antenna tx active" data-type="TX3"></div>
|
||||
<div class="antenna rx active" data-type="RX1"></div>
|
||||
<div class="antenna rx active" data-type="RX2"></div>
|
||||
<div class="antenna rx active" data-type="RX3"></div>
|
||||
<div class="antenna rx active" data-type="RX4"></div>
|
||||
<div class="antenna rx active" data-type="RX5"></div>
|
||||
<div class="antenna rx active" data-type="RX6"></div>
|
||||
</div>
|
||||
<div class="antenna-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color tx"></div>
|
||||
<span>Transmitters (3)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color rx"></div>
|
||||
<span>Receivers (6)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="array-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h3>WiFi Configuration</h3>
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Frequency</label>
|
||||
<div class="config-value">2.4GHz ± 20MHz</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Subcarriers</label>
|
||||
<div class="config-value">30</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Sampling Rate</label>
|
||||
<div class="config-value">100 Hz</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Total Cost</label>
|
||||
<div class="config-value">$30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="csi-data">
|
||||
<h4>Real-time CSI Data</h4>
|
||||
<div class="csi-display">
|
||||
<div class="csi-row">
|
||||
<span>Amplitude:</span>
|
||||
<div class="csi-bar">
|
||||
<div class="csi-fill amplitude" style="width: 75%"></div>
|
||||
</div>
|
||||
<span class="csi-value">0.75</span>
|
||||
</div>
|
||||
<div class="csi-row">
|
||||
<span>Phase:</span>
|
||||
<div class="csi-bar">
|
||||
<div class="csi-fill phase" style="width: 60%"></div>
|
||||
</div>
|
||||
<span class="csi-value">1.2π</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Demo Tab -->
|
||||
<section id="demo" class="tab-content">
|
||||
<h2>Live Demonstration</h2>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button id="startDemo" class="btn btn--primary">Start Stream</button>
|
||||
<button id="stopDemo" class="btn btn--secondary" disabled>Stop Stream</button>
|
||||
<div class="demo-status">
|
||||
<span class="status status--info" id="demoStatus">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="signal-panel">
|
||||
<h3>WiFi Signal Analysis</h3>
|
||||
<div class="signal-display">
|
||||
<canvas id="signalCanvas" width="400" height="200"></canvas>
|
||||
</div>
|
||||
<div class="signal-metrics">
|
||||
<div class="metric">
|
||||
<span>Signal Strength:</span>
|
||||
<span id="signalStrength">-45 dBm</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>Processing Latency:</span>
|
||||
<span id="latency">12 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pose-panel">
|
||||
<h3>Human Pose Detection</h3>
|
||||
<div class="pose-display">
|
||||
<canvas id="poseCanvas" width="400" height="300"></canvas>
|
||||
</div>
|
||||
<div class="detection-info">
|
||||
<div class="info-item">
|
||||
<span>Persons Detected:</span>
|
||||
<span id="personCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>Confidence:</span>
|
||||
<span id="confidence">0.0%</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>Keypoints:</span>
|
||||
<span id="keypoints">0/0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Architecture Tab -->
|
||||
<section id="architecture" class="tab-content">
|
||||
<h2>System Architecture</h2>
|
||||
|
||||
<div class="architecture-flow">
|
||||
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813853/gpt4o_images/m7zztcttnue7vaxclvuw.png"
|
||||
alt="WiFi DensePose Architecture" class="architecture-image">
|
||||
|
||||
<div class="flow-steps">
|
||||
<div class="step-card" data-step="1">
|
||||
<div class="step-number">1</div>
|
||||
<h3>CSI Input</h3>
|
||||
<p>Channel State Information collected from WiFi antenna array</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="2">
|
||||
<div class="step-number">2</div>
|
||||
<h3>Phase Sanitization</h3>
|
||||
<p>Remove hardware-specific noise and normalize signal phase</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="3">
|
||||
<div class="step-number">3</div>
|
||||
<h3>Modality Translation</h3>
|
||||
<p>Convert WiFi signals to visual representation using CNN</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="4">
|
||||
<div class="step-number">4</div>
|
||||
<h3>DensePose-RCNN</h3>
|
||||
<p>Extract human pose keypoints and body part segmentation</p>
|
||||
</div>
|
||||
<div class="step-card" data-step="5">
|
||||
<div class="step-number">5</div>
|
||||
<h3>Wireframe Output</h3>
|
||||
<p>Generate final human pose wireframe visualization</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Tab -->
|
||||
<section id="performance" class="tab-content">
|
||||
<h2>Performance Analysis</h2>
|
||||
|
||||
<div class="performance-chart">
|
||||
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813924/pplx_code_interpreter/af6ef268_nsauu6.jpg"
|
||||
alt="Performance Comparison Chart" class="chart-image">
|
||||
</div>
|
||||
|
||||
<div class="performance-grid">
|
||||
<div class="performance-card">
|
||||
<h3>WiFi-based (Same Layout)</h3>
|
||||
<div class="metric-list">
|
||||
<div class="metric-item">
|
||||
<span>Average Precision:</span>
|
||||
<span class="metric-value">43.5%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@50:</span>
|
||||
<span class="metric-value success">87.2%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@75:</span>
|
||||
<span class="metric-value">44.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="performance-card">
|
||||
<h3>Image-based (Reference)</h3>
|
||||
<div class="metric-list">
|
||||
<div class="metric-item">
|
||||
<span>Average Precision:</span>
|
||||
<span class="metric-value success">84.7%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@50:</span>
|
||||
<span class="metric-value success">94.4%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>AP@75:</span>
|
||||
<span class="metric-value success">77.1%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="limitations-section">
|
||||
<h3>Advantages & Limitations</h3>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>Advantages</h4>
|
||||
<ul>
|
||||
<li>Through-wall detection</li>
|
||||
<li>Privacy preserving</li>
|
||||
<li>Lighting independent</li>
|
||||
<li>Low cost hardware</li>
|
||||
<li>Uses existing WiFi</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>Limitations</h4>
|
||||
<ul>
|
||||
<li>Performance drops in different layouts</li>
|
||||
<li>Requires WiFi-compatible devices</li>
|
||||
<li>Training requires synchronized data</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Applications Tab -->
|
||||
<section id="applications" class="tab-content">
|
||||
<h2>Real-World Applications</h2>
|
||||
|
||||
<div class="applications-grid">
|
||||
<div class="app-card">
|
||||
<div class="app-icon">👴</div>
|
||||
<h3>Elderly Care Monitoring</h3>
|
||||
<p>Monitor elderly individuals for falls or emergencies without invading privacy. Track movement patterns and detect anomalies in daily routines.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Fall Detection</span>
|
||||
<span class="feature-tag">Activity Monitoring</span>
|
||||
<span class="feature-tag">Emergency Alert</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏠</div>
|
||||
<h3>Home Security Systems</h3>
|
||||
<p>Detect intruders and monitor home security without visible cameras. Track multiple persons and identify suspicious movement patterns.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Intrusion Detection</span>
|
||||
<span class="feature-tag">Multi-person Tracking</span>
|
||||
<span class="feature-tag">Invisible Monitoring</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏥</div>
|
||||
<h3>Healthcare Patient Monitoring</h3>
|
||||
<p>Monitor patients in hospitals and care facilities. Track vital signs through movement analysis and detect health emergencies.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Vital Sign Analysis</span>
|
||||
<span class="feature-tag">Movement Tracking</span>
|
||||
<span class="feature-tag">Health Alerts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🏢</div>
|
||||
<h3>Smart Building Occupancy</h3>
|
||||
<p>Optimize building energy consumption by tracking occupancy patterns. Control lighting, HVAC, and security systems automatically.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Energy Optimization</span>
|
||||
<span class="feature-tag">Occupancy Tracking</span>
|
||||
<span class="feature-tag">Smart Controls</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="app-icon">🥽</div>
|
||||
<h3>AR/VR Applications</h3>
|
||||
<p>Enable full-body tracking for virtual and augmented reality applications without wearing additional sensors or cameras.</p>
|
||||
<div class="app-features">
|
||||
<span class="feature-tag">Full Body Tracking</span>
|
||||
<span class="feature-tag">Sensor-free</span>
|
||||
<span class="feature-tag">Immersive Experience</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="implementation-note">
|
||||
<h3>Implementation Considerations</h3>
|
||||
<p>While WiFi DensePose offers revolutionary capabilities, successful implementation requires careful consideration of environment setup, data privacy regulations, and system calibration for optimal performance.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Error Toast -->
|
||||
<div id="globalErrorToast" class="error-toast"></div>
|
||||
|
||||
<!-- Load application scripts as modules -->
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
139
ui/services/api.service.js
Normal file
139
ui/services/api.service.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// API Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildApiUrl } from '../config/api.config.js';
|
||||
|
||||
export class ApiService {
|
||||
constructor() {
|
||||
this.authToken = null;
|
||||
this.requestInterceptors = [];
|
||||
this.responseInterceptors = [];
|
||||
}
|
||||
|
||||
// Set authentication token
|
||||
setAuthToken(token) {
|
||||
this.authToken = token;
|
||||
}
|
||||
|
||||
// Add request interceptor
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Add response interceptor
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Build headers for requests
|
||||
getHeaders(customHeaders = {}) {
|
||||
const headers = {
|
||||
...API_CONFIG.DEFAULT_HEADERS,
|
||||
...customHeaders
|
||||
};
|
||||
|
||||
if (this.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Process request through interceptors
|
||||
async processRequest(url, options) {
|
||||
let processedUrl = url;
|
||||
let processedOptions = options;
|
||||
|
||||
for (const interceptor of this.requestInterceptors) {
|
||||
const result = await interceptor(processedUrl, processedOptions);
|
||||
processedUrl = result.url || processedUrl;
|
||||
processedOptions = result.options || processedOptions;
|
||||
}
|
||||
|
||||
return { url: processedUrl, options: processedOptions };
|
||||
}
|
||||
|
||||
// Process response through interceptors
|
||||
async processResponse(response, url) {
|
||||
let processedResponse = response;
|
||||
|
||||
for (const interceptor of this.responseInterceptors) {
|
||||
processedResponse = await interceptor(processedResponse, url);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
}
|
||||
|
||||
// Generic request method
|
||||
async request(url, options = {}) {
|
||||
try {
|
||||
// Process request through interceptors
|
||||
const processed = await this.processRequest(url, options);
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(processed.url, {
|
||||
...processed.options,
|
||||
headers: this.getHeaders(processed.options.headers)
|
||||
});
|
||||
|
||||
// Process response through interceptors
|
||||
const processedResponse = await this.processResponse(response, url);
|
||||
|
||||
// Handle errors
|
||||
if (!processedResponse.ok) {
|
||||
const error = await processedResponse.json().catch(() => ({
|
||||
message: `HTTP ${processedResponse.status}: ${processedResponse.statusText}`
|
||||
}));
|
||||
throw new Error(error.message || error.detail || 'Request failed');
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
const data = await processedResponse.json().catch(() => null);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API Request Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET request
|
||||
async get(endpoint, params = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint, params);
|
||||
return this.request(url, {
|
||||
method: 'GET',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// POST request
|
||||
async post(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// PUT request
|
||||
async put(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE request
|
||||
async delete(endpoint, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const apiService = new ApiService();
|
||||
138
ui/services/health.service.js
Normal file
138
ui/services/health.service.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// Health Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class HealthService {
|
||||
constructor() {
|
||||
this.healthCheckInterval = null;
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
|
||||
// Get system health
|
||||
async getSystemHealth() {
|
||||
const health = await apiService.get(API_CONFIG.ENDPOINTS.HEALTH.SYSTEM);
|
||||
this.lastHealthStatus = health;
|
||||
this.notifySubscribers(health);
|
||||
return health;
|
||||
}
|
||||
|
||||
// Check readiness
|
||||
async checkReadiness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.READY);
|
||||
}
|
||||
|
||||
// Check liveness
|
||||
async checkLiveness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.LIVE);
|
||||
}
|
||||
|
||||
// Get system metrics
|
||||
async getSystemMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.METRICS);
|
||||
}
|
||||
|
||||
// Get version info
|
||||
async getVersion() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.VERSION);
|
||||
}
|
||||
|
||||
// Get API info
|
||||
async getApiInfo() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.INFO);
|
||||
}
|
||||
|
||||
// Get API status
|
||||
async getApiStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STATUS);
|
||||
}
|
||||
|
||||
// Start periodic health checks
|
||||
startHealthMonitoring(intervalMs = 30000) {
|
||||
if (this.healthCheckInterval) {
|
||||
console.warn('Health monitoring already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial check
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Initial health check failed:', error);
|
||||
});
|
||||
|
||||
// Set up periodic checks
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.notifySubscribers({
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
// Stop health monitoring
|
||||
stopHealthMonitoring() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to health updates
|
||||
subscribeToHealth(callback) {
|
||||
this.healthSubscribers.push(callback);
|
||||
|
||||
// Send last known status if available
|
||||
if (this.lastHealthStatus) {
|
||||
callback(this.lastHealthStatus);
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.healthSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.healthSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(health) {
|
||||
this.healthSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(health);
|
||||
} catch (error) {
|
||||
console.error('Error in health subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if system is healthy
|
||||
isSystemHealthy() {
|
||||
if (!this.lastHealthStatus) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.status === 'healthy';
|
||||
}
|
||||
|
||||
// Get component status
|
||||
getComponentStatus(componentName) {
|
||||
if (!this.lastHealthStatus?.components) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.components[componentName];
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopHealthMonitoring();
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const healthService = new HealthService();
|
||||
303
ui/services/pose.service.js
Normal file
303
ui/services/pose.service.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// Pose Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
import { wsService } from './websocket.service.js';
|
||||
|
||||
export class PoseService {
|
||||
constructor() {
|
||||
this.streamConnection = null;
|
||||
this.eventConnection = null;
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
|
||||
// Get current pose estimation
|
||||
async getCurrentPose(options = {}) {
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
confidence_threshold: options.confidenceThreshold,
|
||||
max_persons: options.maxPersons,
|
||||
include_keypoints: options.includeKeypoints,
|
||||
include_segmentation: options.includeSegmentation
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CURRENT, params);
|
||||
}
|
||||
|
||||
// Analyze pose (requires auth)
|
||||
async analyzePose(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.ANALYZE, request);
|
||||
}
|
||||
|
||||
// Get zone occupancy
|
||||
async getZoneOccupancy(zoneId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.POSE.ZONE_OCCUPANCY.replace('{zone_id}', zoneId);
|
||||
return apiService.get(endpoint);
|
||||
}
|
||||
|
||||
// Get zones summary
|
||||
async getZonesSummary() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ZONES_SUMMARY);
|
||||
}
|
||||
|
||||
// Get historical data (requires auth)
|
||||
async getHistoricalData(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.HISTORICAL, request);
|
||||
}
|
||||
|
||||
// Get recent activities
|
||||
async getActivities(options = {}) {
|
||||
const params = {
|
||||
zone_id: options.zoneId,
|
||||
limit: options.limit || 50
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ACTIVITIES, params);
|
||||
}
|
||||
|
||||
// Calibrate system (requires auth)
|
||||
async calibrate() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.CALIBRATE);
|
||||
}
|
||||
|
||||
// Get calibration status (requires auth)
|
||||
async getCalibrationStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CALIBRATION_STATUS);
|
||||
}
|
||||
|
||||
// Get pose statistics
|
||||
async getStats(hours = 24) {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.STATS, { hours });
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
startPoseStream(options = {}) {
|
||||
if (this.streamConnection) {
|
||||
console.warn('Pose stream already active');
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
min_confidence: options.minConfidence || 0.5,
|
||||
max_fps: options.maxFps || 30,
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.streamConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_POSE,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Pose stream connected');
|
||||
this.notifyPoseSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handlePoseMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Pose stream error:', error);
|
||||
this.notifyPoseSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Pose stream disconnected');
|
||||
this.streamConnection = null;
|
||||
this.notifyPoseSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
// Stop pose stream
|
||||
stopPoseStream() {
|
||||
if (this.streamConnection) {
|
||||
wsService.disconnect(this.streamConnection);
|
||||
this.streamConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to pose updates
|
||||
subscribeToPoseUpdates(callback) {
|
||||
this.poseSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.poseSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.poseSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle pose stream messages
|
||||
handlePoseMessage(data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
switch (type) {
|
||||
case 'pose_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'pose_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'historical_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'historical_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'zone_statistics':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'zone_stats',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'system_event':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'system_event',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown pose message type:', type);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify pose subscribers
|
||||
notifyPoseSubscribers(update) {
|
||||
this.poseSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in pose subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start event stream
|
||||
startEventStream(options = {}) {
|
||||
if (this.eventConnection) {
|
||||
console.warn('Event stream already active');
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
event_types: options.eventTypes?.join(','),
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.eventConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_EVENTS,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Event stream connected');
|
||||
this.notifyEventSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handleEventMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Event stream error:', error);
|
||||
this.notifyEventSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Event stream disconnected');
|
||||
this.eventConnection = null;
|
||||
this.notifyEventSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
// Stop event stream
|
||||
stopEventStream() {
|
||||
if (this.eventConnection) {
|
||||
wsService.disconnect(this.eventConnection);
|
||||
this.eventConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
subscribeToEvents(callback) {
|
||||
this.eventSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.eventSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.eventSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle event stream messages
|
||||
handleEventMessage(data) {
|
||||
this.notifyEventSubscribers({
|
||||
type: 'event',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// Notify event subscribers
|
||||
notifyEventSubscribers(update) {
|
||||
this.eventSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in event subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update stream configuration
|
||||
updateStreamConfig(connectionId, config) {
|
||||
wsService.sendCommand(connectionId, 'update_config', config);
|
||||
}
|
||||
|
||||
// Get stream status
|
||||
requestStreamStatus(connectionId) {
|
||||
wsService.sendCommand(connectionId, 'get_status');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopPoseStream();
|
||||
this.stopEventStream();
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const poseService = new PoseService();
|
||||
59
ui/services/stream.service.js
Normal file
59
ui/services/stream.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Stream Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class StreamService {
|
||||
// Get streaming status
|
||||
async getStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.STATUS);
|
||||
}
|
||||
|
||||
// Start streaming (requires auth)
|
||||
async start() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.START);
|
||||
}
|
||||
|
||||
// Stop streaming (requires auth)
|
||||
async stop() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.STOP);
|
||||
}
|
||||
|
||||
// Get connected clients (requires auth)
|
||||
async getClients() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.CLIENTS);
|
||||
}
|
||||
|
||||
// Disconnect a client (requires auth)
|
||||
async disconnectClient(clientId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.STREAM.DISCONNECT_CLIENT.replace('{client_id}', clientId);
|
||||
return apiService.delete(endpoint);
|
||||
}
|
||||
|
||||
// Broadcast message (requires auth)
|
||||
async broadcast(message, options = {}) {
|
||||
const params = {
|
||||
stream_type: options.streamType,
|
||||
zone_ids: options.zoneIds?.join(',')
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.post(
|
||||
API_CONFIG.ENDPOINTS.STREAM.BROADCAST,
|
||||
message,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
// Get streaming metrics
|
||||
async getMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.METRICS);
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const streamService = new StreamService();
|
||||
305
ui/services/websocket.service.js
Normal file
305
ui/services/websocket.service.js
Normal file
@@ -0,0 +1,305 @@
|
||||
// WebSocket Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildWsUrl } from '../config/api.config.js';
|
||||
|
||||
export class WebSocketService {
|
||||
constructor() {
|
||||
this.connections = new Map();
|
||||
this.messageHandlers = new Map();
|
||||
this.reconnectAttempts = new Map();
|
||||
}
|
||||
|
||||
// Connect to WebSocket endpoint
|
||||
connect(endpoint, params = {}, handlers = {}) {
|
||||
const url = buildWsUrl(endpoint, params);
|
||||
|
||||
// Check if already connected
|
||||
if (this.connections.has(url)) {
|
||||
console.warn(`Already connected to ${url}`);
|
||||
return this.connections.get(url);
|
||||
}
|
||||
|
||||
// Create WebSocket connection
|
||||
const ws = new WebSocket(url);
|
||||
const connectionId = this.generateId();
|
||||
|
||||
// Store connection
|
||||
this.connections.set(url, {
|
||||
id: connectionId,
|
||||
ws,
|
||||
url,
|
||||
handlers,
|
||||
status: 'connecting',
|
||||
lastPing: null,
|
||||
reconnectTimer: null
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
this.setupEventHandlers(url, ws, handlers);
|
||||
|
||||
// Start ping interval
|
||||
this.startPingInterval(url);
|
||||
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
// Set up WebSocket event handlers
|
||||
setupEventHandlers(url, ws, handlers) {
|
||||
const connection = this.connections.get(url);
|
||||
|
||||
ws.onopen = (event) => {
|
||||
console.log(`WebSocket connected: ${url}`);
|
||||
connection.status = 'connected';
|
||||
this.reconnectAttempts.set(url, 0);
|
||||
|
||||
if (handlers.onOpen) {
|
||||
handlers.onOpen(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle different message types
|
||||
this.handleMessage(url, data);
|
||||
|
||||
if (handlers.onMessage) {
|
||||
handlers.onMessage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error(`WebSocket error: ${url}`, event);
|
||||
connection.status = 'error';
|
||||
|
||||
if (handlers.onError) {
|
||||
handlers.onError(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`WebSocket closed: ${url}`);
|
||||
connection.status = 'closed';
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(url);
|
||||
|
||||
if (handlers.onClose) {
|
||||
handlers.onClose(event);
|
||||
}
|
||||
|
||||
// Attempt reconnection if not intentionally closed
|
||||
if (!event.wasClean && this.shouldReconnect(url)) {
|
||||
this.scheduleReconnect(url);
|
||||
} else {
|
||||
this.connections.delete(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
handleMessage(url, data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
// Handle system messages
|
||||
switch (type) {
|
||||
case 'pong':
|
||||
this.handlePong(url);
|
||||
break;
|
||||
|
||||
case 'connection_established':
|
||||
console.log('Connection established:', payload);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('WebSocket error message:', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
// Call registered message handlers
|
||||
const handlers = this.messageHandlers.get(url) || [];
|
||||
handlers.forEach(handler => handler(data));
|
||||
}
|
||||
|
||||
// Send message through WebSocket
|
||||
send(connectionId, message) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
throw new Error(`Connection ${connectionId} is not connected`);
|
||||
}
|
||||
|
||||
const data = typeof message === 'string'
|
||||
? message
|
||||
: JSON.stringify(message);
|
||||
|
||||
connection.ws.send(data);
|
||||
}
|
||||
|
||||
// Send command message
|
||||
sendCommand(connectionId, command, payload = {}) {
|
||||
this.send(connectionId, {
|
||||
type: command,
|
||||
payload,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Register message handler
|
||||
onMessage(connectionId, handler) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (!this.messageHandlers.has(connection.url)) {
|
||||
this.messageHandlers.set(connection.url, []);
|
||||
}
|
||||
|
||||
this.messageHandlers.get(connection.url).push(handler);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const handlers = this.messageHandlers.get(connection.url);
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Disconnect WebSocket
|
||||
disconnect(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear reconnection timer
|
||||
if (connection.reconnectTimer) {
|
||||
clearTimeout(connection.reconnectTimer);
|
||||
}
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(connection.url);
|
||||
|
||||
// Close WebSocket
|
||||
if (connection.ws.readyState === WebSocket.OPEN) {
|
||||
connection.ws.close(1000, 'Client disconnect');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
this.connections.delete(connection.url);
|
||||
this.messageHandlers.delete(connection.url);
|
||||
this.reconnectAttempts.delete(connection.url);
|
||||
}
|
||||
|
||||
// Disconnect all WebSockets
|
||||
disconnectAll() {
|
||||
const connectionIds = Array.from(this.connections.values()).map(c => c.id);
|
||||
connectionIds.forEach(id => this.disconnect(id));
|
||||
}
|
||||
|
||||
// Ping/Pong handling
|
||||
startPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
connection.pingInterval = setInterval(() => {
|
||||
if (connection.status === 'connected') {
|
||||
this.sendPing(url);
|
||||
}
|
||||
}, API_CONFIG.WS_CONFIG.PING_INTERVAL);
|
||||
}
|
||||
|
||||
clearPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.pingInterval) {
|
||||
clearInterval(connection.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
sendPing(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.status === 'connected') {
|
||||
connection.lastPing = Date.now();
|
||||
connection.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}
|
||||
|
||||
handlePong(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection) {
|
||||
const latency = Date.now() - connection.lastPing;
|
||||
console.log(`Pong received. Latency: ${latency}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnection logic
|
||||
shouldReconnect(url) {
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
return attempts < API_CONFIG.WS_CONFIG.MAX_RECONNECT_ATTEMPTS;
|
||||
}
|
||||
|
||||
scheduleReconnect(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
const delay = API_CONFIG.WS_CONFIG.RECONNECT_DELAY * Math.pow(2, attempts);
|
||||
|
||||
console.log(`Scheduling reconnect in ${delay}ms (attempt ${attempts + 1})`);
|
||||
|
||||
connection.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts.set(url, attempts + 1);
|
||||
|
||||
// Get original parameters
|
||||
const params = new URL(url).searchParams;
|
||||
const paramsObj = Object.fromEntries(params);
|
||||
const endpoint = url.replace(/^wss?:\/\/[^\/]+/, '').split('?')[0];
|
||||
|
||||
// Attempt reconnection
|
||||
this.connect(endpoint, paramsObj, connection.handlers);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
findConnectionById(connectionId) {
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.id === connectionId) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
getConnectionStatus(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
return connection ? connection.status : 'disconnected';
|
||||
}
|
||||
|
||||
getActiveConnections() {
|
||||
return Array.from(this.connections.values()).map(conn => ({
|
||||
id: conn.id,
|
||||
url: conn.url,
|
||||
status: conn.status
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const wsService = new WebSocketService();
|
||||
1307
ui/style.css
Normal file
1307
ui/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user