2427 lines
46 KiB
Markdown
2427 lines
46 KiB
Markdown
REPOSITORIO
|
||
|
||
`mesavault-knowledge`
|
||
|
||
RUTA_FINAL
|
||
|
||
`runbooks/hydro-aquaponics-climate/atlas-scientific-wifi-aquaponics-kit.md`
|
||
|
||
CONTENIDO
|
||
|
||
# Runbook operativo: Atlas Scientific WiFi Aquaponics Kit
|
||
|
||
```yaml
|
||
doc_id: runbook-hydro-aquaponics-climate-atlas-kit-01
|
||
doc_type: runbook
|
||
system: hydro-aquaponics-climate
|
||
environment: lab
|
||
asset: atlas_kit_01
|
||
tags:
|
||
- hydro
|
||
- aquaponics
|
||
- atlas-scientific
|
||
- mqtt
|
||
- i2c
|
||
- ezo
|
||
- bme280
|
||
- mlx90614
|
||
- grafana
|
||
- postgresql
|
||
source_date: null
|
||
last_verified: null
|
||
owner: pendiente_de_confirmar
|
||
sensitivity: pendiente_de_confirmar
|
||
canonical: pendiente_de_confirmar
|
||
related_docs: []
|
||
```
|
||
|
||
## Resumen ejecutivo
|
||
|
||
El `Atlas Scientific WiFi Aquaponics Kit` se usa como nodo principal de medición para la vertical `Hydro / Aquaponics Climate`.
|
||
|
||
La configuración estable actual mide parámetros de agua, aire y planta, y publica telemetría por MQTT hacia el stack MESAVAULT.
|
||
|
||
ThingSpeak queda eliminado. El kit publica directamente en MESAVAULT.
|
||
|
||
Flujo operativo:
|
||
|
||
```text
|
||
Aquaponics Kit
|
||
↓ WiFi
|
||
mqtt.mesavault.es:1883
|
||
↓
|
||
OVH
|
||
↓ bridge MQTT por Tailscale
|
||
platform-40
|
||
↓
|
||
PostgreSQL
|
||
↓
|
||
Grafana
|
||
```
|
||
|
||
## Estado actual
|
||
|
||
### Sensores activos
|
||
|
||
```text
|
||
- pH EZO
|
||
- EC EZO
|
||
- DO EZO
|
||
- RTD EZO
|
||
- BME280
|
||
- MLX90614 IR
|
||
```
|
||
|
||
### Sensores retirados
|
||
|
||
```text
|
||
- CO2 EZO
|
||
- HUM EZO del kit Atlas
|
||
```
|
||
|
||
Motivo:
|
||
|
||
```text
|
||
El bus I2C se degrada cuando se conectan demasiados dispositivos.
|
||
El CO2 y/o HUM provocaban fallos de detección, lecturas NaN, direcciones raras y pérdida del MLX90614.
|
||
```
|
||
|
||
Configuración válida actual:
|
||
|
||
```text
|
||
pH + DO + RTD + EC + BME280 + MLX90614
|
||
```
|
||
|
||
Configuración no recomendada:
|
||
|
||
```text
|
||
pH + DO + RTD + EC + HUM + CO2 + BME280 + MLX90614
|
||
```
|
||
|
||
## Firmware
|
||
|
||
El firmware completo debe pegarse exclusivamente entre los marcadores siguientes.
|
||
|
||
No incluir credenciales reales, tokens, claves privadas, contraseñas WiFi ni secretos MQTT en este archivo. Mantener placeholders y guardar secretos reales en Vaultwarden.
|
||
|
||
<!-- BEGIN_FIRMWARE_ATLAS_KIT_01 -->
|
||
|
||
```cpp
|
||
#include <iot_cmd.h>
|
||
#include <WiFi.h>
|
||
#include <PubSubClient.h>
|
||
#include <Ezo_i2c_util.h>
|
||
#include <Ezo_i2c.h>
|
||
#include <Wire.h>
|
||
#include <Adafruit_Sensor.h>
|
||
#include <Adafruit_BME280.h>
|
||
#include <Adafruit_MLX90614.h>
|
||
#include <math.h>
|
||
|
||
// ======================================================
|
||
// WIFI + MQTT MESAVAULT
|
||
// ======================================================
|
||
|
||
WiFiClient wifiClient;
|
||
PubSubClient mqtt(wifiClient);
|
||
|
||
// ---------- WiFi ----------
|
||
const char* ssid = "...";
|
||
const char* pass = "PASSWORD_EN_VAULTWARDEN";
|
||
|
||
// ---------- MQTT MESAVAULT ----------
|
||
const char* mqtt_host = "mqtt.mesavault.es";
|
||
const uint16_t mqtt_port = 1883;
|
||
|
||
const char* mqtt_user = "sensor_aquaponics_01";
|
||
const char* mqtt_pass = "PASSWORD_EN_VAULTWARDEN";
|
||
|
||
const char* mqtt_client_id = "atlas_kit_01";
|
||
|
||
const char* mqtt_topic =
|
||
"vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry";
|
||
|
||
// MQTT activo desde arranque
|
||
const bool MQTT_AUTOSTART = true;
|
||
|
||
// ======================================================
|
||
// TIMING
|
||
// ======================================================
|
||
|
||
// Espera de respuesta EZO
|
||
const unsigned long EZO_RESPONSE_DELAY_MS = 1800;
|
||
|
||
// Espera específica para RTD
|
||
const unsigned long RTD_RESPONSE_DELAY_MS = 2000;
|
||
|
||
// Pausa entre sensores
|
||
const unsigned long SENSOR_GAP_MS = 300;
|
||
|
||
// Pausa entre compensaciones
|
||
const unsigned long COMPENSATION_GAP_MS = 250;
|
||
|
||
// Intervalo entre ciclos completos
|
||
unsigned long sensor_cycle_interval_ms = 12000;
|
||
|
||
// Intervalo mínimo entre publicaciones MQTT
|
||
const unsigned long mqtt_publish_interval_ms = 5000;
|
||
|
||
unsigned long last_sensor_cycle_ms = 0;
|
||
unsigned long last_mqtt_publish_ms = 0;
|
||
unsigned long last_wifi_check_ms = 0;
|
||
unsigned long last_mqtt_check_ms = 0;
|
||
|
||
bool first_sensor_cycle = true;
|
||
|
||
// ======================================================
|
||
// ATLAS EZO BOARDS
|
||
// ======================================================
|
||
|
||
Ezo_board PH = Ezo_board(99, "PH"); // 0x63
|
||
Ezo_board DO = Ezo_board(97, "DO"); // 0x61
|
||
Ezo_board RTD = Ezo_board(102, "RTD"); // 0x66
|
||
Ezo_board EC = Ezo_board(100, "EC"); // 0x64
|
||
Ezo_board PMP = Ezo_board(103, "PMP"); // 0x67, no usado
|
||
|
||
bool ph_ok = false;
|
||
bool do_ok = false;
|
||
bool rtd_ok = false;
|
||
bool ec_ok = false;
|
||
|
||
// ======================================================
|
||
// BME280 + MLX90614
|
||
// ======================================================
|
||
|
||
#define BME_ADDR 0x76
|
||
#define MLX_ADDR 0x5A
|
||
|
||
Adafruit_BME280 bme;
|
||
Adafruit_MLX90614 mlx = Adafruit_MLX90614();
|
||
|
||
bool bme_ok = false;
|
||
bool mlx_ok = false;
|
||
|
||
float bme_air_temp = NAN;
|
||
float bme_air_hum = NAN;
|
||
float bme_pressure_hpa = NAN;
|
||
float bme_vpd_kpa = NAN;
|
||
|
||
float mlx_leaf_temp_c = NAN;
|
||
float mlx_ambient_c = NAN;
|
||
float leaf_air_delta_c = NAN;
|
||
|
||
// ======================================================
|
||
// ÚLTIMAS LECTURAS NORMALIZADAS
|
||
// ======================================================
|
||
|
||
float ph_value = NAN;
|
||
float do_value = NAN;
|
||
float rtd_value = NAN;
|
||
float ec_value = NAN;
|
||
|
||
// ======================================================
|
||
// DEVICE LIST PARA COMANDOS EZO
|
||
// ======================================================
|
||
|
||
Ezo_board device_list[] = {
|
||
PH,
|
||
DO,
|
||
RTD,
|
||
EC,
|
||
PMP
|
||
};
|
||
|
||
Ezo_board* default_board = &device_list[0];
|
||
const uint8_t device_list_len = sizeof(device_list) / sizeof(device_list[0]);
|
||
|
||
// ======================================================
|
||
// ENABLE PINS
|
||
// ======================================================
|
||
|
||
// ------ Board version 1.7 ------
|
||
const int EN_PH = 13;
|
||
const int EN_DO = 12;
|
||
const int EN_RTD = 33;
|
||
const int EN_EC = 27;
|
||
const int EN_HUM = 32;
|
||
const int EN_CO2 = 15;
|
||
|
||
// ------ Board version 1.8 ------
|
||
// const int EN_PH = 12;
|
||
// const int EN_DO = 27;
|
||
// const int EN_RTD = 15;
|
||
// const int EN_EC = 33;
|
||
// const int EN_HUM = 14;
|
||
// const int EN_CO2 = 32;
|
||
|
||
// ======================================================
|
||
// PUMP CONFIGURATION
|
||
// ======================================================
|
||
// Bomba desactivada en este sketch.
|
||
|
||
#define PUMP_BOARD PMP
|
||
#define PUMP_DOSE -0.5
|
||
#define EZO_BOARD EC
|
||
#define IS_GREATER_THAN true
|
||
#define COMPARISON_VALUE 1000
|
||
|
||
// ======================================================
|
||
// GLOBAL STATE
|
||
// ======================================================
|
||
|
||
float k_val = 0;
|
||
|
||
bool polling = true;
|
||
bool send_to_stack = MQTT_AUTOSTART;
|
||
|
||
// ======================================================
|
||
// FORWARD DECLARATIONS
|
||
// ======================================================
|
||
|
||
bool wifi_isconnected();
|
||
void reconnect_wifi();
|
||
void reconnect_mqtt();
|
||
void service_network();
|
||
void wait_with_network(unsigned long wait_ms);
|
||
|
||
bool i2c_device_present(byte address);
|
||
uint8_t read_i2c_reg8(byte address, byte reg);
|
||
void detect_i2c_devices();
|
||
void refresh_i2c_state();
|
||
void scan_i2c();
|
||
|
||
String jsonFloat(float value, uint8_t decimals);
|
||
String build_telemetry_payload();
|
||
void mqtt_send_if_due();
|
||
|
||
void start_datalogging();
|
||
void run_sensor_cycle();
|
||
bool read_rtd_and_compensate();
|
||
void read_ezo_sensor(Ezo_board &sensor, bool sensor_ok, float &target_value);
|
||
void read_bme280();
|
||
void read_mlx90614();
|
||
|
||
float calc_vpd_kpa(float temp_c, float rh_percent);
|
||
|
||
bool process_coms(const String &string_buffer);
|
||
void get_ec_k_value();
|
||
void print_help();
|
||
|
||
void pump_function(Ezo_board &pump, Ezo_board &sensor, float value, float dose, bool greater_than);
|
||
|
||
// ======================================================
|
||
// WIFI
|
||
// ======================================================
|
||
|
||
bool wifi_isconnected() {
|
||
return (WiFi.status() == WL_CONNECTED);
|
||
}
|
||
|
||
void reconnect_wifi() {
|
||
if (!wifi_isconnected()) {
|
||
WiFi.begin(ssid, pass);
|
||
Serial.println("connecting to wifi");
|
||
}
|
||
}
|
||
|
||
// ======================================================
|
||
// MQTT
|
||
// ======================================================
|
||
|
||
void reconnect_mqtt() {
|
||
if (!wifi_isconnected()) {
|
||
return;
|
||
}
|
||
|
||
if (mqtt.connected()) {
|
||
return;
|
||
}
|
||
|
||
Serial.print("connecting to MQTT ");
|
||
|
||
if (mqtt.connect(mqtt_client_id, mqtt_user, mqtt_pass)) {
|
||
Serial.println("connected");
|
||
} else {
|
||
Serial.print("failed, rc=");
|
||
Serial.println(mqtt.state());
|
||
}
|
||
}
|
||
|
||
void service_network() {
|
||
unsigned long now = millis();
|
||
|
||
if ((now - last_wifi_check_ms) > 10000) {
|
||
last_wifi_check_ms = now;
|
||
reconnect_wifi();
|
||
}
|
||
|
||
if ((now - last_mqtt_check_ms) > 5000) {
|
||
last_mqtt_check_ms = now;
|
||
reconnect_mqtt();
|
||
}
|
||
|
||
if (mqtt.connected()) {
|
||
mqtt.loop();
|
||
}
|
||
}
|
||
|
||
void wait_with_network(unsigned long wait_ms) {
|
||
unsigned long start = millis();
|
||
|
||
while ((millis() - start) < wait_ms) {
|
||
service_network();
|
||
delay(25);
|
||
}
|
||
}
|
||
|
||
// ======================================================
|
||
// JSON + MQTT PUBLISH
|
||
// ======================================================
|
||
|
||
String jsonFloat(float value, uint8_t decimals) {
|
||
if (isnan(value) || isinf(value)) {
|
||
return "null";
|
||
}
|
||
|
||
char buffer[24];
|
||
snprintf(buffer, sizeof(buffer), "%.*f", (int)decimals, (double)value);
|
||
return String(buffer);
|
||
}
|
||
|
||
String build_telemetry_payload() {
|
||
String payload;
|
||
payload.reserve(1100);
|
||
|
||
payload += "{";
|
||
|
||
payload += "\"tenant\":\"mesavault_lab\",";
|
||
payload += "\"site\":\"bench_aquaponics\",";
|
||
payload += "\"product\":\"aquaponics\",";
|
||
payload += "\"asset\":\"atlas_kit_01\",";
|
||
|
||
payload += "\"ph\":";
|
||
payload += jsonFloat(ph_value, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"ec_us_cm\":";
|
||
payload += jsonFloat(ec_value, 0);
|
||
payload += ",";
|
||
|
||
payload += "\"do_mg_l\":";
|
||
payload += jsonFloat(do_value, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"water_temp_c\":";
|
||
payload += jsonFloat(rtd_value, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"hum_atlas_pct\":null,";
|
||
payload += "\"co2_ppm\":null,";
|
||
payload += "\"co2_valid\":false,";
|
||
|
||
payload += "\"air_temp_c\":";
|
||
payload += jsonFloat(bme_air_temp, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"air_humidity_pct\":";
|
||
payload += jsonFloat(bme_air_hum, 1);
|
||
payload += ",";
|
||
|
||
payload += "\"pressure_hpa\":";
|
||
payload += jsonFloat(bme_pressure_hpa, 1);
|
||
payload += ",";
|
||
|
||
payload += "\"vpd_kpa\":";
|
||
payload += jsonFloat(bme_vpd_kpa, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"leaf_temp_c\":";
|
||
payload += jsonFloat(mlx_leaf_temp_c, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"mlx_ambient_c\":";
|
||
payload += jsonFloat(mlx_ambient_c, 2);
|
||
payload += ",";
|
||
|
||
payload += "\"leaf_air_delta_c\":";
|
||
payload += jsonFloat(leaf_air_delta_c, 2);
|
||
|
||
payload += "}";
|
||
|
||
return payload;
|
||
}
|
||
|
||
void mqtt_send_if_due() {
|
||
if (!send_to_stack) {
|
||
return;
|
||
}
|
||
|
||
unsigned long now = millis();
|
||
|
||
if ((now - last_mqtt_publish_ms) < mqtt_publish_interval_ms) {
|
||
return;
|
||
}
|
||
|
||
last_mqtt_publish_ms = now;
|
||
|
||
if (!wifi_isconnected()) {
|
||
Serial.println("wifi not connected, mqtt skipped");
|
||
return;
|
||
}
|
||
|
||
if (!mqtt.connected()) {
|
||
reconnect_mqtt();
|
||
}
|
||
|
||
if (!mqtt.connected()) {
|
||
Serial.println("mqtt not connected, publish skipped");
|
||
return;
|
||
}
|
||
|
||
String payload = build_telemetry_payload();
|
||
|
||
bool ok = mqtt.publish(mqtt_topic, payload.c_str(), false);
|
||
|
||
if (ok) {
|
||
Serial.print("sent to MESAVAULT MQTT: ");
|
||
Serial.println(payload);
|
||
} else {
|
||
Serial.println("couldnt send to MESAVAULT MQTT");
|
||
}
|
||
}
|
||
|
||
// ======================================================
|
||
// SETUP
|
||
// ======================================================
|
||
|
||
void setup() {
|
||
pinMode(EN_PH, OUTPUT);
|
||
pinMode(EN_DO, OUTPUT);
|
||
pinMode(EN_RTD, OUTPUT);
|
||
pinMode(EN_EC, OUTPUT);
|
||
pinMode(EN_HUM, OUTPUT);
|
||
pinMode(EN_CO2, OUTPUT);
|
||
|
||
digitalWrite(EN_PH, LOW);
|
||
digitalWrite(EN_DO, LOW);
|
||
digitalWrite(EN_RTD, HIGH);
|
||
digitalWrite(EN_EC, LOW);
|
||
|
||
// HUM y CO2 deshabilitados
|
||
digitalWrite(EN_HUM, LOW);
|
||
digitalWrite(EN_CO2, LOW);
|
||
|
||
Serial.begin(9600);
|
||
delay(500);
|
||
|
||
Wire.begin();
|
||
|
||
// Muy conservador para este bus compartido
|
||
Wire.setClock(10000);
|
||
|
||
// Tiempo para que el hardware quede estable
|
||
delay(2000);
|
||
|
||
scan_i2c();
|
||
refresh_i2c_state();
|
||
|
||
WiFi.mode(WIFI_STA);
|
||
WiFi.setSleep(false);
|
||
|
||
mqtt.setServer(mqtt_host, mqtt_port);
|
||
mqtt.setBufferSize(1536);
|
||
mqtt.setKeepAlive(30);
|
||
|
||
reconnect_wifi();
|
||
|
||
Serial.println("Aquaponics Kit hybrid firmware ready (water + BME280 + MLX90614).");
|
||
|
||
if (MQTT_AUTOSTART) {
|
||
Serial.println("MQTT telemetry autostart enabled.");
|
||
start_datalogging();
|
||
} else {
|
||
Serial.println("Use POLL for serial readings.");
|
||
Serial.println("Use DATALOG for MQTT telemetry.");
|
||
}
|
||
}
|
||
|
||
// ======================================================
|
||
// LOOP
|
||
// ======================================================
|
||
|
||
void loop() {
|
||
service_network();
|
||
|
||
String cmd;
|
||
|
||
if (receive_command(cmd)) {
|
||
if (!process_coms(cmd)) {
|
||
polling = false;
|
||
send_to_stack = false;
|
||
process_command(cmd, device_list, device_list_len, default_board);
|
||
}
|
||
}
|
||
|
||
if (polling) {
|
||
unsigned long now = millis();
|
||
|
||
if (first_sensor_cycle || ((now - last_sensor_cycle_ms) >= sensor_cycle_interval_ms)) {
|
||
first_sensor_cycle = false;
|
||
last_sensor_cycle_ms = now;
|
||
run_sensor_cycle();
|
||
}
|
||
}
|
||
|
||
delay(10);
|
||
}
|
||
|
||
// ======================================================
|
||
// SENSOR CYCLE
|
||
// ======================================================
|
||
|
||
void run_sensor_cycle() {
|
||
Serial.println();
|
||
Serial.println("----- hybrid sensor cycle start -----");
|
||
|
||
read_rtd_and_compensate();
|
||
|
||
read_ezo_sensor(PH, ph_ok, ph_value);
|
||
read_ezo_sensor(DO, do_ok, do_value);
|
||
read_ezo_sensor(EC, ec_ok, ec_value);
|
||
|
||
read_bme280();
|
||
wait_with_network(150);
|
||
|
||
read_mlx90614();
|
||
wait_with_network(150);
|
||
|
||
mqtt_send_if_due();
|
||
|
||
Serial.println();
|
||
Serial.println("----- hybrid sensor cycle end -----");
|
||
}
|
||
|
||
bool read_rtd_and_compensate() {
|
||
if (!rtd_ok) {
|
||
rtd_value = 25.0;
|
||
|
||
Serial.print(" RTD:not detected. Using default compensation ");
|
||
Serial.print(rtd_value, 2);
|
||
Serial.print(" C");
|
||
|
||
PH.send_cmd_with_num("T,", 25.0);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
DO.send_cmd_with_num("T,", 20.0);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
EC.send_cmd_with_num("T,", 25.0);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
Serial.println();
|
||
|
||
return false;
|
||
}
|
||
|
||
RTD.send_read_cmd();
|
||
wait_with_network(RTD_RESPONSE_DELAY_MS);
|
||
|
||
Serial.print(" ");
|
||
receive_and_print_reading(RTD);
|
||
|
||
if ((RTD.get_error() == Ezo_board::SUCCESS) && (RTD.get_last_received_reading() > -1000.0)) {
|
||
rtd_value = RTD.get_last_received_reading();
|
||
} else {
|
||
rtd_value = 25.0;
|
||
}
|
||
|
||
PH.send_cmd_with_num("T,", rtd_value);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
DO.send_cmd_with_num("T,", rtd_value);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
EC.send_cmd_with_num("T,", rtd_value);
|
||
wait_with_network(COMPENSATION_GAP_MS);
|
||
|
||
Serial.print(" temp compensation: ");
|
||
Serial.print(rtd_value, 2);
|
||
Serial.println(" C");
|
||
|
||
wait_with_network(SENSOR_GAP_MS);
|
||
|
||
return true;
|
||
}
|
||
|
||
void read_ezo_sensor(Ezo_board &sensor, bool sensor_ok, float &target_value) {
|
||
Serial.print(" ");
|
||
|
||
if (!sensor_ok) {
|
||
Serial.print(sensor.get_name());
|
||
Serial.print(":not detected");
|
||
target_value = NAN;
|
||
wait_with_network(SENSOR_GAP_MS);
|
||
return;
|
||
}
|
||
|
||
sensor.send_read_cmd();
|
||
wait_with_network(EZO_RESPONSE_DELAY_MS);
|
||
|
||
receive_and_print_reading(sensor);
|
||
|
||
if (sensor.get_error() == Ezo_board::SUCCESS) {
|
||
target_value = sensor.get_last_received_reading();
|
||
} else {
|
||
target_value = NAN;
|
||
}
|
||
|
||
wait_with_network(SENSOR_GAP_MS);
|
||
}
|
||
|
||
// ======================================================
|
||
// BME280 + MLX90614
|
||
// ======================================================
|
||
|
||
float calc_vpd_kpa(float temp_c, float rh_percent) {
|
||
if (isnan(temp_c) || isnan(rh_percent)) {
|
||
return NAN;
|
||
}
|
||
|
||
if (rh_percent < 0.0 || rh_percent > 100.0) {
|
||
return NAN;
|
||
}
|
||
|
||
float es = 0.6108 * exp((17.27 * temp_c) / (temp_c + 237.3));
|
||
return es * (1.0 - (rh_percent / 100.0));
|
||
}
|
||
|
||
void read_bme280() {
|
||
Serial.print(" ");
|
||
|
||
if (!bme_ok) {
|
||
bme_air_temp = NAN;
|
||
bme_air_hum = NAN;
|
||
bme_pressure_hpa = NAN;
|
||
bme_vpd_kpa = NAN;
|
||
|
||
Serial.print("BME280:not detected");
|
||
return;
|
||
}
|
||
|
||
float temp = bme.readTemperature();
|
||
float hum = bme.readHumidity();
|
||
float pressure = bme.readPressure() / 100.0F;
|
||
|
||
if (isnan(temp) || temp < -40.0 || temp > 85.0) {
|
||
temp = NAN;
|
||
}
|
||
|
||
if (isnan(hum) || hum < 0.0 || hum > 100.0) {
|
||
hum = NAN;
|
||
}
|
||
|
||
if (isnan(pressure) || pressure < 300.0 || pressure > 1100.0) {
|
||
pressure = NAN;
|
||
}
|
||
|
||
bme_air_temp = temp;
|
||
bme_air_hum = hum;
|
||
bme_pressure_hpa = pressure;
|
||
|
||
if (!isnan(bme_air_temp) && !isnan(bme_air_hum)) {
|
||
bme_vpd_kpa = calc_vpd_kpa(bme_air_temp, bme_air_hum);
|
||
} else {
|
||
bme_vpd_kpa = NAN;
|
||
}
|
||
|
||
Serial.print("BME ");
|
||
Serial.print(BME_ADDR, HEX);
|
||
Serial.print(": ");
|
||
Serial.print(bme_air_temp, 2);
|
||
Serial.print(" C, ");
|
||
Serial.print(bme_air_hum, 1);
|
||
Serial.print(" %, ");
|
||
Serial.print(bme_pressure_hpa, 1);
|
||
Serial.print(" hPa, VPD ");
|
||
Serial.print(bme_vpd_kpa, 2);
|
||
Serial.print(" kPa");
|
||
}
|
||
|
||
void read_mlx90614() {
|
||
Serial.print(" ");
|
||
|
||
if (!mlx_ok) {
|
||
mlx_leaf_temp_c = NAN;
|
||
mlx_ambient_c = NAN;
|
||
leaf_air_delta_c = NAN;
|
||
|
||
Serial.print("MLX90614:not detected");
|
||
return;
|
||
}
|
||
|
||
float object_temp = mlx.readObjectTempC();
|
||
wait_with_network(100);
|
||
float ambient_temp = mlx.readAmbientTempC();
|
||
|
||
if (isnan(object_temp) || object_temp < -20.0 || object_temp > 100.0) {
|
||
object_temp = NAN;
|
||
}
|
||
|
||
if (isnan(ambient_temp) || ambient_temp < -20.0 || ambient_temp > 100.0) {
|
||
ambient_temp = NAN;
|
||
}
|
||
|
||
mlx_leaf_temp_c = object_temp;
|
||
mlx_ambient_c = ambient_temp;
|
||
|
||
if (!isnan(mlx_leaf_temp_c) && !isnan(bme_air_temp)) {
|
||
leaf_air_delta_c = mlx_leaf_temp_c - bme_air_temp;
|
||
} else {
|
||
leaf_air_delta_c = NAN;
|
||
}
|
||
|
||
Serial.print("MLX ");
|
||
Serial.print(MLX_ADDR, HEX);
|
||
Serial.print(": leaf ");
|
||
Serial.print(mlx_leaf_temp_c, 2);
|
||
Serial.print(" C, ambient ");
|
||
Serial.print(mlx_ambient_c, 2);
|
||
Serial.print(" C, leaf-air ");
|
||
Serial.print(leaf_air_delta_c, 2);
|
||
Serial.print(" C");
|
||
}
|
||
|
||
// ======================================================
|
||
// COMMANDS
|
||
// ======================================================
|
||
|
||
void start_datalogging() {
|
||
polling = true;
|
||
send_to_stack = true;
|
||
first_sensor_cycle = true;
|
||
last_mqtt_publish_ms = 0;
|
||
|
||
reconnect_wifi();
|
||
reconnect_mqtt();
|
||
|
||
Serial.println("MESAVAULT MQTT datalogging enabled");
|
||
}
|
||
|
||
bool process_coms(const String &string_buffer) {
|
||
String cmd = string_buffer;
|
||
cmd.trim();
|
||
cmd.toUpperCase();
|
||
|
||
if (cmd == "HELP") {
|
||
print_help();
|
||
return true;
|
||
}
|
||
|
||
else if (cmd.startsWith("DATALOG")) {
|
||
start_datalogging();
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "MQTT:ON") {
|
||
send_to_stack = true;
|
||
reconnect_mqtt();
|
||
Serial.println("MQTT sending enabled");
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "MQTT:OFF") {
|
||
send_to_stack = false;
|
||
Serial.println("MQTT sending disabled");
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "MQTT:STATUS") {
|
||
Serial.print("WiFi: ");
|
||
Serial.println(wifi_isconnected() ? "connected" : "disconnected");
|
||
|
||
if (wifi_isconnected()) {
|
||
Serial.print("IP: ");
|
||
Serial.println(WiFi.localIP());
|
||
}
|
||
|
||
Serial.print("MQTT host: ");
|
||
Serial.println(mqtt_host);
|
||
|
||
Serial.print("MQTT port: ");
|
||
Serial.println(mqtt_port);
|
||
|
||
Serial.print("MQTT connected: ");
|
||
Serial.println(mqtt.connected() ? "yes" : "no");
|
||
|
||
Serial.print("send_to_stack: ");
|
||
Serial.println(send_to_stack ? "true" : "false");
|
||
|
||
Serial.print("cycle interval ms: ");
|
||
Serial.println(sensor_cycle_interval_ms);
|
||
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "SCAN") {
|
||
scan_i2c();
|
||
refresh_i2c_state();
|
||
first_sensor_cycle = true;
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "BME:R") {
|
||
read_bme280();
|
||
Serial.println();
|
||
return true;
|
||
}
|
||
|
||
else if (cmd == "MLX:R") {
|
||
read_bme280();
|
||
wait_with_network(150);
|
||
read_mlx90614();
|
||
Serial.println();
|
||
return true;
|
||
}
|
||
|
||
else if (cmd.startsWith("POLL")) {
|
||
polling = true;
|
||
send_to_stack = MQTT_AUTOSTART;
|
||
|
||
int16_t index = cmd.indexOf(',');
|
||
|
||
if (index != -1) {
|
||
float new_delay_s = cmd.substring(index + 1).toFloat();
|
||
|
||
if (new_delay_s >= 8.0) {
|
||
sensor_cycle_interval_ms = (unsigned long)(new_delay_s * 1000.0);
|
||
Serial.print("new sensor cycle interval: ");
|
||
Serial.print(new_delay_s, 1);
|
||
Serial.println(" s");
|
||
} else {
|
||
Serial.println("delay too short; minimum recommended is 8 seconds");
|
||
}
|
||
}
|
||
|
||
first_sensor_cycle = true;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// ======================================================
|
||
// HELP
|
||
// ======================================================
|
||
|
||
void get_ec_k_value() {
|
||
if (!ec_ok) {
|
||
k_val = 0;
|
||
return;
|
||
}
|
||
|
||
char rx_buf[10];
|
||
|
||
EC.send_cmd("k,?");
|
||
wait_with_network(400);
|
||
|
||
if (EC.receive_cmd(rx_buf, 10) == Ezo_board::SUCCESS) {
|
||
k_val = String(rx_buf).substring(3).toFloat();
|
||
}
|
||
}
|
||
|
||
void print_help() {
|
||
get_ec_k_value();
|
||
|
||
Serial.println(F("Atlas Scientific I2C Aquaponics kit - water + BME280 + MLX90614"));
|
||
Serial.println(F("Commands:"));
|
||
Serial.println(F("datalog Start readings + MQTT telemetry"));
|
||
Serial.println(F("poll Start readings. With MQTT_AUTOSTART=true, MQTT stays enabled."));
|
||
Serial.println(F("poll,n Poll every n seconds. Minimum recommended: 8 s"));
|
||
Serial.println(F("mqtt:on Enable MQTT publishing"));
|
||
Serial.println(F("mqtt:off Disable MQTT publishing"));
|
||
Serial.println(F("mqtt:status Show WiFi/MQTT status"));
|
||
Serial.println(F("scan I2C scan + refresh device state"));
|
||
Serial.println(F("bme:r Read BME280"));
|
||
Serial.println(F("mlx:r Read MLX90614"));
|
||
Serial.println(F(""));
|
||
Serial.println(F("Direct EZO commands stop polling/MQTT until DATALOG is sent again."));
|
||
Serial.println(F(""));
|
||
Serial.println(F("ph:cal,mid,7 calibrate to pH 7"));
|
||
Serial.println(F("ph:cal,low,4 calibrate to pH 4"));
|
||
Serial.println(F("ph:cal,high,10 calibrate to pH 10"));
|
||
Serial.println(F("ph:cal,clear clear calibration"));
|
||
Serial.println(F(""));
|
||
Serial.println(F("ec:cal,dry calibrate a dry EC probe"));
|
||
Serial.println(F("ec:k,[n] switch K values: 0.1, 1, 10"));
|
||
Serial.println(F("ec:cal,clear clear calibration"));
|
||
|
||
if (k_val > 9) {
|
||
Serial.println(F("For K10 probes:"));
|
||
Serial.println(F(" ec:cal,low,12880"));
|
||
Serial.println(F(" ec:cal,high,150000"));
|
||
}
|
||
else if (k_val > .9) {
|
||
Serial.println(F("For K1 probes:"));
|
||
Serial.println(F(" ec:cal,low,12880"));
|
||
Serial.println(F(" ec:cal,high,80000"));
|
||
}
|
||
else if (k_val > .09) {
|
||
Serial.println(F("For K0.1 probes:"));
|
||
Serial.println(F(" ec:cal,low,84"));
|
||
Serial.println(F(" ec:cal,high,1413"));
|
||
}
|
||
|
||
Serial.println(F(""));
|
||
Serial.println(F("rtd:cal,t calibrate temp probe to chosen value"));
|
||
Serial.println(F("rtd:cal,clear clear calibration"));
|
||
Serial.println(F(""));
|
||
Serial.println(F("do:cal calibrate DO probe to air"));
|
||
Serial.println(F("do:cal,0 calibrate DO probe to 0 dissolved oxygen"));
|
||
Serial.println(F("do:cal,clear clear calibration"));
|
||
}
|
||
|
||
// ======================================================
|
||
// I2C
|
||
// ======================================================
|
||
|
||
bool i2c_device_present(byte address) {
|
||
Wire.beginTransmission(address);
|
||
byte error = Wire.endTransmission();
|
||
return (error == 0);
|
||
}
|
||
|
||
uint8_t read_i2c_reg8(byte address, byte reg) {
|
||
Wire.beginTransmission(address);
|
||
Wire.write(reg);
|
||
|
||
if (Wire.endTransmission(false) != 0) {
|
||
return 0xFF;
|
||
}
|
||
|
||
Wire.requestFrom(address, (uint8_t)1);
|
||
|
||
if (Wire.available()) {
|
||
return Wire.read();
|
||
}
|
||
|
||
return 0xFF;
|
||
}
|
||
|
||
void detect_i2c_devices() {
|
||
ph_ok = i2c_device_present(99);
|
||
do_ok = i2c_device_present(97);
|
||
rtd_ok = i2c_device_present(102);
|
||
ec_ok = i2c_device_present(100);
|
||
|
||
Serial.println("I2C device status:");
|
||
Serial.print(" PH 0x63: "); Serial.println(ph_ok ? "OK" : "missing");
|
||
Serial.print(" DO 0x61: "); Serial.println(do_ok ? "OK" : "missing");
|
||
Serial.print(" RTD 0x66: "); Serial.println(rtd_ok ? "OK" : "missing");
|
||
Serial.print(" EC 0x64: "); Serial.println(ec_ok ? "OK" : "missing");
|
||
}
|
||
|
||
void refresh_i2c_state() {
|
||
detect_i2c_devices();
|
||
|
||
bme_ok = bme.begin(BME_ADDR, &Wire);
|
||
|
||
if (bme_ok) {
|
||
Serial.println("BME280 ready at 0x76");
|
||
} else {
|
||
Serial.println("BME280 not ready at 0x76");
|
||
}
|
||
|
||
mlx_ok = mlx.begin(MLX_ADDR, &Wire);
|
||
|
||
if (mlx_ok) {
|
||
Serial.println("MLX90614 ready at 0x5A");
|
||
} else {
|
||
Serial.println("MLX90614 not ready at 0x5A");
|
||
}
|
||
}
|
||
|
||
void scan_i2c() {
|
||
Serial.println("I2C scan:");
|
||
|
||
for (byte address = 1; address < 127; address++) {
|
||
Wire.beginTransmission(address);
|
||
byte error = Wire.endTransmission();
|
||
|
||
if (error == 0) {
|
||
Serial.print(" - found 0x");
|
||
|
||
if (address < 16) {
|
||
Serial.print("0");
|
||
}
|
||
|
||
Serial.print(address, HEX);
|
||
Serial.print(" / ");
|
||
Serial.print(address);
|
||
Serial.print(" decimal");
|
||
|
||
if (address == 0x5A) Serial.print(" MLX90614");
|
||
if (address == 0x61) Serial.print(" DO");
|
||
if (address == 0x63) Serial.print(" PH");
|
||
if (address == 0x64) Serial.print(" EC");
|
||
if (address == 0x66) Serial.print(" RTD");
|
||
|
||
if (address == 0x76 || address == 0x77) {
|
||
uint8_t chip_id = read_i2c_reg8(address, 0xD0);
|
||
|
||
Serial.print(" | chip_id=0x");
|
||
Serial.print(chip_id, HEX);
|
||
|
||
if (chip_id == 0x60) {
|
||
Serial.print(" BME280");
|
||
} else if (chip_id == 0x58) {
|
||
Serial.print(" BMP280");
|
||
}
|
||
}
|
||
|
||
Serial.println();
|
||
}
|
||
}
|
||
|
||
Serial.println("I2C scan done.");
|
||
}
|
||
|
||
// ======================================================
|
||
// PUMP FUNCTION
|
||
// ======================================================
|
||
|
||
void pump_function(Ezo_board &pump, Ezo_board &sensor, float value, float dose, bool greater_than) {
|
||
if (sensor.get_error() == Ezo_board::SUCCESS) {
|
||
bool comparison = false;
|
||
|
||
if (greater_than) {
|
||
comparison = (sensor.get_last_received_reading() >= value);
|
||
} else {
|
||
comparison = (sensor.get_last_received_reading() <= value);
|
||
}
|
||
|
||
if (comparison) {
|
||
pump.send_cmd_with_num("d,", dose);
|
||
wait_with_network(100);
|
||
|
||
Serial.print(pump.get_name());
|
||
Serial.print(" ");
|
||
|
||
char response[20];
|
||
|
||
if (pump.receive_cmd(response, 20) == Ezo_board::SUCCESS) {
|
||
Serial.print("pump dispensed ");
|
||
} else {
|
||
Serial.print("pump error ");
|
||
}
|
||
|
||
Serial.println(response);
|
||
} else {
|
||
pump.send_cmd("x");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
<!-- END_FIRMWARE_ATLAS_KIT_01 -->
|
||
|
||
## Mapa I2C actual
|
||
|
||
Con el kit en configuración estable, el `SCAN` esperado es:
|
||
|
||
```text
|
||
I2C scan:
|
||
- found 0x5A / 90 decimal MLX90614
|
||
- found 0x61 / 97 decimal DO
|
||
- found 0x63 / 99 decimal PH
|
||
- found 0x64 / 100 decimal EC
|
||
- found 0x66 / 102 decimal RTD
|
||
- found 0x76 / 118 decimal | chip_id=0x60 BME280
|
||
I2C scan done.
|
||
```
|
||
|
||
| Dispositivo | Dirección decimal | Dirección hex |
|
||
| ----------- | ----------------: | ------------: |
|
||
| MLX90614 | 90 | 0x5A |
|
||
| DO EZO | 97 | 0x61 |
|
||
| pH EZO | 99 | 0x63 |
|
||
| EC EZO | 100 | 0x64 |
|
||
| RTD EZO | 102 | 0x66 |
|
||
| BME280 | 118 | 0x76 |
|
||
|
||
Direcciones retiradas o no usadas ahora:
|
||
|
||
| Dispositivo | Dirección decimal | Dirección hex | Estado |
|
||
| ----------- | ----------------: | ------------: | -------- |
|
||
| CO2 EZO | 105 | 0x69 | Retirado |
|
||
| HUM EZO | 111 | 0x6F | Retirado |
|
||
| PMP EZO | 103 | 0x67 | No usado |
|
||
|
||
## Sensores conectados
|
||
|
||
### pH EZO
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir pH de la solución.
|
||
```
|
||
|
||
Uso en el sistema:
|
||
|
||
```text
|
||
control de disponibilidad de nutrientes
|
||
detección de desviaciones de pH
|
||
diagnóstico de solución nutritiva
|
||
```
|
||
|
||
Rango objetivo inicial para tomate:
|
||
|
||
```text
|
||
OK: 6.0–6.5
|
||
Warning: 5.5–6.8
|
||
Peligro: fuera de 5.5–6.8
|
||
```
|
||
|
||
Comandos útiles:
|
||
|
||
```text
|
||
PH:R
|
||
PH:CAL,MID,7
|
||
PH:CAL,LOW,4
|
||
PH:CAL,HIGH,10
|
||
PH:CAL,CLEAR
|
||
PH:CAL,?
|
||
```
|
||
|
||
### EC EZO
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir conductividad eléctrica de la solución.
|
||
```
|
||
|
||
Uso en el sistema:
|
||
|
||
```text
|
||
estimar concentración de sales/nutrientes
|
||
detectar solución demasiado diluida
|
||
detectar acumulación de sales
|
||
```
|
||
|
||
Rango inicial usado en dashboard:
|
||
|
||
```text
|
||
OK: 1500–3500 µS/cm
|
||
Warning: 800–5000 µS/cm
|
||
Peligro: <800 o >5000 µS/cm
|
||
```
|
||
|
||
Comandos útiles:
|
||
|
||
```text
|
||
EC:R
|
||
EC:K,?
|
||
EC:K,1
|
||
EC:CAL,DRY
|
||
EC:CAL,LOW,12880
|
||
EC:CAL,HIGH,80000
|
||
EC:CAL,CLEAR
|
||
EC:CAL,?
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
Para sonda K1 se ha usado calibración low con 12880 µS/cm.
|
||
Después de EC:CAL,DRY conviene esperar a que la lectura se estabilice.
|
||
No calibrar si la lectura no está estable.
|
||
```
|
||
|
||
### DO EZO
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir oxígeno disuelto.
|
||
```
|
||
|
||
Uso en el sistema:
|
||
|
||
```text
|
||
salud de raíz
|
||
aireación
|
||
biofiltro
|
||
riesgo para peces si se usa como acuaponía real
|
||
```
|
||
|
||
Rangos iniciales:
|
||
|
||
```text
|
||
OK: >= 6 mg/L
|
||
Warning: 5–6 mg/L
|
||
Peligro: <5 mg/L
|
||
```
|
||
|
||
Comandos útiles:
|
||
|
||
```text
|
||
DO:R
|
||
DO:CAL
|
||
DO:CAL,0
|
||
DO:CAL,CLEAR
|
||
DO:CAL,?
|
||
```
|
||
|
||
### RTD EZO
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir temperatura del agua.
|
||
```
|
||
|
||
Uso en el sistema:
|
||
|
||
```text
|
||
temperatura real de solución
|
||
compensación térmica de pH, EC y DO
|
||
diagnóstico de raíz
|
||
```
|
||
|
||
Comandos útiles:
|
||
|
||
```text
|
||
RTD:R
|
||
RTD:CAL,<temperatura>
|
||
RTD:CAL,CLEAR
|
||
RTD:CAL,?
|
||
```
|
||
|
||
Uso en firmware:
|
||
|
||
```text
|
||
El RTD se lee primero.
|
||
Después se envía compensación de temperatura a PH, DO y EC.
|
||
```
|
||
|
||
Ejemplo:
|
||
|
||
```text
|
||
RTD: 20.19
|
||
temp compensation: 20.19 C
|
||
```
|
||
|
||
Si falla RTD:
|
||
|
||
```text
|
||
se usa 25.0 ºC como compensación por defecto
|
||
```
|
||
|
||
### BME280
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir temperatura de aire, humedad relativa y presión.
|
||
```
|
||
|
||
Dirección:
|
||
|
||
```text
|
||
0x76
|
||
```
|
||
|
||
Variables enviadas:
|
||
|
||
```text
|
||
air_temp_c
|
||
air_humidity_pct
|
||
pressure_hpa
|
||
vpd_kpa
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
El BME280 no se usa para compensar pH/EC/DO.
|
||
La compensación de agua debe venir del RTD.
|
||
```
|
||
|
||
### MLX90614
|
||
|
||
Función:
|
||
|
||
```text
|
||
Medir temperatura superficial sin contacto.
|
||
```
|
||
|
||
Dirección:
|
||
|
||
```text
|
||
0x5A
|
||
```
|
||
|
||
Variables enviadas:
|
||
|
||
```text
|
||
leaf_temp_c
|
||
mlx_ambient_c
|
||
leaf_air_delta_c
|
||
```
|
||
|
||
Uso en la vertical:
|
||
|
||
```text
|
||
temperatura de hoja
|
||
riesgo preliminar de estrés hídrico
|
||
diferencial hoja-aire
|
||
condensación si se cruza con punto de rocío
|
||
```
|
||
|
||
Notas importantes:
|
||
|
||
```text
|
||
El MLX90614 funciona correctamente con cable corto.
|
||
El problema no era el sensor ni el soldador.
|
||
El fallo aparecía al compartir bus con demasiados sensores o con cableado largo.
|
||
```
|
||
|
||
Prueba aislada válida:
|
||
|
||
```text
|
||
I2C scan:
|
||
- found 0x5A / 90 decimal MLX90614 expected address
|
||
|
||
MLX90614 initialized OK at 0x5A
|
||
Ambient sensor: 19.01 C | Object / leaf: 18.91 C
|
||
```
|
||
|
||
## Sensores descartados en esta fase
|
||
|
||
### CO2 EZO
|
||
|
||
Problema observado:
|
||
|
||
```text
|
||
Lecturas negativas alrededor de -3100 ppm.
|
||
```
|
||
|
||
Diagnóstico:
|
||
|
||
```text
|
||
Lectura físicamente imposible.
|
||
El circuito respondía por I2C, pero entregaba valor inválido.
|
||
```
|
||
|
||
Decisión actual:
|
||
|
||
```text
|
||
CO2 fuera del kit.
|
||
No se lee.
|
||
No se usa en dashboard.
|
||
JSON envía co2_ppm=null y co2_valid=false.
|
||
```
|
||
|
||
Posible recuperación futura:
|
||
|
||
```text
|
||
montarlo en otro microcontrolador
|
||
probar sensor nuevo
|
||
separarlo del bus I2C principal
|
||
```
|
||
|
||
### HUM EZO
|
||
|
||
Problema:
|
||
|
||
```text
|
||
Añade carga al bus I2C.
|
||
No es necesario porque el BME280 ya mide humedad del aire.
|
||
```
|
||
|
||
Decisión actual:
|
||
|
||
```text
|
||
HUM EZO fuera del kit.
|
||
```
|
||
|
||
El payload mantiene:
|
||
|
||
```json
|
||
"hum_atlas_pct": null
|
||
```
|
||
|
||
## Pines enable del Aquaponics Kit
|
||
|
||
Para board version `1.7`:
|
||
|
||
```cpp
|
||
const int EN_PH = 13;
|
||
const int EN_DO = 12;
|
||
const int EN_RTD = 33;
|
||
const int EN_EC = 27;
|
||
const int EN_HUM = 32;
|
||
const int EN_CO2 = 15;
|
||
```
|
||
|
||
Estados usados en la configuración estable:
|
||
|
||
```cpp
|
||
digitalWrite(EN_PH, LOW);
|
||
digitalWrite(EN_DO, LOW);
|
||
digitalWrite(EN_RTD, HIGH);
|
||
digitalWrite(EN_EC, LOW);
|
||
|
||
digitalWrite(EN_HUM, LOW); // deshabilitado
|
||
digitalWrite(EN_CO2, LOW); // deshabilitado
|
||
```
|
||
|
||
Para board version `1.8`, los pines cambian:
|
||
|
||
```cpp
|
||
// const int EN_PH = 12;
|
||
// const int EN_DO = 27;
|
||
// const int EN_RTD = 15;
|
||
// const int EN_EC = 33;
|
||
// const int EN_HUM = 14;
|
||
// const int EN_CO2 = 32;
|
||
```
|
||
|
||
Antes de reutilizar el firmware en otra placa, confirmar versión física del kit.
|
||
|
||
## Estrategia de lectura del firmware
|
||
|
||
El firmware actual no lee todos los sensores de golpe. Lee de forma secuencial para no forzar el bus.
|
||
|
||
Secuencia:
|
||
|
||
```text
|
||
1. Leer RTD
|
||
2. Enviar compensación de temperatura a PH, DO y EC
|
||
3. Leer PH
|
||
4. Leer DO
|
||
5. Leer EC
|
||
6. Leer BME280
|
||
7. Leer MLX90614
|
||
8. Construir JSON
|
||
9. Publicar MQTT
|
||
```
|
||
|
||
Tiempos usados:
|
||
|
||
```cpp
|
||
const unsigned long EZO_RESPONSE_DELAY_MS = 1800;
|
||
const unsigned long RTD_RESPONSE_DELAY_MS = 2000;
|
||
const unsigned long SENSOR_GAP_MS = 300;
|
||
const unsigned long COMPENSATION_GAP_MS = 250;
|
||
unsigned long sensor_cycle_interval_ms = 12000;
|
||
const unsigned long mqtt_publish_interval_ms = 5000;
|
||
```
|
||
|
||
I2C:
|
||
|
||
```cpp
|
||
Wire.begin();
|
||
Wire.setClock(10000);
|
||
```
|
||
|
||
Motivo:
|
||
|
||
```text
|
||
El bus I2C compartido con varios EZO y sensores externos es sensible.
|
||
Se prioriza estabilidad por encima de velocidad.
|
||
```
|
||
|
||
## Payload MQTT
|
||
|
||
Topic:
|
||
|
||
```text
|
||
vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry
|
||
```
|
||
|
||
Ejemplo real:
|
||
|
||
```json
|
||
{
|
||
"tenant": "mesavault_lab",
|
||
"site": "bench_aquaponics",
|
||
"product": "aquaponics",
|
||
"asset": "atlas_kit_01",
|
||
"ph": 8.54,
|
||
"ec_us_cm": 327,
|
||
"do_mg_l": 3.83,
|
||
"water_temp_c": 20.19,
|
||
"hum_atlas_pct": null,
|
||
"co2_ppm": null,
|
||
"co2_valid": false,
|
||
"air_temp_c": 18.09,
|
||
"air_humidity_pct": 77.9,
|
||
"pressure_hpa": 1007.6,
|
||
"vpd_kpa": 0.46,
|
||
"leaf_temp_c": 19.27,
|
||
"mlx_ambient_c": 18.81,
|
||
"leaf_air_delta_c": 1.18
|
||
}
|
||
```
|
||
|
||
| Campo | Significado |
|
||
| ------------------ | ------------------------------------ |
|
||
| `ph` | pH de solución |
|
||
| `ec_us_cm` | EC en µS/cm |
|
||
| `do_mg_l` | oxígeno disuelto |
|
||
| `water_temp_c` | temperatura de agua |
|
||
| `hum_atlas_pct` | humedad Atlas, actualmente null |
|
||
| `co2_ppm` | CO2, actualmente null |
|
||
| `co2_valid` | false mientras CO2 esté retirado |
|
||
| `air_temp_c` | temperatura aire BME280 |
|
||
| `air_humidity_pct` | humedad aire BME280 |
|
||
| `pressure_hpa` | presión BME280 |
|
||
| `vpd_kpa` | VPD calculado |
|
||
| `leaf_temp_c` | temperatura superficial hoja |
|
||
| `mlx_ambient_c` | temperatura ambiente interna del MLX |
|
||
| `leaf_air_delta_c` | hoja - aire |
|
||
|
||
## MQTT
|
||
|
||
### Parámetros del firmware
|
||
|
||
```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 = "PASSWORD_EN_VAULTWARDEN";
|
||
|
||
const char* mqtt_client_id = "atlas_kit_01";
|
||
|
||
const char* mqtt_topic =
|
||
"vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry";
|
||
```
|
||
|
||
### Autostart
|
||
|
||
El firmware debe tener:
|
||
|
||
```cpp
|
||
const bool MQTT_AUTOSTART = true;
|
||
```
|
||
|
||
Esto evita tener que escribir:
|
||
|
||
```text
|
||
DATALOG
|
||
```
|
||
|
||
Problema resuelto:
|
||
|
||
```text
|
||
Antes el kit conectaba al broker, pero solo enviaba PINGREQ.
|
||
No publicaba payload hasta escribir DATALOG por serie.
|
||
```
|
||
|
||
Señal mala en OVH:
|
||
|
||
```text
|
||
Received PINGREQ from atlas_kit_01
|
||
```
|
||
|
||
Señal buena en OVH:
|
||
|
||
```text
|
||
Received PUBLISH from atlas_kit_01
|
||
Sending PUBLISH to local...to-platform40
|
||
```
|
||
|
||
## Comandos serie disponibles
|
||
|
||
Baudios:
|
||
|
||
```text
|
||
9600
|
||
```
|
||
|
||
Comandos principales:
|
||
|
||
```text
|
||
HELP
|
||
SCAN
|
||
DATALOG
|
||
POLL
|
||
POLL,12
|
||
MQTT:STATUS
|
||
MQTT:ON
|
||
MQTT:OFF
|
||
BME:R
|
||
MLX:R
|
||
```
|
||
|
||
### SCAN
|
||
|
||
Uso:
|
||
|
||
```text
|
||
SCAN
|
||
```
|
||
|
||
Función:
|
||
|
||
```text
|
||
escanea bus I2C
|
||
actualiza estado interno de dispositivos
|
||
reinicializa BME280 y MLX90614
|
||
fuerza nuevo ciclo
|
||
```
|
||
|
||
Salida esperada:
|
||
|
||
```text
|
||
I2C scan:
|
||
- found 0x5A / 90 decimal MLX90614
|
||
- found 0x61 / 97 decimal DO
|
||
- found 0x63 / 99 decimal PH
|
||
- found 0x64 / 100 decimal EC
|
||
- found 0x66 / 102 decimal RTD
|
||
- found 0x76 / 118 decimal | chip_id=0x60 BME280
|
||
I2C scan done.
|
||
```
|
||
|
||
### DATALOG
|
||
|
||
Uso:
|
||
|
||
```text
|
||
DATALOG
|
||
```
|
||
|
||
Función:
|
||
|
||
```text
|
||
activa polling
|
||
activa publicación MQTT
|
||
fuerza ciclo nuevo
|
||
```
|
||
|
||
En firmware actual debería arrancar solo, pero este comando sirve para reactivar si se ha parado.
|
||
|
||
### POLL
|
||
|
||
Uso:
|
||
|
||
```text
|
||
POLL
|
||
POLL,15
|
||
```
|
||
|
||
Función:
|
||
|
||
```text
|
||
activa lectura de sensores
|
||
permite cambiar intervalo de ciclo
|
||
```
|
||
|
||
En este firmware, si `MQTT_AUTOSTART=true`, `POLL` mantiene MQTT activo.
|
||
|
||
### MQTT:STATUS
|
||
|
||
Uso:
|
||
|
||
```text
|
||
MQTT:STATUS
|
||
```
|
||
|
||
Salida esperada:
|
||
|
||
```text
|
||
WiFi: connected
|
||
IP: ...
|
||
MQTT host: mqtt.mesavault.es
|
||
MQTT port: 1883
|
||
MQTT connected: yes
|
||
send_to_stack: true
|
||
cycle interval ms: 12000
|
||
```
|
||
|
||
### BME:R
|
||
|
||
Uso:
|
||
|
||
```text
|
||
BME:R
|
||
```
|
||
|
||
Lee solo `BME280`.
|
||
|
||
### MLX:R
|
||
|
||
Uso:
|
||
|
||
```text
|
||
MLX:R
|
||
```
|
||
|
||
Lee `BME280` y `MLX90614` para calcular `leaf_air_delta_c`.
|
||
|
||
## Calibración
|
||
|
||
### pH
|
||
|
||
Secuencia típica:
|
||
|
||
```text
|
||
PH:CAL,CLEAR
|
||
PH:CAL,MID,7
|
||
PH:CAL,LOW,4
|
||
PH:CAL,HIGH,10
|
||
PH:CAL,?
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
Calibrar primero punto medio pH 7.
|
||
No calibrar si la lectura no está estable.
|
||
Aclarar sonda entre soluciones.
|
||
```
|
||
|
||
### EC
|
||
|
||
Consultar K:
|
||
|
||
```text
|
||
EC:K,?
|
||
```
|
||
|
||
Para K1:
|
||
|
||
```text
|
||
EC:CAL,CLEAR
|
||
EC:CAL,DRY
|
||
EC:CAL,LOW,12880
|
||
EC:CAL,HIGH,80000
|
||
EC:CAL,?
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
EC:CAL,DRY debe hacerse con sonda seca.
|
||
LOW con solución 12880 µS/cm.
|
||
Esperar estabilización antes de enviar CAL.
|
||
No corregir una lectura inestable con calibración.
|
||
```
|
||
|
||
### DO
|
||
|
||
Calibración en aire:
|
||
|
||
```text
|
||
DO:CAL
|
||
```
|
||
|
||
Calibración a cero:
|
||
|
||
```text
|
||
DO:CAL,0
|
||
```
|
||
|
||
Borrar calibración:
|
||
|
||
```text
|
||
DO:CAL,CLEAR
|
||
```
|
||
|
||
Consultar:
|
||
|
||
```text
|
||
DO:CAL,?
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
La sonda DO necesita tiempo de estabilización.
|
||
Revisar membrana, burbujas y solución interna si la lectura es incoherente.
|
||
```
|
||
|
||
### RTD
|
||
|
||
Calibración:
|
||
|
||
```text
|
||
RTD:CAL,<temperatura_real>
|
||
```
|
||
|
||
Ejemplo:
|
||
|
||
```text
|
||
RTD:CAL,20.0
|
||
```
|
||
|
||
Borrar:
|
||
|
||
```text
|
||
RTD:CAL,CLEAR
|
||
```
|
||
|
||
Notas:
|
||
|
||
```text
|
||
El RTD condiciona la compensación de pH, EC y DO.
|
||
Si falla, el firmware usa 25 ºC por defecto.
|
||
```
|
||
|
||
## Validación local del kit
|
||
|
||
### Validación por monitor serie
|
||
|
||
Al arrancar debe aparecer algo parecido a:
|
||
|
||
```text
|
||
Aquaponics Kit hybrid firmware ready (water + BME280 + MLX90614).
|
||
MQTT telemetry autostart enabled.
|
||
MESAVAULT MQTT datalogging enabled
|
||
```
|
||
|
||
Ciclo correcto:
|
||
|
||
```text
|
||
----- hybrid sensor cycle start -----
|
||
RTD: 20.19 temp compensation: 20.19 C
|
||
PH: 8.54 DO: 3.83 EC: 327.20
|
||
BME 76: 18.09 C, 77.9 %, 1007.6 hPa, VPD 0.46 kPa
|
||
MLX 5A: leaf 19.27 C, ambient 18.81 C, leaf-air 1.18 C
|
||
sent to MESAVAULT MQTT: {...}
|
||
----- hybrid sensor cycle end -----
|
||
```
|
||
|
||
### Validación en OVH
|
||
|
||
```bash
|
||
docker logs -f mv_mqtt_public 2>&1 | grep --line-buffered -E 'atlas_kit_01|Received PUBLISH|PINGREQ|not authorised|Bad username'
|
||
```
|
||
|
||
Bueno:
|
||
|
||
```text
|
||
Received PUBLISH from atlas_kit_01
|
||
```
|
||
|
||
Insuficiente:
|
||
|
||
```text
|
||
Received PINGREQ from atlas_kit_01
|
||
```
|
||
|
||
### Validación en platform-40
|
||
|
||
```bash
|
||
docker exec -it mv_mosquitto \
|
||
mosquitto_sub \
|
||
-h 127.0.0.1 \
|
||
-p 1883 \
|
||
-t 'vertical/mesavault_lab/bench_aquaponics/#' \
|
||
-v
|
||
```
|
||
|
||
Debe verse el JSON publicado por el kit.
|
||
|
||
### Validación en PostgreSQL
|
||
|
||
```sql
|
||
SELECT
|
||
ts,
|
||
EXTRACT(EPOCH FROM (now() - ts))::int AS age_s,
|
||
ph,
|
||
ec_us_cm,
|
||
do_mg_l,
|
||
water_temp_c,
|
||
air_temp_c,
|
||
air_humidity_pct,
|
||
pressure_hpa,
|
||
vpd_kpa,
|
||
leaf_temp_c,
|
||
mlx_ambient_c,
|
||
leaf_air_delta_c
|
||
FROM mv_hot.aquaponics_readings
|
||
WHERE asset = 'atlas_kit_01'
|
||
ORDER BY ts DESC
|
||
LIMIT 10;
|
||
```
|
||
|
||
Criterio:
|
||
|
||
```text
|
||
age_s bajo = está llegando dato actual
|
||
```
|
||
|
||
## Problemas conocidos y resolución
|
||
|
||
### El kit conecta, pero no publica
|
||
|
||
Síntoma en OVH:
|
||
|
||
```text
|
||
Received PINGREQ from atlas_kit_01
|
||
Sending PINGRESP to atlas_kit_01
|
||
```
|
||
|
||
Causa:
|
||
|
||
```text
|
||
el firmware está conectado, pero no está publicando payload
|
||
```
|
||
|
||
Solución:
|
||
|
||
```text
|
||
verificar MQTT_AUTOSTART=true
|
||
escribir DATALOG
|
||
comprobar send_to_stack=true con MQTT:STATUS
|
||
```
|
||
|
||
### MLX90614 no aparece
|
||
|
||
Si el MLX no aparece en I2C:
|
||
|
||
```text
|
||
no aparece 0x5A
|
||
```
|
||
|
||
Comprobar:
|
||
|
||
```text
|
||
cable corto
|
||
alimentación 3.3 V
|
||
SDA/SCL correctos
|
||
masa común
|
||
bus sin CO2/HUM
|
||
```
|
||
|
||
Diagnóstico realizado:
|
||
|
||
```text
|
||
MLX solo con cable corto funciona.
|
||
MLX con bus cargado falla.
|
||
```
|
||
|
||
Conclusión:
|
||
|
||
```text
|
||
no era sensor dañado
|
||
era convivencia/carga del bus I2C
|
||
```
|
||
|
||
### RTD detectado pero devuelve No Data
|
||
|
||
Síntoma:
|
||
|
||
```text
|
||
RTD: No Data
|
||
```
|
||
|
||
Interpretación:
|
||
|
||
```text
|
||
el circuito está en el bus, pero no devolvió lectura válida en ese ciclo
|
||
```
|
||
|
||
Soluciones aplicadas:
|
||
|
||
```text
|
||
RTD_RESPONSE_DELAY_MS=2000
|
||
bus a 10 kHz
|
||
lectura secuencial
|
||
pausas entre sensores
|
||
```
|
||
|
||
Si persiste:
|
||
|
||
```text
|
||
revisar conexión RTD
|
||
revisar sonda
|
||
probar RTD:R manual
|
||
probar sin BME/MLX
|
||
```
|
||
|
||
### Direcciones raras como 0x01
|
||
|
||
Síntoma:
|
||
|
||
```text
|
||
I2C scan:
|
||
- found 0x01
|
||
```
|
||
|
||
Interpretación:
|
||
|
||
```text
|
||
bus I2C sucio, inestable o cargado
|
||
```
|
||
|
||
Causas posibles:
|
||
|
||
```text
|
||
pull-ups acumulados
|
||
cable largo
|
||
sensor bloqueando SDA/SCL
|
||
CO2/HUM alterando el bus
|
||
ruido eléctrico
|
||
```
|
||
|
||
Solución:
|
||
|
||
```text
|
||
retirar sensores problemáticos
|
||
cable corto
|
||
bus a 10 kHz
|
||
probar incrementalmente
|
||
```
|
||
|
||
### Picos falsos en Grafana
|
||
|
||
Síntoma:
|
||
|
||
```text
|
||
temperatura 180 ºC
|
||
VPD >100 kPa
|
||
pH/EC/DO a cero simultáneamente
|
||
```
|
||
|
||
Interpretación:
|
||
|
||
```text
|
||
muestra corrupta
|
||
```
|
||
|
||
Solución aplicada:
|
||
|
||
```text
|
||
vista mv_hot.aquaponics_readings_clean
|
||
vista mv_hot.aquaponics_dashboard
|
||
```
|
||
|
||
No borrar datos brutos. Filtrarlos para visualización y diagnóstico.
|
||
|
||
### CO2 negativo
|
||
|
||
Síntoma:
|
||
|
||
```text
|
||
CO2:R → -3100 ppm
|
||
```
|
||
|
||
Interpretación:
|
||
|
||
```text
|
||
lectura físicamente imposible
|
||
```
|
||
|
||
Decisión:
|
||
|
||
```text
|
||
CO2 retirado
|
||
co2_ppm=null
|
||
co2_valid=false
|
||
```
|
||
|
||
## Buenas prácticas de hardware
|
||
|
||
### Cableado I2C
|
||
|
||
Evitar:
|
||
|
||
```text
|
||
cables largos
|
||
más de 1 metro de I2C
|
||
muchos módulos con pull-ups en paralelo
|
||
pasar cerca de fuentes, relés o potencia
|
||
```
|
||
|
||
Recomendado:
|
||
|
||
```text
|
||
cable corto
|
||
GND común
|
||
SDA/SCL bien identificados
|
||
bus a 10 kHz si está cargado
|
||
```
|
||
|
||
### Si se necesitan sensores lejanos
|
||
|
||
No usar I2C directo largo.
|
||
|
||
Alternativas:
|
||
|
||
```text
|
||
otro microcontrolador cerca del sensor
|
||
RS485
|
||
MQTT por WiFi
|
||
LoRa
|
||
multiplexor TCA9548A
|
||
extensor I2C diferencial
|
||
```
|
||
|
||
### Si se quiere recuperar CO2
|
||
|
||
No conectarlo directamente al bus actual.
|
||
|
||
Opciones:
|
||
|
||
```text
|
||
nuevo microcontrolador solo para CO2
|
||
TCA9548A
|
||
otro bus I2C en ESP32 si hay pines disponibles
|
||
sensor diferente
|
||
```
|
||
|
||
## Buenas prácticas de firmware
|
||
|
||
### No publicar valores falsos como cero
|
||
|
||
Si una lectura falla:
|
||
|
||
```text
|
||
publicar null
|
||
```
|
||
|
||
No publicar:
|
||
|
||
```text
|
||
0
|
||
```
|
||
|
||
salvo que el valor cero sea físicamente posible y medido con certeza.
|
||
|
||
### Publicar después del ciclo completo
|
||
|
||
No publicar al inicio del ciclo.
|
||
|
||
Orden correcto:
|
||
|
||
```text
|
||
leer todo
|
||
validar básico
|
||
calcular derivados
|
||
publicar
|
||
```
|
||
|
||
### Mantener payload_json
|
||
|
||
Aunque se filtren valores en vistas SQL, conservar payload bruto en PostgreSQL.
|
||
|
||
Motivo:
|
||
|
||
```text
|
||
diagnóstico posterior
|
||
auditoría
|
||
depuración de firmware
|
||
comparación con Grafana
|
||
```
|
||
|
||
## Relación con Grafana
|
||
|
||
El kit no sabe nada de Grafana.
|
||
|
||
El flujo es:
|
||
|
||
```text
|
||
kit → MQTT → sink → PostgreSQL → vistas SQL → Grafana
|
||
```
|
||
|
||
Paneles que dependen del kit:
|
||
|
||
```text
|
||
pH
|
||
EC
|
||
DO
|
||
temperatura agua
|
||
temperatura aire
|
||
humedad aire
|
||
presión
|
||
VPD
|
||
temperatura hoja
|
||
leaf-air
|
||
diagnóstico operativo
|
||
riesgo condensación
|
||
riesgo estrés hídrico
|
||
```
|
||
|
||
La vista recomendada para paneles es:
|
||
|
||
```text
|
||
mv_hot.aquaponics_dashboard
|
||
```
|
||
|
||
La vista recomendada para explicación textual es:
|
||
|
||
```text
|
||
mv_hot.aquaponics_diagnosis_latest
|
||
```
|
||
|
||
## Estado actual del kit
|
||
|
||
```text
|
||
Nombre lógico: atlas_kit_01
|
||
Tenant: mesavault_lab
|
||
Site: bench_aquaponics
|
||
Product: aquaponics
|
||
Topic: vertical/mesavault_lab/bench_aquaponics/aquaponics/atlas_kit_01/telemetry
|
||
Estado: funcional con pH + DO + RTD + EC + BME280 + MLX90614
|
||
CO2: retirado
|
||
HUM Atlas: retirado
|
||
Publicación MQTT: automática
|
||
ThingSpeak: eliminado
|
||
```
|
||
|
||
## Próximas ampliaciones posibles
|
||
|
||
### Nodo climático separado
|
||
|
||
Si se decide separar `BME280` y `MLX90614` del kit principal:
|
||
|
||
```text
|
||
Pycom / ESP32 / microcontrolador dedicado
|
||
↓
|
||
BME280
|
||
MLX90614
|
||
↓
|
||
MQTT
|
||
```
|
||
|
||
Topic sugerido:
|
||
|
||
```text
|
||
vertical/mesavault_lab/bench_aquaponics/climate/bme_ir_node_01/telemetry
|
||
```
|
||
|
||
### Recuperar CO2
|
||
|
||
Topic sugerido si va en nodo aparte:
|
||
|
||
```text
|
||
vertical/mesavault_lab/bench_aquaponics/climate/co2_node_01/telemetry
|
||
```
|
||
|
||
### Añadir más sensores IR
|
||
|
||
Por ejemplo:
|
||
|
||
```text
|
||
varias hojas
|
||
varias alturas
|
||
varios puntos del invernadero
|
||
```
|
||
|
||
No meter todos en el mismo bus del Aquaponics Kit sin multiplexar.
|
||
|
||
### Convertirlo en kit comercial
|
||
|
||
Para escalar esta vertical, el kit debería quedar definido como:
|
||
|
||
```text
|
||
Kit Hydro Climate v1
|
||
- Aquaponics Kit con pH/EC/DO/RTD
|
||
- nodo climático separado con BME/IR
|
||
- MQTT estándar
|
||
- dashboard base
|
||
- diagnóstico SQL
|
||
- alertas Grafana
|
||
- runbook de instalación
|
||
- checklist de calibración
|
||
```
|
||
|
||
## Checklist rápido de puesta en marcha
|
||
|
||
1. Encender kit.
|
||
2. Abrir monitor serie a `9600`.
|
||
3. Confirmar:
|
||
|
||
```text
|
||
MQTT telemetry autostart enabled
|
||
```
|
||
|
||
4. Ejecutar:
|
||
|
||
```text
|
||
SCAN
|
||
```
|
||
|
||
5. Confirmar sensores:
|
||
|
||
```text
|
||
0x61 DO
|
||
0x63 PH
|
||
0x64 EC
|
||
0x66 RTD
|
||
0x76 BME280
|
||
0x5A MLX90614
|
||
```
|
||
|
||
6. Esperar ciclo:
|
||
|
||
```text
|
||
sent to MESAVAULT MQTT: {...}
|
||
```
|
||
|
||
7. En OVH confirmar:
|
||
|
||
```text
|
||
Received PUBLISH from atlas_kit_01
|
||
```
|
||
|
||
8. En `platform-40` confirmar:
|
||
|
||
```bash
|
||
docker logs -f aquaponics_atlas01_pg
|
||
```
|
||
|
||
9. En PostgreSQL confirmar `age_s` bajo.
|
||
10. En Grafana confirmar actualización de paneles.
|
||
|
||
## Regla final
|
||
|
||
El Aquaponics Kit no debe crecer indefinidamente como placa donde conectar todo.
|
||
|
||
Su función estable debe ser:
|
||
|
||
```text
|
||
agua + lectura básica de clima/planta si el bus lo permite
|
||
```
|
||
|
||
Si añadir sensores compromete estabilidad, la decisión correcta es:
|
||
|
||
```text
|
||
separar sensores en otro nodo
|
||
mantener MQTT como integración común
|
||
unificar en PostgreSQL/Grafana
|
||
```
|
||
|
||
## Huecos y validaciones pendientes
|
||
|
||
### Huecos documentales detectados
|
||
|
||
* `source_date` no especificado.
|
||
* `last_verified` no especificado.
|
||
* `owner` no especificado.
|
||
* Sensibilidad documental pendiente de confirmar.
|
||
* No consta versión exacta del firmware.
|
||
* No consta commit, hash o release del firmware.
|
||
* No consta versión física confirmada de la placa desplegada.
|
||
* No consta ubicación física exacta del kit dentro del lab.
|
||
* No consta procedimiento de backup del firmware.
|
||
* No consta política de rotación de la contraseña MQTT.
|
||
|
||
### Validaciones pendientes
|
||
|
||
* Confirmar que el firmware pegado entre `BEGIN_FIRMWARE_ATLAS_KIT_01` y `END_FIRMWARE_ATLAS_KIT_01` no contiene secretos reales.
|
||
* Confirmar que `MQTT_AUTOSTART=true`.
|
||
* Confirmar que `mqtt_pass` usa placeholder y la credencial real está en Vaultwarden.
|
||
* Confirmar que el `SCAN` coincide con la configuración estable.
|
||
* Confirmar que `platform-40` recibe el topic `vertical/mesavault_lab/bench_aquaponics/#`.
|
||
* Confirmar que PostgreSQL actualiza `mv_hot.aquaponics_readings`.
|
||
* Confirmar que Grafana consume `mv_hot.aquaponics_dashboard`.
|
||
* Confirmar si este documento debe marcarse como `canonical=true`.
|