Migra dal codice salvato in locale al codice condiviso

This commit is contained in:
Giuseppe Raffa
2026-01-06 17:36:58 +01:00
parent 8a88c31c75
commit ff1566d36b
30 changed files with 8985 additions and 0 deletions

258
plugin/tools/crypt.js Normal file
View File

@@ -0,0 +1,258 @@
/**
* Modulo di crittografia centralizzato per MEB Plugin
* Supporta AES-256-GCM per file sensibili e log CSV
*
* BEST PRACTICES SICUREZZA ENTERPRISE:
* 1. La MASTER_KEY dovrebbe essere in variabile d'ambiente (process.env.MEB_MASTER_KEY)
* 2. In produzione usare AWS KMS, HashiCorp Vault, o Azure Key Vault
* 3. Rotazione periodica delle chiavi (ogni 90 giorni)
* 4. Separazione chiavi: una per users, una per logs_references, una per log files
* 5. Audit log di ogni accesso ai file sensibili
*
* GENERAZIONE CHIAVE SICURA:
* Esegui nel terminale: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
* Poi imposta: export MEB_MASTER_KEY="la_chiave_generata"
*/
const crypto = require("crypto");
const fs = require('fs');
const MASTER_KEY_HEX = process.env.CRYPTOKEY || null;
const TOKEN_CHARSET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
const specialCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*_+-=';
/**
* Ottiene la chiave master (32 byte per AES-256)
* @returns {Buffer} Chiave di 32 byte
*/
function getMasterKey() {
if (!MASTER_KEY_HEX) {
throw new Error("MASTER_KEY non definita. Imposta MEB_MASTER_KEY nelle variabili d'ambiente.");
}
const key = Buffer.from(MASTER_KEY_HEX, 'hex');
if (key.length !== 32) {
throw new Error("MASTER_KEY deve essere di 32 byte (64 caratteri hex).");
}
return key;
}
/**
* Normalizza qualsiasi chiave custom a 32 byte Buffer per AES-256.
* Accetta chiavi di qualsiasi lunghezza/formato.
* @param {string|Buffer|null} customKey - Chiave custom o null per usare master key
* @returns {Buffer} Chiave di 32 byte
*/
function normalizeKey(customKey) {
if (!customKey) return getMasterKey();
if (typeof customKey === 'string') {
// Se è hex di 64 caratteri, convertilo direttamente
if (/^[0-9a-fA-F]{64}$/.test(customKey)) {
return Buffer.from(customKey, 'hex');
}
// Altrimenti hash SHA-256 per ottenere 32 byte
return crypto.createHash('sha256').update(customKey, 'utf8').digest();
}
if (Buffer.isBuffer(customKey)) {
if (customKey.length === 32) return customKey;
return crypto.createHash('sha256').update(customKey).digest();
}
throw new Error("customKey deve essere una stringa o un Buffer");
}
// ==================== GENERAZIONE TOKEN ====================
/**
* Genera un token esadecimale casuale UNICO ogni volta
* @param {number} bytes - Numero di byte (default 24 = 48 caratteri hex)
* @returns {string} Token esadecimale unico
*/
function generateToken(bytes = 24) {
return crypto.randomBytes(bytes).toString('hex');
}
/**
* Genera un token leggibile (senza caratteri ambigui come 0/O, 1/l/I)
* Più facile da comunicare verbalmente
* @param {number} length - Lunghezza del token (default 32)
* @returns {string} Token alfanumerico leggibile
*/
function generateReadableToken(length = 32) {
const bytes = crypto.randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += TOKEN_CHARSET[bytes[i] % TOKEN_CHARSET.length];
}
return result;
}
/**
* Genera un token con caratteri speciali (più sicuro per chiavi sensibili)
* @param {number} length - Lunghezza del token (default 64)
* @returns {string} Token con caratteri speciali
*/
function generateSecureToken(length = 64) {
const bytes = crypto.randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += specialCharset[bytes[i] % specialCharset.length];
}
return result;
}
// ==================== CRITTOGRAFIA OGGETTI JSON (per file sensibili) ====================
/**
* Cripta un oggetto JSON in Buffer binario (AES-256-GCM)
* Usato per telegram_users.json e logs_references.json
* @param {object} obj - Oggetto da criptare
* @param {string|Buffer|null} customKey - Chiave custom (opzionale)
* @returns {Buffer} Dati criptati [IV(12) + TAG(16) + CIPHERTEXT]
*/
// DISABILITATO: salviamo in chiaro
function encrypt(obj, customKey = null) {
const plaintext = Buffer.from(JSON.stringify(obj), 'utf8');
return plaintext; // ritorna direttamente il contenuto in chiaro
}
/**
* Decripta un Buffer in oggetto JSON
* @param {Buffer} buffer - Dati criptati
* @param {string|Buffer|null} customKey - Chiave custom (opzionale)
* @returns {object} Oggetto decriptato (array vuoto se fallisce)
*/
// DISABILITATO: leggiamo direttamente in chiaro
function decrypt(buffer, customKey = null) {
try {
if (!buffer) return [];
const content = buffer.toString('utf8');
return JSON.parse(content);
} catch (error) {
console.error('[decrypt] Errore:', error.message);
return [];
}
}
// ==================== CRITTOGRAFIA FILE LOG CSV ====================
/**
* Cripta un file CSV/testo sul disco
* @param {string} filePath - Percorso del file
* @param {string|Buffer|null} customKey - Chiave custom (qualsiasi lunghezza)
* @returns {boolean} True se successo
*/
// DISABILITATO: i file log rimangono sempre in chiaro
function encryptLog(filePath, customKey = null) {
try {
// Non fare nulla, lascia il file in chiaro
return true;
} catch (error) {
console.error('[encryptLog] Errore:', error.message);
return false;
}
}
/**
* Decripta un file CSV/testo e lo riscrive sul disco
* @param {string} filePath - Percorso del file criptato
* @param {string|Buffer|null} customKey - Chiave custom
* @returns {string|null} Contenuto decriptato o null se errore
*/
// DISABILITATO: i file sono già in chiaro
function decryptLog(filePath, customKey = null) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return content; // ritorna contenuto in chiaro senza modifiche
} catch (error) {
console.error('[decryptLog] Errore:', error.message);
return null;
}
}
/**
* Decripta un file log e restituisce il contenuto SENZA modificare il file
* @param {string} filePath - Percorso del file criptato
* @param {string|Buffer|null} customKey - Chiave custom
* @returns {string|null} Contenuto decriptato o null se errore
*/
// DISABILITATO: i file sono già in chiaro
function decryptLogToMemory(filePath, customKey = null) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
console.error('[decryptLogToMemory] Errore:', error.message);
return null;
}
}
// ==================== GESTIONE FILE SENSIBILI (telegram_users, logs_references) ====================
/**
* Carica e decripta un file JSON sensibile
* Gestisce automaticamente file in chiaro (migrazione) e criptati
* @param {string} filePath - Percorso del file
* @param {object} defaultValue - Valore di default se file non esiste
* @returns {object} Dati decriptati
*/
function loadSecureFile(filePath, defaultValue = {}) {
try {
if (!fs.existsSync(filePath)) {
return defaultValue;
}
const content = fs.readFileSync(filePath, 'utf8').trim();
try {
return JSON.parse(content);
} catch (e) {
console.error(`[loadSecureFile] JSON non valido in ${filePath}:`, e.message);
return defaultValue;
}
} catch (error) {
console.error(`[loadSecureFile] Errore caricamento ${filePath}:`, error.message);
return defaultValue;
}
}
/**
* Cripta e salva un file JSON sensibile
* @param {string} filePath - Percorso del file
* @param {object} data - Dati da salvare
* @returns {boolean} True se successo
*/
function saveSecureFile(filePath, data) {
try {
const content = JSON.stringify(data, null, 2);
fs.writeFileSync(filePath, content, 'utf8');
return true;
} catch (error) {
console.error(`[saveSecureFile] Errore salvataggio ${filePath}:`, error.message);
return false;
}
}
module.exports = {
// Generazione token
generateToken,
generateReadableToken,
generateSecureToken,
// Crittografia oggetti JSON
encrypt,
decrypt,
// Crittografia file log
encryptLog,
decryptLog,
decryptLogToMemory,
// Gestione file sensibili
loadSecureFile,
saveSecureFile,
// Utility
normalizeKey
};

