From 39c4a039610167fcfd49a30feb1e296846ff1b79 Mon Sep 17 00:00:00 2001 From: "victor.fraile" Date: Sun, 3 May 2026 18:03:24 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20esp32-climate-node-01.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esp32-climate-node-01.md | 1464 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1464 insertions(+) create mode 100644 esp32-climate-node-01.md diff --git a/esp32-climate-node-01.md b/esp32-climate-node-01.md new file mode 100644 index 0000000..8dc65f8 --- /dev/null +++ b/esp32-climate-node-01.md @@ -0,0 +1,1464 @@ +# 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 = ""; + +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 = ""; + +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 '' \ + -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: +``` + +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 + + + +``` + +```cpp + +#include +#include +#include +#include +#include +#include +#include + +// ====================================================== +// 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 +``` + +``` +```