134 lines
3.7 KiB
JavaScript
134 lines
3.7 KiB
JavaScript
|
|
/**
|
||
|
|
* 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
|
||
|
|
};
|
||
|
|
})();
|