219
plugin/tools/logRecorder.js Normal file
View File

@@ -0,0 +1,219 @@
/**
* logRecorder.js - Gestione registrazione dati separata
* Centralizza tutte le funzioni di logging del dataset
*/
const path = require('path');
const { datasetInit, appendData } = require('../datasetModels/datasetCore');
let app = null;
let recordingInterval = null;
let isRecording = false;
// Stato condiviso della registrazione
const recordingState = {
active: false,
startTime: null,
entryCount: 0,
currentFile: null,
stream: null
};
/**
* Inizializza il recorder con l'istanza di SignalK app
*/
function init(signalkApp) {
app = signalkApp;
console.log('[LogRecorder] Inizializzato');
}
/**
* Raccoglie i dati dai sensori SignalK
*/
function collectSensorData() {
const getSK = (p) => {
const v = app.getSelfPath(p);
return v && v.value !== undefined && v.value !== null ? v.value : null;
};
return {
timestamp: new Date().toISOString(),
// Posizione
latitude: getSK('navigation.position')?.latitude ?? null,
longitude: getSK('navigation.position')?.longitude ?? null,
speed: getSK('navigation.speedOverGround'),
heading: getSK('navigation.headingTrue'),
// Batteria Trazione
traction_voltage: getSK('electrical.batteries.traction.Voltage'),
traction_current: getSK('electrical.batteries.traction.current'),
traction_soc: getSK('electrical.batteries.traction.stateOfCharge'),
traction_temperature: getSK('electrical.batteries.traction.temperature'),
traction_power: getSK('electrical.batteries.traction.power'),
// Batteria Servizio
service_voltage: getSK('electrical.batteries.service.Voltage'),
service_current: getSK('electrical.batteries.service.current'),
service_soc: getSK('electrical.batteries.service.stateOfCharge'),
service_temperature: getSK('electrical.batteries.service.temperature'),
// Meteo (da OpenMeteo condiviso)
temperature: getSK('meb.temperature'),
windSpeed: getSK('meb.appleWindSpeed'),
windDirection: getSK('meb.appleWindDirection'),
// Onde
waveHeight: getSK('meb.waves.waveHeight'),
wavePeriod: getSK('meb.waves.wavePeriod'),
waveDirection: getSK('meb.waves.waveDirection')
};
}
/**
* Crea un nuovo file di log
*/
function createNewLogFile() {
const headers = [
'timestamp',
'latitude', 'longitude', 'speed', 'heading',
'traction_voltage', 'traction_current', 'traction_soc', 'traction_temperature', 'traction_power',
'service_voltage', 'service_current', 'service_soc', 'service_temperature',
'temperature', 'windSpeed', 'windDirection',
'waveHeight', 'wavePeriod', 'waveDirection'
];
const result = datasetInit(headers);
if (result) {
recordingState.currentFile = result.fileName;
recordingState.stream = result.stream;
console.log(`[LogRecorder] Nuovo file: ${result.fileName}`);
}
return result;
}
/**
* Scrive una riga di dati nel log
*/
function writeLogEntry(data) {
if (!recordingState.stream) return false;
const values = [
data.timestamp,
data.latitude, data.longitude, data.speed, data.heading,
data.traction_voltage, data.traction_current, data.traction_soc, data.traction_temperature, data.traction_power,
data.service_voltage, data.service_current, data.service_soc, data.service_temperature,
data.temperature, data.windSpeed, data.windDirection,
data.waveHeight, data.wavePeriod, data.waveDirection
];
appendData(values);
recordingState.entryCount++;
return true;
}
/**
* Avvia la registrazione
* @param {number} intervalMs - Intervallo in millisecondi (default 2000)
*/
function startRecording(intervalMs = 2000) {
if (isRecording) {
console.log('[LogRecorder] Registrazione già attiva');
return false;
}
if (!app) {
console.error('[LogRecorder] App non inizializzata');
return false;
}
const fileResult = createNewLogFile();
if (!fileResult) {
console.error('[LogRecorder] Impossibile creare file di log');
return false;
}
recordingState.active = true;
recordingState.startTime = Date.now();
recordingState.entryCount = 0;
isRecording = true;
recordingInterval = setInterval(() => {
const data = collectSensorData();
writeLogEntry(data);
}, intervalMs);
console.log(`[LogRecorder] Registrazione avviata (ogni ${intervalMs}ms)`);
return true;
}
/**
* Ferma la registrazione
*/
function stopRecording() {
if (!isRecording) {
console.log('[LogRecorder] Nessuna registrazione attiva');
return false;
}
if (recordingInterval) {
clearInterval(recordingInterval);
recordingInterval = null;
}
if (recordingState.stream) {
recordingState.stream.end();
}
const duration = Date.now() - recordingState.startTime;
console.log(`[LogRecorder] Registrazione fermata. Durata: ${Math.round(duration / 1000)}s, Entries: ${recordingState.entryCount}`);
recordingState.active = false;
recordingState.stream = null;
isRecording = false;
return {
duration,
entries: recordingState.entryCount,
file: recordingState.currentFile
};
}
/**
* Riavvia la registrazione (nuovo file)
*/
function restartRecording(intervalMs = 2000) {
stopRecording();
return startRecording(intervalMs);
}
/**
* Ottiene lo stato corrente della registrazione
*/
function getStatus() {
return {
isRecording,
active: recordingState.active,
startTime: recordingState.startTime,
entryCount: recordingState.entryCount,
currentFile: recordingState.currentFile,
runningTime: isRecording ? Date.now() - recordingState.startTime : 0
};
}
/**
* Verifica se la registrazione è attiva
*/
function isActive() {
return isRecording;
}
module.exports = {
init,
startRecording,
stopRecording,
restartRecording,
getStatus,
isActive,
collectSensorData
};

