Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions ui/config/api.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// API Configuration for WiFi-DensePose UI

// Auto-detect the backend URL from the page origin so the UI works whether
// served from Docker (:3000), local dev (:8080), or any other port.
const _origin = (typeof window !== 'undefined' && window.location && window.location.origin)
? window.location.origin
: 'http://localhost:3000';
// Backend URL configuration
// When running locally, frontend is on :3000 and backend is on :8000
const _backendUrl = (typeof window !== 'undefined' && window.location)
? (window.location.port === '3000'
? 'http://localhost:8000' // Local dev: frontend on :3000, backend on :8000
: window.location.origin) // Production/Docker: same origin
: 'http://localhost:8000';

export const API_CONFIG = {
BASE_URL: _origin,
BASE_URL: _backendUrl,
API_VERSION: '/api/v1',
WS_PREFIX: 'ws://',
WSS_PREFIX: 'wss://',
Expand Down Expand Up @@ -121,9 +123,10 @@ export function buildWsUrl(endpoint, params = {}) {
? API_CONFIG.WSS_PREFIX
: API_CONFIG.WS_PREFIX;

// Derive host from the page origin so it works on any port (Docker :3000, dev :8080, etc.)
const host = window.location.host;
let url = `${protocol}${host}${endpoint}`;
// Extract host from BASE_URL (e.g., "http://localhost:8000" → "localhost:8000")
// This ensures WebSocket connects to backend port, not frontend port
const backendHost = API_CONFIG.BASE_URL.replace(/^https?:\/\//, '');
let url = `${protocol}${backendHost}${endpoint}`;

// Add query parameters
const queryParams = new URLSearchParams(params);
Expand Down
69 changes: 57 additions & 12 deletions ui/services/sensing.service.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/**
* Sensing WebSocket Service
*
* Manages the connection to the Python sensing WebSocket server
* (ws://localhost:8765) and provides a callback-based API for the UI.
* Manages the connection to the Python backend WebSocket server
* (ws://localhost:8000/api/v1/stream/pose) and provides a callback-based API for the UI.
*
* Falls back to simulated data only after MAX_RECONNECT_ATTEMPTS exhausted.
* While reconnecting the service stays in "reconnecting" state and does NOT
* emit simulated frames so the UI can clearly distinguish live vs. fallback data.
*/

// Derive WebSocket URL from the page origin so it works on any port.
// The /ws/sensing endpoint is available on the same HTTP port (3000).
// Derive WebSocket URL - connect to backend on port 8000 for local dev
const _isLocalDev = (typeof window !== 'undefined' && window.location.port === '3000');
const _wsProto = (typeof window !== 'undefined' && window.location.protocol === 'https:') ? 'wss:' : 'ws:';
const _wsHost = (typeof window !== 'undefined' && window.location.host) ? window.location.host : 'localhost:3000';
const SENSING_WS_URL = `${_wsProto}//${_wsHost}/ws/sensing`;
const _wsHost = _isLocalDev ? 'localhost:8000' : window.location.host;
const SENSING_WS_URL = `${_wsProto}//${_wsHost}/api/v1/stream/pose`;
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000];
const MAX_RECONNECT_ATTEMPTS = 20;
// Number of failed attempts that must occur before simulation starts.
Expand Down Expand Up @@ -308,20 +308,65 @@ class SensingService {
// ---- Data handling -----------------------------------------------------

_handleData(data) {
this._lastMessage = data;
// Transform backend pose_data format to sensing format for compatibility
let transformedData = data;

if (data.type === 'pose_data') {
// Backend is sending pose data - transform to sensing format
transformedData = {
type: 'sensing_update',
timestamp: new Date(data.timestamp).getTime() / 1000,
source: 'server-simulated', // Backend is in mock mode
nodes: [{
node_id: 1,
rssi_dbm: -45,
position: [2, 0, 1.5],
amplitude: [],
subcarrier_count: 0,
}],
features: {
mean_rssi: -45,
variance: data.data?.confidence || 0.5,
std: Math.sqrt(data.data?.confidence || 0.5),
motion_band_power: data.data?.pose?.count > 0 ? 0.15 : 0.05,
breathing_band_power: 0.05,
dominant_freq_hz: 0.3,
change_points: data.data?.pose?.count || 0,
spectral_power: 0.2,
range: 1.5,
iqr: 1.0,
skewness: 0,
kurtosis: 1,
},
classification: {
motion_level: data.data?.pose?.count > 0 ? 'active' : 'present_still',
presence: data.data?.pose?.count > 0,
confidence: data.data?.confidence || 0.5,
},
// Store original pose data for components that need it
_pose_data: data.data
};
} else if (data.type === 'connection_established') {
// Connection confirmation - set data source to server-simulated
console.log('[Sensing] WebSocket connected:', data);
this._setDataSource('server-simulated');
return; // Don't emit connection message as data
}

this._lastMessage = transformedData;

// Track the server's source field from each frame so the UI
// can react if the server switches between esp32 ↔ simulated at runtime.
if (data.source && this._state === 'connected') {
const raw = data.source;
if (transformedData.source && this._state === 'connected') {
const raw = transformedData.source;
if (raw !== this._serverSource) {
this._applyServerSource(raw);
}
}

// Update RSSI history for sparkline
if (data.features && data.features.mean_rssi != null) {
this._rssiHistory.push(data.features.mean_rssi);
if (transformedData.features && transformedData.features.mean_rssi != null) {
this._rssiHistory.push(transformedData.features.mean_rssi);
if (this._rssiHistory.length > this._maxHistory) {
this._rssiHistory.shift();
}
Expand All @@ -330,7 +375,7 @@ class SensingService {
// Notify all listeners
for (const cb of this._listeners) {
try {
cb(data);
cb(transformedData);
} catch (e) {
console.error('[Sensing] Listener error:', e);
}
Expand Down
17 changes: 15 additions & 2 deletions v1/src/services/pose_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,29 @@ async def initialize(self):
async def _initialize_models(self):
"""Initialize neural network models."""
try:
# DensePose model configuration
densepose_config = {
'input_channels': 256, # Feature channels from modality translator
'num_body_parts': 24, # 24 body parts for DensePose
'num_uv_coordinates': 2, # U and V coordinates
'hidden_channels': [256, 128, 64],
'kernel_size': 3,
'padding': 1,
'dropout_rate': 0.1,
'use_fpn': False,
'output_stride': 4
}

# Initialize DensePose model
if self.settings.pose_model_path:
self.densepose_model = DensePoseHead()
self.densepose_model = DensePoseHead(densepose_config)
# 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()
self.densepose_model = DensePoseHead(densepose_config)

# Initialize modality translation
config = {
Expand Down