Enfoque más profecional uso de codex y cambio a gémini

main
EMOTIONS-HUNTER 6 days ago
parent 5f0f97a41c
commit 3cc3929c69

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

@ -129,7 +129,7 @@ Compiled from `ezortd_daemon.c + ezortd.c`. Runs an infinite loop with 1-second
1. Reads temperature via `getTemperature()`
2. Overwrites `data/EZORTD.json` with current value
3. Appends a Unix-timestamp + temperature row to `logs/temp.csv`
3. Appends a Unix-timestamp + temperature row to `logs/temperature.csv`
**Output paths** (hardcoded in source — update before compiling):
@ -137,7 +137,7 @@ Compiled from `ezortd_daemon.c + ezortd.c`. Runs an infinite loop with 1-second
// JSON
"/home/cristian/airquality/basic-ui-dashboard/data/EZORTD.json"
// CSV log
"/home/cristian/airquality/basic-ui-dashboard/logs/temp.csv"
"/home/cristian/airquality/basic-ui-dashboard/logs/temperature.csv"
```
**JSON output format:**
@ -167,7 +167,7 @@ timestamp,temperature ← header row (from EZORTD.csv template)
├──► data/EZORTD.json (overwrite, every 1 s)
│ { "temperature": 24.414 }
└──► logs/temp.csv (append, every 1 s)
└──► logs/temperature.csv (append, every 1 s)
1717027200,24.414
@ -257,7 +257,7 @@ const res = await Promise.all([
| Sensor read delay | 1,000,000 µs (1 s) | `sensors/EZORTD/ezortd.c` |
| Daemon loop interval | `sleep(1)` — 1 s | `sensors/EZORTD/ezortd_daemon.c` |
| JSON output path | `/home/cristian/…/data/EZORTD.json` | `ezortd_daemon.c` (**update per deployment**) |
| CSV log path | `/home/cristian/…/logs/temp.csv` | `ezortd_daemon.c` (**update per deployment**) |
| CSV log path | `/home/cristian/…/logs/temperature.csv` | `ezortd_daemon.c` (**update per deployment**) |
| Frontend poll interval (primary) | 1000 ms | `index.html` |
| Frontend poll interval (extended) | 6000 ms | `index-css.html` |
| Nginx document root | `/home/pi/<repo-path>` | `/etc/nginx/sites-available/default` |
@ -322,7 +322,7 @@ Retained in `sensors/HTU21D/` for reference and backward compatibility with `ind
| File | Writer | Reader | Format | Update Rate |
|---|---|---|---|---|
| `data/EZORTD.json` | `ezortd_daemon` | Nginx → browser | JSON object | 1 s |
| `logs/temp.csv` | `ezortd_daemon` | (manual / future viz) | CSV (epoch, float) | 1 s (append) |
| `logs/temperature.csv` | `ezortd_daemon` | Browser historical chart | CSV (`timestamp,value`) | 1 s (append) |
| `data/HTU21D.json` | `HTU21D` binary (manual / cron) | Nginx → browser | JSON object | On demand |
| `data/EZORTD.csv` | (template only — unused) | — | CSV | — |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,229 @@
# Project Status - Photobioreactor Dashboard v1.0 + Phase 3
## Estado general
Este repositorio contiene un sistema de monitoreo local para un fotobiorreactor basado en sensores Atlas Scientific EZO. La arquitectura actual separa la captura de datos en C, los archivos de intercambio en `data/`, los historiales CSV en `logs/` y un dashboard web estatico en `frontend/`, pensado para desplegarse posteriormente en Raspberry Pi con Nginx.
## Arquitectura actual
El flujo principal del sistema es:
1. Los sensores Atlas Scientific se comunican por I2C con la Raspberry Pi.
2. Los programas en `sensors/` leen cada circuito EZO.
3. Cada lectura actual se escribe como JSON en `data/`.
4. Los historiales se almacenan como CSV en `logs/`.
5. Nginx sirve el repositorio como contenido estatico.
6. `frontend/index.html` carga `dashboard.css`, Chart.js y `dashboard.js`.
7. El navegador consulta los JSON cada segundo y actualiza las lecturas actuales.
8. La seccion `Alarm Summary` evalua umbrales desde `config/alarms.json`.
9. La seccion `Historical Trends` consulta los CSV cada 10 segundos y actualiza las graficas.
No hay backend web ni base de datos en esta version. El filesystem funciona como interfaz entre los daemons de sensores y el frontend.
## Sensores y archivos de datos actuales
| Sensor | Variable | Archivo JSON | Unidad |
|---|---|---|---|
| EZO-RTD | Temperatura | `data/EZORTD.json` | degC |
| EZO-pH | pH | `data/EZOPH.json` | pH |
| EZO-DO | Oxigeno disuelto | `data/EZODO.json` | mg/L |
| EZO-EC | Conductividad | `data/EZOEC.json` | uS/cm |
## Archivos de historial
| Variable | Archivo CSV | Grafica |
|---|---|---|
| Temperatura | `logs/temperature.csv` | Temperature vs Time |
| pH | `logs/ph.csv` | pH vs Time |
| Oxigeno disuelto | `logs/do.csv` | Dissolved Oxygen vs Time |
| Conductividad | `logs/ec.csv` | Conductivity vs Time |
Formato CSV soportado:
```csv
timestamp,value
1717027200,25.488
```
El frontend tambien acepta timestamps ISO 8601 en la primera columna.
## Archivos utilizados
| Ruta | Proposito |
|---|---|
| `frontend/index.html` | Estructura del dashboard, seccion `System Status` y seccion `Historical Trends`. |
| `frontend/dashboard.css` | Estilos responsive para metricas, estado del sistema y graficas. |
| `frontend/dashboard.js` | Lectura periodica de JSON, validacion de datos, lectura CSV y actualizacion de Chart.js. |
| `data/EZORTD.json` | Lectura actual de temperatura. |
| `data/EZOPH.json` | Lectura actual de pH. |
| `data/EZODO.json` | Lectura actual de oxigeno disuelto. |
| `data/EZOEC.json` | Lectura actual de conductividad. |
| `logs/temperature.csv` | Historial de temperatura para graficas. |
| `logs/ph.csv` | Historial de pH para graficas. |
| `logs/do.csv` | Historial de oxigeno disuelto para graficas. |
| `logs/ec.csv` | Historial de conductividad para graficas. |
| `config/sensors.json` | Configuracion declarativa de sensores Atlas Scientific. |
| `config/alarms.json` | Umbrales configurables para estados NORMAL, WARNING y CRITICAL. |
| `sensors/EZORTD/` | Codigo C existente para lectura EZO-RTD. |
| `sensors/EZOPH/` | Codigo C existente para lectura EZO-pH. |
| `sensors/EZODO/` | Codigo C existente para lectura EZO-DO. |
| `sensors/EZOEC/` | Codigo C existente para lectura EZO-EC. |
## Funciones implementadas en v1.0
- Dashboard profesional para monitoreo del fotobiorreactor.
- Lectura de temperatura, pH, oxigeno disuelto y conductividad.
- Actualizacion automatica de lecturas actuales cada 1 segundo.
- Fecha y hora de ultima actualizacion.
- Validacion independiente por sensor.
- Estado `OFFLINE` cuando un JSON no existe, no responde, contiene JSON invalido o no incluye un valor numerico valido.
- Estado global del sistema: `ALL SYSTEMS ONLINE`, `PARTIAL DATA` o `SYSTEM OFFLINE`.
- Seccion inferior `System Status`.
- Conteo de sensores activos y sensores offline.
- Diseno responsive para escritorio, tablet y movil.
## Funciones implementadas en Phase 2 - Historical Trends
- Nueva seccion `Historical Trends` debajo de `System Status`.
- Integracion de Chart.js para graficas de linea.
- Cuatro graficas independientes:
- Temperature vs Time.
- pH vs Time.
- Dissolved Oxygen vs Time.
- Conductivity vs Time.
- Lectura de historiales desde `logs/temperature.csv`, `logs/ph.csv`, `logs/do.csv` y `logs/ec.csv`.
- Actualizacion automatica de graficas cada 10 segundos.
- Estado visual `No historical data available` cuando un CSV no existe, esta vacio o no contiene datos numericos validos.
- Parser CSV reutilizable con soporte principal para `timestamp,value` y compatibilidad adicional con encabezados como `time`, `date`, nombres de variable y `reading`.
- Las instancias Chart.js se conservan en memoria y las actualizaciones cambian datasets existentes con `chart.update("none")`.
- Configuracion Chart.js para dashboard cientifico: `responsive: true`, `maintainAspectRatio: false` y `animation: false`.
- Funciones reutilizables para futuras graficas:
- `readHistoricalData()`
- `parseHistoricalCsv()`
- `buildChartDataset()`
- `getChartOptions()`
- `renderHistoricalChart()`
- `updateHistoricalTrends()`
- Diseno responsive para dos columnas en escritorio y una columna en movil.
## Funciones implementadas en Phase 3 - Alarmas y umbrales configurables
- Archivo `config/alarms.json` normalizado con umbrales por variable:
- Temperatura: 20 a 30 degC.
- pH: 6.8 a 7.5.
- Oxigeno disuelto: 4.0 a 12.0 mg/L.
- Conductividad: 500 a 2500 uS/cm.
- El frontend lee `../config/alarms.json` durante el ciclo de actualizacion.
- Cada sensor se evalua como:
- `NORMAL`: valor dentro del rango y fuera de la banda de advertencia.
- `WARNING`: valor dentro del rango, pero cerca de `min` o `max`.
- `CRITICAL`: valor por debajo de `min` o por encima de `max`.
- `OFFLINE`: JSON faltante, invalido o sin valor numerico.
- La banda `WARNING` usa el 10% del ancho del rango configurado, definido en `WARNING_MARGIN_RATIO`.
- Las tarjetas cambian visualmente por estado:
- Verde para `NORMAL`.
- Amarillo para `WARNING`.
- Rojo para `CRITICAL`.
- Gris para `OFFLINE`.
- Nueva seccion `Alarm Summary` con:
- `Active Critical Alarms`.
- `Active Warnings`.
- `Offline Sensors`.
- `Overall Risk Level`.
- Lista activa de sensores en alarma u offline.
- Estructura de eventos de alarma preparada para integraciones futuras:
- `sensorId`
- `sensorName`
- `severity`
- `value`
- `message`
- `createdAt`
## Arquitectura de alarmas
El flujo de alarmas es:
1. `dashboard.js` lee los JSON de sensores desde `data/`.
2. `dashboard.js` lee los umbrales desde `config/alarms.json`.
3. `evaluateSensorAlarm()` compara cada valor contra `min` y `max`.
4. `renderSystemStatus()` actualiza el estado general.
5. `renderAlarmSummary()` actualiza conteos, riesgo global y lista de alarmas.
La logica queda encapsulada para que una fase posterior pueda enviar los eventos generados por `getAlarmEvents()` a correo, Telegram o MQTT sin acoplar esas salidas al renderizado visual.
## Dependencias nuevas
| Dependencia | Uso | Carga |
|---|---|---|
| Chart.js | Renderizado de graficas historicas de linea | CDN en `frontend/index.html` |
Nota de despliegue: Chart.js por CDN requiere conectividad desde el navegador. Para una Raspberry Pi aislada o una red local sin internet, se recomienda descargar una copia local versionada y servirla desde `frontend/vendor/`.
## Nomenclatura de historiales CSV
Se detecto una inconsistencia entre `logs/temp.csv` y `logs/temperature.csv`:
- `frontend/dashboard.js` consume `logs/temperature.csv`.
- El archivo presente en `logs/` es `temperature.csv`.
- Versiones anteriores de `ARCHITECTURE.md` y `sensors/EZORTD/ezortd_daemon.c` apuntaban a `logs/temp.csv`.
Nomenclatura unica propuesta y aplicada para todo el proyecto:
| Variable | Nombre canonico |
|---|---|
| Temperatura | `logs/temperature.csv` |
| pH | `logs/ph.csv` |
| Oxigeno disuelto | `logs/do.csv` |
| Conductividad | `logs/ec.csv` |
La razon es mantener nombres descriptivos, consistentes con las etiquetas cientificas del dashboard y faciles de extender en exportaciones futuras.
## Proximas fases
1. Historial CSV persistente
- Unificar la escritura de logs en `logs/` o `data/`.
- Definir encabezados estables para cada sensor.
- Registrar timestamp ISO y valor numerico por lectura.
2. Controles de rango para graficas
- Agregar rangos de tiempo: 5 min, 1 h, 24 h.
- Permitir pausa/reanudacion de actualizacion historica.
- Mostrar minimos, maximos y promedios por ventana.
3. Notificaciones de alarmas
- Enviar eventos de `getAlarmEvents()` por correo.
- Enviar alertas por Telegram.
- Publicar alarmas por MQTT.
- Preparar salidas GPIO locales para alarmas criticas.
4. Exportacion CSV
- Descargar historiales por sensor.
- Exportar ventanas de tiempo seleccionadas.
- Mantener compatibilidad con herramientas de analisis cientifico.
5. Exportacion Excel
- Generar archivos `.xlsx` con hojas por sensor.
- Incluir metadatos del experimento.
- Preparar tablas y graficas basicas.
6. Calibracion Atlas Scientific
- Crear interfaz guiada para rutinas de calibracion.
- Registrar fecha, operador y resultado de calibracion.
- Proteger comandos criticos con confirmaciones.
7. Comandos EZO desde la web
- Agregar una API local para enviar comandos a los circuitos EZO.
- Implementar endpoints seguros para lectura, calibracion y diagnostico.
- Separar permisos de monitoreo y administracion.
## Notas de despliegue
Para Raspberry Pi con Nginx, el `root` del sitio debe apuntar al directorio raiz del repositorio. El dashboard se abre desde:
```text
/frontend/index.html
```
Desde esa ubicacion, el frontend lee los datos usando rutas relativas hacia `../data/*.json` y `../logs/*.csv`.
Chart.js se carga desde CDN en esta fase. Para uso sin internet en la Raspberry Pi, la siguiente mejora recomendada es descargar una copia local versionada de Chart.js y servirla desde `frontend/vendor/`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,18 @@
{
"temperature": {
"min": 20,
"max": 30
},
"ph": {
"min": 6.8,
"max": 7.5
},
"do": {
"min": 4.0,
"max": 12.0
},
"ec": {
"min": 500,
"max": 2500
}
}

