Migra dal codice salvato in locale al codice condiviso
This commit is contained in:
596
plugin/index.cjs
Normal file
596
plugin/index.cjs
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user