View File

@@ -0,0 +1,35 @@
const fs = require("fs");
const path = require("path");
module.exports = function(app, settings) {
// Serve mappa
app.get('/meb/map', (req, res) => {
const filePath = path.join(__dirname, "public", "map.html");
fs.readFile(filePath, "utf8", (err, html) => {
if (err) {
res.status(500).send("Errore nel caricamento della mappa");
return;
}
const token = settings?.mapboxKey ?? "";
const finalHtml = html.replace("{{MAPBOX_KEY}}", token);
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(finalHtml);
});
});
// WebSocket forward: posizione in tempo reale
let lastPosition = null;
app.streambundle.getSelfStream("navigation.position").onValue(pos => {
lastPosition = pos;
});
// Endpoint JSON per marker barca (se vuoi usarlo invece del WS SignalK)
app.get('/meb/map/boat', (req, res) => {
if (!lastPosition) {
res.json({ error: "No position data available" });
return;
}
res.json(lastPosition);
});
}

View File

@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Mappa Meteo SignalK</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet">
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
background-color: #e0e0e0;
}
.icon {
width: 80px;
height: 80px;
transform-origin: center center;
display: flex;
justify-content: center;
align-items: center;
}
.info {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(60, 60, 60, 0.85);
padding: 12px 18px;
border-radius: 10px;
font-size: 20px;
font-family: Arial, sans-serif;
line-height: 1.25;
min-width: 270px;
z-index: 999;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="infoBox" class="info">
Caricamento dati...
</div>
<script>
// MAPBOX TOKEN
mapboxgl.accessToken =
"pk.eyJ1Ijoic2VzZWUzIiwiYSI6ImNtZ2dydndkMDBsNjUya3NjeW91dW41MzcifQ.M2qxj0wL1W7plRzIataojQ";
// Centro predefinito
const defaultCenter = [9.19, 44.41];
// MAPPA
const map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/streets-v12",
center: defaultCenter,
zoom: 15
});
// FUNZIONE CALCOLO DESTINAZIONE VETTORE
function destinationPoint([lon, lat], bearingDeg, distanceMeters) {
const R = 6371000;
const brng = bearingDeg * Math.PI / 180;
const d = distanceMeters;
const lat1 = lat * Math.PI / 180;
const lon1 = lon * Math.PI / 180;
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(d / R) +
Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)
);
const lon2 = lon1 + Math.atan2(
Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1),
Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)
);
return [ lon2 * 180/Math.PI, lat2 * 180/Math.PI ];
}
// QUANDO LA MAPPA È PRONTA
map.on("load", () => {
map.addSource("waveVector", {
type: "geojson",
data: { type: "Feature", geometry: { type: "LineString", coordinates: [defaultCenter, defaultCenter] }}
});
map.addLayer({
id: "waveVectorLine",
type: "line",
source: "waveVector",
paint: { "line-color": "#0000ff", "line-width": 6 }
});
map.addLayer({
id: "waveLabelText",
type: "symbol",
source: "waveVector",
layout: {
"symbol-placement": "line",
"text-field": "Direzione Onde",
"text-size": 30,
"text-allow-overlap": true
},
paint: {
"text-color": "#0000ff",
"text-halo-color": "white",
"text-halo-width": 4
}
});
map.addSource("windVector", {
type: "geojson",
data: { type: "Feature", geometry: { type: "LineString", coordinates: [defaultCenter, defaultCenter] }}
});
map.addLayer({
id: "windVectorLine",
type: "line",
source: "windVector",
paint: { "line-color": "lime", "line-width": 6 }
});
map.addLayer({
id: "windLabelText",
type: "symbol",
source: "windVector",
layout: {
"symbol-placement": "line",
"text-field": "Direzione Vento",
"text-size": 30,
"text-allow-overlap": true
},
paint: {
"text-color": "lime",
"text-halo-color": "black",
"text-halo-width": 3
}
});
});
// MARKER BARCA
const boatEl = document.createElement("div");
boatEl.className = "icon";
boatEl.innerHTML = "⛵";
boatEl.style.fontSize = "60px";
const boatMarker = new mapboxgl.Marker({ element: boatEl }).setLngLat(defaultCenter).addTo(map);
// FRECCE
const arrowEl = document.createElement("div");
arrowEl.className = "icon";
arrowEl.innerHTML = `<svg width="80" height="80" viewBox="0 0 100 100"><g id="arrowGroup" transform="rotate(0 50 50)"><polygon points="50,0 90,100 50,75 10,100" fill="blue" /></g></svg>`;
const arrowMarker = new mapboxgl.Marker({ element: arrowEl }).setLngLat(defaultCenter).addTo(map);
const windEl = document.createElement("div");
windEl.className = "icon";
windEl.innerHTML = `<svg width="80" height="80" viewBox="0 0 100 100"><g id="windArrowGroup" transform="rotate(0 50 50)"><polygon points="50,0 90,100 50,75 10,100" fill="lime" /></g></svg>`;
const windMarker = new mapboxgl.Marker({ element: windEl }).setLngLat(defaultCenter).addTo(map);
// SIGNALK VARIABILI
let position=null, waveDir=null, waveHeight=null, wavePeriod=null, windDir=null, windSpeed=null, temperature=null;
// WebSocket SK
const ws = new WebSocket(`ws://${location.host}/signalk/v1/stream?subscribe=all`);
ws.onmessage = msg => {
const data = JSON.parse(msg.data);
if (!data.updates) return;
data.updates.forEach(u => {
u.values?.forEach(v => {
if (v.path === "navigation.position") position = [v.value.longitude, v.value.latitude];
if (v.path === "meb.waves.direction") waveDir = v.value;
if (v.path === "meb.waves.height") waveHeight = v.value;
if (v.path === "meb.waves.period") wavePeriod = v.value;
if (v.path === "meb.wind.direction") windDir = v.value;
if (v.path === "environment.wind.speedTrue") windSpeed = v.value;
if (v.path === "environment.outside.temperature") temperature = v.value;
});
});
updateMap();
};
// UPDATE MAP
function updateMap() {
if (!position) return;
boatMarker.setLngLat(position);
if (waveDir !== null) {
const endPoint = destinationPoint(position, waveDir, 1000);
map.getSource("waveVector").setData({
type: "Feature",
geometry: { type: "LineString", coordinates: [position, endPoint] }
});
arrowMarker.setLngLat(endPoint);
document.getElementById("arrowGroup").setAttribute("transform", `rotate(${waveDir} 50 50)`);
}
if (windDir !== null) {
const windHeading = (windDir + 180) % 360;
const windEndPoint = destinationPoint(position, windHeading, 1000);
map.getSource("windVector").setData({
type: "Feature",
geometry: { type: "LineString", coordinates: [position, windEndPoint] }
});
windMarker.setLngLat(windEndPoint);
document.getElementById("windArrowGroup").setAttribute("transform", `rotate(${windHeading} 50 50)`);
}
updateInfoBox();
map.easeTo({ center: position });
}
// UPDATE INFO BOX
function updateInfoBox() {
const box = document.getElementById("infoBox");
if (!position) return;
const lat = position[1].toFixed(5);
const lon = position[0].toFixed(5);
const waveDeg = waveDir !== null ? `${waveDir.toFixed(0)}°` : "Need Request";
const heightM = waveHeight !== null ? `${waveHeight.toFixed(2)} m` : "Need Request";
const periodS = wavePeriod !== null ? `${wavePeriod.toFixed(1)} s` : "Need Request";
const windDeg = windDir !== null ? `${windDir.toFixed(0)}°` : "Need Request";
const windKMH = windSpeed !== null ? `${(windSpeed*3.6).toFixed(1)} km/h` : "Need Request";
const tempC = temperature !== null ? `${(temperature - 273.15).toFixed(1)} °C` : "Need Request";
box.innerHTML = `
<b>Direzione Onde:</b> ${waveDeg}<br>
<b>Altezza Onda:</b> ${heightM}<br>
<b>Periodo Onda:</b> ${periodS}<br>
<b>Direzione Vento:</b> ${windDeg}<br>
<b>Intensità Vento:</b> ${windKMH}<br>
<b>Temperatura:</b> ${tempC}<br>
<b>Lat:</b> ${lat} | <b>Lon:</b> ${lon}
`;
}
</script>
</body>
</html>

