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.
This commit is contained in:
Giuseppe Raffa
2026-05-12 10:17:54 +02:00
parent bb8d267cd4
commit c2c1598226
27 changed files with 1061 additions and 326 deletions

View File

@@ -1,17 +1,40 @@
const WebSocket = require('ws');
const os = require('os');
const { encode } = require('@msgpack/msgpack');
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 è riuscita
* @returns {Promise<boolean>} true se la connessione e' riuscita
*/
function connect(socketToken, onClose) {
return new Promise((resolve) => {
@@ -32,23 +55,57 @@ function connect(socketToken, onClose) {
ws.on('open', () => {
console.log('[REALTIME|WS] Connesso');
// Invia init con system uptime
const initPayload = {
_t: 'init',
uptime: Math.floor(os.uptime())
};
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', () => {
// Il server non invia messaggi ai sensori per ora
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 risponde automaticamente con pong
});
ws.on('ping', () => { /* ws library risponde con pong automaticamente */ });
ws.on('error', (err) => {
console.error(`[REALTIME|WS] Errore: ${err.message}`);
@@ -58,6 +115,7 @@ function connect(socketToken, onClose) {
ws.on('close', (code) => {
console.log(`[REALTIME|WS] Disconnesso (code: ${code})`);
ws = null;
currentSessionId = null;
if (onDisconnect) onDisconnect();
});
});
@@ -68,47 +126,51 @@ function connect(socketToken, onClose) {
* @param {Array} data - Array nel formato [timestamp, measurement, fields]
*/
function send(data) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
let buf;
try {
const [timestamp, measurement, fields] = data;
const packet = { ts: timestamp, _m: measurement, ...fields };
ws.send(encode(packet));
buf = encode(packet);
} catch (err) {
console.error('[REALTIME|WS] Errore invio:', err.message);
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); }
}
/**
* Invia un oggetto raw al server, codificato in msgpack.
* A differenza di send(), non fa transform [ts, measurement, fields].
* @param {Object} obj - Oggetto da inviare direttamente
*/
function sendRaw(obj) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
try {
ws.send(encode(obj));
} catch (err) {
console.error('[REALTIME|WS] Errore invio raw:', err.message);
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); }
}
/**
* @returns {boolean} true se la connessione è attiva
*/
function isConnected() {
return ws !== null && ws.readyState === WebSocket.OPEN;
}
/**
* Chiude la connessione WebSocket.
*/
function getSessionId() { return currentSessionId; }
function close() {
onDisconnect = null;
if (ws) {
ws.close();
ws = null;
currentSessionId = null;
}
}
module.exports = { connect, send, sendRaw, isConnected, close };
module.exports = { connect, send, sendRaw, isConnected, getSessionId, close };