@ -0,0 +1,25 @@
{
"rtd":
{
"address":"0x66",
"enabled":true
},
"ph":
{
"address":"0x63",
"enabled":true
},
"do":
{
"address":"0x61",
"enabled":true
},
"ec":
{
"address":"0x64",
"enabled":true
}
}

@ -0,0 +1,3 @@
{
"do": 8.45
}

@ -0,0 +1,3 @@
{
"ec": 1234
}

@ -0,0 +1,3 @@
{
"ph": 7.12
}

@ -1 +1 @@
{ "temperature": 25.488 }
{ "temperature": 29.500 }

@ -1 +0,0 @@
{ "temperature": 24.60, "humidity": 52.68 }

@ -0,0 +1,505 @@
:root {
--bg: #f3f6f4;
--panel: #ffffff;
--panel-soft: #eef5f1;
--text: #17211c;
--muted: #66736c;
--border: #d7e1dc;
--accent: #097969;
--accent-strong: #075f53;
--ok: #168a4a;
--warning: #b97300;
--critical: #b42318;
--offline: #b42318;
--offline-neutral: #7d8790;
--shadow: 0 18px 45px rgba(22, 40, 32, 0.10);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: Arial, Helvetica, sans-serif;
}
.dashboard-shell {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0;
}
.dashboard-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-bottom: 22px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--accent-strong);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0;
font-size: clamp(2rem, 5vw, 3.4rem);
line-height: 1.02;
}
.header-status {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 42px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
color: var(--accent-strong);
font-size: 0.85rem;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 8px 20px rgba(22, 40, 32, 0.06);
}
.pulse-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--warning);
}
.pulse-dot.online {
background: var(--ok);
}
.pulse-dot.offline {
background: var(--offline-neutral);
}
.pulse-dot.warning {
background: var(--warning);
}
.pulse-dot.critical {
background: var(--critical);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.metric-card,
.system-panel,
.trends-panel {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
}
.metric-card {
min-height: 210px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.metric-card.offline {
border-color: rgba(125, 135, 144, 0.45);
}
.metric-card.normal {
border-color: rgba(22, 138, 74, 0.45);
}
.metric-card.warning {
border-color: rgba(185, 115, 0, 0.5);
}
.metric-card.critical {
border-color: rgba(180, 35, 24, 0.55);
}
.metric-topline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.metric-label {
color: var(--muted);
font-size: 0.92rem;
font-weight: 700;
}
.metric-state {
min-width: 74px;
padding: 5px 8px;
border-radius: 8px;
background: var(--panel-soft);
color: var(--warning);
font-size: 0.72rem;
font-weight: 800;
text-align: center;
}
.metric-state.online {
color: var(--ok);
}
.metric-state.normal {
color: var(--ok);
}
.metric-state.warning {
color: var(--warning);
}
.metric-state.critical {
color: var(--critical);
}
.metric-state.offline {
color: var(--offline-neutral);
}
.metric-reading {
display: flex;
align-items: baseline;
gap: 8px;
margin: 26px 0 18px;
min-width: 0;
}
.metric-reading span:first-child {
overflow-wrap: anywhere;
}
.metric-reading span:first-child {
font-size: clamp(2.35rem, 6vw, 4.3rem);
font-weight: 800;
line-height: 0.95;
}
.metric-unit {
color: var(--muted);
font-size: 1rem;
font-weight: 700;
}
.metric-source {
margin: 0;
color: var(--muted);
font-size: 0.82rem;
}
.system-panel,
.alarm-panel,
.trends-panel {
margin-top: 18px;
padding: 20px;
}
.alarm-panel {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
}
.panel-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.panel-heading h2 {
margin-bottom: 0;
font-size: 1.2rem;
}
.panel-heading p {
margin-bottom: 0;
color: var(--muted);
font-size: 0.92rem;
}
.status-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 16px;
}
.status-item {
min-height: 78px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fbfdfc;
}
.status-label {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
}
.status-item strong {
font-size: 1.02rem;
}
.sensor-health {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
padding: 0;
margin: 16px 0 0;
list-style: none;
}
.sensor-health li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px;
border-radius: 8px;
background: var(--panel-soft);
color: var(--muted);
font-size: 0.84rem;
font-weight: 700;
}
.sensor-health .online {
color: var(--ok);
}
.sensor-health .offline {
color: var(--offline-neutral);
}
.sensor-health .warning {
color: var(--warning);
}
.sensor-health .critical {
color: var(--critical);
}
.alarm-list {
display: grid;
gap: 10px;
padding: 0;
margin: 16px 0 0;
list-style: none;
}
.alarm-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px 14px;
border-radius: 8px;
background: var(--panel-soft);
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.alarm-list li.warning {
border-left: 4px solid var(--warning);
}
.alarm-list li.critical {
border-left: 4px solid var(--critical);
}
.alarm-list li.offline {
border-left: 4px solid var(--offline-neutral);
}
.alarm-empty {
justify-content: center;
text-align: center;
}
.risk-normal {
color: var(--ok);
}
.risk-warning {
color: var(--warning);
}
.risk-critical {
color: var(--critical);
}
.risk-offline {
color: var(--offline-neutral);
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
.chart-card {
min-width: 0;
padding: 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fbfdfc;
}
.chart-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.chart-heading h3 {
margin: 0;
font-size: 1rem;
}
.chart-heading span {
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.chart-frame {
position: relative;
min-height: 260px;
}
.chart-frame canvas {
width: 100%;
min-height: 260px;
}
.chart-empty {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
margin: 0;
border: 1px dashed var(--border);
border-radius: 8px;
background: var(--panel-soft);
color: var(--muted);
font-size: 0.95rem;
font-weight: 700;
text-align: center;
}
.chart-frame.empty canvas {
display: none;
}
.chart-frame.empty .chart-empty {
display: flex;
}
@media (max-width: 980px) {
.metrics-grid,
.status-grid,
.sensor-health {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.dashboard-shell {
width: min(100% - 24px, 1180px);
padding: 18px 0;
}
.dashboard-header,
.panel-heading {
align-items: flex-start;
flex-direction: column;
}
.header-status {
width: 100%;
justify-content: center;
}
.metrics-grid,
.status-grid,
.sensor-health,
.charts-grid {
grid-template-columns: 1fr;
}
.metric-card {
min-height: 178px;
}
.chart-heading {
align-items: flex-start;
flex-direction: column;
}
.chart-heading span {
white-space: normal;
}
}
.chart-filters {
display: flex;
gap: 10px;
margin: 18px 0;
flex-wrap: wrap;
}
.chart-filter {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
cursor: pointer;
font-weight: 700;
}
.chart-filter.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}

@ -0,0 +1,610 @@
const POLLING_INTERVAL_MS = 1000;
const HISTORY_POLLING_INTERVAL_MS = 10000;
const ALARM_CONFIG_FILE = "../config/alarms.json";
const WARNING_MARGIN_RATIO = 0.1;
const sensors = [
{
id: "temperature",
name: "Temperature",
file: "../data/EZORTD.json",
key: "temperature",
decimals: 3,
valueElement: "temperature-value",
stateElement: "temperature-state",
cardElement: "card-temperature"
},
{
id: "ph",
name: "pH",
file: "../data/EZOPH.json",
key: "ph",
decimals: 2,
valueElement: "ph-value",
stateElement: "ph-state",
cardElement: "card-ph"
},
{
id: "do",
name: "Dissolved Oxygen",
file: "../data/EZODO.json",
key: "do",
decimals: 2,
valueElement: "do-value",
stateElement: "do-state",
cardElement: "card-do"
},
{
id: "ec",
name: "Conductivity",
file: "../data/EZOEC.json",
key: "ec",
decimals: 0,
valueElement: "ec-value",
stateElement: "ec-state",
cardElement: "card-ec"
}
];
const historicalCharts = [
{
id: "temperature",
title: "Temperature vs Time",
file: "../logs/temperature.csv",
valueKey: "temperature",
unit: "degC",
canvasElement: "temperature-chart",
emptyElement: "temperature-chart-empty",
color: "#097969"
},
{
id: "ph-history",
title: "pH vs Time",
file: "../logs/ph.csv",
valueKey: "ph",
unit: "pH",
canvasElement: "ph-chart",
emptyElement: "ph-chart-empty",
color: "#4f46e5"
},
{
id: "do-history",
title: "Dissolved Oxygen vs Time",
file: "../logs/do.csv",
valueKey: "do",
unit: "mg/L",
canvasElement: "do-chart",
emptyElement: "do-chart-empty",
color: "#0f766e"
},
{
id: "ec-history",
title: "Conductivity vs Time",
file: "../logs/ec.csv",
valueKey: "ec",
unit: "uS/cm",
canvasElement: "ec-chart",
emptyElement: "ec-chart-empty",
color: "#b97300"
}
];
const chartInstances = new Map();
async function readSensor(sensor) {
try {
const response = await fetch(`${sensor.file}?t=${Date.now()}`, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const rawValue = Number(data[sensor.key]);
if (!Number.isFinite(rawValue)) {
throw new Error(`Invalid value for ${sensor.key}`);
}
return {
...sensor,
online: true,
numericValue: rawValue,
value: rawValue.toFixed(sensor.decimals)
};
} catch (error) {
console.warn(`${sensor.name} offline:`, error);
return {
...sensor,
online: false,
numericValue: null,
value: "OFFLINE"
};
}
}
function setSensorState(result) {
const valueElement = document.getElementById(result.valueElement);
const stateElement = document.getElementById(result.stateElement);
const cardElement = document.getElementById(result.cardElement);
const state = result.alarmState || (result.online ? "NORMAL" : "OFFLINE");
const stateClass = state.toLowerCase();
valueElement.textContent = result.value;
stateElement.textContent = state;
["online", "normal", "warning", "critical", "offline"].forEach((className) => {
stateElement.classList.toggle(className, className === stateClass);
cardElement.classList.toggle(className, className === stateClass);
});
}
function renderSystemStatus(results) {
const activeSensors = results.filter((result) => result.online).length;
const offlineSensors = results.length - activeSensors;
const criticalSensors = results.filter((result) => result.alarmState === "CRITICAL").length;
const warningSensors = results.filter((result) => result.alarmState === "WARNING").length;
const allNormal = results.every((result) => result.alarmState === "NORMAL");
const anyOnline = activeSensors > 0;
const overallDot = document.getElementById("overall-dot");
const overallStatus = document.getElementById("overall-status");
const healthList = document.getElementById("sensor-health");
document.getElementById("active-count").textContent =
`${activeSensors} / ${results.length}`;
document.getElementById("offline-count").textContent = String(offlineSensors);
document.getElementById("last-update").textContent =
new Date().toLocaleString();
overallDot.classList.toggle("online", allNormal);
overallDot.classList.toggle("warning", warningSensors > 0 && criticalSensors === 0);
overallDot.classList.toggle("critical", criticalSensors > 0);
overallDot.classList.toggle("offline", !anyOnline);
if (criticalSensors > 0) {
overallStatus.textContent = "CRITICAL ALARM";
} else if (warningSensors > 0) {
overallStatus.textContent = "WARNING ACTIVE";
} else if (allNormal) {
overallStatus.textContent = "ALL SYSTEMS NORMAL";
} else if (anyOnline) {
overallStatus.textContent = "PARTIAL DATA";
} else {
overallStatus.textContent = "SYSTEM OFFLINE";
}
healthList.innerHTML = results
.map((result) => {
const statusText = result.alarmState || (result.online ? "NORMAL" : "OFFLINE");
const statusClass = statusText.toLowerCase();
return `
<li>
<span>${result.name}</span>
<span class="${statusClass}">${statusText}</span>
</li>
`;
})
.join("");
}
async function updateDashboard() {
const [results, alarmConfig] = await Promise.all([
Promise.all(sensors.map(readSensor)),
readAlarmConfig()
]);
const evaluatedResults = results.map((result) =>
evaluateSensorAlarm(result, alarmConfig.thresholds)
);
evaluatedResults.forEach(setSensorState);
renderSystemStatus(evaluatedResults);
renderAlarmSummary(evaluatedResults, alarmConfig);
}
async function readAlarmConfig() {
try {
const response = await fetch(`${ALARM_CONFIG_FILE}?t=${Date.now()}`, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const thresholds = await response.json();
return {
loaded: true,
thresholds
};
} catch (error) {
console.warn("Alarm configuration unavailable:", error);
return {
loaded: false,
thresholds: {}
};
}
}
function evaluateSensorAlarm(sensorResult, thresholds) {
if (!sensorResult.online) {
return {
...sensorResult,
alarmState: "OFFLINE",
alarmMessage: "Sensor data unavailable"
};
}
const limits = thresholds[sensorResult.id];
if (!isValidThreshold(limits)) {
return {
...sensorResult,
alarmState: "WARNING",
alarmMessage: "Alarm limits unavailable"
};
}
if (sensorResult.numericValue < limits.min || sensorResult.numericValue > limits.max) {
return {
...sensorResult,
alarmState: "CRITICAL",
alarmMessage: buildAlarmMessage(sensorResult, limits, "outside")
};
}
if (isNearThreshold(sensorResult.numericValue, limits)) {
return {
...sensorResult,
alarmState: "WARNING",
alarmMessage: buildAlarmMessage(sensorResult, limits, "near")
};
}
return {
...sensorResult,
alarmState: "NORMAL",
alarmMessage: "Within configured range"
};
}
function isValidThreshold(limits) {
return Boolean(limits) &&
Number.isFinite(Number(limits.min)) &&
Number.isFinite(Number(limits.max)) &&
Number(limits.min) < Number(limits.max);
}
function isNearThreshold(value, limits) {
const min = Number(limits.min);
const max = Number(limits.max);
const warningMargin = (max - min) * WARNING_MARGIN_RATIO;
return value <= min + warningMargin || value >= max - warningMargin;
}
function buildAlarmMessage(sensorResult, limits, alarmType) {
const direction = sensorResult.numericValue < limits.min ? "below" : "above";
const range = `${limits.min} - ${limits.max}`;
if (alarmType === "outside") {
return `${sensorResult.value} is ${direction} configured range (${range})`;
}
return `${sensorResult.value} is near configured range (${range})`;
}
function getAlarmEvents(results) {
return results
.filter((result) => result.alarmState !== "NORMAL")
.map((result) => ({
sensorId: result.id,
sensorName: result.name,
severity: result.alarmState,
value: result.value,
message: result.alarmMessage,
createdAt: new Date().toISOString()
}));
}
function getOverallRiskLevel(results) {
if (results.some((result) => result.alarmState === "CRITICAL")) {
return "CRITICAL";
}
if (results.some((result) => result.alarmState === "WARNING")) {
return "WARNING";
}
if (results.some((result) => result.alarmState === "OFFLINE")) {
return "OFFLINE";
}
return "NORMAL";
}
function renderAlarmSummary(results, alarmConfig) {
const criticalCount = results.filter((result) => result.alarmState === "CRITICAL").length;
const warningCount = results.filter((result) => result.alarmState === "WARNING").length;
const offlineCount = results.filter((result) => result.alarmState === "OFFLINE").length;
const overallRisk = getOverallRiskLevel(results);
const alarmEvents = getAlarmEvents(results);
const riskElement = document.getElementById("overall-risk");
const alarmList = document.getElementById("alarm-list");
document.getElementById("critical-count").textContent = String(criticalCount);
document.getElementById("warning-count").textContent = String(warningCount);
document.getElementById("alarm-offline-count").textContent = String(offlineCount);
document.getElementById("alarm-config-status").textContent =
alarmConfig.loaded ? "loaded" : "unavailable";
riskElement.textContent = overallRisk;
["risk-normal", "risk-warning", "risk-critical", "risk-offline"].forEach((className) => {
riskElement.classList.remove(className);
});
riskElement.classList.add(`risk-${overallRisk.toLowerCase()}`);
if (alarmEvents.length === 0) {
alarmList.innerHTML = '<li class="alarm-empty">No active alarms</li>';
return;
}
alarmList.innerHTML = alarmEvents
.map((event) => `
<li class="${event.severity.toLowerCase()}">
<span>${event.sensorName}</span>
<span>${event.severity}: ${event.message}</span>
</li>
`)
.join("");
}
async function readHistoricalData(chartConfig) {
try {
const response = await fetch(`${chartConfig.file}?t=${Date.now()}`, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const csvText = await response.text();
return parseHistoricalCsv(csvText, chartConfig.valueKey);
} catch (error) {
console.warn(`${chartConfig.title} unavailable:`, error);
return [];
}
}
function parseHistoricalCsv(csvText, valueKey) {
const lines = csvText
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (lines.length === 0) {
return [];
}
const firstRow = splitCsvLine(lines[0]);
const lowerFirstRow = firstRow.map((cell) => cell.toLowerCase());
const hasNamedHeader = lowerFirstRow.some((cell) =>
["timestamp", "time", "date", valueKey, "value", "reading"].some((candidate) =>
cell.includes(candidate)
)
);
const hasHeader = hasNamedHeader || !Number.isFinite(Number(firstRow[1]));
const header = hasHeader ? firstRow.map((cell) => cell.toLowerCase()) : [];
const rows = hasHeader ? lines.slice(1) : lines;
const timestampIndex = getCsvColumnIndex(header, ["timestamp", "time", "date"], 0);
const valueIndex = getCsvColumnIndex(header, [valueKey, "value", "reading"], 1);
return rows
.map((line) => splitCsvLine(line))
.map((columns) => {
const rawValue = Number(columns[valueIndex]);
if (!Number.isFinite(rawValue)) {
return null;
}
return {
label: formatTimestamp(columns[timestampIndex]),
value: rawValue
};
})
.filter(Boolean);
}
function splitCsvLine(line) {
return line
.split(",")
.map((cell) => cell.trim().replace(/^"|"$/g, ""));
}
function getCsvColumnIndex(header, candidates, fallbackIndex) {
if (header.length === 0) {
return fallbackIndex;
}
const index = header.findIndex((columnName) =>
candidates.some((candidate) => columnName.includes(candidate))
);
return index >= 0 ? index : fallbackIndex;
}
function formatTimestamp(rawTimestamp) {
if (!rawTimestamp) {
return "";
}
const numericTimestamp = Number(rawTimestamp);
if (Number.isFinite(numericTimestamp)) {
if (numericTimestamp > 1000000000000) {
return new Date(numericTimestamp).toLocaleTimeString();
}
if (numericTimestamp > 1000000000) {
return new Date(numericTimestamp * 1000).toLocaleTimeString();
}
}
const parsedDate = new Date(rawTimestamp);
if (!Number.isNaN(parsedDate.getTime())) {
return parsedDate.toLocaleTimeString();
}
return rawTimestamp;
}
function setChartAvailability(chartConfig, hasData) {
const emptyElement = document.getElementById(chartConfig.emptyElement);
const frameElement = emptyElement.closest(".chart-frame");
frameElement.classList.toggle("empty", !hasData);
}
function buildChartDataset(chartConfig, points) {
return {
labels: points.map((point) => point.label),
datasets: [
{
label: `${chartConfig.title} (${chartConfig.unit})`,
data: points.map((point) => point.value),
borderColor: chartConfig.color,
backgroundColor: `${chartConfig.color}1f`,
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.32,
fill: true
}
]
};
}
function getChartOptions(chartConfig) {
return {
responsive: true,
maintainAspectRatio: false,
animation: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label(context) {
return `${context.parsed.y} ${chartConfig.unit}`;
}
}
}
},
scales: {
x: {
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 6
},
grid: {
display: false
}
},
y: {
beginAtZero: false,
ticks: {
callback(value) {
return `${value}`;
}
}
}
}
};
}
function renderHistoricalChart(chartConfig, points) {
const chartLibraryReady = typeof Chart !== "undefined";
const hasData = points.length > 0 && chartLibraryReady;
const existingChart = chartInstances.get(chartConfig.id);
setChartAvailability(chartConfig, hasData);
if (!hasData) {
return;
}
const chartData = buildChartDataset(chartConfig, points);
if (existingChart) {
existingChart.data = chartData;
existingChart.update("none");
return;
}
const canvas = document.getElementById(chartConfig.canvasElement);
const chart = new Chart(canvas, {
type: "line",
data: chartData,
options: getChartOptions(chartConfig)
});
chartInstances.set(chartConfig.id, chart);
}
async function updateHistoricalTrends() {
const chartData = await Promise.all(
historicalCharts.map(async (chartConfig) => ({
chartConfig,
points: await readHistoricalData(chartConfig)
}))
);
chartData.forEach(({ chartConfig, points }) => {
renderHistoricalChart(chartConfig, points);
});
}
updateDashboard();
setInterval(updateDashboard, POLLING_INTERVAL_MS);
updateHistoricalTrends();
setInterval(updateHistoricalTrends, HISTORY_POLLING_INTERVAL_MS);
const chartCards = {
temperature: document.getElementById("chart-card-temperature"),
ph: document.getElementById("chart-card-ph"),
do: document.getElementById("chart-card-do"),
ec: document.getElementById("chart-card-ec")
};
document.querySelectorAll(".chart-filter").forEach((button) => {
button.addEventListener("click", () => {
const selected = button.dataset.chart;
document.querySelectorAll(".chart-filter").forEach((btn) => {
btn.classList.remove("active");
});
button.classList.add("active");
if (selected === "all") {
Object.values(chartCards).forEach((card) => {
card.style.display = "";
});
return;
}
Object.entries(chartCards).forEach(([key, card]) => {
card.style.display = key === selected ? "" : "none";
});
});
});

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Photobioreactor Dashboard</title>
<link rel="stylesheet" href="dashboard.css">
</head>
<body>
<main class="dashboard-shell">
<header class="dashboard-header">
<div>
<p class="eyebrow">Atlas Scientific Monitoring</p>
<h1>Photobioreactor Dashboard</h1>
</div>
<div class="header-status" aria-live="polite">
<span class="pulse-dot" id="overall-dot"></span>
<span id="overall-status">INITIALIZING</span>
</div>
</header>
<section class="metrics-grid" aria-label="Live sensor readings">
<article class="metric-card" id="card-temperature">
<div class="metric-topline">
<span class="metric-label">Temperature</span>
<span class="metric-state" id="temperature-state">WAITING</span>
</div>
<div class="metric-reading">
<span id="temperature-value">--</span>
<span class="metric-unit">&deg;C</span>
</div>
<p class="metric-source">EZO-RTD &middot; data/EZORTD.json</p>
</article>
<article class="metric-card" id="card-ph">
<div class="metric-topline">
<span class="metric-label">pH</span>
<span class="metric-state" id="ph-state">WAITING</span>
</div>
<div class="metric-reading">
<span id="ph-value">--</span>
<span class="metric-unit">pH</span>
</div>
<p class="metric-source">EZO-pH &middot; data/EZOPH.json</p>
</article>
<article class="metric-card" id="card-do">
<div class="metric-topline">
<span class="metric-label">Dissolved Oxygen</span>
<span class="metric-state" id="do-state">WAITING</span>
</div>
<div class="metric-reading">
<span id="do-value">--</span>
<span class="metric-unit">mg/L</span>
</div>
<p class="metric-source">EZO-DO &middot; data/EZODO.json</p>
</article>
<article class="metric-card" id="card-ec">
<div class="metric-topline">
<span class="metric-label">Conductivity</span>
<span class="metric-state" id="ec-state">WAITING</span>
</div>
<div class="metric-reading">
<span id="ec-value">--</span>
<span class="metric-unit">&micro;S/cm</span>
</div>
<p class="metric-source">EZO-EC &middot; data/EZOEC.json</p>
</article>
</section>
<section class="system-panel" aria-label="System status">
<div class="panel-heading">
<h2>System Status</h2>
<p>Last update: <span id="last-update">--</span></p>
</div>
<div class="status-grid">
<div class="status-item">
<span class="status-label">Polling interval</span>
<strong>1 second</strong>
</div>
<div class="status-item">
<span class="status-label">Data source</span>
<strong>Static JSON files</strong>
</div>
<div class="status-item">
<span class="status-label">Active sensors</span>
<strong id="active-count">0 / 4</strong>
</div>
<div class="status-item">
<span class="status-label">Offline sensors</span>
<strong id="offline-count">0</strong>
</div>
</div>
<ul class="sensor-health" id="sensor-health" aria-live="polite"></ul>
</section>
<section class="alarm-panel" aria-label="Alarm summary">
<div class="panel-heading">
<h2>Alarm Summary</h2>
<p>Thresholds: <span id="alarm-config-status">loading</span></p>
</div>
<div class="status-grid alarm-grid">
<div class="status-item">
<span class="status-label">Active Critical Alarms</span>
<strong id="critical-count">0</strong>
</div>
<div class="status-item">
<span class="status-label">Active Warnings</span>
<strong id="warning-count">0</strong>
</div>
<div class="status-item">
<span class="status-label">Offline Sensors</span>
<strong id="alarm-offline-count">0</strong>
</div>
<div class="status-item">
<span class="status-label">Overall Risk Level</span>
<strong id="overall-risk">NORMAL</strong>
</div>
</div>
<ul class="alarm-list" id="alarm-list" aria-live="polite">
<li class="alarm-empty">No active alarms</li>
</ul>
</section>
<section class="trends-panel" aria-label="Historical trends">
<div class="panel-heading">
<h2>Historical Trends</h2>
<p>Charts update every <span id="history-interval">10 seconds</span></p>
</div>
<div class="export-toolbar">
<button id="export-temperature">Export Temperature CSV</button>
<button id="export-ph">Export pH CSV</button>
<button id="export-do">Export DO CSV</button>
<button id="export-ec">Export EC CSV</button>
<button id="export-all">Export All CSV</button>
</div>
<div class="charts-grid">
<article class="chart-card">
<div class="chart-heading">
<h3>Temperature vs Time</h3>
<span>logs/temperature.csv</span>
</div>
<div class="chart-frame">
<canvas id="temperature-chart"></canvas>
<p class="chart-empty" id="temperature-chart-empty">No historical data available</p>
</div>
</article>
<article class="chart-card">
<div class="chart-heading">
<h3>pH vs Time</h3>
<span>logs/ph.csv</span>
</div>
<div class="chart-frame">
<canvas id="ph-chart"></canvas>
<p class="chart-empty" id="ph-chart-empty">No historical data available</p>
</div>
</article>
<article class="chart-card">
<div class="chart-heading">
<h3>Dissolved Oxygen vs Time</h3>
<span>logs/do.csv</span>
</div>
<div class="chart-frame">
<canvas id="do-chart"></canvas>
<p class="chart-empty" id="do-chart-empty">No historical data available</p>
</div>
</article>
<article class="chart-card">
<div class="chart-heading">
<h3>Conductivity vs Time</h3>
<span>logs/ec.csv</span>
</div>
<div class="chart-frame">
<canvas id="ec-chart"></canvas>
<p class="chart-empty" id="ec-chart-empty">No historical data available</p>
</div>
</article>
</div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="dashboard.js"></script>
</body>
</html>

@ -0,0 +1,11 @@
timestamp,do
1717027200,8.1
1717027260,8.2
1717027320,8.3
1717027380,8.2
1717027440,8.4
1717027500,8.5
1717027560,8.4
1717027620,8.6
1717027680,8.5
1717027740,8.7
1 timestamp do
2 1717027200 8.1
3 1717027260 8.2
4 1717027320 8.3
5 1717027380 8.2
6 1717027440 8.4
7 1717027500 8.5
8 1717027560 8.4
9 1717027620 8.6
10 1717027680 8.5
11 1717027740 8.7

@ -0,0 +1,11 @@
timestamp,ec
1717027200,1200
1717027260,1210
1717027320,1225
1717027380,1230
1717027440,1240
1717027500,1235
1717027560,1245
1717027620,1250
1717027680,1248
1717027740,1260
1 timestamp ec
2 1717027200 1200
3 1717027260 1210
4 1717027320 1225
5 1717027380 1230
6 1717027440 1240
7 1717027500 1235
8 1717027560 1245
9 1717027620 1250
10 1717027680 1248
11 1717027740 1260

@ -0,0 +1,11 @@
timestamp,ph
1717027200,7.10
1717027260,7.11
1717027320,7.09
1717027380,7.12
1717027440,7.08
1717027500,7.13
1717027560,7.10
1717027620,7.14
1717027680,7.12
1717027740,7.11
1 timestamp ph
2 1717027200 7.10
3 1717027260 7.11
4 1717027320 7.09
5 1717027380 7.12
6 1717027440 7.08
7 1717027500 7.13
8 1717027560 7.10
9 1717027620 7.14
10 1717027680 7.12
11 1717027740 7.11

@ -0,0 +1,11 @@
timestamp,temperature
1717027200,24.4
1717027260,24.5
1717027320,24.6
1717027380,24.7
1717027440,24.8
1717027500,24.9
1717027560,25.0
1717027620,25.1
1717027680,25.2
1717027740,25.3
1 timestamp temperature
2 1717027200 24.4
3 1717027260 24.5
4 1717027320 24.6
5 1717027380 24.7
6 1717027440 24.8
7 1717027500 24.9
8 1717027560 25.0
9 1717027620 25.1
10 1717027680 25.2
11 1717027740 25.3

@ -0,0 +1,15 @@
CC=gcc
CFLAGS=-I.
OBJ=main.o sensor.o
TARGET=sensor
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJ)
$(CC) -o $@ $^ $(CFLAGS)
clean:
rm -f *.o $(TARGET)

