/** * WebSocket Relay Client * Bridge mode: sends decoded BLE telemetry to VPS over 5G/WiFi * Viewer mode: receives telemetry broadcast from VPS */ const WSRelay = (() => { let ws = null; let onDataCallback = null; let onStatusCallback = null; let reconnectTimer = null; let isSource = false; // true if this client is the BLE bridge const RECONNECT_DELAY = 2000; /** * Get the WebSocket URL based on current location */ function getWsUrl() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${proto}//${location.host}/opusbike/ws`; } /** * Connect to the WebSocket relay server */ function connect(asSource = false) { isSource = asSource; if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; } clearTimeout(reconnectTimer); try { ws = new WebSocket(getWsUrl()); ws.onopen = () => { console.log('WS relay connected (source:', isSource, ')'); // Register role with server ws.send(JSON.stringify({ type: 'register', role: isSource ? 'source' : 'viewer' })); if (onStatusCallback) onStatusCallback('connected'); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); // Handle both live telemetry and initial state snapshot if ((msg.type === 'telemetry' || msg.type === 'state') && onDataCallback) { onDataCallback(msg.data); } else if (msg.type === 'source_connected' && onStatusCallback) { onStatusCallback('source_connected'); } else if (msg.type === 'source_disconnected' && onStatusCallback) { onStatusCallback('source_disconnected'); } } catch (e) { // Ignore non-JSON messages } }; ws.onclose = () => { console.log('WS relay disconnected'); if (onStatusCallback) onStatusCallback('disconnected'); scheduleReconnect(); }; ws.onerror = (err) => { console.warn('WS relay error:', err); }; } catch (e) { console.warn('WS connect failed:', e); scheduleReconnect(); } } /** * Schedule a reconnection attempt */ function scheduleReconnect() { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => connect(isSource), RECONNECT_DELAY); } /** * Send telemetry data to relay (bridge mode — phone uploading to VPS) */ function sendTelemetry(data) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'telemetry', source: 'ble', data })); } } /** * Disconnect from relay */ function disconnect() { clearTimeout(reconnectTimer); if (ws) { ws.close(); ws = null; } } /** * Set callback for incoming telemetry (viewer mode) */ function onData(callback) { onDataCallback = callback; } /** * Set callback for connection status changes */ function onStatus(callback) { onStatusCallback = callback; } /** * Check if connected */ function isConnected() { return ws && ws.readyState === WebSocket.OPEN; } return { connect, disconnect, sendTelemetry, onData, onStatus, isConnected }; })();