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:
@@ -8,10 +8,10 @@ const REALTIME_URL = process.env.REALTIME_URL;
|
||||
*/
|
||||
async function authenticate() {
|
||||
const SENSOR_CODE = configManager.getSensorCode();
|
||||
const SENSOR_NAME = configManager.getSensorName();
|
||||
const SENSOR_ID = configManager.getSensorId();
|
||||
|
||||
if (!REALTIME_URL || !SENSOR_CODE || !SENSOR_NAME) {
|
||||
console.error('[REALTIME|AUTH] REALTIME_URL, SENSOR_CODE o SENSOR_NAME non configurati');
|
||||
if (!REALTIME_URL || !SENSOR_CODE || !SENSOR_ID) {
|
||||
console.error('[REALTIME|AUTH] REALTIME_URL, SENSOR_CODE o SENSOR_ID non configurati');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ async function authenticate() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: SENSOR_NAME,
|
||||
id: SENSOR_ID,
|
||||
code: SENSOR_CODE
|
||||
})
|
||||
});
|
||||
@@ -38,8 +38,8 @@ async function authenticate() {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[REALTIME|AUTH] Autenticato: ${SENSOR_NAME}, token valido 5s`);
|
||||
return { socketToken: data.t };
|
||||
console.log(`[REALTIME|AUTH] Autenticato: ${SENSOR_ID}`);
|
||||
return { socketToken: data.t, sensorId: SENSOR_ID };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[REALTIME|AUTH] error: ${error.message}`);
|
||||
|
||||
@@ -22,21 +22,32 @@ async function init() {
|
||||
async function connectToServer() {
|
||||
if (isShuttingDown) return;
|
||||
|
||||
console.log('CONNECTING......')
|
||||
console.log('[REALTIME] connecting...');
|
||||
|
||||
const result = await auth.authenticate();
|
||||
console.log('AUTH RESULT:', result);
|
||||
let result;
|
||||
try {
|
||||
result = await auth.authenticate();
|
||||
} catch (err) {
|
||||
console.error('[REALTIME] auth error:', err.message);
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const connected = await socket.connect(result.socketToken, () => {
|
||||
if (!isShuttingDown) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
});
|
||||
let connected = false;
|
||||
try {
|
||||
connected = await socket.connect(result.socketToken, () => {
|
||||
if (!isShuttingDown) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[REALTIME] socket connect error:', err.message);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
scheduleReconnect();
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user