9740bc64c9
Continuous Deployment / Deploy to Production (push) Blocked by required conditions
Continuous Deployment / Rollback Deployment (push) Blocked by required conditions
Continuous Deployment / Post-deployment Monitoring (push) Blocked by required conditions
Continuous Deployment / Notify Deployment Status (push) Blocked by required conditions
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier1) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (full-adr060) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (tdm-3node) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / Swarm Test (ADR-062) (push) Has been skipped
npm packages / tools/ruview-mcp (node 22) (push) Failing after 1s
nvsim-server → ghcr.io / build-and-publish (push) Failing after 1s
ruview-swarm CI guard / tests (full+train) (push) Failing after 2s
Bench Regression Guard / bench compile-verify (--no-run) (push) Failing after 0s
Bench Regression Guard / bench fast-run (informational, non-gating) (push) Has been skipped
Firmware CI / Verify version.txt matches release tag (push) Has been skipped
Dashboard a11y + cross-browser / a11y (push) Failing after 0s
nvsim Dashboard → GitHub Pages / build-and-deploy (push) Failing after 2s
Firmware CI / Build firmware (esp32s3 / 4mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / Build Espressif QEMU (push) Failing after 1s
Firmware QEMU Tests (ADR-061) / Fuzz Testing (ADR-061 Layer 6) (push) Failing after 1s
Continuous Deployment / Pre-deployment Checks (push) Has been skipped
Continuous Deployment / Deploy to Staging (push) Waiting to run
Firmware CI / Build firmware (esp32c6 / c6-4mb) (push) Failing after 15s
Firmware CI / Build firmware (esp32s3 / 8mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-max) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-min) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (default) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier0) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / NVS Matrix Generation (push) Failing after 1s
Security Scanning / Security Policy Compliance (push) Failing after 0s
Security Scanning / Security Report (push) Waiting to run
Security Scanning / Dependency Vulnerability Scan (push) Failing after 0s
Security Scanning / Static Application Security Testing (push) Failing after 1s
Security Scanning / Infrastructure Security Scan (push) Failing after 1s
Security Scanning / Secret Scanning (push) Failing after 1s
npm packages / harness/ruview (node 22) (push) Failing after 17s
Security Scanning / License Compliance Scan (push) Failing after 1s
Security Scanning / Container Security Scan (push) Failing after 4s
three.js demos → GitHub Pages / build-and-deploy (push) Failing after 1s
Verify Pipeline Determinism / Verify Pipeline Determinism (3.11) (push) Failing after 1s
Fix-Marker Regression Guard / Verify fix markers (push) Failing after 1s
ADR-115 MQTT integration tests / mqtt-integration (push) Failing after 1s
npm packages / harness/ruview (node 20) (push) Failing after 1s
npm packages / tools/ruview-mcp (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 22) (push) Failing after 1s
BFLD MQTT Integration / cargo test --features mqtt (live mosquitto) (push) Failing after 29s
ruview-swarm CI guard / build train_marl bin (push) Failing after 2s
ruview-swarm CI guard / clippy (-D warnings, --no-deps) (push) Failing after 3s
ruview-swarm CI guard / tests (ruflo) (push) Failing after 1s
ruview-swarm CI guard / tests (train) (push) Failing after 2s
ruview-swarm CI guard / tests (default) (push) Failing after 2s
Point Cloud Viewer → GitHub Pages / build-and-deploy (push) Failing after 8s
ruview-swarm CI guard / ITAR / publish guard (push) Failing after 0s
wifi-densepose sensing-server → Docker Hub + ghcr.io / build · push · smoke-test (push) Failing after 1s
269 lines
7.6 KiB
JavaScript
269 lines
7.6 KiB
JavaScript
// WebSocket Client for Three.js Visualization - WiFi DensePose
|
|
// Default endpoint is `/ws/sensing` on the same host the page was served from.
|
|
// Callers (e.g. viz.html) usually pass an explicit `url` derived from
|
|
// `buildSensingWsUrl()` so HTTP/WS port pairings are handled centrally.
|
|
|
|
function _defaultWsUrl() {
|
|
if (typeof window === 'undefined' || !window.location) {
|
|
return 'ws://localhost:8765/ws/sensing';
|
|
}
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
return `${protocol}//${window.location.host}/ws/sensing`;
|
|
}
|
|
|
|
export class WebSocketClient {
|
|
constructor(options = {}) {
|
|
this.url = options.url || _defaultWsUrl();
|
|
this.ws = null;
|
|
this.state = 'disconnected'; // disconnected, connecting, connected, error
|
|
this.isRealData = false;
|
|
|
|
// Reconnection settings
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = options.maxReconnectAttempts || 15;
|
|
this.reconnectDelays = [500, 1000, 2000, 4000, 8000, 15000, 30000];
|
|
this.reconnectTimer = null;
|
|
this.autoReconnect = options.autoReconnect !== false;
|
|
|
|
// Heartbeat
|
|
this.heartbeatInterval = null;
|
|
this.heartbeatFrequency = options.heartbeatFrequency || 25000;
|
|
this.lastPong = 0;
|
|
|
|
// Metrics
|
|
this.metrics = {
|
|
messageCount: 0,
|
|
errorCount: 0,
|
|
connectTime: null,
|
|
lastMessageTime: null,
|
|
latency: 0,
|
|
bytesReceived: 0
|
|
};
|
|
|
|
// Callbacks
|
|
this._onMessage = options.onMessage || (() => {});
|
|
this._onStateChange = options.onStateChange || (() => {});
|
|
this._onError = options.onError || (() => {});
|
|
}
|
|
|
|
// Attempt to connect
|
|
connect() {
|
|
if (this.state === 'connecting' || this.state === 'connected') {
|
|
console.warn('[WS-VIZ] Already connected or connecting');
|
|
return;
|
|
}
|
|
|
|
this._setState('connecting');
|
|
console.log(`[WS-VIZ] Connecting to ${this.url}`);
|
|
|
|
try {
|
|
this.ws = new WebSocket(this.url);
|
|
this.ws.binaryType = 'arraybuffer';
|
|
|
|
this.ws.onopen = () => this._handleOpen();
|
|
this.ws.onmessage = (event) => this._handleMessage(event);
|
|
this.ws.onerror = (event) => this._handleError(event);
|
|
this.ws.onclose = (event) => this._handleClose(event);
|
|
|
|
// Connection timeout
|
|
this._connectTimeout = setTimeout(() => {
|
|
if (this.state === 'connecting') {
|
|
console.warn('[WS-VIZ] Connection timeout');
|
|
this.ws.close();
|
|
this._setState('error');
|
|
this._scheduleReconnect();
|
|
}
|
|
}, 8000);
|
|
|
|
} catch (err) {
|
|
console.error('[WS-VIZ] Failed to create WebSocket:', err);
|
|
this._setState('error');
|
|
this._onError(err);
|
|
this._scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
disconnect() {
|
|
this.autoReconnect = false;
|
|
this._clearTimers();
|
|
|
|
if (this.ws) {
|
|
this.ws.onclose = null; // Prevent reconnect on intentional close
|
|
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
this.ws.close(1000, 'Client disconnect');
|
|
}
|
|
this.ws = null;
|
|
}
|
|
|
|
this._setState('disconnected');
|
|
this.isRealData = false;
|
|
console.log('[WS-VIZ] Disconnected');
|
|
}
|
|
|
|
// Send a message
|
|
send(data) {
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
console.warn('[WS-VIZ] Cannot send - not connected');
|
|
return false;
|
|
}
|
|
|
|
const msg = typeof data === 'string' ? data : JSON.stringify(data);
|
|
this.ws.send(msg);
|
|
return true;
|
|
}
|
|
|
|
_handleOpen() {
|
|
clearTimeout(this._connectTimeout);
|
|
this.reconnectAttempts = 0;
|
|
this.metrics.connectTime = Date.now();
|
|
this._setState('connected');
|
|
console.log('[WS-VIZ] Connected successfully');
|
|
|
|
// Start heartbeat
|
|
this._startHeartbeat();
|
|
|
|
// Request initial state
|
|
this.send({ type: 'get_status', timestamp: Date.now() });
|
|
}
|
|
|
|
_handleMessage(event) {
|
|
this.metrics.messageCount++;
|
|
this.metrics.lastMessageTime = Date.now();
|
|
|
|
const rawSize = typeof event.data === 'string' ? event.data.length : event.data.byteLength;
|
|
this.metrics.bytesReceived += rawSize;
|
|
|
|
try {
|
|
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
|
|
// Handle pong
|
|
if (data.type === 'pong') {
|
|
this.lastPong = Date.now();
|
|
if (data.timestamp) {
|
|
this.metrics.latency = Date.now() - data.timestamp;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle connection_established
|
|
if (data.type === 'connection_established') {
|
|
console.log('[WS-VIZ] Server confirmed connection:', data.payload);
|
|
return;
|
|
}
|
|
|
|
// Detect real vs mock data from metadata
|
|
if (data.data && data.data.metadata) {
|
|
this.isRealData = data.data.metadata.mock_data === false && data.data.metadata.source !== 'mock';
|
|
} else if (data.metadata) {
|
|
this.isRealData = data.metadata.mock_data === false;
|
|
}
|
|
|
|
// Calculate latency from message timestamp
|
|
if (data.timestamp) {
|
|
const msgTime = new Date(data.timestamp).getTime();
|
|
if (!isNaN(msgTime)) {
|
|
this.metrics.latency = Date.now() - msgTime;
|
|
}
|
|
}
|
|
|
|
// Forward to callback
|
|
this._onMessage(data);
|
|
|
|
} catch (err) {
|
|
this.metrics.errorCount++;
|
|
console.error('[WS-VIZ] Failed to parse message:', err);
|
|
}
|
|
}
|
|
|
|
_handleError(event) {
|
|
this.metrics.errorCount++;
|
|
console.error('[WS-VIZ] WebSocket error:', event);
|
|
this._onError(event);
|
|
}
|
|
|
|
_handleClose(event) {
|
|
clearTimeout(this._connectTimeout);
|
|
this._stopHeartbeat();
|
|
this.ws = null;
|
|
|
|
const wasConnected = this.state === 'connected';
|
|
console.log(`[WS-VIZ] Connection closed: code=${event.code}, reason=${event.reason}, clean=${event.wasClean}`);
|
|
|
|
if (event.wasClean || !this.autoReconnect) {
|
|
this._setState('disconnected');
|
|
} else {
|
|
this._setState('error');
|
|
this._scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
_setState(newState) {
|
|
if (this.state === newState) return;
|
|
const oldState = this.state;
|
|
this.state = newState;
|
|
this._onStateChange(newState, oldState);
|
|
}
|
|
|
|
_startHeartbeat() {
|
|
this._stopHeartbeat();
|
|
this.heartbeatInterval = setInterval(() => {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
this.send({ type: 'ping', timestamp: Date.now() });
|
|
}
|
|
}, this.heartbeatFrequency);
|
|
}
|
|
|
|
_stopHeartbeat() {
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
this.heartbeatInterval = null;
|
|
}
|
|
}
|
|
|
|
_scheduleReconnect() {
|
|
if (!this.autoReconnect) return;
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
console.error('[WS-VIZ] Max reconnect attempts reached');
|
|
this._setState('error');
|
|
return;
|
|
}
|
|
|
|
const delayIdx = Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1);
|
|
const delay = this.reconnectDelays[delayIdx];
|
|
this.reconnectAttempts++;
|
|
|
|
console.log(`[WS-VIZ] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
|
|
_clearTimers() {
|
|
clearTimeout(this._connectTimeout);
|
|
clearTimeout(this.reconnectTimer);
|
|
this._stopHeartbeat();
|
|
}
|
|
|
|
getMetrics() {
|
|
return {
|
|
...this.metrics,
|
|
state: this.state,
|
|
isRealData: this.isRealData,
|
|
reconnectAttempts: this.reconnectAttempts,
|
|
uptime: this.metrics.connectTime ? (Date.now() - this.metrics.connectTime) / 1000 : 0
|
|
};
|
|
}
|
|
|
|
isConnected() {
|
|
return this.state === 'connected';
|
|
}
|
|
|
|
dispose() {
|
|
this.disconnect();
|
|
this._onMessage = () => {};
|
|
this._onStateChange = () => {};
|
|
this._onError = () => {};
|
|
}
|
|
}
|