Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 11:59:54 +08:00

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 = () => {};
}
}