@ -0,0 +1,35 @@
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <stdlib.h>
#include "ezodo.h"
int getDO(int fd,double *oxygen)
{
if(ioctl(fd,I2C_SLAVE,EZODO_I2C_ADDR)<0)
{
return -1;
}
char cmd[]="R";
write(fd,cmd,strlen(cmd));
usleep(1000000);
unsigned char response[32];
read(fd,response,sizeof(response));
if(response[0]!=0x01)
{
return -1;
}
*oxygen = atof((char*)&response[1]);
return 0;
}

@ -0,0 +1,8 @@
#ifndef EZODO_H
#define EZODO_H
#define EZODO_I2C_ADDR 0x61
int getDO(int fd,double *oxygen);
#endif

@ -0,0 +1,15 @@
CC=gcc
CFLAGS=-I.
OBJ=main.o sensor.o
TARGET=sensor
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJ)
$(CC) -o $@ $^ $(CFLAGS)
clean:
rm -f *.o $(TARGET)

@ -0,0 +1,35 @@
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <stdlib.h>
#include "ezoec.h"
int getEC(int fd,double *ec)
{
if(ioctl(fd,I2C_SLAVE,EZOEC_I2C_ADDR)<0)
{
return -1;
}
char cmd[]="R";
write(fd,cmd,strlen(cmd));
usleep(1000000);
unsigned char response[32];
read(fd,response,sizeof(response));
if(response[0]!=0x01)
{
return -1;
}
*ec = atof((char*)&response[1]);
return 0;
}

