const express = require('express'); const cors = require('cors'); const fs = require('node:fs/promises'); const path = require('node:path'); const { detectMode, executeEzoCommand } = require('./ezo-command-service'); const { detectAcquisitionMode, readRuntimeConfig, writeRuntimeConfig } = require('./acquisition-service'); const app = express(); const PORT = Number(process.env.PORT || 3000); const HOST = process.env.HOST || '127.0.0.1'; const LOGS_DIRECTORY = process.env.LOGS_DIRECTORY ? path.resolve(process.env.LOGS_DIRECTORY) : path.join(__dirname, '..', 'logs'); const HISTORY_FILES = { temperature: 'timestamp,value\n', ph: 'timestamp,value\n', do: 'timestamp,value\n', ec: 'timestamp,value\n' }; const HISTORY_SENSOR_MAP = { rtd: { file: 'temperature.csv', name: 'RTD' }, ph: { file: 'ph.csv', name: 'PH' }, do: { file: 'do.csv', name: 'DO' }, ec: { file: 'ec.csv', name: 'EC' } }; const HISTORY_SENSOR_IDS = { temperature: 'rtd', ph: 'ph', do: 'do', ec: 'ec' }; app.use(cors()); app.use(express.json()); app.use('/frontend', express.static(path.join(__dirname, '..', 'frontend'))); app.use('/data', express.static(path.join(__dirname, '..', 'data'), { etag: false, maxAge: 0 })); app.use('/logs', express.static(LOGS_DIRECTORY, { etag: false, maxAge: 0 })); app.use('/config', express.static(path.join(__dirname, '..', 'config'), { etag: false, maxAge: 0 })); app.get('/vendor/chart.js', (req, res) => { res.sendFile(path.join( __dirname, '..', 'node_modules', 'chart.js', 'dist', 'chart.umd.js' )); }); app.get('/vendor/xlsx.js', (req, res) => { res.sendFile(path.join( __dirname, '..', 'node_modules', 'xlsx', 'dist', 'xlsx.full.min.js' )); }); app.get('/', (req, res) => { res.redirect('/frontend/index.html'); }); app.get('/api/system/ezo', (req, res) => { const commandMode = detectMode(); const acquisitionMode = detectAcquisitionMode(); res.json({ mode: commandMode.mode, message: commandMode.message, acquisitionMode: acquisitionMode.mode, acquisitionMessage: acquisitionMode.message }); }); function parseHistoryCsv(csvText, sensorName) { return csvText .split(/\r?\n/) .slice(1) .map((line) => line.trim()) .filter(Boolean) .map((line) => { const separatorIndex = line.indexOf(','); if (separatorIndex < 0) return null; const timestamp = line.slice(0, separatorIndex).trim(); const value = Number(line.slice(separatorIndex + 1).trim()); if (!timestamp || !Number.isFinite(value)) return null; return { Timestamp: timestamp, Sensor: sensorName, Valor: value }; }) .filter(Boolean); } async function readSensorHistory(sensorType) { const sensor = HISTORY_SENSOR_MAP[sensorType]; const csvText = await fs.readFile( path.join(LOGS_DIRECTORY, sensor.file), 'utf8' ); return parseHistoryCsv(csvText, sensor.name); } app.get('/api/sensors/:type', async (req, res) => { const sensorType = req.params.type.toLowerCase(); if (sensorType !== 'all' && !HISTORY_SENSOR_MAP[sensorType]) { return res.status(400).json({ error: 'Sensor no válido.' }); } try { if (sensorType === 'all') { const runtimeConfig = await readRuntimeConfig(); const enabledHistorySensors = runtimeConfig.enabledSensors .map((sensorId) => HISTORY_SENSOR_IDS[sensorId]) .filter(Boolean); const histories = await Promise.all( enabledHistorySensors.map(readSensorHistory) ); return res.json( histories.flat().sort((left, right) => String(left.Timestamp).localeCompare(String(right.Timestamp)) ) ); } return res.json(await readSensorHistory(sensorType)); } catch (error) { if (error.code === 'ENOENT') return res.json([]); console.error('[HISTORY] No se pudo leer el historial:', error); return res.status(500).json({ error: 'No se pudo leer el historial.' }); } }); app.post('/api/sensors/:type/command', async (req, res) => { try { const result = await executeEzoCommand( req.params.type.toLowerCase(), req.body.command, { dangerousConfirmed: req.body.dangerousConfirmed === true } ); res.json(result); } catch (error) { res.status(error.status || 500).json({ success: false, error: error.message }); } }); app.get('/api/config/logging', async (req, res) => { const config = await readRuntimeConfig(); res.json({ success: true, rate: config.loggingRateSeconds, enabledSensors: config.enabledSensors }); }); app.post('/api/config/logging', async (req, res) => { try { const config = await writeRuntimeConfig({ rate: req.body.rate }); console.log( `[CONFIG] Frecuencia de adquisición: ${config.loggingRateSeconds}s` ); res.json({ success: true, rate: config.loggingRateSeconds, message: 'La frecuencia será aplicada por el recolector en el siguiente ciclo.' }); } catch (error) { res.status(error.status || 500).json({ success: false, error: error.message }); } }); app.get('/api/config/runtime', async (req, res) => { const config = await readRuntimeConfig(); res.json({ success: true, rate: config.loggingRateSeconds, enabledSensors: config.enabledSensors, availableSensors: Object.keys(HISTORY_FILES) }); }); app.post('/api/config/runtime', async (req, res) => { try { const config = await writeRuntimeConfig({ rate: req.body.rate, enabledSensors: req.body.enabledSensors }); console.log( `[CONFIG] Sensores activos: ${config.enabledSensors.join(', ')}` ); res.json({ success: true, rate: config.loggingRateSeconds, enabledSensors: config.enabledSensors, message: 'La configuracion sera aplicada por el recolector en el siguiente ciclo.' }); } catch (error) { res.status(error.status || 500).json({ success: false, error: error.message }); } }); app.post('/api/history/clear', async (req, res) => { try { await fs.mkdir(LOGS_DIRECTORY, { recursive: true }); await Promise.all( Object.entries(HISTORY_FILES).map(([name, header]) => fs.writeFile( path.join(LOGS_DIRECTORY, `${name}.csv`), header, 'utf8' ) ) ); console.log('[HISTORY] Archivos CSV truncados.'); res.json({ success: true, message: 'Archivos de registro truncados correctamente.' }); } catch (error) { console.error('[HISTORY] No se pudieron truncar los historiales:', error); res.status(500).json({ success: false, error: 'No se pudieron borrar los archivos históricos.' }); } }); if (require.main === module) { app.listen(PORT, HOST, () => { console.log(`[API] Backend Node.js activo en http://${HOST}:${PORT}`); }); } module.exports = { app, HISTORY_FILES, parseHistoryCsv };