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.
170 lines
5.0 KiB
JavaScript
170 lines
5.0 KiB
JavaScript
// api/server.js
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const fs = require('fs/promises');
|
|
const path = require('path');
|
|
const {
|
|
detectMode,
|
|
executeEzoCommand
|
|
} = require('./ezo-command-service');
|
|
const app = express();
|
|
const PORT = Number(process.env.PORT || 3000);
|
|
const HISTORY_FILES = {
|
|
temperature: 'timestamp,temperature\n',
|
|
ph: 'timestamp,ph\n',
|
|
do: 'timestamp,do\n',
|
|
ec: 'timestamp,ec\n'
|
|
};
|
|
const LOGS_DIRECTORY = process.env.LOGS_DIRECTORY
|
|
? path.resolve(process.env.LOGS_DIRECTORY)
|
|
: path.join(__dirname, '..', 'logs');
|
|
|
|
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('/', (req, res) => {
|
|
res.redirect('/frontend/index.html');
|
|
});
|
|
|
|
app.get('/api/system/ezo', (req, res) => {
|
|
const mode = detectMode();
|
|
res.json({
|
|
mode: mode.mode,
|
|
message: mode.message
|
|
});
|
|
});
|
|
|
|
// Función auxiliar para simular latencia
|
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
// Endpoint principal para obtener datos de los sensores (MOCK)
|
|
app.get('/api/sensors/:type', async (req, res) => {
|
|
const sensorType = req.params.type;
|
|
console.log(`[GET] Petición recibida para sensor: ${sensorType}`);
|
|
|
|
// Simulación de latencia I2C y procesamiento (400ms)
|
|
await delay(400);
|
|
|
|
const data = [];
|
|
const now = new Date();
|
|
|
|
// Generamos 60 registros simulados
|
|
for (let i = 60; i >= 0; i--) {
|
|
const timestamp = new Date(now.getTime() - i * 1000).toISOString();
|
|
|
|
// Simulación de valores con ruido térmico/químico
|
|
const mockValues = {
|
|
rtd: (25.0 + (Math.random() * 0.1 - 0.05)).toFixed(2),
|
|
ph: (7.2 + (Math.random() * 0.04 - 0.02)).toFixed(2),
|
|
do: (8.5 + (Math.random() * 0.1 - 0.05)).toFixed(2),
|
|
ec: (1050 + (Math.random() * 10 - 5)).toFixed(0)
|
|
};
|
|
|
|
if (sensorType === 'all') {
|
|
data.push({
|
|
Timestamp: timestamp,
|
|
Temperatura_C: mockValues.rtd,
|
|
pH: mockValues.ph,
|
|
DO_mgL: mockValues.do,
|
|
EC_uS: mockValues.ec
|
|
});
|
|
} else if (mockValues[sensorType]) {
|
|
data.push({
|
|
Timestamp: timestamp,
|
|
Sensor: sensorType.toUpperCase(),
|
|
Valor: mockValues[sensorType]
|
|
});
|
|
} else {
|
|
return res.status(400).json({ error: "Sensor no válido" });
|
|
}
|
|
}
|
|
|
|
res.json(data);
|
|
});
|
|
|
|
// Consola EZO: usa I2C real en Raspberry Pi y modo demo en otros equipos.
|
|
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
|
|
});
|
|
}
|
|
});
|
|
|
|
// --- RUTAS DE GESTIÓN DE ALMACENAMIENTO ---
|
|
|
|
// Variable global para simular la configuración de escritura del demonio C++
|
|
let loggingRateSeconds = 1;
|
|
|
|
app.post('/api/config/logging', (req, res) => {
|
|
const requestedRate = Number(req.body.rate);
|
|
const allowedRates = new Set([1, 5, 10, 60]);
|
|
|
|
if (!allowedRates.has(requestedRate)) {
|
|
return res.status(400).json({ success: false, error: 'Frecuencia no válida' });
|
|
}
|
|
|
|
loggingRateSeconds = requestedRate;
|
|
console.log(`[SIMULACIÓN] Frecuencia configurada a: 1 registro cada ${loggingRateSeconds}s`);
|
|
res.json({
|
|
success: true,
|
|
simulated: true,
|
|
rate: loggingRateSeconds,
|
|
message: 'Configuración aplicada únicamente al backend simulado.'
|
|
});
|
|
});
|
|
|
|
app.post('/api/history/clear', async (req, res) => {
|
|
try {
|
|
await Promise.all(
|
|
Object.entries(HISTORY_FILES).map(([name, header]) =>
|
|
fs.writeFile(path.join(LOGS_DIRECTORY, `${name}.csv`), header, 'utf8')
|
|
)
|
|
);
|
|
|
|
console.log('[SISTEMA] Archivos CSV históricos truncados.');
|
|
res.json({
|
|
success: true,
|
|
message: 'Archivos de registro truncados correctamente.'
|
|
});
|
|
} catch (error) {
|
|
console.error('[ERROR] 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, () => {
|
|
console.log(`[MOCK SERVER] Backend Node.js corriendo en http://localhost:${PORT}`);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
app,
|
|
HISTORY_FILES
|
|
};
|