@ -0,0 +1,8 @@
#ifndef EZOEC_H
#define EZOEC_H
#define EZOEC_I2C_ADDR 0x64
int getEC(int fd,double *ec);
#endif

@ -0,0 +1,17 @@
#include <stdio.h>
#include <fcntl.h>
#include "ezoec.h"
int main()
{
int fd = open("/dev/i2c-1",O_RDWR);
double ec;
getEC(fd,&ec);
printf("{ \"ec\": %.3f }\n",ec);
return 0;
}

@ -0,0 +1,15 @@
CC=gcc
CFLAGS=-I.
OBJ=main.o sensor.o
TARGET=sensor
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJ)
$(CC) -o $@ $^ $(CFLAGS)
clean:
rm -f *.o $(TARGET)

@ -0,0 +1,40 @@
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <stdlib.h>
#include "ezoph.h"
int getPH(int fd, double *ph)
{
if(ioctl(fd, I2C_SLAVE, EZOPH_I2C_ADDR) < 0)
{
perror("ioctl");
return -1;
}
char cmd[] = "R";
write(fd, cmd, strlen(cmd));
usleep(1000000);
unsigned char response[32];
if(read(fd,response,sizeof(response)) < 0)
{
perror("read");
return -1;
}
if(response[0] != 0x01)
{
return -1;
}
*ph = atof((char*)&response[1]);
return 0;
}

@ -0,0 +1,8 @@
#ifndef EZOPH_H
#define EZOPH_H
#define EZOPH_I2C_ADDR 0x63
int getPH(int fd, double *ph);
#endif

@ -0,0 +1,28 @@
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "ezoph.h"
int main()
{
int fd = open("/dev/i2c-1",O_RDWR);
if(fd < 0)
{
fprintf(stderr,"%s\n",strerror(errno));
return -1;
}
double ph;
if(getPH(fd,&ph) < 0)
{
return -1;
}
printf("{ \"ph\": %.3f }\n",ph);
return 0;
}

@ -35,22 +35,29 @@ int main()
fclose(f);
}
}
FILE *log = fopen(
"/home/cristian/airquality/basic-ui-dashboard/logs/temp.csv",
"a");
FILE *log = fopen(
"/home/cristian/airquality/basic-ui-dashboard/logs/temperature.csv",
"a+");
if (log)
{
time_t now = time(NULL);
if (log)
{
time_t now = time(NULL);
fseek(log, 0, SEEK_END);
fprintf(log,
"%ld,%.3f\n",
now,
temperature);
if (ftell(log) == 0)
{
fprintf(log, "timestamp,value\n");
}
fclose(log);
fprintf(log,
"%ld,%.3f\n",
now,
temperature);
fclose(log);
}
}
sleep(1);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,16 +0,0 @@
CC=gcc
CFLAGS=-I.
DEPS =
OBJ = main.o htu21d.o
EXTRA_LIBS=-li2c
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
HTU21D: $(OBJ)
$(CC) -o $@ $^ $(CFLAGS) $(EXTRA_LIBS)
.PHONY: clean
clean:
rm -f HTU21D $(OBJ)

