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

596
plugin/index.cjs Normal file
View File

@@ -0,0 +1,596 @@
const { config } = require("./config.js");
const { setupRoutes, getOpenApiSpec } = require("./tools/routes.js");
const { aisStream } = require("./api_models/aisstream.js")
const mapHandler = require("./tools/map.handler.js");
const { linkBot, send } = require("./bot/telegram.core.js");
const dataset = require("./datasetModels/datasetCore.js");
const dataUtils = require("./datasetModels/datasetUtils.js");
const graphsCore = require("./datasetModels/graphsCore.js");
const { generateToken, encryptLog, loadSecureFile, saveSecureFile } = require("./tools/crypt.js");
const fs = require("fs");
const path = require("path");
const { getForecast, getSeaConditions } = require("./api_models/openmeteo.js");
const { publish } = require("./tools/publisher.js");
// CONFIG modificabile runtime (non più frozen per permettere modifiche admin)
const CONFIG = {
log_interval: 2000, // Dataset entry ogni 2 secondi
openmeteo_interval: 300000, // OpenMeteo ogni 5 minuti
hourly_archive_interval: 3600000, // Archivio orario per grafici
number_value_fallback: 999999999999,
value_fallback: "Funzionalità da Sviluppare"
};
// Funzione per aggiornare gli intervalli runtime
function updateInterval(type, newIntervalMs) {
if (type === 'api' || type === 'openmeteo') {
CONFIG.openmeteo_interval = newIntervalMs;
return { type: 'openmeteo_interval', value: newIntervalMs };
} else if (type === 'log') {
CONFIG.log_interval = newIntervalMs;
return { type: 'log_interval', value: newIntervalMs };
}
return null;
}
// Getter per CONFIG (usato da altri moduli)
function getConfig() {
return { ...CONFIG };
}
const CSV_HEADERS = Object.freeze([
'timestamp',
'wavesHeight',
'wavesPeriod',
'wavesDirection',
'windSpeed',
'windDirection',
'temperature',
// 'currentSpeed',
// 'currentDirection',
'speedOverGround',
'courseOverGround',
'headingTrue',
'latitude',
'longitude',
'1Voltage',
'1Current',
'1StateOfCharge',
'1Temperature',
'0Voltage',
'0Current',
'0CellsStateOfCharge',
'0AverageCellTemperature',
'0Power',
'propultionShaftSpeed',
'systemUptime'
]);
const state = {
logTimer: null,
logStreamer: null,
logsCount: 0,
isRecordingLogs: false,
currentLogFile: null,
currentLogKey: null,
openMeteoTimer: null,
hourlyArchiveTimer: null,
unsubPos: null,
app: null,
startTime: null
};
const logsDirectory = dataUtils.getDirectory(__dirname + '/datasetModels/saved_datas');
const logsReferencesFile = path.join(__dirname, 'datasetModels/logs_references.json');
const lastCallRef = { current: null };
const getSKValue = (path, fallback = CONFIG.value_fallback) => {
if (!state.app) {
console.warn(`[getSKValue] App not initialized, returning fallback for path: ${path}`);
return fallback;
}
try {
const value = state.app.getSelfPath(path)?.value;
return (value !== undefined && value !== null) ? value : fallback;
} catch (error) {
console.error(`[getSKValue] Error reading path ${path}:`, error.message);
return fallback;
}
};
const closeStream = (stream) => {
return new Promise((resolve) => {
if (!stream || stream.destroyed) {
resolve();
return;
}
stream.end(() => {
resolve();
});
setTimeout(resolve, 1000);
});
};
const clearIntervalSafe = (timerId) => {
if (timerId) {
clearInterval(timerId);
}
return null;
};
const collectSensorData = (settings = {}) => {
// Prendi la posizione dalla navigazione se disponibile
const position = state.app?.getSelfPath('navigation.position')?.value;
const lat = position?.latitude ?? settings.latitude ?? CONFIG.number_value_fallback;
const lon = position?.longitude ?? settings.longitude ?? CONFIG.number_value_fallback;
return {
timestamp: new Date().toISOString(),
wavesHeight: getSKValue("meb.waves.height"),
wavesPeriod: getSKValue("meb.waves.period"),
wavesDirection: getSKValue("meb.waves.direction"),
windSpeed: getSKValue("meb.wind.speed"),
windDirection: getSKValue("meb.wind.direction"),
temperature: getSKValue("meb.temperature"),
// currentSpeed: getSKValue("meb.currents.speed"),
// currentDirection: getSKValue("meb.currents.direction"),
speedOverGround: getSKValue("navigation.speedOverGround"),
courseOverGround: getSKValue("navigation.courseOverGroundTrue"),
headingTrue: getSKValue("navigation.headingTrue"),
latitude: lat,
longitude: lon,
'1Voltage': getSKValue("electrical.batteries.service.Voltage"),
'1Current': getSKValue("electrical.batteries.service.current"),
'1StateOfCharge': getSKValue("electrical.batteries.service.stateOfCharge"),
'1Temperature': getSKValue("electrical.batteries.service.temperature"),
'0Voltage': getSKValue("electrical.batteries.traction.Voltage"),
'0Current': getSKValue("electrical.batteries.traction.current"),
'0CellsStateOfCharge': getSKValue("electrical.batteries.traction.stateOfCharge"),
'0AverageCellTemperature': getSKValue("electrical.batteries.traction.temperature"),
'0Power': getSKValue("electrical.batteries.traction.power"),
propultionShaftSpeed: getSKValue("propulsion.0.revolutions"),
systemUptime: process.uptime() ?? CONFIG.number_value_fallback
};
};
function createNewFiles() {
try {
const now = new Date();
const dateStr = now.toLocaleString('it-IT', {
timeZone: 'Europe/Rome',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).replace(/:/g, '-');
const logFileName = `log_${dateStr}.csv`;
const logFile = path.join(logsDirectory, logFileName);
// Close existing stream gracefully
if (state.logStreamer && !state.logStreamer.destroyed) {
state.logStreamer.end();
}
state.logStreamer = fs.createWriteStream(logFile, { flags: 'a' });
state.logStreamer.on('error', (err) => {
console.error('[log_file] Errore nello stream:', err);
});
dataset.datasetInit(CSV_HEADERS, state.logStreamer);
state.logsCount = 0;
state.currentLogFile = logFileName;
state.currentLogKey = generateToken();
return true;
} catch (error) {
console.error('[log_file] Errore nella creazione di un nuovo file:', error);
return false;
}
}
// ==================== RECORDING CONTROL ====================
/**
* Stops the data recording process
* @returns {boolean} True if stopped successfully, false if already stopped
*/
function stopRecording() {
if (!state.isRecordingLogs) {
return false;
}
try {
state.logTimer = clearIntervalSafe(state.logTimer);
if (state.logStreamer && !state.logStreamer.destroyed) {
state.logStreamer.end();
}
state.isRecordingLogs = false;
// Usa la chiave generata all'inizio della sessione
if (state.currentLogFile && state.currentLogKey) {
const logFilePath = path.join(logsDirectory, state.currentLogFile);
// Carica, aggiorna e salva references criptate
const logsData = loadSecureFile(logsReferencesFile, { references: [] });
logsData.references.push({
name: state.currentLogFile,
token: state.currentLogKey
});
saveSecureFile(logsReferencesFile, logsData);
// Cripta il file log con la stessa chiave
// encryptLog(logFilePath, state.currentLogKey);
console.log(`[stopRecording] Log ${state.currentLogFile} criptato e salvato.`);
}
state.logsCount = 0;
state.currentLogFile = null;
state.currentLogKey = null;
return true;
} catch (error) {
console.error('[log_stop] Errore durante l\'arresto della registrazione:', error);
return false;
}
}
/**
* Starts the data recording process
* @param {object} settings - Plugin settings
* @returns {boolean} True if started successfully, false if already running
*/
function startRecording(settings = {}) {
if (state.isRecordingLogs) {
return false;
}
try {
state.isRecordingLogs = true;
state.startTime = Date.now();
if (!createNewFiles()) {
state.isRecordingLogs = false;
return false;
}
state.logTimer = setInterval(() => {
try {
if (!state.logStreamer || state.logStreamer.destroyed) {
console.error('[log_dataset_error] Stream non disponibile');
return;
}
const data = collectSensorData(settings);
const success = dataset.appendData(data, CSV_HEADERS, state.logStreamer);
if (success) {
state.logsCount++;
}
} catch (error) {
console.error('[log_dataset_error] Errore durante la raccolta dei dati:', error);
}
}, CONFIG.log_interval);
return true;
} catch (error) {
console.error('[log_dataset_error] Errore nell\'avvio della registrazione', error);
state.isRecordingLogs = false;
return false;
}
}
/**
* Restarts the recording process
* @param {object} settings - Plugin settings
* @returns {boolean} Success status
*/
function restartRecording(settings = {}) {
stopRecording();
startRecording(settings);
return true;
}
/**
* Gets current recording status with detailed metrics
* @returns {object} Status object
*/
function getRecordingStatus() {
return {
isRecording: state.isRecordingLogs,
recordCount: state.logsCount,
recordingInterval: CONFIG.log_interval,
uptime: state.startTime ? Date.now() - state.startTime : 0,
timestamp: new Date().toISOString()
};
}
module.exports = function (app) {
state.app = app;
const plugin = {
id: "meb",
name: "MEB Plugin",
start: async (settings) => {
try {
// ==================== WEB SOCKET AISSTREAM ====================
try {
aisStream();
} catch (error) {
console.error('[ERROR] Errore in AISStream:', error);
}
// ==================== WEATHER UPDATES (OpenMeteo condiviso ogni 2 min) ====================
let location = {
latitude: app.getSelfPath('navigation.position')?.value?.latitude,
longitude: app.getSelfPath('navigation.position')?.value?.longitude,
};
const updateWeatherData = async () => {
const currentPos = app.getSelfPath('navigation.position')?.value;
if (currentPos?.latitude && currentPos?.longitude) {
location = { latitude: currentPos.latitude, longitude: currentPos.longitude };
} else if (!location.latitude || !location.longitude) {
location = {
latitude: Number(settings?.latitude),
longitude: Number(settings?.longitude),
};
}
if (!location.latitude || !location.longitude) {
console.warn("[OpenMeteo] Posizione non disponibile");
return;
}
try {
const [forecastData, wavesData] = await Promise.all([
getForecast(location),
getSeaConditions(location)
]);
// Log per debug
if (forecastData) {
console.log("[OpenMeteo] Forecast ricevuto:", {
temp: forecastData.temperature,
wind: forecastData.windSpeed,
humidity: forecastData.humidity
});
}
if (wavesData) {
console.log("[OpenMeteo] Marine ricevuto:", {
waveHeight: wavesData.waveHeight,
wavePeriod: wavesData.wavePeriod
});
}
// Aggiorna dati condivisi per grafici
graphsCore.updateSharedWeatherData(forecastData, wavesData);
// Pubblica su SignalK solo se abbiamo dati validi
const weatherPayload = {
temperature: forecastData?.temperature ?? null,
humidity: forecastData?.humidity ?? null,
pressure: forecastData?.pressure ?? null,
wind: {
speed: forecastData?.windSpeed ?? null,
direction: forecastData?.windDirection ?? null,
gusts: forecastData?.windGusts ?? null
},
waves: {
height: wavesData?.waveHeight ?? null,
period: wavesData?.wavePeriod ?? null,
direction: wavesData?.waveDirection ?? null
},
rain: forecastData?.rain ?? null,
precipitation: forecastData?.precipitation ?? null
};
publish(app, weatherPayload, settings);
console.log("[OpenMeteo] Dati pubblicati su SignalK");
} catch (error) {
console.error("[OpenMeteo] Errore aggiornamento:", error.message);
}
};
// Funzione per archiviare dati orari per grafici
const archiveHourlyData = () => {
const sharedData = graphsCore.getSharedWeatherData();
if (sharedData.forecast || sharedData.waves) {
graphsCore.archiveHourlyData({
temperature: sharedData.forecast?.temperature,
humidity: sharedData.forecast?.humidity,
pressure: sharedData.forecast?.pressure,
windSpeed: sharedData.forecast?.windSpeed,
windDirection: sharedData.forecast?.windDirection,
waveHeight: sharedData.waves?.waveHeight,
wavePeriod: sharedData.waves?.wavePeriod,
waveDirection: sharedData.waves?.waveDirection,
// currentSpeed: sharedData.waves?.currentVelocity,
// currentDirection: sharedData.waves?.currentDirection
});
}
};
// Avvia aggiornamento meteo immediato + timer 2 minuti
updateWeatherData();
state.openMeteoTimer = setInterval(updateWeatherData, CONFIG.openmeteo_interval);
// Archivia dati ogni ora per i grafici
state.hourlyArchiveTimer = setInterval(archiveHourlyData, CONFIG.hourly_archive_interval);
// ==================== MAPPA INTERATTIVA ====================
try {
mapHandler(app, settings);
} catch (error) {
console.error('[ERROR] Errore nell\'avvio della mappa:', error);
}
// ==================== LOG DATI ====================
try {
startRecording(settings);
} catch (error) {
console.error('[ERROR] Errore nell\'avvio dei log:', error);
}
app.datasetControl = {
start: () => startRecording(settings),
stop: stopRecording,
restart: () => restartRecording(settings),
getStatus: getRecordingStatus
};
// Esponi funzioni per modifica intervalli
app.intervalControl = {
updateInterval: (type, newIntervalMs) => {
const result = updateInterval(type, newIntervalMs);
if (!result) return null;
// Riavvia il timer appropriato
if (result.type === 'openmeteo_interval') {
state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer);
updateWeatherData(); // Aggiorna subito
state.openMeteoTimer = setInterval(updateWeatherData, newIntervalMs);
console.log(`[IntervalControl] OpenMeteo interval aggiornato a ${newIntervalMs}ms`);
} else if (result.type === 'log_interval') {
// Riavvia recording con nuovo intervallo
const wasRecording = state.isRecordingLogs;
if (wasRecording) {
state.logTimer = clearIntervalSafe(state.logTimer);
state.logTimer = setInterval(() => {
try {
if (!state.logStreamer || state.logStreamer.destroyed) {
console.error('[log_dataset_error] Stream non disponibile');
return;
}
const data = collectSensorData(settings);
const success = dataset.appendData(data, CSV_HEADERS, state.logStreamer);
if (success) {
state.logsCount++;
}
} catch (error) {
console.error('[log_dataset_error] Errore durante la raccolta dei dati:', error);
}
}, newIntervalMs);
}
console.log(`[IntervalControl] Log interval aggiornato a ${newIntervalMs}ms`);
}
return result;
},
getIntervals: () => ({
log_interval: CONFIG.log_interval,
openmeteo_interval: CONFIG.openmeteo_interval,
hourly_archive_interval: CONFIG.hourly_archive_interval
})
};
// ==================== BOT TELEGRAM (dopo intervalControl) ====================
if (config.telegramBotToken) {
try {
await linkBot(app);
await send("✅ Computer di bordo attivo e pronto.");
console.log('[MEB TELEGRAM] Bot avviato con app.intervalControl disponibile');
} catch (error) {
console.error('[ERROR] Errore nell\'avvio del bot telegram', error);
}
} else {
console.warn('[MEB TELEGRAM] Bot disabilitato: TELEGRAM_BOT_TOKEN non configurato.');
}
// ===== Shutdown Hooks =====
const shutdown = async (reason = 'signal') => {
try {
console.log(`[shutdown] Received ${reason}. Stopping plugin...`);
await plugin.stop();
process.exit(0);
} catch (err) {
console.error('[shutdown] Error during stop:', err);
process.exit(1);
}
};
// Evita di registrare multipli handler
if (!process.__meb_shutdown_hooks_installed) {
process.__meb_shutdown_hooks_installed = true;
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err);
shutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('[unhandledRejection]', reason);
shutdown('unhandledRejection');
});
}
} catch (error) {
console.error('[Errore] Errore durante l\'avvio del plugin:', error);
throw error;
}
},
stop: async () => {
try {
state.openMeteoTimer = clearIntervalSafe(state.openMeteoTimer);
state.hourlyArchiveTimer = clearIntervalSafe(state.hourlyArchiveTimer);
if (typeof state.unsubPos === "function") {
try {
state.unsubPos();
state.unsubPos = null;
} catch (error) {
console.error('[ERROR] Errore durante la cancellazione dell\'iscrizione alla posizione:', error);
}
}
// stopRecording gestisce già criptazione e salvataggio reference
if (app.datasetControl) {
try {
app.datasetControl.stop();
} catch (error) {
console.error('[ERROR] Errore durante l\'arresto del controllo del dataset:', error);
}
}
await closeStream(state.logStreamer);
console.log('[stop] Plugin arrestato correttamente.');
} catch (error) {
console.error('[ERROR] Errore durante l\'arresto del plugin:', error);
}
},
schema: () => ({
type: "object",
required: [],
properties: {},
}),
registerWithRouter: (router) => {
setupRoutes(router, lastCallRef, app);
},
getOpenApi: getOpenApiSpec,
};
return plugin;
};