Files
signalk-plugin/plugin/cores/realtime/socket.js
Giuseppe Raffa c2c1598226 feat: Implement rulesets and layout management for kiosk plugin
- Added rulesets manager to handle various data types and updates via HTTP and WebSocket.
- Introduced layout store for managing kiosk layouts with caching and server synchronization.
- Enhanced dashboard and data routes to support new layout and ruleset features.
- Updated kiosk HTML and JavaScript to utilize new layout rendering and data binding.
- Removed obsolete map route and integrated map functionality into the new tile renderer.
- Improved Telegram commands to reflect changes in data structure and logging.
- Refactored weather fetching intervals to prevent multiple instances.
- Added SSE stream for real-time layout updates in the kiosk.
2026-05-12 10:17:54 +02:00

177 lines
5.7 KiB
JavaScript

const WebSocket = require('ws');
const os = require('os');
const { encode, decode } = require('@msgpack/msgpack');
const SOCKET_URL = process.env.REALTIME_SOCKET_URL;
let ws = null;
let onDisconnect = null;
let currentSessionId = null;
const QUEUE_MAX = 500;
const pendingQueue = [];
function flushQueue() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
while (pendingQueue.length > 0) {
const buf = pendingQueue.shift();
try { ws.send(buf); }
catch (err) {
console.error('[REALTIME|WS] Errore flush:', err.message);
return;
}
}
}
function enqueue(buf) {
if (pendingQueue.length >= QUEUE_MAX) {
pendingQueue.shift();
}
pendingQueue.push(buf);
}
/**
* Apre una connessione WebSocket al server realtime usando il token temporaneo.
* @param {string} socketToken - Token temporaneo ottenuto da auth.authenticate()
* @param {Function} onClose - Callback chiamata quando la connessione si chiude
* @returns {Promise<boolean>} true se la connessione e' riuscita
*/
function connect(socketToken, onClose) {
return new Promise((resolve) => {
if (!SOCKET_URL) {
console.error('[REALTIME|WS] REALTIME_SOCKET_URL non configurato nel .env');
return resolve(false);
}
onDisconnect = onClose;
try {
const wsUrl = `${SOCKET_URL}/?token=${encodeURIComponent(socketToken)}`;
ws = new WebSocket(wsUrl);
} catch (err) {
console.error('[REALTIME|WS] Errore creazione:', err.message);
return resolve(false);
}
ws.on('open', () => {
console.log('[REALTIME|WS] Connesso');
const initPayload = { _t: 'init', uptime: Math.floor(os.uptime()) };
ws.send(encode(initPayload));
console.log('[REALTIME|WS] Init inviato:', initPayload);
flushQueue();
resolve(true);
});
ws.on('message', (raw) => {
let msg;
try { msg = decode(raw); }
catch (err) {
console.warn('[REALTIME|WS] payload non msgpack:', err.message);
return;
}
if (msg && typeof msg === 'object') {
if (msg._t === 'hello') {
currentSessionId = msg.sessionId || null;
console.log(`[REALTIME|WS] hello dal server (session=${currentSessionId})`);
return;
}
if (msg._t === 'session_id') {
currentSessionId = msg.sessionId || null;
console.log(`[REALTIME|WS] session_reset ack: ${msg.prev} -> ${msg.sessionId}`);
return;
}
if (msg._t === 'ruleset_update') {
console.log(`[REALTIME|WS] ruleset_update type=${msg.type} v=${msg.ruleset?.version?.str || '?'}`);
try {
require('../rulesets').applyRemote(msg.type, msg.ruleset, { source: 'push' });
} catch (err) { console.warn('[REALTIME|WS] ruleset apply error:', err.message); }
return;
}
if (msg._t === 'kiosk_layout_update') {
console.log(`[REALTIME|WS] kiosk_layout_update v=${msg.layout?.version || '?'}`);
try {
require('../../tools/kiosk/server-layout-store').applyRemote(msg.layout);
} catch (err) { console.warn('[REALTIME|WS] layout apply error:', err.message); }
return;
}
if (msg._t === 'error') {
console.warn('[REALTIME|WS] error dal server:', msg.message);
return;
}
if (msg._t === 'ack') {
return; // diagnostica, no-op
}
}
});
ws.on('ping', () => { /* ws library risponde con pong automaticamente */ });
ws.on('error', (err) => {
console.error(`[REALTIME|WS] Errore: ${err.message}`);
resolve(false);
});
ws.on('close', (code) => {
console.log(`[REALTIME|WS] Disconnesso (code: ${code})`);
ws = null;
currentSessionId = null;
if (onDisconnect) onDisconnect();
});
});
}
/**
* Invia dati al server tramite WebSocket, codificati in msgpack.
* @param {Array} data - Array nel formato [timestamp, measurement, fields]
*/
function send(data) {
let buf;
try {
const [timestamp, measurement, fields] = data;
const packet = { ts: timestamp, _m: measurement, ...fields };
buf = encode(packet);
} catch (err) {
console.error('[REALTIME|WS] Errore encode:', err.message);
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
enqueue(buf);
return;
}
try { ws.send(buf); }
catch (err) { console.error('[REALTIME|WS] Errore invio:', err.message); }
}
function sendRaw(obj) {
let buf;
try { buf = encode(obj); }
catch (err) {
console.error('[REALTIME|WS] Errore encode raw:', err.message);
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
enqueue(buf);
return;
}
try { ws.send(buf); }
catch (err) { console.error('[REALTIME|WS] Errore invio raw:', err.message); }
}
function isConnected() {
return ws !== null && ws.readyState === WebSocket.OPEN;
}
function getSessionId() { return currentSessionId; }
function close() {
onDisconnect = null;
if (ws) {
ws.close();
ws = null;
currentSessionId = null;
}
}
module.exports = { connect, send, sendRaw, isConnected, getSessionId, close };