Integrar adquisición y despliegue en Raspberry Pi
parent
426b9c247b
commit
bcbee4e479
@ -1,234 +1,101 @@
|
|||||||
# Sistema de Monitoreo de Parámetros Fisicoquímicos para Fotobiorreactor
|
# Photobioreactor Dashboard
|
||||||
|
|
||||||
**Plataforma de hardware:** Raspberry Pi 4 Model B
|
Sistema de monitoreo para Raspberry Pi y circuitos Atlas Scientific EZO:
|
||||||
**Estado del proyecto:** Desarrollo activo — Integración de hardware en curso (Fase 9)
|
|
||||||
**Pila tecnológica:** C, Node.js, Express, Vanilla JavaScript, Nginx
|
|
||||||
**Protocolo de comunicación físico:** I²C (Inter-Integrated Circuit), 100 kHz — 400 kHz
|
|
||||||
|
|
||||||
---
|
| Variable | Circuito | Dirección | Unidad |
|
||||||
|
|
||||||
## 1. Resumen Ejecutivo
|
|
||||||
|
|
||||||
Este repositorio documenta la arquitectura de software y las especificaciones de diseño de hardware de un sistema de adquisición de datos (DAQ) en tiempo real orientado a fotobiorreactores de escala de laboratorio. El proyecto se inició como un sistema de medición de temperatura de precisión basado en el módulo **EZO-RTD™** de Atlas Scientific y una sonda de platino PT1000; sin embargo, ha evolucionado en una plataforma integral de monitoreo que gestiona de forma simultánea cuatro parámetros fisicoquímicos críticos para el control de cultivos fotosintéticos.
|
|
||||||
|
|
||||||
Los cuatro parámetros monitoreados y los módulos OEM asociados son los siguientes:
|
|
||||||
|
|
||||||
| Parámetro | Módulo EZO | Dirección I²C | Unidad de medida |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Temperatura | EZO-RTD™ (ISCCB-2) | `0x66` | °C |
|
| Temperatura | EZO-RTD | `0x66` | °C |
|
||||||
| Potencial de Hidrógeno | EZO-pH™ | `0x63` | pH |
|
| pH | EZO-pH | `0x63` | pH |
|
||||||
| Oxígeno Disuelto | EZO-DO™ | `0x61` | mg/L |
|
| Oxígeno disuelto | EZO-DO | `0x61` | mg/L |
|
||||||
| Conductividad Eléctrica | EZO-EC™ | `0x64` | µS/cm |
|
| Conductividad | EZO-EC | `0x64` | µS/cm |
|
||||||
|
|
||||||
La arquitectura del sistema implementa una topología de red distribuida localmente, separando de forma explícita las responsabilidades en cuatro capas: presentación (frontend), enrutamiento (Nginx), lógica de negocio y API (Node.js/Express) y adquisición de datos en hardware (demonios en C). Este diseño por capas garantiza la extensibilidad del sistema y permite la sustitución o incorporación de nuevos módulos sensores sin alterar la lógica de presentación.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Pila Tecnológica
|
|
||||||
|
|
||||||
### 2.1. Capa de Presentación (Frontend)
|
|
||||||
|
|
||||||
- **Lenguaje:** Vanilla JavaScript ES6+, HTML5, CSS3 puro
|
|
||||||
- **Biblioteca de visualización:** Chart.js (entregada vía CDN)
|
|
||||||
- **Biblioteca de exportación tabular:** SheetJS (`xlsx.full.min.js`, vía CDN)
|
|
||||||
- **Ciclo de actualización de lecturas en vivo:** 1000 ms (intervalo de *polling*)
|
|
||||||
- **Ciclo de actualización de tendencias históricas:** 10 000 ms
|
|
||||||
- **Idioma de la interfaz:** Español neutro
|
|
||||||
|
|
||||||
### 2.2. Capa de Enrutamiento y Proxy
|
El sistema incluye dashboard responsive, históricos Chart.js, alarmas,
|
||||||
|
exportación CSV/Excel, consola de comandos y panel de calibración.
|
||||||
|
|
||||||
- **Servidor:** Nginx
|
## Arquitectura
|
||||||
- **Puerto de entrada:** `8888`
|
|
||||||
- **Función:** Entrega de archivos estáticos del frontend y proxy inverso transparente hacia el puerto `3000` para el prefijo `/api/`
|
|
||||||
|
|
||||||
### 2.3. Capa de Lógica de Negocio y API
|
|
||||||
|
|
||||||
- **Entorno de ejecución:** Node.js (≥ v18)
|
|
||||||
- **Framework HTTP:** Express v5
|
|
||||||
- **Puerto de escucha:** `3000`
|
|
||||||
- **Dependencias de producción:** `express ^5.2.1`, `cors ^2.8.6`
|
|
||||||
|
|
||||||
### 2.4. Capa de Adquisición de Datos (Hardware)
|
|
||||||
|
|
||||||
- **Lenguaje:** C (estándar C99)
|
|
||||||
- **Interfaz de hardware:** Bus I²C del sistema operativo Linux mediante `/dev/i2c-1`
|
|
||||||
- **Encabezados del sistema utilizados:** `<linux/i2c-dev.h>`, `<sys/ioctl.h>`
|
|
||||||
- **Sistema de construcción:** GNU Make
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Estructura del Directorio Fuente
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/
|
EZO por I2C
|
||||||
├── api/
|
|
|
||||||
│ └── server.js # Servidor Express: endpoints REST y parser léxico EZO
|
EZO_ACQUIRE / EZO_COMMAND
|
||||||
│
|
|
|
||||||
├── config/
|
Recolector Node ---------> data/*.json
|
||||||
│ ├── alarms.json # Umbrales operativos configurables por variable
|
| logs/*.csv
|
||||||
│ └── sensors.json # Metadatos declarativos de los módulos EZO (dirección I²C, habilitación)
|
|
|
||||||
│
|
API Express <------------ Dashboard
|
||||||
├── data/
|
^
|
||||||
│ ├── EZORTD.json # Vector de estado actual: temperatura (escrito por el demonio C)
|
|
|
||||||
│ ├── EZOPH.json # Vector de estado actual: pH
|
Nginx
|
||||||
│ ├── EZODO.json # Vector de estado actual: oxígeno disuelto
|
|
||||||
│ └── EZOEC.json # Vector de estado actual: conductividad eléctrica
|
|
||||||
│
|
|
||||||
├── frontend/
|
|
||||||
│ ├── index.html # Punto de entrada del dashboard de monitoreo
|
|
||||||
│ ├── dashboard.css # Hoja de estilos responsiva del sistema
|
|
||||||
│ ├── dashboard.js # Lógica principal: polling, evaluación de alarmas, gráficas
|
|
||||||
│ ├── ezo-service.js # Consola EZO y panel de calibración por sensor
|
|
||||||
│ ├── export-service.js # Serialización de datos históricos a formato CSV
|
|
||||||
│ └── export-excel.js # Generación de reportes multipagina en formato XLSX
|
|
||||||
│
|
|
||||||
├── logs/
|
|
||||||
│ ├── temperature.csv # Historial persistente de temperatura (escrito por el demonio)
|
|
||||||
│ ├── ph.csv # Historial persistente de pH
|
|
||||||
│ ├── do.csv # Historial persistente de oxígeno disuelto
|
|
||||||
│ └── ec.csv # Historial persistente de conductividad eléctrica
|
|
||||||
│
|
|
||||||
├── sensors/
|
|
||||||
│ ├── EZORTD/
|
|
||||||
│ │ ├── ezortd.h # API pública: constante de dirección I²C y firma de getTemperature()
|
|
||||||
│ │ ├── ezortd.c # Implementación del protocolo I²C para el EZO-RTD
|
|
||||||
│ │ ├── main.c # Ejecutable de lectura única (one-shot), salida JSON a stdout
|
|
||||||
│ │ ├── ezortd_daemon.c # Demonio de lectura continua: escribe JSON y appends CSV
|
|
||||||
│ │ └── Makefile # Sistema de construcción del módulo RTD
|
|
||||||
│ │
|
|
||||||
│ ├── EZOPH/
|
|
||||||
│ │ ├── ezoph.h # API pública del módulo pH
|
|
||||||
│ │ ├── ezoph.c # Implementación del protocolo I²C para el EZO-pH
|
|
||||||
│ │ ├── main.c # Ejecutable de lectura única
|
|
||||||
│ │ └── Makefile
|
|
||||||
│ │
|
|
||||||
│ ├── EZODO/
|
|
||||||
│ │ ├── ezodo.h # API pública del módulo DO
|
|
||||||
│ │ ├── ezodo.c # Implementación del protocolo I²C para el EZO-DO
|
|
||||||
│ │ ├── main.c # Ejecutable de lectura única
|
|
||||||
│ │ └── Makefile
|
|
||||||
│ │
|
|
||||||
│ └── EZOEC/
|
|
||||||
│ ├── ezoec.h # API pública del módulo EC
|
|
||||||
│ ├── ezoec.c # Implementación del protocolo I²C para el EZO-EC
|
|
||||||
│ ├── main.c # Ejecutable de lectura única
|
|
||||||
│ └── Makefile
|
|
||||||
│
|
|
||||||
├── package.json # Manifiesto de dependencias Node.js
|
|
||||||
├── package-lock.json # Árbol de dependencias resuelto y bloqueado
|
|
||||||
├── README.md # Este documento
|
|
||||||
├── ARCHITECTURE.md # Especificación técnica de la arquitectura del sistema
|
|
||||||
└── PROJECT_STATUS.md # Estado de hitos, riesgos y fases pendientes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
`EZO_ACQUIRE` obtiene las cuatro lecturas en un ciclo agrupado.
|
||||||
|
`EZO_COMMAND` ejecuta comandos interactivos y calibraciones. Ambos comparten el
|
||||||
## 4. Instrucciones de Despliegue
|
bloqueo `/tmp/photobioreactor-i2c.lock`.
|
||||||
|
|
||||||
### 4.1. Requisitos Previos
|
|
||||||
|
|
||||||
- Node.js v18 o superior instalado en el sistema anfitrión
|
## Desarrollo sin sensores
|
||||||
- Nginx instalado (`sudo apt install nginx` en sistemas Debian/Ubuntu)
|
|
||||||
- Acceso al directorio raíz del repositorio
|
|
||||||
|
|
||||||
### 4.2. Inicialización del Servidor de Backend (Node.js)
|
|
||||||
|
|
||||||
Desde el directorio raíz del repositorio, instalar las dependencias de producción y levantar el servidor:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm ci
|
||||||
node api/server.js
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
El servidor quedará escuchando en `http://localhost:3000`. Verificar el inicio exitoso con el mensaje:
|
Ejecute en dos terminales:
|
||||||
|
|
||||||
```
|
|
||||||
[MOCK SERVER] Backend Node.js corriendo en http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
Para desarrollo local, abrir directamente:
|
```bash
|
||||||
|
EZO_MODE=demo npm run acquire
|
||||||
```text
|
EZO_MODE=demo npm start
|
||||||
http://localhost:3000/frontend/index.html
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Express sirve las carpetas necesarias para el dashboard. Nginx sigue siendo la
|
Abra `http://localhost:3000/frontend/index.html`.
|
||||||
opcion recomendada para el despliegue final en Raspberry Pi.
|
|
||||||
|
|
||||||
Para ejecución persistente en segundo plano se recomienda el gestor de procesos `pm2`:
|
En PowerShell:
|
||||||
|
|
||||||
```bash
|
```powershell
|
||||||
npm install -g pm2
|
$env:EZO_MODE = "demo"
|
||||||
pm2 start api/server.js --name fotobiorreactor-api
|
npm run acquire
|
||||||
pm2 save
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.3. Configuración del Proxy Inverso Nginx
|
Y en una segunda terminal:
|
||||||
|
|
||||||
Crear o editar el bloque de servidor activo de Nginx. En sistemas Debian-based, el archivo de configuración canónico es `/etc/nginx/sites-available/fotobiorreactor`:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 8888;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# Raíz del contenido estático: directorio raíz del repositorio
|
|
||||||
root /ruta/absoluta/al/repositorio;
|
|
||||||
index frontend/index.html;
|
|
||||||
|
|
||||||
# Entrega de archivos estáticos del frontend
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy inverso transparente hacia el backend Node.js
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Habilitar el sitio y recargar el servicio:
|
|
||||||
|
|
||||||
```bash
|
```powershell
|
||||||
sudo ln -s /etc/nginx/sites-available/fotobiorreactor /etc/nginx/sites-enabled/
|
$env:EZO_MODE = "demo"
|
||||||
sudo nginx -t
|
npm start
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.4. Acceso al Cliente
|
## Raspberry Pi
|
||||||
|
|
||||||
Abrir un navegador web y dirigirse a:
|
El despliegue de producción usa `EZO_MODE=hardware`, systemd y Nginx:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
http://localhost:8888/frontend/index.html
|
chmod +x scripts/install-raspberry-pi.sh
|
||||||
|
sudo ./scripts/install-raspberry-pi.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
El dashboard inicia automáticamente el ciclo de polling de los archivos JSON
|
La guía de preparación, detección I2C, calibración y prueba integral está en
|
||||||
ubicados en `data/`. La API se usa por separado para la consola de comandos
|
[`docs/RASPBERRY_PI_DEPLOYMENT.md`](docs/RASPBERRY_PI_DEPLOYMENT.md).
|
||||||
simulada, las exportaciones simuladas y las operaciones administrativas.
|
|
||||||
|
|
||||||
La consola EZO detecta automaticamente el transporte disponible. En Raspberry
|
## Archivos de datos
|
||||||
Pi puede enviar comandos reales por I2C mediante `sensors/EZOCommand/EZO_COMMAND`;
|
|
||||||
en otros equipos permanece en modo demo. Consulte `docs/EZO_COMMANDS.md`.
|
|
||||||
|
|
||||||
### 4.5. Construcción de los Demonios en C (Hardware Real)
|
- Lecturas actuales: `data/EZORTD.json`, `EZOPH.json`, `EZODO.json`,
|
||||||
|
`EZOEC.json`.
|
||||||
|
- Históricos: `logs/temperature.csv`, `ph.csv`, `do.csv`, `ec.csv`.
|
||||||
|
- Umbrales: `config/alarms.json`.
|
||||||
|
- Frecuencia de adquisición: `config/runtime.json`.
|
||||||
|
|
||||||
Para compilar los controladores de hardware en la Raspberry Pi, ejecutar el siguiente procedimiento por módulo sensor (se ilustra con el módulo RTD):
|
Los CSV usan una única nomenclatura y el formato:
|
||||||
|
|
||||||
```bash
|
```csv
|
||||||
cd sensors/EZORTD
|
timestamp,value
|
||||||
make
|
2026-06-22T12:00:00.000Z,25.123
|
||||||
```
|
```
|
||||||
|
|
||||||
Esto generará el ejecutable `EZORTD`. Antes de compilar el demonio continuo (`ezortd_daemon.c`), actualizar las rutas absolutas codificadas en el código fuente para que correspondan al directorio de despliegue real en la Raspberry Pi.
|
## Documentación
|
||||||
|
|
||||||
**Nota:** El bus I²C debe estar habilitado en la Raspberry Pi mediante `sudo raspi-config` → *Interface Options* → *I2C* → *Enable*. Se recomienda agregar el usuario de ejecución al grupo `i2c` para evitar la ejecución con privilegios de superusuario:
|
- Estado actual: [`PROJECT_STATUS.md`](PROJECT_STATUS.md)
|
||||||
|
- Comandos y calibración: [`docs/EZO_COMMANDS.md`](docs/EZO_COMMANDS.md)
|
||||||
|
- Despliegue: [`docs/RASPBERRY_PI_DEPLOYMENT.md`](docs/RASPBERRY_PI_DEPLOYMENT.md)
|
||||||
|
- Diseño extendido: [`ARCHITECTURE.md`](ARCHITECTURE.md)
|
||||||
|
|
||||||
```bash
|
No se utiliza un driver personalizado del kernel. Linux ya proporciona la capa
|
||||||
sudo usermod -aG i2c $USER
|
I2C mediante `/dev/i2c-1`; el protocolo ASCII, la adquisición y la calibración
|
||||||
```
|
se mantienen en espacio de usuario para facilitar mantenimiento y diagnóstico.
|
||||||
|
|||||||
@ -0,0 +1,222 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const fsPromises = require('node:fs/promises');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { execFile } = require('node:child_process');
|
||||||
|
const { promisify } = require('node:util');
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
const ROOT_DIRECTORY = path.join(__dirname, '..');
|
||||||
|
const DATA_DIRECTORY = process.env.DATA_DIRECTORY
|
||||||
|
? path.resolve(process.env.DATA_DIRECTORY)
|
||||||
|
: path.join(ROOT_DIRECTORY, 'data');
|
||||||
|
const LOGS_DIRECTORY = process.env.LOGS_DIRECTORY
|
||||||
|
? path.resolve(process.env.LOGS_DIRECTORY)
|
||||||
|
: path.join(ROOT_DIRECTORY, 'logs');
|
||||||
|
const RUNTIME_CONFIG_FILE = process.env.RUNTIME_CONFIG_FILE
|
||||||
|
? path.resolve(process.env.RUNTIME_CONFIG_FILE)
|
||||||
|
: path.join(ROOT_DIRECTORY, 'config', 'runtime.json');
|
||||||
|
const ACQUISITION_HELPER = process.env.EZO_ACQUIRE_HELPER
|
||||||
|
? path.resolve(process.env.EZO_ACQUIRE_HELPER)
|
||||||
|
: path.join(ROOT_DIRECTORY, 'sensors', 'EZOCommand', 'EZO_ACQUIRE');
|
||||||
|
|
||||||
|
const SENSOR_FILES = {
|
||||||
|
temperature: {
|
||||||
|
json: 'EZORTD.json',
|
||||||
|
csv: 'temperature.csv',
|
||||||
|
key: 'temperature'
|
||||||
|
},
|
||||||
|
ph: { json: 'EZOPH.json', csv: 'ph.csv', key: 'ph' },
|
||||||
|
do: { json: 'EZODO.json', csv: 'do.csv', key: 'do' },
|
||||||
|
ec: { json: 'EZOEC.json', csv: 'ec.csv', key: 'ec' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOWED_LOGGING_RATES = new Set([1, 5, 10, 60]);
|
||||||
|
|
||||||
|
async function atomicWriteFile(filePath, content) {
|
||||||
|
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
||||||
|
await fsPromises.writeFile(temporaryPath, content, 'utf8');
|
||||||
|
await fsPromises.rename(temporaryPath, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRuntimeConfig() {
|
||||||
|
try {
|
||||||
|
const rawConfig = await fsPromises.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
||||||
|
const config = JSON.parse(rawConfig);
|
||||||
|
const rate = Number(config.loggingRateSeconds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loggingRateSeconds: ALLOWED_LOGGING_RATES.has(rate) ? rate : 1
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { loggingRateSeconds: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRuntimeConfig(rate) {
|
||||||
|
const numericRate = Number(rate);
|
||||||
|
|
||||||
|
if (!ALLOWED_LOGGING_RATES.has(numericRate)) {
|
||||||
|
const error = new Error('Frecuencia no válida.');
|
||||||
|
error.status = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
loggingRateSeconds: numericRate,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
await atomicWriteFile(
|
||||||
|
RUNTIME_CONFIG_FILE,
|
||||||
|
`${JSON.stringify(config, null, 2)}\n`
|
||||||
|
);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectAcquisitionMode() {
|
||||||
|
const requestedMode = String(process.env.EZO_MODE || 'auto').toLowerCase();
|
||||||
|
const hardwareReady = process.platform === 'linux' &&
|
||||||
|
fs.existsSync('/dev/i2c-1') &&
|
||||||
|
fs.existsSync(ACQUISITION_HELPER);
|
||||||
|
|
||||||
|
if (requestedMode === 'hardware' && !hardwareReady) {
|
||||||
|
return {
|
||||||
|
mode: 'unavailable',
|
||||||
|
message: 'Se solicitó hardware, pero /dev/i2c-1 o EZO_ACQUIRE no está disponible.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedMode === 'hardware' || (requestedMode === 'auto' && hardwareReady)) {
|
||||||
|
return { mode: 'hardware', message: 'Adquisición I2C real activa.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mode: 'demo', message: 'Adquisición de demostración activa.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDemoReadings() {
|
||||||
|
return {
|
||||||
|
temperature: Number((25 + Math.random() * 0.1 - 0.05).toFixed(3)),
|
||||||
|
ph: Number((7.2 + Math.random() * 0.04 - 0.02).toFixed(3)),
|
||||||
|
do: Number((8.5 + Math.random() * 0.1 - 0.05).toFixed(3)),
|
||||||
|
ec: Number((1050 + Math.random() * 10 - 5).toFixed(1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readHardwareSensors() {
|
||||||
|
const { stdout } = await execFileAsync(
|
||||||
|
ACQUISITION_HELPER,
|
||||||
|
['/dev/i2c-1'],
|
||||||
|
{ timeout: 5000, windowsHide: true }
|
||||||
|
);
|
||||||
|
const readings = JSON.parse(stdout.trim());
|
||||||
|
|
||||||
|
if (!readings || typeof readings !== 'object') {
|
||||||
|
throw new Error('EZO_ACQUIRE devolvió una respuesta inválida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return readings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendReading(filePath, timestamp, value) {
|
||||||
|
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
let needsHeader = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await fsPromises.stat(filePath);
|
||||||
|
needsHeader = stats.size === 0;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ENOENT') throw error;
|
||||||
|
needsHeader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = `${timestamp},${value}\n`;
|
||||||
|
await fsPromises.appendFile(
|
||||||
|
filePath,
|
||||||
|
`${needsHeader ? 'timestamp,value\n' : ''}${row}`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishReading(sensorId, rawValue, timestamp, mode) {
|
||||||
|
const sensor = SENSOR_FILES[sensorId];
|
||||||
|
const value = Number(rawValue);
|
||||||
|
const jsonPath = path.join(DATA_DIRECTORY, sensor.json);
|
||||||
|
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
await atomicWriteFile(jsonPath, `${JSON.stringify({
|
||||||
|
online: false,
|
||||||
|
timestamp,
|
||||||
|
error: 'Lectura no disponible'
|
||||||
|
})}\n`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
atomicWriteFile(jsonPath, `${JSON.stringify({
|
||||||
|
[sensor.key]: value,
|
||||||
|
online: true,
|
||||||
|
timestamp,
|
||||||
|
mode
|
||||||
|
})}\n`),
|
||||||
|
appendReading(path.join(LOGS_DIRECTORY, sensor.csv), timestamp, value)
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectReadings() {
|
||||||
|
const modeInfo = detectAcquisitionMode();
|
||||||
|
|
||||||
|
if (modeInfo.mode === 'unavailable') {
|
||||||
|
throw new Error(modeInfo.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readings = modeInfo.mode === 'hardware'
|
||||||
|
? await readHardwareSensors()
|
||||||
|
: buildDemoReadings();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const results = await Promise.all(
|
||||||
|
Object.keys(SENSOR_FILES).map(async (sensorId) => ({
|
||||||
|
sensorId,
|
||||||
|
online: await publishReading(
|
||||||
|
sensorId,
|
||||||
|
readings[sensorId],
|
||||||
|
timestamp,
|
||||||
|
modeInfo.mode
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { mode: modeInfo.mode, timestamp, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishAllOffline(error) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(SENSOR_FILES).map((sensor) =>
|
||||||
|
atomicWriteFile(
|
||||||
|
path.join(DATA_DIRECTORY, sensor.json),
|
||||||
|
`${JSON.stringify({
|
||||||
|
online: false,
|
||||||
|
timestamp,
|
||||||
|
error: message
|
||||||
|
})}\n`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ACQUISITION_HELPER,
|
||||||
|
ALLOWED_LOGGING_RATES,
|
||||||
|
RUNTIME_CONFIG_FILE,
|
||||||
|
SENSOR_FILES,
|
||||||
|
atomicWriteFile,
|
||||||
|
collectReadings,
|
||||||
|
detectAcquisitionMode,
|
||||||
|
publishAllOffline,
|
||||||
|
readRuntimeConfig,
|
||||||
|
writeRuntimeConfig
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
const {
|
||||||
|
collectReadings,
|
||||||
|
publishAllOffline,
|
||||||
|
readRuntimeConfig
|
||||||
|
} = require('./acquisition-service');
|
||||||
|
|
||||||
|
let stopping = false;
|
||||||
|
|
||||||
|
function wait(milliseconds) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('[ACQUISITION] Recolector iniciado.');
|
||||||
|
|
||||||
|
while (!stopping) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await collectReadings();
|
||||||
|
const onlineCount = result.results.filter((item) => item.online).length;
|
||||||
|
console.log(
|
||||||
|
`[ACQUISITION] ${result.timestamp} ${result.mode}: ${onlineCount}/4 sensores.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ACQUISITION] ${error.message}`);
|
||||||
|
await publishAllOffline(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await readRuntimeConfig();
|
||||||
|
const elapsed = Date.now() - startedAt;
|
||||||
|
await wait(Math.max(100, config.loggingRateSeconds * 1000 - elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
stopping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', stop);
|
||||||
|
process.on('SIGTERM', stop);
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error('[ACQUISITION] Error fatal:', error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"loggingRateSeconds": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /opt/photobioreactor;
|
||||||
|
index frontend/index.html;
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
return 302 /frontend/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /frontend/ {
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /data/ {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /logs/ {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /config/ {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /vendor/chart.js {
|
||||||
|
alias /opt/photobioreactor/node_modules/chart.js/dist/chart.umd.js;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /vendor/xlsx.js {
|
||||||
|
alias /opt/photobioreactor/node_modules/xlsx/dist/xlsx.full.min.js;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Photobioreactor Atlas EZO Acquisition
|
||||||
|
After=dev-i2c\x2d1.device
|
||||||
|
Wants=dev-i2c\x2d1.device
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=photobioreactor
|
||||||
|
Group=photobioreactor
|
||||||
|
SupplementaryGroups=i2c
|
||||||
|
WorkingDirectory=/opt/photobioreactor
|
||||||
|
EnvironmentFile=/etc/default/photobioreactor
|
||||||
|
ExecStart=/usr/bin/node api/acquisition.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Photobioreactor Dashboard API
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=photobioreactor
|
||||||
|
Group=photobioreactor
|
||||||
|
SupplementaryGroups=i2c
|
||||||
|
WorkingDirectory=/opt/photobioreactor
|
||||||
|
EnvironmentFile=/etc/default/photobioreactor
|
||||||
|
ExecStart=/usr/bin/node api/server.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
HOST=127.0.0.1
|
||||||
|
EZO_MODE=hardware
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
# Despliegue y validación en Raspberry Pi
|
||||||
|
|
||||||
|
## Prueba sin hardware
|
||||||
|
|
||||||
|
En una computadora de desarrollo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Para ejecutar el sistema completo en demo, use dos terminales:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EZO_MODE=demo npm run acquire
|
||||||
|
EZO_MODE=demo npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra `http://localhost:3000/frontend/index.html`. El recolector genera JSON y
|
||||||
|
CSV de prueba; la consola y el panel de calibración muestran transporte DEMO.
|
||||||
|
|
||||||
|
## Preparación del bus I2C
|
||||||
|
|
||||||
|
En Raspberry Pi OS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo raspi-config
|
||||||
|
sudo reboot
|
||||||
|
ls -l /dev/i2c-1
|
||||||
|
sudo apt install i2c-tools
|
||||||
|
i2cdetect -y 1
|
||||||
|
```
|
||||||
|
|
||||||
|
El mapa esperado es:
|
||||||
|
|
||||||
|
| Circuito | Dirección |
|
||||||
|
|---|---|
|
||||||
|
| EZO-DO | `0x61` |
|
||||||
|
| EZO-pH | `0x63` |
|
||||||
|
| EZO-EC | `0x64` |
|
||||||
|
| EZO-RTD | `0x66` |
|
||||||
|
|
||||||
|
No continúe con calibraciones si falta una dirección, aparece una dirección
|
||||||
|
inesperada o el barrido del bus es inestable.
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
Desde el repositorio clonado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/install-raspberry-pi.sh
|
||||||
|
sudo ./scripts/install-raspberry-pi.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El instalador copia la aplicación a `/opt/photobioreactor`, instala
|
||||||
|
dependencias, compila `EZO_COMMAND` y `EZO_ACQUIRE`, crea el usuario de servicio,
|
||||||
|
activa los dos servicios systemd y configura Nginx en el puerto 80.
|
||||||
|
|
||||||
|
Validación:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status photobioreactor-api
|
||||||
|
systemctl status photobioreactor-acquisition
|
||||||
|
journalctl -u photobioreactor-acquisition -f
|
||||||
|
curl http://127.0.0.1:3000/api/system/ezo
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta de producción debe informar `hardware` tanto para comandos como
|
||||||
|
para adquisición. `EZO_MODE=hardware` evita que una instalación incompleta
|
||||||
|
caiga silenciosamente a valores simulados.
|
||||||
|
|
||||||
|
La API escucha únicamente en `127.0.0.1` y se publica mediante Nginx. El
|
||||||
|
dashboard todavía no implementa autenticación; despliegue esta versión solo en
|
||||||
|
una red local confiable y no exponga el puerto 80 directamente a Internet.
|
||||||
|
|
||||||
|
## Validación previa a calibración
|
||||||
|
|
||||||
|
Desde la consola web, pruebe individualmente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
i
|
||||||
|
Status
|
||||||
|
R
|
||||||
|
Cal,?
|
||||||
|
```
|
||||||
|
|
||||||
|
Compruebe que cada lectura coincide con el medio físico y que los archivos de
|
||||||
|
`data/` cambian. El helper agrupado envía `R` a los cuatro circuitos, espera una
|
||||||
|
sola ventana de conversión y recoge las respuestas bajo el mismo bloqueo usado
|
||||||
|
por la consola.
|
||||||
|
|
||||||
|
## Calibración
|
||||||
|
|
||||||
|
Realice cada procedimiento con soluciones de referencia vigentes y espere la
|
||||||
|
estabilización de la sonda. El panel consulta `Cal,?` automáticamente después
|
||||||
|
de cada comando de calibración exitoso.
|
||||||
|
|
||||||
|
- RTD: `Cal,<temperatura>`.
|
||||||
|
- pH: `Cal,mid,7.00`, `Cal,low,4.00`, `Cal,high,10.00`.
|
||||||
|
- DO de dos puntos: `Cal,0` y después `Cal`; configure antes las compensaciones.
|
||||||
|
- EC: configure `K`, ejecute `Cal,dry` y después uno o dos puntos húmedos.
|
||||||
|
|
||||||
|
Consulte `docs/EZO_COMMANDS.md` para restricciones y comandos de diagnóstico.
|
||||||
|
|
||||||
|
## Prueba integral
|
||||||
|
|
||||||
|
1. Confirme lecturas, históricos, exportaciones y alarmas.
|
||||||
|
2. Desconecte un sensor: debe aparecer `DESCONECTADO` mientras los demás siguen.
|
||||||
|
3. Detenga el recolector: las lecturas deben pasar a `DESCONECTADO` al superar
|
||||||
|
la tolerancia calculada desde la frecuencia configurada.
|
||||||
|
4. Reinicie la Raspberry y confirme el arranque automático.
|
||||||
|
5. Mantenga el sistema varias horas y revise `journalctl`, tamaño de CSV,
|
||||||
|
estabilidad de valores y recuperación después de comandos de consola.
|
||||||
|
|
||||||
|
Las pruebas físicas y la exactitud metrológica no pueden certificarse fuera de
|
||||||
|
la Raspberry Pi con las sondas y soluciones reales conectadas.
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "Ejecute este script con sudo." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_SOURCE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PROJECT_TARGET="/opt/photobioreactor"
|
||||||
|
SERVICE_USER="photobioreactor"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y nodejs npm nginx i2c-tools build-essential
|
||||||
|
|
||||||
|
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
||||||
|
useradd --system --create-home --groups i2c "${SERVICE_USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${PROJECT_TARGET}"
|
||||||
|
cp -a "${PROJECT_SOURCE}/." "${PROJECT_TARGET}/"
|
||||||
|
rm -rf "${PROJECT_TARGET}/node_modules"
|
||||||
|
|
||||||
|
cd "${PROJECT_TARGET}"
|
||||||
|
npm ci --omit=dev
|
||||||
|
make -C sensors/EZOCommand
|
||||||
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${PROJECT_TARGET}"
|
||||||
|
|
||||||
|
install -m 0644 deployment/photobioreactor.env /etc/default/photobioreactor
|
||||||
|
install -m 0644 deployment/photobioreactor-api.service /etc/systemd/system/
|
||||||
|
install -m 0644 deployment/photobioreactor-acquisition.service /etc/systemd/system/
|
||||||
|
install -m 0644 deployment/nginx-photobioreactor.conf \
|
||||||
|
/etc/nginx/sites-available/photobioreactor
|
||||||
|
ln -sfn /etc/nginx/sites-available/photobioreactor \
|
||||||
|
/etc/nginx/sites-enabled/photobioreactor
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
nginx -t
|
||||||
|
systemctl enable --now photobioreactor-api
|
||||||
|
systemctl enable --now photobioreactor-acquisition
|
||||||
|
systemctl reload nginx
|
||||||
|
|
||||||
|
echo "Instalación terminada. Revise: systemctl status photobioreactor-*"
|
||||||
@ -1,9 +1,14 @@
|
|||||||
CC=gcc
|
CC=gcc
|
||||||
CFLAGS=-Wall -Wextra -O2
|
CFLAGS=-Wall -Wextra -O2
|
||||||
TARGET=EZO_COMMAND
|
TARGETS=EZO_COMMAND EZO_ACQUIRE
|
||||||
|
|
||||||
$(TARGET): main.c
|
all: $(TARGETS)
|
||||||
$(CC) $(CFLAGS) -o $(TARGET) main.c
|
|
||||||
|
EZO_COMMAND: main.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
EZO_ACQUIRE: acquire.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(TARGET)
|
rm -f $(TARGETS)
|
||||||
|
|||||||
@ -0,0 +1,154 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <linux/i2c-dev.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
int address;
|
||||||
|
double value;
|
||||||
|
int valid;
|
||||||
|
} sensor_t;
|
||||||
|
|
||||||
|
static int select_sensor(int fd, int address)
|
||||||
|
{
|
||||||
|
return ioctl(fd, I2C_SLAVE, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int request_reading(int fd, sensor_t *sensor)
|
||||||
|
{
|
||||||
|
const char command = 'R';
|
||||||
|
|
||||||
|
if (select_sensor(fd, sensor->address) < 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (write(fd, &command, 1) != 1)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor->valid = -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int receive_reading(int fd, sensor_t *sensor)
|
||||||
|
{
|
||||||
|
unsigned char response[64] = {0};
|
||||||
|
|
||||||
|
if (select_sensor(fd, sensor->address) < 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytes_read = read(fd, response, sizeof(response) - 1);
|
||||||
|
|
||||||
|
for (int retry = 0;
|
||||||
|
bytes_read > 0 && response[0] == 254 && retry < 10;
|
||||||
|
retry++)
|
||||||
|
{
|
||||||
|
usleep(100000);
|
||||||
|
memset(response, 0, sizeof(response));
|
||||||
|
bytes_read = read(fd, response, sizeof(response) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes_read < 2 || response[0] != 1)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
response[bytes_read] = '\0';
|
||||||
|
char *end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
double value = strtod((char *)&response[1], &end);
|
||||||
|
|
||||||
|
if (errno != 0 || end == (char *)&response[1])
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor->value = value;
|
||||||
|
sensor->valid = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc != 2)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "Uso: EZO_ACQUIRE /dev/i2c-1\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor_t sensors[] = {
|
||||||
|
{"temperature", 0x66, 0.0, 0},
|
||||||
|
{"ph", 0x63, 0.0, 0},
|
||||||
|
{"do", 0x61, 0.0, 0},
|
||||||
|
{"ec", 0x64, 0.0, 0}};
|
||||||
|
const size_t sensor_count = sizeof(sensors) / sizeof(sensors[0]);
|
||||||
|
int lock_fd = open(
|
||||||
|
"/tmp/photobioreactor-i2c.lock",
|
||||||
|
O_CREAT | O_RDWR,
|
||||||
|
0660);
|
||||||
|
|
||||||
|
if (lock_fd < 0 || flock(lock_fd, LOCK_EX) < 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "No se pudo bloquear el bus I2C\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = open(argv[1], O_RDWR);
|
||||||
|
|
||||||
|
if (fd < 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "No se pudo abrir %s\n", argv[1]);
|
||||||
|
flock(lock_fd, LOCK_UN);
|
||||||
|
close(lock_fd);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t index = 0; index < sensor_count; index++)
|
||||||
|
{
|
||||||
|
request_reading(fd, &sensors[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(1000000);
|
||||||
|
|
||||||
|
for (size_t index = 0; index < sensor_count; index++)
|
||||||
|
{
|
||||||
|
if (sensors[index].valid == -1)
|
||||||
|
{
|
||||||
|
receive_reading(fd, &sensors[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("{");
|
||||||
|
for (size_t index = 0; index < sensor_count; index++)
|
||||||
|
{
|
||||||
|
printf(
|
||||||
|
"%s\"%s\":",
|
||||||
|
index == 0 ? "" : ",",
|
||||||
|
sensors[index].name);
|
||||||
|
if (sensors[index].valid == 1)
|
||||||
|
{
|
||||||
|
printf("%.6f", sensors[index].value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("}\n");
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
flock(lock_fd, LOCK_UN);
|
||||||
|
close(lock_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue