You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

221 lines
6.2 KiB
JavaScript

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' }
};
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 histories = await Promise.all(
Object.keys(HISTORY_SENSOR_MAP).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 });
});
app.post('/api/config/logging', async (req, res) => {
try {
const config = await writeRuntimeConfig(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.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
};