@ -1,180 +0,0 @@
# Introduction
This repository guides you to implement and use the HTU21D temperature and humidity sensor by using I2C communication. The repository contains the following files:
- `htu21d.h` library header files
- `htu21d.c` implemenation methods file
- `main.c` an example file to test I2C communication using the library `htu21d`
- `example` output file precompiled in a raspberry pi zero that returns temperature and humidity
# Setup the raspberry
## Enable I2C on the Raspberry Pi:
- Run sudo raspi-config.
- Navigate to Interfacing Options → I2C and enable it.
- Reboot the Pi.
## Install I2C Tools and Development Libraries:
```
sudo apt-get update
sudo apt-get install i2c-tools libi2c-dev
```
## Check if the sensor is detected:
```
sudo i2cdetect -y 1
```
This command should show an address for the HTU21D, typically 0x40.
```
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
```
# My own header files for HTU21D sensor
This is a detailed guide to configure and create your own files to comunicate with each sensor used by the air-quality sensor.
## Header
Create the Header File `htu21d.h` with Header Guard and Includes:
```c
#ifndef HTU21D_H
#define HTU21D_H
// I2C Address
#define HTU21D_I2C_ADDR 0x40
// Commands
#define HTU21D_TEMP 0xE3
#define HTU21D_HUMID 0xE5
#define HTU21D_RESET 0xFE
// Function declarations:
// Temp
int getTemperature(int fd, double *temperature);
// Humidity
int getHumidity(int fd, double *humidity);
#endif // HTU21D_H
```
## Implement the Sensor Communication `htu21d.c`
```c
#include <unistd.h> //to send commands to and receive from I2C device
#include <sys/ioctl.h>//setting up and controlling the I2C device settings
#include <linux/i2c-dev.h>//definitions for system calls and structures specific to I2C
#include <i2c/smbus.h>//SMBus commands in a more standardized way for I2C
#include <stdio.h>//perror
#include "htu21d.h" // my own header file
// Reset function:
int reset(int fd)
{
if(0 > ioctl(fd, I2C_SLAVE, HTU21D_I2C_ADDR))
{
perror("Failed to open the bus");
return -1;
}
i2c_smbus_write_byte(fd, HTU21D_RESET);
return 0;
}
// Get temperature:
int getTemperature(int fd, double *temperature)
{
reset(fd);
char buf[3];
__s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_TEMP,3,buf);
if(res<0)
{
perror("Failed to read from the device");
return -1;
}
*temperature = -46.85 + 175.72 * (buf[0]*256 + buf[1]) / 65536.0;
return 0;
}
// Get humidity:
int getHumidity(int fd, double *humidity)
{
reset(fd);
char buf[3];
__s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_HUMID, 3, buf);
if(res<0)
{
perror("Failed to read from the device");
return -1;
}
*humidity = -6 + 125 * (buf[0]*256 + buf[1]) / 65536.0;
return 0;
}
```
### Using the library
```c
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include "htu21d.h"
int main()
{
char filename[20];
snprintf(filename, 19, "/dev/i2c-%d", 1);
int fd = open(filename, O_RDWR);
if (0 > fd)
{
fprintf(stderr, "ERROR: Unable to access HTU21D sensor module: %s\n", strerror (errno));
exit(-1);
}
// Retrieve temperature and humidity
double temperature = 0;
double humidity = 0;
if ( (0 > getHumidity(fd, &humidity)) || (0 > getTemperature(fd, &temperature)) )
{
fprintf(stderr, "ERROR: HTU21D sensor module not found\n");
exit(-1);
}
// Print temperature and humidity on the screen
printf("HTU21D Sensor Module\n");
printf("%5.2fC\n", temperature);
printf("%5.2f%%rh\n", humidity);
return 0;
}
```
# Compiling and testing
Then to properly compile whitout a make file:
```sh
gcc -o example main.c htu21d.c -li2c
```
or
```sh
gcc -o example main.c htu21d.c -I. -li2c
```
### Wiring htu21d to Rasp-zero
Htu -> Rasp-Zero
VIN -> GPIO 1
GND -> GPIO 9 or (6)
SCL -> GPIO 5
SDA -> GPIO 3
![Raspberry Pi Zero GPIO layout](rpiz.png)

