1465 lines
30 KiB
Markdown
1465 lines
30 KiB
Markdown
# Runbook operativo del nodo ESP32 Climate Node 01
|
|
|
|
## Sustrato + BME280 + temperatura IR de hoja + MQTT MESAVAULT
|
|
|
|
## Clasificación
|
|
|
|
```yaml
|
|
doc_type: runbook
|
|
titulo: Runbook operativo del nodo ESP32 Climate Node 01
|
|
sistema: aquaponics
|
|
servicio_afectado: esp32_climate_node_01
|
|
criticidad: pendiente_de_confirmar
|
|
estado_madurez: operativo_parcial
|
|
sensibilidad: contiene_referencias_a_credenciales_sin_secreto
|
|
owner: pendiente_de_confirmar
|
|
````
|
|
|
|
## Metadatos RAG
|
|
|
|
```yaml
|
|
doc_id: runbook-aquaponics-esp32-climate-node-01
|
|
doc_type: runbook
|
|
system: aquaponics
|
|
environment: lab
|
|
tags:
|
|
- mesavault
|
|
- aquaponics
|
|
- esp32
|
|
- mqtt
|
|
- bme280
|
|
- mlx90614
|
|
- soil-moisture
|
|
- grafana
|
|
- timescaledb
|
|
source_date: "2026-05-03"
|
|
last_verified: "2026-05-03"
|
|
owner: pendiente_de_confirmar
|
|
sensitivity: contains_secret_placeholders
|
|
canonical: pendiente_de_confirmar
|
|
related_docs: []
|
|
```
|
|
|
|
## Resumen ejecutivo
|
|
|
|
Este runbook documenta el nodo `ESP32 Climate Node 01`, usado en el banco hidropónico/acuapónico del lab MESAVAULT.
|
|
|
|
La función del nodo es medir variables de ambiente, planta y sustrato, separando estas medidas del `Atlas Scientific Aquaponics Kit`, que queda dedicado a química y parámetros del agua.
|
|
|
|
```text
|
|
Atlas Scientific Aquaponics Kit
|
|
→ química y parámetros del agua:
|
|
- pH
|
|
- EC
|
|
- oxígeno disuelto
|
|
- temperatura del agua
|
|
|
|
ESP32 Climate Node 01
|
|
→ ambiente, planta y sustrato:
|
|
- humedad relativa del sustrato
|
|
- temperatura ambiente
|
|
- humedad relativa ambiente
|
|
- presión atmosférica
|
|
- temperatura IR de hoja
|
|
- delta hoja-aire
|
|
- VPD aire
|
|
- VPD hoja
|
|
```
|
|
|
|
El nodo publica telemetría por MQTT hacia el stack MESAVAULT.
|
|
|
|
## Estado actual
|
|
|
|
```text
|
|
ESP32 carga firmware correctamente.
|
|
PuTTY muestra salida serie.
|
|
LED parpadea.
|
|
BME280 detectado y leyendo.
|
|
MLX90614 detectado y leyendo.
|
|
Soil moisture leyendo por GPIO34.
|
|
MQTT aparentemente transmitiendo.
|
|
```
|
|
|
|
Pendiente:
|
|
|
|
```text
|
|
- calibrar soil_raw en sustrato seco real
|
|
- calibrar soil_raw en sustrato saturado
|
|
- validar recepción MQTT con mosquitto_sub
|
|
- ver en Grafana en directo
|
|
- montar persistencia histórica MQTT → PostgreSQL/TimescaleDB
|
|
- fijar físicamente el MLX90614 apuntando a hoja real
|
|
```
|
|
|
|
## Hardware
|
|
|
|
### Microcontrolador
|
|
|
|
```text
|
|
Placa: DFRobot ESP-WROOM-32 / ESP32
|
|
Módulo: ESP-WROOM-32
|
|
USB serie: COM4 en el PC usado durante las pruebas
|
|
```
|
|
|
|
### Sensores
|
|
|
|
```text
|
|
DFRobot Soil Moisture Sensor v1.2
|
|
Tipo: capacitivo
|
|
Salida: analógica
|
|
Uso: índice relativo de humedad/dryback del sustrato
|
|
|
|
BME280
|
|
Bus: I2C
|
|
Uso:
|
|
- temperatura ambiente
|
|
- humedad relativa
|
|
- presión atmosférica
|
|
|
|
Sensor IR de temperatura de hoja
|
|
Modelo asumido: MLX90614 / GY-906
|
|
Bus: I2C
|
|
Uso:
|
|
- temperatura superficial de hoja
|
|
- temperatura ambiente interna del sensor IR
|
|
```
|
|
|
|
## Cableado
|
|
|
|
### DFRobot Soil Moisture v1.2
|
|
|
|
```text
|
|
DFRobot Soil Moisture v1.2 ESP32
|
|
-------------------------------------
|
|
VCC 3V3
|
|
GND GND
|
|
AOUT / Signal GPIO34
|
|
```
|
|
|
|
Se usa `GPIO34` porque pertenece a `ADC1` y no interfiere con WiFi.
|
|
|
|
### BME280
|
|
|
|
```text
|
|
BME280 ESP32
|
|
-------------------------------------
|
|
VCC 3V3
|
|
GND GND
|
|
SDA GPIO21
|
|
SCL GPIO22
|
|
```
|
|
|
|
### MLX90614 / GY-906
|
|
|
|
```text
|
|
MLX90614 ESP32
|
|
-------------------------------------
|
|
VCC 3V3
|
|
GND GND
|
|
SDA GPIO21
|
|
SCL GPIO22
|
|
```
|
|
|
|
### Bus I2C compartido
|
|
|
|
```text
|
|
SDA común → GPIO21
|
|
SCL común → GPIO22
|
|
Alimentación común → 3V3
|
|
GND común → GND
|
|
```
|
|
|
|
Todo el bus I2C debe trabajar a `3,3 V`.
|
|
|
|
No alimentar los módulos I2C a `5 V` si las líneas `SDA` / `SCL` quedan tiradas a `5 V`.
|
|
|
|
## Direcciones I2C esperadas
|
|
|
|
```text
|
|
BME280:
|
|
0x76 o 0x77
|
|
|
|
MLX90614:
|
|
0x5A
|
|
```
|
|
|
|
Lectura esperada en arranque:
|
|
|
|
```text
|
|
I2C encontrado en 0x5A
|
|
I2C encontrado en 0x76
|
|
BME280 OK
|
|
MLX90614 OK
|
|
```
|
|
|
|
## Arduino IDE
|
|
|
|
Configuración recomendada:
|
|
|
|
```text
|
|
Board: ESP32 Dev Module
|
|
Upload Speed: 115200
|
|
CPU Frequency: 240MHz
|
|
Flash Frequency: 40MHz
|
|
Flash Mode: DIO
|
|
Flash Size: 4MB
|
|
Partition Scheme: Default 4MB
|
|
Erase All Flash Before Sketch Upload: Enabled si hay problemas
|
|
Port: COM4
|
|
```
|
|
|
|
La selección `Adafruit ESP32 Feather` no funcionó correctamente para esta placa.
|
|
|
|
Durante las pruebas, con velocidades altas como `921600`, aparecieron errores de escritura en flash.
|
|
|
|
Error observado:
|
|
|
|
```text
|
|
Warning: Failed to communicate with the flash chip
|
|
Packet content transfer stopped
|
|
```
|
|
|
|
Solución aplicada:
|
|
|
|
```text
|
|
- desconectar sensores
|
|
- usar ESP32 Dev Module
|
|
- bajar Upload Speed a 115200
|
|
- Flash Frequency 40MHz
|
|
- Flash Mode DIO
|
|
```
|
|
|
|
## Puerto serie
|
|
|
|
El sketch cargaba correctamente y el LED parpadeaba, pero el Serial Monitor de Arduino IDE no mostraba texto.
|
|
|
|
Diagnóstico:
|
|
|
|
```text
|
|
- firmware cargado correctamente
|
|
- ESP32 arrancando correctamente
|
|
- loop ejecutándose correctamente
|
|
- LED parpadeando correctamente
|
|
- salida Serial funcionando en realidad
|
|
- problema localizado en el Serial Monitor de Arduino IDE
|
|
```
|
|
|
|
Solución práctica:
|
|
|
|
```text
|
|
Usar PuTTY para ver la salida serie.
|
|
```
|
|
|
|
Configuración PuTTY:
|
|
|
|
```text
|
|
Connection type: Serial
|
|
Serial line: COM4
|
|
Speed: 115200
|
|
Data bits: 8
|
|
Stop bits: 1
|
|
Parity: None
|
|
Flow control: None
|
|
```
|
|
|
|
Regla importante:
|
|
|
|
```text
|
|
Cerrar PuTTY antes de subir un nuevo sketch desde Arduino IDE.
|
|
```
|
|
|
|
PuTTY bloquea el puerto COM mientras está abierto.
|
|
|
|
## Lecturas validadas
|
|
|
|
Ejemplo real con el sensor de humedad al aire:
|
|
|
|
```text
|
|
----- LECTURA -----
|
|
soil_raw=3412 soil_voltage=2.750
|
|
air_temp_c=23.24 air_rh_pct=71.87 pressure_hpa=999.80
|
|
leaf_temp_c=21.51 ir_ambient_c=21.63
|
|
```
|
|
|
|
Interpretación:
|
|
|
|
```text
|
|
soil_raw=3412
|
|
Lectura al aire.
|
|
Se toma como referencia provisional de seco, pero no como calibración definitiva.
|
|
|
|
soil_voltage=2.750 V
|
|
Coherente con salida analógica del sensor.
|
|
|
|
air_temp_c=23.24
|
|
air_rh_pct=71.87
|
|
pressure_hpa=999.80
|
|
Lectura razonable del BME280.
|
|
|
|
leaf_temp_c=21.51
|
|
ir_ambient_c=21.63
|
|
Lectura coherente del MLX90614, aunque todavía depende de a qué apunte el sensor IR.
|
|
```
|
|
|
|
## Calibración del sensor de humedad de sustrato
|
|
|
|
El `DFRobot Soil Moisture v1.2` no debe tratarse como sensor profesional de VWC absoluto.
|
|
|
|
Se usará como índice relativo de humedad/dryback.
|
|
|
|
Nombre recomendado para la variable:
|
|
|
|
```text
|
|
soil_index_pct
|
|
```
|
|
|
|
No llamarlo todavía:
|
|
|
|
```text
|
|
VWC real
|
|
humedad volumétrica real
|
|
```
|
|
|
|
Lectura provisional actual:
|
|
|
|
```text
|
|
Sensor al aire:
|
|
soil_raw ≈ 3412
|
|
```
|
|
|
|
Calibración pendiente:
|
|
|
|
```text
|
|
1. Sensor en sustrato seco real
|
|
DRY_SUBSTRATE_RAW = pendiente
|
|
|
|
2. Sensor en sustrato saturado
|
|
WET_SUBSTRATE_RAW = pendiente
|
|
```
|
|
|
|
Procedimiento:
|
|
|
|
```text
|
|
1. Insertar el sensor en el sustrato seco con la profundidad real de uso.
|
|
2. Esperar 30-60 s.
|
|
3. Anotar soil_raw.
|
|
|
|
4. Regar o saturar el sustrato.
|
|
5. Esperar 1-2 min.
|
|
6. Anotar soil_raw.
|
|
|
|
7. Sustituir los valores en el firmware:
|
|
SOIL_DRY_RAW
|
|
SOIL_WET_RAW
|
|
```
|
|
|
|
Valores provisionales usados en el sketch:
|
|
|
|
```cpp
|
|
int SOIL_DRY_RAW = 3412;
|
|
int SOIL_WET_RAW = 1500;
|
|
```
|
|
|
|
Dirección observada:
|
|
|
|
```text
|
|
seco → raw alto
|
|
húmedo → raw bajo
|
|
```
|
|
|
|
## Cálculos del firmware
|
|
|
|
### Humedad relativa del sustrato
|
|
|
|
```text
|
|
soil_index_pct =
|
|
(raw_actual - SOIL_DRY_RAW) / (SOIL_WET_RAW - SOIL_DRY_RAW) * 100
|
|
```
|
|
|
|
Limitado a:
|
|
|
|
```text
|
|
0 % mínimo
|
|
100 % máximo
|
|
```
|
|
|
|
### VPD del aire
|
|
|
|
Se calcula a partir de:
|
|
|
|
```text
|
|
air_temp_c
|
|
air_rh_pct
|
|
```
|
|
|
|
Variable publicada:
|
|
|
|
```text
|
|
vpd_air_kpa
|
|
```
|
|
|
|
### VPD de hoja
|
|
|
|
Se calcula usando:
|
|
|
|
```text
|
|
leaf_temp_c
|
|
air_temp_c
|
|
air_rh_pct
|
|
```
|
|
|
|
Variable publicada:
|
|
|
|
```text
|
|
vpd_leaf_kpa
|
|
```
|
|
|
|
### Delta hoja-aire
|
|
|
|
```text
|
|
leaf_air_delta_c = leaf_temp_c - air_temp_c
|
|
```
|
|
|
|
Interpretación inicial:
|
|
|
|
```text
|
|
leaf_air_delta_c negativo:
|
|
la hoja está más fría que el aire, posible transpiración activa.
|
|
|
|
leaf_air_delta_c cercano a cero:
|
|
hoja y aire muy parecidos.
|
|
|
|
leaf_air_delta_c positivo:
|
|
posible estrés hídrico, mala transpiración, radiación/fondo caliente o montaje mal orientado.
|
|
```
|
|
|
|
No interpretar esta variable sin fijar físicamente el sensor IR apuntando siempre a hoja real.
|
|
|
|
## MQTT
|
|
|
|
### Configuración Atlas Scientific Aquaponics Kit
|
|
|
|
```cpp
|
|
const char* mqtt_host = "mqtt.mesavault.es";
|
|
const uint16_t mqtt_port = 1883;
|
|
|
|
const char* mqtt_user = "sensor_aquaponics_01";
|
|
const char* mqtt_pass = "<MQTT_PASSWORD>";
|
|
|
|
const char* mqtt_client_id = "atlas_kit_01";
|
|
|
|
const char* mqtt_topic =
|
|
"vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry";
|
|
```
|
|
|
|
### Configuración ESP32 Climate Node 01
|
|
|
|
```cpp
|
|
const char* mqtt_host = "mqtt.mesavault.es";
|
|
const uint16_t mqtt_port = 1883;
|
|
|
|
const char* mqtt_user = "sensor_aquaponics_01";
|
|
const char* mqtt_pass = "<MQTT_PASSWORD>";
|
|
|
|
const char* mqtt_client_id = "esp32_climate_node_01";
|
|
|
|
const char* mqtt_topic =
|
|
"vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/telemetry";
|
|
|
|
const char* mqtt_status_topic =
|
|
"vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/status";
|
|
```
|
|
|
|
Regla importante:
|
|
|
|
```text
|
|
No reutilizar el mismo mqtt_client_id del Atlas.
|
|
```
|
|
|
|
Si dos clientes MQTT usan el mismo `client_id`, uno expulsa al otro.
|
|
|
|
## Topics MQTT
|
|
|
|
### Atlas Scientific Aquaponics Kit
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry
|
|
```
|
|
|
|
### ESP32 Climate Node 01
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/telemetry
|
|
```
|
|
|
|
### Estado del ESP32
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/status
|
|
```
|
|
|
|
### Suscripción conjunta
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/+/telemetry
|
|
```
|
|
|
|
## Payload MQTT del ESP32
|
|
|
|
Ejemplo de estructura publicada:
|
|
|
|
```json
|
|
{
|
|
"asset": "esp32_climate_node_01",
|
|
"site": "bench_aquaponics",
|
|
"vertical": "aquaponics",
|
|
"node_type": "esp32_soil_bme280_mlx90614",
|
|
"fw": "0.1.0",
|
|
"uptime_ms": 123456,
|
|
"ts": "2026-05-03T12:00:00Z",
|
|
"wifi_rssi_dbm": -54,
|
|
"bme_ok": true,
|
|
"mlx_ok": true,
|
|
"soil_raw": 3412,
|
|
"soil_voltage": 2.750,
|
|
"soil_index_pct": 0.0,
|
|
"soil_dry_raw": 3412,
|
|
"soil_wet_raw": 1500,
|
|
"air_temp_c": 23.24,
|
|
"air_rh_pct": 71.87,
|
|
"air_pressure_hpa": 999.80,
|
|
"leaf_temp_c": 21.51,
|
|
"ir_ambient_c": 21.63,
|
|
"leaf_air_delta_c": -1.73,
|
|
"vpd_air_kpa": 0.790,
|
|
"vpd_leaf_kpa": 0.530
|
|
}
|
|
```
|
|
|
|
## Verificación MQTT
|
|
|
|
Desde un equipo con `mosquitto_sub`:
|
|
|
|
```bash
|
|
mosquitto_sub -h mqtt.mesavault.es \
|
|
-p 1883 \
|
|
-u sensor_aquaponics_01 \
|
|
-P '<MQTT_PASSWORD>' \
|
|
-t 'vertical/mesavault_lab/bench_aquaponics/aquaponics/+/telemetry' \
|
|
-v
|
|
```
|
|
|
|
Resultado esperado:
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/telemetry {"asset":"esp32_climate_node_01",...}
|
|
```
|
|
|
|
Si no aparece:
|
|
|
|
```text
|
|
1. revisar WiFi del ESP32
|
|
2. revisar usuario/clave MQTT
|
|
3. revisar ACL del broker
|
|
4. revisar topic
|
|
5. revisar si el ESP32 imprime "MQTT OK" por PuTTY
|
|
```
|
|
|
|
## ACL MQTT recomendada
|
|
|
|
### Opción lab
|
|
|
|
```text
|
|
user sensor_aquaponics_01
|
|
topic write vertical/mesavault_lab/bench_aquaponics/aquaponics/+/telemetry
|
|
topic write vertical/mesavault_lab/bench_aquaponics/aquaponics/+/status
|
|
```
|
|
|
|
### Opción restrictiva
|
|
|
|
```text
|
|
user sensor_aquaponics_01
|
|
topic write vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry
|
|
topic write vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/telemetry
|
|
topic write vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/status
|
|
```
|
|
|
|
## Grafana
|
|
|
|
Grafana no ve MQTT automáticamente.
|
|
|
|
### Opción rápida: MQTT datasource
|
|
|
|
Sirve para ver datos en vivo.
|
|
|
|
Limitación:
|
|
|
|
```text
|
|
No guarda histórico.
|
|
Solo muestra mensajes mientras el panel está abierto.
|
|
```
|
|
|
|
Configuración del datasource:
|
|
|
|
```text
|
|
Name: MQTT MESAVAULT
|
|
URI: tcp://mqtt.mesavault.es:1883
|
|
Username: sensor_aquaponics_01
|
|
Password: <MQTT_PASSWORD>
|
|
```
|
|
|
|
Topic del panel:
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/esp32_climate_node_01/telemetry
|
|
```
|
|
|
|
Topic conjunto Atlas + ESP32:
|
|
|
|
```text
|
|
vertical/mesavault_lab/bench_aquaponics/aquaponics/+/telemetry
|
|
```
|
|
|
|
### Opción recomendada para histórico
|
|
|
|
```text
|
|
ESP32 / Atlas
|
|
↓ MQTT
|
|
mqtt.mesavault.es
|
|
↓ sink
|
|
PostgreSQL / TimescaleDB
|
|
↓
|
|
Grafana
|
|
```
|
|
|
|
Variables relevantes para paneles:
|
|
|
|
```text
|
|
soil_index_pct
|
|
soil_raw
|
|
soil_voltage
|
|
air_temp_c
|
|
air_rh_pct
|
|
air_pressure_hpa
|
|
leaf_temp_c
|
|
ir_ambient_c
|
|
leaf_air_delta_c
|
|
vpd_air_kpa
|
|
vpd_leaf_kpa
|
|
wifi_rssi_dbm
|
|
bme_ok
|
|
mlx_ok
|
|
```
|
|
|
|
Paneles iniciales recomendados:
|
|
|
|
```text
|
|
Panel 1: Sustrato
|
|
- soil_index_pct
|
|
- soil_raw
|
|
- soil_voltage
|
|
|
|
Panel 2: Clima
|
|
- air_temp_c
|
|
- air_rh_pct
|
|
- vpd_air_kpa
|
|
|
|
Panel 3: Hoja
|
|
- leaf_temp_c
|
|
- leaf_air_delta_c
|
|
- vpd_leaf_kpa
|
|
|
|
Panel 4: Estado del nodo
|
|
- wifi_rssi_dbm
|
|
- bme_ok
|
|
- mlx_ok
|
|
```
|
|
|
|
## Reglas de interpretación
|
|
|
|
### Sustrato
|
|
|
|
```text
|
|
soil_raw alto:
|
|
tendencia seca
|
|
|
|
soil_raw bajo:
|
|
tendencia húmeda
|
|
|
|
soil_index_pct:
|
|
índice relativo calibrado entre seco y saturado
|
|
```
|
|
|
|
No interpretar como VWC real.
|
|
|
|
### Hoja
|
|
|
|
```text
|
|
leaf_air_delta_c = leaf_temp_c - air_temp_c
|
|
```
|
|
|
|
Uso potencial:
|
|
|
|
```text
|
|
VPD alto + soil_index_pct bajo + leaf_air_delta_c subiendo
|
|
→ posible riesgo de estrés hídrico
|
|
|
|
riego ejecutado + soil_index_pct no sube
|
|
→ posible gotero obstruido, mala distribución o sensor mal colocado
|
|
|
|
soil_index_pct alto + drenaje frecuente
|
|
→ posible exceso de riego
|
|
|
|
VPD bajo + humedad alta + poca diferencia hoja-aire
|
|
→ ambiente poco demandante, posible riesgo por exceso de humedad
|
|
```
|
|
|
|
## Montaje físico recomendado
|
|
|
|
### BME280
|
|
|
|
No colocarlo pegado al ESP32 ni dentro de una caja cerrada.
|
|
|
|
```text
|
|
- en zona ventilada
|
|
- protegido de salpicaduras
|
|
- sin sol directo
|
|
- alejado del calor del microcontrolador
|
|
```
|
|
|
|
### MLX90614
|
|
|
|
Debe mirar a hoja real.
|
|
|
|
```text
|
|
- distancia fija
|
|
- ángulo fijo
|
|
- apuntar siempre a una hoja
|
|
- evitar que vea fondo, pared, tubo, agua o sustrato
|
|
- evitar reflejos
|
|
```
|
|
|
|
### Soil moisture
|
|
|
|
```text
|
|
- insertar siempre a la misma profundidad
|
|
- evitar moverlo continuamente
|
|
- no sumergir electrónica ni conector
|
|
- usarlo como tendencia relativa
|
|
- recalibrar si se cambia de sustrato
|
|
```
|
|
|
|
## Problemas conocidos y soluciones
|
|
|
|
### Error al subir firmware
|
|
|
|
Error:
|
|
|
|
```text
|
|
Failed to communicate with the flash chip
|
|
Packet content transfer stopped
|
|
```
|
|
|
|
Solución:
|
|
|
|
```text
|
|
- seleccionar ESP32 Dev Module
|
|
- Upload Speed 115200
|
|
- Flash Frequency 40MHz
|
|
- Flash Mode DIO
|
|
- desconectar sensores
|
|
- cerrar PuTTY o cualquier monitor serie antes de subir
|
|
```
|
|
|
|
### No aparece texto en Arduino Serial Monitor
|
|
|
|
Diagnóstico:
|
|
|
|
```text
|
|
El LED parpadea, pero no se ve texto.
|
|
```
|
|
|
|
Solución:
|
|
|
|
```text
|
|
Usar PuTTY en COM4 a 115200 baudios.
|
|
```
|
|
|
|
### El sketch no sube
|
|
|
|
Comprobar:
|
|
|
|
```text
|
|
- PuTTY cerrado
|
|
- puerto COM correcto
|
|
- cable USB de datos
|
|
- BOOT no pulsado permanentemente
|
|
- sensores desconectados si hay dudas
|
|
```
|
|
|
|
### BME280 no detectado
|
|
|
|
Comprobar:
|
|
|
|
```text
|
|
VCC → 3V3
|
|
GND → GND
|
|
SDA → GPIO21
|
|
SCL → GPIO22
|
|
dirección 0x76 o 0x77
|
|
```
|
|
|
|
### MLX90614 no detectado
|
|
|
|
Comprobar:
|
|
|
|
```text
|
|
VCC → 3V3
|
|
GND → GND
|
|
SDA → GPIO21
|
|
SCL → GPIO22
|
|
dirección 0x5A
|
|
```
|
|
|
|
### Soil raw siempre 0 o 4095
|
|
|
|
Comprobar:
|
|
|
|
```text
|
|
VCC → 3V3
|
|
GND → GND
|
|
AOUT → GPIO34
|
|
sensor no alimentado a 5V si la señal excede 3,3V
|
|
```
|
|
|
|
## Firmware actual
|
|
|
|
Pegar aquí el firmware actual del nodo `esp32_climate_node_01`.
|
|
|
|
No incluir secretos reales. Sustituir contraseñas, tokens o claves por marcadores como:
|
|
|
|
```text
|
|
<WIFI_PASSWORD>
|
|
<MQTT_PASSWORD>
|
|
<API_TOKEN>
|
|
```
|
|
|
|
```cpp
|
|
|
|
#include <WiFi.h>
|
|
#include <Wire.h>
|
|
#include <PubSubClient.h>
|
|
#include <Adafruit_Sensor.h>
|
|
#include <Adafruit_BME280.h>
|
|
#include <Adafruit_MLX90614.h>
|
|
#include <time.h>
|
|
|
|
// ======================================================
|
|
// WIFI
|
|
// ======================================================
|
|
|
|
const char* wifi_ssid = "TP-Link_AF90";
|
|
const char* wifi_pass = "84604422";
|
|
|
|
// ======================================================
|
|
// MQTT MESAVAULT
|
|
// ======================================================
|
|
|
|
const char* mqtt_host = "mqtt.mesavault.es";
|
|
const uint16_t mqtt_port = 1883;
|
|
|
|
const char* mqtt_user = "sensor_climate_node_01";
|
|
const char* mqtt_pass = "06Xq9vll5RtV";
|
|
|
|
const char* mqtt_client_id = "esp32_climate_node_01";
|
|
|
|
const char* mqtt_topic =
|
|
"vertical/mesavault_lab/bench_aquaponics/climate/esp32_climate_node_01/telemetry";
|
|
|
|
const char* mqtt_state_topic =
|
|
"vertical/mesavault_lab/bench_aquaponics/climate/esp32_climate_node_01/state";
|
|
|
|
// ======================================================
|
|
// HARDWARE
|
|
// ======================================================
|
|
|
|
#define I2C_SDA 21
|
|
#define I2C_SCL 22
|
|
|
|
#define SOIL_PIN 34
|
|
#define LED_PIN 2
|
|
|
|
// ======================================================
|
|
// CALIBRACIÓN HUMEDAD SUSTRATO
|
|
// ======================================================
|
|
//
|
|
// Ahora mismo has medido al aire:
|
|
// soil_raw ≈ 3412
|
|
//
|
|
// Esto NO es todavía el valor seco real del sustrato.
|
|
// Cuando calibres en sustrato seco y saturado, sustituye estos valores.
|
|
//
|
|
// Dirección observada:
|
|
// seco -> raw alto
|
|
// húmedo -> raw bajo
|
|
|
|
int SOIL_DRY_RAW = 3412; // provisional: lectura al aire
|
|
int SOIL_WET_RAW = 1500; // provisional: ajustar con sustrato saturado real
|
|
|
|
// ======================================================
|
|
// INTERVALOS
|
|
// ======================================================
|
|
|
|
const unsigned long PUBLISH_INTERVAL_MS = 10000;
|
|
const unsigned long MQTT_RETRY_INTERVAL_MS = 5000;
|
|
const unsigned long WIFI_RETRY_INTERVAL_MS = 10000;
|
|
|
|
// ======================================================
|
|
// OBJETOS
|
|
// ======================================================
|
|
|
|
WiFiClient wifiClient;
|
|
PubSubClient mqtt(wifiClient);
|
|
|
|
Adafruit_BME280 bme;
|
|
Adafruit_MLX90614 mlx = Adafruit_MLX90614();
|
|
|
|
bool bme_ok = false;
|
|
bool mlx_ok = false;
|
|
|
|
unsigned long last_publish_ms = 0;
|
|
unsigned long last_mqtt_retry_ms = 0;
|
|
unsigned long last_wifi_retry_ms = 0;
|
|
|
|
// ======================================================
|
|
// FUNCIONES VPD
|
|
// ======================================================
|
|
|
|
float saturationVaporPressureKpa(float tempC) {
|
|
return 0.6108 * exp((17.27 * tempC) / (tempC + 237.3));
|
|
}
|
|
|
|
float calculateAirVpdKpa(float airTempC, float rhPct) {
|
|
float svp = saturationVaporPressureKpa(airTempC);
|
|
float avp = svp * (rhPct / 100.0);
|
|
float vpd = svp - avp;
|
|
if (vpd < 0) vpd = 0;
|
|
return vpd;
|
|
}
|
|
|
|
float calculateLeafVpdKpa(float leafTempC, float airTempC, float rhPct) {
|
|
float svpLeaf = saturationVaporPressureKpa(leafTempC);
|
|
float svpAir = saturationVaporPressureKpa(airTempC);
|
|
float avpAir = svpAir * (rhPct / 100.0);
|
|
|
|
float vpdLeaf = svpLeaf - avpAir;
|
|
if (vpdLeaf < 0) vpdLeaf = 0;
|
|
|
|
return vpdLeaf;
|
|
}
|
|
|
|
// ======================================================
|
|
// HUMEDAD SUSTRATO
|
|
// ======================================================
|
|
|
|
int readSoilRawAverage() {
|
|
const int samples = 30;
|
|
long acc = 0;
|
|
|
|
for (int i = 0; i < samples; i++) {
|
|
acc += analogRead(SOIL_PIN);
|
|
delay(4);
|
|
}
|
|
|
|
return acc / samples;
|
|
}
|
|
|
|
float soilIndexPct(int raw) {
|
|
if (SOIL_DRY_RAW == SOIL_WET_RAW) {
|
|
return 0.0;
|
|
}
|
|
|
|
float pct =
|
|
((float)(raw - SOIL_DRY_RAW) / (float)(SOIL_WET_RAW - SOIL_DRY_RAW)) * 100.0;
|
|
|
|
if (pct < 0) pct = 0;
|
|
if (pct > 100) pct = 100;
|
|
|
|
return pct;
|
|
}
|
|
|
|
// ======================================================
|
|
// TIEMPO
|
|
// ======================================================
|
|
|
|
String isoTimestampUtc() {
|
|
time_t now = time(nullptr);
|
|
|
|
if (now < 1700000000) {
|
|
return "";
|
|
}
|
|
|
|
struct tm timeinfo;
|
|
gmtime_r(&now, &timeinfo);
|
|
|
|
char buffer[25];
|
|
strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
|
|
|
|
return String(buffer);
|
|
}
|
|
|
|
// ======================================================
|
|
// I2C
|
|
// ======================================================
|
|
|
|
void scanI2C() {
|
|
Serial.println("Escaneando I2C...");
|
|
|
|
int count = 0;
|
|
|
|
for (byte addr = 1; addr < 127; addr++) {
|
|
Wire.beginTransmission(addr);
|
|
byte error = Wire.endTransmission();
|
|
|
|
if (error == 0) {
|
|
Serial.print("I2C encontrado en 0x");
|
|
if (addr < 16) Serial.print("0");
|
|
Serial.println(addr, HEX);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
Serial.print("Total I2C encontrados: ");
|
|
Serial.println(count);
|
|
}
|
|
|
|
// ======================================================
|
|
// WIFI
|
|
// ======================================================
|
|
|
|
void connectWiFiIfNeeded() {
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
return;
|
|
}
|
|
|
|
unsigned long now = millis();
|
|
|
|
if (now - last_wifi_retry_ms < WIFI_RETRY_INTERVAL_MS) {
|
|
return;
|
|
}
|
|
|
|
last_wifi_retry_ms = now;
|
|
|
|
Serial.print("Conectando WiFi a ");
|
|
Serial.println(wifi_ssid);
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(wifi_ssid, wifi_pass);
|
|
|
|
unsigned long start = millis();
|
|
|
|
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
}
|
|
|
|
Serial.println();
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.print("WiFi OK. IP: ");
|
|
Serial.println(WiFi.localIP());
|
|
|
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
|
} else {
|
|
Serial.println("WiFi KO.");
|
|
}
|
|
}
|
|
|
|
// ======================================================
|
|
// MQTT
|
|
// ======================================================
|
|
|
|
void connectMQTTIfNeeded() {
|
|
if (mqtt.connected()) {
|
|
return;
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
return;
|
|
}
|
|
|
|
unsigned long now = millis();
|
|
|
|
if (now - last_mqtt_retry_ms < MQTT_RETRY_INTERVAL_MS) {
|
|
return;
|
|
}
|
|
|
|
last_mqtt_retry_ms = now;
|
|
|
|
Serial.print("Conectando MQTT a ");
|
|
Serial.print(mqtt_host);
|
|
Serial.print(":");
|
|
Serial.println(mqtt_port);
|
|
|
|
mqtt.setServer(mqtt_host, mqtt_port);
|
|
mqtt.setBufferSize(1200);
|
|
mqtt.setKeepAlive(30);
|
|
mqtt.setSocketTimeout(10);
|
|
|
|
bool ok = mqtt.connect(
|
|
mqtt_client_id,
|
|
mqtt_user,
|
|
mqtt_pass,
|
|
mqtt_state_topic,
|
|
0,
|
|
true,
|
|
"offline"
|
|
);
|
|
|
|
if (ok) {
|
|
Serial.println("MQTT OK.");
|
|
mqtt.publish(mqtt_state_topic, "online", true);
|
|
} else {
|
|
Serial.print("MQTT KO. state=");
|
|
Serial.println(mqtt.state());
|
|
}
|
|
}
|
|
|
|
// ======================================================
|
|
// SETUP
|
|
// ======================================================
|
|
|
|
void setup() {
|
|
pinMode(LED_PIN, OUTPUT);
|
|
|
|
Serial.begin(115200);
|
|
delay(3000);
|
|
|
|
Serial.println();
|
|
Serial.println("");
|
|
Serial.println("MESAVAULT ESP32 CLIMATE NODE 01");
|
|
Serial.println("Soil + BME280 + MLX90614 + MQTT");
|
|
Serial.println("");
|
|
|
|
analogReadResolution(12);
|
|
analogSetPinAttenuation(SOIL_PIN, ADC_11db);
|
|
|
|
Wire.begin(I2C_SDA, I2C_SCL);
|
|
delay(500);
|
|
|
|
scanI2C();
|
|
|
|
bme_ok = bme.begin(0x76, &Wire);
|
|
if (!bme_ok) {
|
|
bme_ok = bme.begin(0x77, &Wire);
|
|
}
|
|
|
|
if (bme_ok) {
|
|
Serial.println("BME280 OK.");
|
|
} else {
|
|
Serial.println("BME280 NO DETECTADO.");
|
|
}
|
|
|
|
mlx_ok = mlx.begin(0x5A, &Wire);
|
|
|
|
if (mlx_ok) {
|
|
Serial.println("MLX90614 OK.");
|
|
} else {
|
|
Serial.println("MLX90614 NO DETECTADO.");
|
|
}
|
|
|
|
connectWiFiIfNeeded();
|
|
connectMQTTIfNeeded();
|
|
|
|
Serial.println("Setup terminado.");
|
|
}
|
|
|
|
// ======================================================
|
|
// LOOP
|
|
// ======================================================
|
|
|
|
void loop() {
|
|
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
|
|
|
|
connectWiFiIfNeeded();
|
|
connectMQTTIfNeeded();
|
|
|
|
if (mqtt.connected()) {
|
|
mqtt.loop();
|
|
}
|
|
|
|
unsigned long now = millis();
|
|
|
|
if (now - last_publish_ms < PUBLISH_INTERVAL_MS) {
|
|
delay(50);
|
|
return;
|
|
}
|
|
|
|
last_publish_ms = now;
|
|
|
|
// -----------------------------
|
|
// Lectura humedad sustrato
|
|
// -----------------------------
|
|
|
|
int soil_raw = readSoilRawAverage();
|
|
float soil_voltage = soil_raw * 3.3 / 4095.0;
|
|
float soil_index_pct = soilIndexPct(soil_raw);
|
|
|
|
// -----------------------------
|
|
// Lectura BME280
|
|
// -----------------------------
|
|
|
|
float air_temp_c = NAN;
|
|
float air_rh_pct = NAN;
|
|
float air_pressure_hpa = NAN;
|
|
|
|
if (bme_ok) {
|
|
air_temp_c = bme.readTemperature();
|
|
air_rh_pct = bme.readHumidity();
|
|
air_pressure_hpa = bme.readPressure() / 100.0;
|
|
}
|
|
|
|
// -----------------------------
|
|
// Lectura MLX90614
|
|
// -----------------------------
|
|
|
|
float leaf_temp_c = NAN;
|
|
float ir_ambient_c = NAN;
|
|
|
|
if (mlx_ok) {
|
|
leaf_temp_c = mlx.readObjectTempC();
|
|
ir_ambient_c = mlx.readAmbientTempC();
|
|
}
|
|
|
|
// -----------------------------
|
|
// Cálculos
|
|
// -----------------------------
|
|
|
|
float vpd_air_kpa = NAN;
|
|
float vpd_leaf_kpa = NAN;
|
|
float leaf_air_delta_c = NAN;
|
|
|
|
if (bme_ok && !isnan(air_temp_c) && !isnan(air_rh_pct)) {
|
|
vpd_air_kpa = calculateAirVpdKpa(air_temp_c, air_rh_pct);
|
|
}
|
|
|
|
if (bme_ok && mlx_ok && !isnan(leaf_temp_c) && !isnan(air_temp_c) && !isnan(air_rh_pct)) {
|
|
leaf_air_delta_c = leaf_temp_c - air_temp_c;
|
|
vpd_leaf_kpa = calculateLeafVpdKpa(leaf_temp_c, air_temp_c, air_rh_pct);
|
|
}
|
|
|
|
String ts = isoTimestampUtc();
|
|
|
|
// -----------------------------
|
|
// JSON MQTT
|
|
// -----------------------------
|
|
|
|
String payload;
|
|
payload.reserve(1300);
|
|
|
|
payload += "{";
|
|
|
|
payload += ""tenant":"mesavault_lab",";
|
|
payload += ""site":"bench_aquaponics",";
|
|
payload += ""product":"climate",";
|
|
payload += ""asset":"esp32_climate_node_01",";
|
|
payload += ""node_type":"esp32_soil_bme280_mlx90614",";
|
|
payload += ""fw":"0.1.0",";
|
|
payload += ""uptime_ms":";
|
|
payload += String(millis());
|
|
payload += ",";
|
|
|
|
payload += ""ts":"";
|
|
payload += ts;
|
|
payload += "",";
|
|
|
|
payload += ""wifi_rssi_dbm":";
|
|
payload += String(WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0);
|
|
payload += ",";
|
|
|
|
payload += ""bme_ok":";
|
|
payload += bme_ok ? "true" : "false";
|
|
payload += ",";
|
|
|
|
payload += ""mlx_ok":";
|
|
payload += mlx_ok ? "true" : "false";
|
|
payload += ",";
|
|
|
|
payload += ""soil_raw":";
|
|
payload += String(soil_raw);
|
|
payload += ",";
|
|
|
|
payload += ""soil_voltage":";
|
|
payload += String(soil_voltage, 3);
|
|
payload += ",";
|
|
|
|
payload += ""soil_index_pct":";
|
|
payload += String(soil_index_pct, 1);
|
|
payload += ",";
|
|
|
|
payload += ""soil_dry_raw":";
|
|
payload += String(SOIL_DRY_RAW);
|
|
payload += ",";
|
|
|
|
payload += ""soil_wet_raw":";
|
|
payload += String(SOIL_WET_RAW);
|
|
payload += ",";
|
|
|
|
payload += ""air_temp_c":";
|
|
if (isnan(air_temp_c)) payload += "null"; else payload += String(air_temp_c, 2);
|
|
payload += ",";
|
|
|
|
payload += ""air_humidity_pct":";
|
|
if (isnan(air_rh_pct)) payload += "null"; else payload += String(air_rh_pct, 2);
|
|
payload += ",";
|
|
|
|
payload += ""pressure_hpa":";
|
|
if (isnan(air_pressure_hpa)) payload += "null"; else payload += String(air_pressure_hpa, 2);
|
|
payload += ",";
|
|
|
|
payload += ""leaf_temp_c":";
|
|
if (isnan(leaf_temp_c)) payload += "null"; else payload += String(leaf_temp_c, 2);
|
|
payload += ",";
|
|
|
|
payload += ""mlx_ambient_c":";
|
|
if (isnan(ir_ambient_c)) payload += "null"; else payload += String(ir_ambient_c, 2);
|
|
payload += ",";
|
|
|
|
payload += ""leaf_air_delta_c":";
|
|
if (isnan(leaf_air_delta_c)) payload += "null"; else payload += String(leaf_air_delta_c, 2);
|
|
payload += ",";
|
|
|
|
payload += ""vpd_kpa":";
|
|
if (isnan(vpd_air_kpa)) payload += "null"; else payload += String(vpd_air_kpa, 3);
|
|
payload += ",";
|
|
|
|
payload += ""vpd_leaf_kpa":";
|
|
if (isnan(vpd_leaf_kpa)) payload += "null"; else payload += String(vpd_leaf_kpa, 3);
|
|
|
|
payload += "}";
|
|
|
|
// -----------------------------
|
|
// Debug por Serial / PuTTY
|
|
// -----------------------------
|
|
|
|
Serial.println();
|
|
Serial.println("----- LECTURA -----");
|
|
|
|
Serial.print("soil_raw=");
|
|
Serial.print(soil_raw);
|
|
Serial.print(" soil_voltage=");
|
|
Serial.print(soil_voltage, 3);
|
|
Serial.print(" soil_index_pct=");
|
|
Serial.println(soil_index_pct, 1);
|
|
|
|
if (bme_ok) {
|
|
Serial.print("air_temp_c=");
|
|
Serial.print(air_temp_c, 2);
|
|
Serial.print(" air_rh_pct=");
|
|
Serial.print(air_rh_pct, 2);
|
|
Serial.print(" pressure_hpa=");
|
|
Serial.println(air_pressure_hpa, 2);
|
|
} else {
|
|
Serial.println("BME280 sin lectura.");
|
|
}
|
|
|
|
if (mlx_ok) {
|
|
Serial.print("leaf_temp_c=");
|
|
Serial.print(leaf_temp_c, 2);
|
|
Serial.print(" ir_ambient_c=");
|
|
Serial.print(ir_ambient_c, 2);
|
|
Serial.print(" leaf_air_delta_c=");
|
|
Serial.println(leaf_air_delta_c, 2);
|
|
} else {
|
|
Serial.println("MLX90614 sin lectura.");
|
|
}
|
|
|
|
Serial.print("vpd_air_kpa=");
|
|
if (isnan(vpd_air_kpa)) Serial.print("null"); else Serial.print(vpd_air_kpa, 3);
|
|
|
|
Serial.print(" vpd_leaf_kpa=");
|
|
if (isnan(vpd_leaf_kpa)) Serial.println("null"); else Serial.println(vpd_leaf_kpa, 3);
|
|
|
|
Serial.println("Payload:");
|
|
Serial.println(payload);
|
|
|
|
// -----------------------------
|
|
// Publicación MQTT
|
|
// -----------------------------
|
|
|
|
if (mqtt.connected()) {
|
|
bool ok = mqtt.publish(mqtt_topic, payload.c_str(), false);
|
|
|
|
if (ok) {
|
|
Serial.print("MQTT publicado en: ");
|
|
Serial.println(mqtt_topic);
|
|
} else {
|
|
Serial.println("MQTT publish KO.");
|
|
}
|
|
} else {
|
|
Serial.println("MQTT no conectado. No se publica.");
|
|
}
|
|
|
|
delay(50);
|
|
}
|
|
```
|
|
|
|
## Checklist operativo
|
|
|
|
```text
|
|
[ ] Firmware cargado con Board = ESP32 Dev Module
|
|
[ ] Upload Speed = 115200
|
|
[ ] Flash Frequency = 40MHz
|
|
[ ] Flash Mode = DIO
|
|
[ ] PuTTY cerrado antes de subir firmware
|
|
[ ] PuTTY abierto después de subir firmware
|
|
[ ] BME280 detectado en 0x76 o 0x77
|
|
[ ] MLX90614 detectado en 0x5A
|
|
[ ] Soil moisture leyendo por GPIO34
|
|
[ ] soil_raw no queda fijo en 0 o 4095
|
|
[ ] MQTT usa client_id esp32_climate_node_01
|
|
[ ] MQTT no reutiliza client_id atlas_kit_01
|
|
[ ] Telemetría visible con mosquitto_sub
|
|
[ ] Grafana muestra datos en vivo
|
|
[ ] Contraseña MQTT real almacenada fuera de la documentación
|
|
[ ] SOIL_DRY_RAW calibrado en sustrato seco real
|
|
[ ] SOIL_WET_RAW calibrado en sustrato saturado
|
|
[ ] MLX90614 fijado apuntando a hoja real
|
|
```
|
|
|
|
## Huecos y validaciones pendientes
|
|
|
|
```text
|
|
- no se incluye todavía el firmware actual completo
|
|
- no se especifica versión exacta de librerías Arduino usadas
|
|
- no se especifica modelo exacto confirmado del sensor IR; se asume MLX90614 / GY-906
|
|
- no se documenta configuración WiFi
|
|
- no se documenta contraseña MQTT real, correctamente omitida
|
|
- no se confirma ACL realmente aplicada en el broker MQTT
|
|
- no se confirma recepción MQTT con mosquitto_sub
|
|
- no se confirma visualización real en Grafana
|
|
- no se documenta sink MQTT → PostgreSQL / TimescaleDB
|
|
- no se documenta dashboard final de Grafana
|
|
- no se documenta ubicación física definitiva del nodo
|
|
- no se documenta distancia ni ángulo final del MLX90614
|
|
- no se documenta calibración real de SOIL_DRY_RAW
|
|
- no se documenta calibración real de SOIL_WET_RAW
|
|
- no se documenta procedimiento de backup del firmware
|
|
- no se documenta rollback formal a una versión anterior del firmware
|
|
```
|
|
|
|
## Acciones recomendadas
|
|
|
|
```text
|
|
- guardar contraseña MQTT real en Vaultwarden
|
|
- pegar el firmware actual saneado en la sección "Firmware actual"
|
|
- versionar el firmware como archivo separado si se desea mantener trazabilidad de código
|
|
- crear ADR sobre separación Atlas / ESP32 por responsabilidad de sensorización
|
|
- crear documento de arquitectura MQTT → PostgreSQL / TimescaleDB → Grafana
|
|
- crear runbook específico de calibración de sensores de sustrato
|
|
- crear runbook de troubleshooting MQTT MESAVAULT
|
|
```
|
|
|
|
```
|
|
```
|