74
plugin/tools/publisher.js Normal file
View File

@@ -0,0 +1,74 @@
/**
* publisher.js - Pubblica dati su SignalK
*/
/**
* Genera valori SignalK da un oggetto dati
* @param {Object} data - Dati da convertire
* @param {string} prefix - Prefisso per i path SignalK
* @returns {Array} Array di valori SignalK
*/
function generateValues(data, prefix = "meb") {
if (!data || typeof data !== 'object') {
return [];
}
const values = [];
function traverse(obj, pathParts) {
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const val = obj[key];
if (val === undefined || val === null) continue;
const newPath = [...pathParts, key];
if (typeof val === "object" && !Array.isArray(val)) {
traverse(val, newPath);
} else if (!Array.isArray(val)) {
// Ignora array, pubblica solo valori primitivi
values.push({
path: newPath.join("."),
value: val,
meta: { displayName: key },
});
}
}
}
traverse(data, [prefix]);
return values;
}
/**
* Pubblica dati meteo su SignalK
* @param {Object} app - Istanza app SignalK
* @param {Object} weatherData - Dati meteo da pubblicare
* @param {Object} settings - Impostazioni plugin
*/
function publishWeatherData(app, weatherData, settings) {
if (!app || !weatherData) {
console.warn('[Publisher] App o dati non disponibili');
return;
}
const values = generateValues(weatherData);
if (values.length === 0) {
console.debug('[Publisher] Nessun valore da pubblicare');
return;
}
console.debug(`📤 Pubblicazione ${values.length} valori SignalK`);
try {
app.handleMessage("meb", {
updates: [{ values }],
});
} catch (error) {
console.error('[Publisher] Errore pubblicazione:', error.message);
}
}
module.exports = { publish: publishWeatherData };

