diff --git a/esp32-climate-node-01.md b/esp32-climate-node-01.md deleted file mode 100644 index 8dc65f8..0000000 --- a/esp32-climate-node-01.md +++ /dev/null @@ -1,1464 +0,0 @@ -# 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 -``` - -``` -```