@ -1,52 +0,0 @@
#include <unistd.h> //to send commands to and receive from I2C device
#include <sys/ioctl.h>//setting up and controlling the I2C device settings
#include <linux/i2c-dev.h>//definitions for system calls and structures specific to I2C
#include <i2c/smbus.h>//SMBus commands in a more standardized way for I2C
#include <stdio.h>//perror
#include "htu21d.h" // my own header file
// Reset function:
int reset(int fd)
{
if(0 > ioctl(fd, I2C_SLAVE, HTU21D_I2C_ADDR))
{
perror("Failed to open the bus");
return -1;
}
i2c_smbus_write_byte(fd, HTU21D_RESET);
return 0;
}
// Get temperature:
int getTemperature(int fd, double *temperature)
{
reset(fd);
char buf[3];
__s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_TEMP,3,buf);
if(res<0)
{
perror("Failed to read from the device");
return -1;
}
*temperature = -46.85 + 175.72 * (buf[0]*256 + buf[1]) / 65536.0;
return 0;
}
// Get humidity:
int getHumidity(int fd, double *humidity)
{
reset(fd);
char buf[3];
__s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_HUMID, 3, buf);
if(res<0)
{
perror("Failed to read from the device");
return -1;
}
*humidity = -6 + 125 * (buf[0]*256 + buf[1]) / 65536.0;
return 0;
}