321
plugin/tools/routes.js Normal file
View File

@@ -0,0 +1,321 @@
function setupRoutes(router, lastCallRef, app) {
router.get("/ping", async (req, res) => {
try {
const text = lastCallRef.current || "pong";
res.status(200).sendFile(__dirname + "/steering_support/helm_steering_destra.html");
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/helm_steering_destro", (req, res) => {
try {
res.status(200).sendFile(__dirname + "/steering_support/helm_steering_destro.html");
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/tools", (req, res) => {
try {
const path = require("path");
const filePath = path.join(__dirname, "..", "public", "decrypt_tool.html");
res.status(200).sendFile(filePath);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// LOGS DATASETS
router.post("/dataset/start", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control non disponibile" });
}
const result = app.datasetControl.start();
res.json({ success: result, message: result ? "Registrazione avviata" : "Registrazione già in corso" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.post("/dataset/stop", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control non disponibile" });
}
const result = app.datasetControl.stop();
res.json({ success: result, message: result ? "Registrazione fermata" : "Nessuna registrazione in corso" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.post("/dataset/restart", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control non disponibile" });
}
const result = app.datasetControl.restart();
res.json({ success: result, message: "Registrazione riavviata" });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/dataset/status", (req, res) => {
try {
if (!app.datasetControl) {
return res.status(503).json({ error: "Dataset control non disponibile" });
}
const status = app.datasetControl.getStatus();
res.json(status);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
router.get("/dataset/files", (req, res) => {
try {
const fs = require('fs');
const path = require('path');
const logsDirectory = path.join(__dirname, '..', 'datasetModels', 'saved_datas');
if (!fs.existsSync(logsDirectory)) {
return res.json({ files: [], count: 0 });
}
const items = fs.readdirSync(logsDirectory);
const files = items
.filter(item => {
const fullPath = path.join(logsDirectory, item);
return fs.statSync(fullPath).isFile();
})
.map(file => {
const fullPath = path.join(logsDirectory, file);
const stats = fs.statSync(fullPath);
return {
name: file,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime
};
})
.sort((a, b) => b.modified.getTime() - a.modified.getTime());
res.json({ files, count: files.length });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ==================== GRAPHS API ====================
const graphsCore = require('../datasetModels/graphsCore.js');
// Serve la pagina HTML dei grafici
router.get("/graphs", (req, res) => {
try {
const path = require("path");
const filePath = path.join(__dirname, "..", "public", "graphs.html");
res.status(200).sendFile(filePath);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API per ottenere dati grafici
router.get("/api/graphs", (req, res) => {
try {
const hours = parseInt(req.query.hours) || 24;
const data = graphsCore.getAllGraphsData(hours);
// Aggiungi valori attuali dalla cache condivisa
const sharedData = graphsCore.getSharedWeatherData();
data.current = {
temperature: sharedData.forecast?.temperature,
windSpeed: sharedData.forecast?.windSpeed,
waveHeight: sharedData.waves?.waveHeight,
humidity: sharedData.forecast?.humidity
};
// Aggiungi unità di misura
data.units = graphsCore.getUnits();
res.json(data);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// API per statistiche archivio
router.get("/api/graphs/stats", (req, res) => {
try {
const stats = graphsCore.getArchiveStats();
res.json(stats);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
}
function getOpenApiSpec() {
return {
openapi: "3.0.0",
info: { title: "MebWeather API Portal", version: "1.0.0" },
servers: [{ url: "/plugins/meb-weather" }],
paths: {
"/ping": {
get: {
summary: "Called /ping route",
responses: {
200: {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: { message: { type: "string" } },
},
},
},
},
},
},
},
"/meb/suggestion": {
get: {
summary: "Pagina di test MEB Suggestion",
responses: {
200: {
description: "OK",
content: {
"text/html": {
schema: { type: "string" },
},
},
},
},
},
},
"/dataset/start": {
post: {
summary: "Avvia la registrazione dataset",
responses: {
200: {
description: "Registrazione avviata",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
message: { type: "string" }
},
},
},
},
},
},
},
},
"/dataset/stop": {
post: {
summary: "Ferma la registrazione dataset",
responses: {
200: {
description: "Registrazione fermata",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
message: { type: "string" }
},
},
},
},
},
},
},
},
"/dataset/restart": {
post: {
summary: "Riavvia la registrazione dataset",
responses: {
200: {
description: "Registrazione riavviata",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
message: { type: "string" }
},
},
},
},
},
},
},
},
"/dataset/status": {
get: {
summary: "Ottieni lo stato della registrazione dataset",
responses: {
200: {
description: "Stato corrente",
content: {
"application/json": {
schema: {
type: "object",
properties: {
isRecording: { type: "boolean" },
recordCount: { type: "number" }
},
},
},
},
},
},
},
},
"/dataset/files": {
get: {
summary: "Ottieni la lista dei file log salvati",
responses: {
200: {
description: "Lista file log",
content: {
"application/json": {
schema: {
type: "object",
properties: {
files: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
size: { type: "number" },
created: { type: "string" },
modified: { type: "string" }
}
}
},
count: { type: "number" }
},
},
},
},
},
},
},
},
},
};
}
module.exports = { setupRoutes, getOpenApiSpec };

78
plugin/tools/utils.js Normal file
View File

@@ -0,0 +1,78 @@
const fs = require('fs');
const path = require('path');
//Ottieni il percodi dal nome di una cartella, se questa non esiste, viene creata
function getDirectory(directoryName) {
const directoryPath = path.resolve(__dirname, directoryName);
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, { recursive: true });
} else {
return directoryPath;
}
}
/**
* Scrivi un file con
* @param {string} fileName - Il nome del file.
* @param {string} extension - L'estensione
* @param {string} content - Il contenuto del file.
* @param {string} inDirectory - Il percorso in cui scrivere il file. Se non viene specificato, il file verrà aggiunto alla cartella principale del server.
*
* 🧠 Esempio duso
* (async () => {
* await writeFileToFolder("data", "prova.json", JSON.stringify({ name: "Giuseppe", age: 17 }, null, 2));
* })();
*
*/
async function write(fileName, extension, content, inDirectory) {
try {
const directoryPath = inDirectory ? getDirectory(inDirectory) : path.resolve(__dirname, '..');
fs.mkdirSync(directoryPath, {recursive: true});
const filePath = path.join(directoryPath, `${fileName}.${extension}`);
await fs.writeFileSync(filePath, content, 'utf-8');
} catch (error) {
console.error(`Error writing file ${fileName}.${extension}:`, error);
}
}
//Funzione per ottenere la data nel formato dd/mm/yyyy hh:mm
function getDate(isoString) {
const date = new Date(isoString);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}/${month}/${year} ${hours}:${minutes}`;
}
// Funzione per ottenere il tempo relativo ("2 ore fa", "tra 4 ore")
function relativeData(isoString) {
const date = new Date(isoString);
const now = new Date();
const diffMs = date - now; // differenza in millisecondi
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHr = Math.round(diffMin / 60);
const diffDay = Math.round(diffHr / 24);
const rtf = new Intl.RelativeTimeFormat("it", { numeric: "auto" });
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, "second");
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, "minute");
if (Math.abs(diffHr) < 24) return rtf.format(diffHr, "hour");
return rtf.format(diffDay, "day");
}
module.exports = {
getDirectory,
write,
getDate,
relativeData,
}