- 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.
177 lines
5.7 KiB
JavaScript
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 };
|