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
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
|
|
};
|