@ -1,19 +0,0 @@
#ifndef HTU21D_H
#define HTU21D_H
// I2C Address
#define HTU21D_I2C_ADDR 0x40
// Commands
#define HTU21D_TEMP 0xE3
#define HTU21D_HUMID 0xE5
#define HTU21D_RESET 0xFE
// Function declarations:
// Temp
int getTemperature(int fd, double *temperature);
// Humidity
int getHumidity(int fd, double *humidity);
#endif // HTU21D_H

Binary file not shown.

@ -1,39 +0,0 @@
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include "htu21d.h"
int main()
{
char filename[20];
snprintf(filename, 19, "/dev/i2c-%d", 1);
int fd = open(filename, O_RDWR);
if (0 > fd)
{
fprintf(stderr, "ERROR: Unable to access HTU21D sensor module: %s\n", strerror (errno));
exit(-1);
}
// Retrieve temperature and humidity
double temperature = 0;
double humidity = 0;
if ( (0 > getHumidity(fd, &humidity)) || (0 > getTemperature(fd, &temperature)) )
{
fprintf(stderr, "ERROR: HTU21D sensor module not found\n");
exit(-1);
}
// Print temperature and humidity on the screen
printf("{ ");
printf("\"temperature\": %5.2f, ", temperature);
printf("\"humidity\": %5.2f ", humidity);
printf("}");
//printf("HTU21D Sensor Module\n");
//printf("%5.2fC\n", temperature);
//printf("%5.2f%%rh\n", humidity);
return 0;
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Loading…
Cancel
Save