const fs = require('fs').promises; const fsSync = require('fs'); const pth = require('path'); const os = require('os'); const skFlow = require('../config/skFlow'); const realtime = require('./realtime/core'); const logsDirectory = pth.join(__dirname, '../../data/logs/'); // Intervallo di scrittura fisso: 1 secondo const WRITE_INTERVAL = 1000; // Stato della sessione attiva let session = null; let writeInterval = null; // Paths da registrare (impostati da init) let logPaths = []; /** * Risolutori speciali per path che non sono direttamente accessibili via skFlow.get(). * Per ogni path speciale, definisce una funzione che restituisce il valore. */ const SPECIAL_RESOLVERS = { 'navigation.position.latitude': () => { const pos = skFlow.get('navigation.position'); return pos?.latitude ?? null; }, 'navigation.position.longitude': () => { const pos = skFlow.get('navigation.position'); return pos?.longitude ?? null; }, 'system.uptime': () => Math.floor(process.uptime()) }; /** * Risolve il valore di un path, gestendo i casi speciali. * @param {String} path - il path da risolvere * @returns {*} il valore */ function resolveValue(path) { // Controlla se c'e un risolutore speciale if (SPECIAL_RESOLVERS[path]) { return SPECIAL_RESOLVERS[path](); } // Path standard: leggi dal databrowser Signal K return skFlow.get(path); } /** * Inizializza i path da registrare. * @param {Array} paths - array di path da rules.js LOG_PATHS */ function init(paths) { logPaths = paths; console.log(`[LOGS] Inizializzati ${paths.length} path`); } /** * Assicura che la cartella logs esista */ async function ensureDir() { try { await fs.mkdir(logsDirectory, { recursive: true }); } catch (e) {} } /** * Avvia la registrazione: crea un nuovo file CSV e inizia a scrivere ogni secondo. * @param {String} name - nome del file (opzionale, default: data/ora corrente) */ async function startRecording(name) { // Se c'e gia una sessione attiva, fermala prima if (session) { await stopRecording(); } if (!name) { const now = new Date(); name = now.toISOString().replace(/[:.]/g, '-'); } if (logPaths.length === 0) { console.warn('[LOGS] Nessun path configurato, la registrazione non catturera dati'); } await ensureDir(); // Header CSV: timestamp + tutti i path const header = ['timestamp', ...logPaths].join(',') + '\n'; const filePath = pth.join(logsDirectory, `${name}.csv`); await fs.writeFile(filePath, header); session = { name: name, paths: logPaths, startTime: new Date(), elements: 0, filePath: filePath }; // Scrivi ogni secondo writeInterval = setInterval(() => { writeLog(); }, WRITE_INTERVAL); console.log(`[LOGS] Registrazione avviata: ${name} (${logPaths.length} colonne, intervallo ${WRITE_INTERVAL}ms)`); return session; } /** * Scrive una riga nel CSV e invia i dati al server via WebSocket. */ async function writeLog() { if (!session) return; try { const timestamp = new Date().toISOString(); // Risolvi tutti i valori const fields = {}; const csvValues = []; for (const path of session.paths) { const val = resolveValue(path); fields[path] = val; // Formatta per CSV if (val === null || val === undefined) { csvValues.push(''); } else if (typeof val === 'object') { csvValues.push(JSON.stringify(val).replace(/,/g, ';')); } else { csvValues.push(val); } } // Scrivi riga CSV nel file locale const row = [timestamp, ...csvValues].join(',') + '\n'; await fs.appendFile(session.filePath, row); session.elements++; // Invia al server via WebSocket (se connesso) if (realtime.isConnected()) { realtime.send([Date.now(), 'logs', fields]); } } catch (error) { console.error('[LOGS] Errore scrittura:', error.message); } } /** * Interrompe la registrazione e chiude il file. */ async function stopRecording() { if (writeInterval) { clearInterval(writeInterval); writeInterval = null; } if (session) { console.log(`[LOGS] Registrazione fermata: ${session.name} (${session.elements} righe)`); session = null; } } /** * Ottieni i dati del file come stringa CSV. * @param {String} name - il nome del file (senza estensione) * @returns {String|null} */ async function getLog(name) { try { const filePath = pth.join(logsDirectory, `${name}.csv`); const content = await fs.readFile(filePath, 'utf-8'); return content; } catch (error) { console.error('[LOGS] Errore lettura log:', error.message); return null; } } /** * Ottieni il percorso del file CSV. * @param {String} name - il nome del file (senza estensione) * @returns {String|null} */ function getLogFile(name) { const filePath = pth.join(logsDirectory, `${name}.csv`); if (fsSync.existsSync(filePath)) { return filePath; } return null; } /** * Ottieni la lista di tutti i file di log disponibili. * @returns {Array} */ async function listLogs() { await ensureDir(); try { const files = await fs.readdir(logsDirectory); const csvFiles = files.filter(f => f.endsWith('.csv')); const result = []; for (const file of csvFiles) { const filePath = pth.join(logsDirectory, file); const stat = await fs.stat(filePath); result.push({ name: file.replace('.csv', ''), filename: file, size: (stat.size / (1024 * 1024)).toFixed(2), created: stat.birthtime, modified: stat.mtime }); } return result.sort((a, b) => b.modified - a.modified); } catch (error) { console.error('[LOGS] Errore lista log:', error.message); return []; } } /** * Ottieni informazioni sulla sessione di registrazione attiva. * @returns {Object|null} */ function getSession() { if (!session) return null; return { name: session.name, paths: session.paths, startTime: session.startTime, elements: session.elements, delay: WRITE_INTERVAL }; } module.exports = { init, startRecording, stopRecording, getLog, getLogFile, getSession, listLogs };