Sistema Inteligente de Detección de Fugas de Agua y Control de Válvulas
Componentes y materiales
| Componente | Cantidad |
|---|---|
| Caja de aluminio | 1 |
| Arduino UNO | 1 |
| Arduino Ethernet Shield 2 | 1 |
| Fuente de alimentación | 1 |
| Carcasa AC | 1 |
| Kit de LEDs con resistencias | 1 |
| Relés (1 dual 5V y 1 dual 12V) | 1 |
| Válvula motorizada | 1 |
| Conectores JST | 1 |
| Sensor de agua | 1 |
Descripción del proyecto
Resumen
Durante una conversación con un amigo, me di cuenta de que las fugas de agua son un problema grave. Mi amigo tuvo que reemplazar todos los muebles de su sótano porque una tubería se rompió mientras él estaba en el trabajo.
Este proyecto es similar al que presenté para el concurso Microsoft IoT, pero esta versión está basada en Arduino, no en Raspberry Pi. Mi enfoque es el siguiente: en lugar de centralizar muchas responsabilidades en una plataforma grande (como RasPi, DragonBoard o una PC), prefiero delegar responsabilidades simples a dispositivos simples (como Arduino y otros). Estos harán lo que deben hacer y, opcionalmente, se conectarán a través de la red a otros dispositivos (simples o complejos) para ofrecer servicios avanzados. En caso de fallo de red, siguen funcionando localmente.
Básicamente, el sistema monitoriza fugas de agua y activa acciones como cerrar la entrada principal de agua, dependiendo de la falla detectada. También publica mensajes en un broker MQTT. La idea es que el dispositivo gestione el agua localmente, pero también participe, junto con otros dispositivos, en un sistema mayor de automatización del hogar.
Aspecto del sistema
La entrada principal de agua se encuentra en la parte inferior. El primer dispositivo lo instala la compañía de suministro de agua para controlar la presión. Yo instalé la parte superior de la imagen. La válvula motorizada (en azul) está instalada en paralelo con una válvula manual. En este dibujo, la válvula manual está abierta y la motorizada está bypassada. Es útil en caso de corte de energía. En modo normal, la válvula manual debe estar cerrada.
Dentro de la válvula hay un motor DC (12V) que gira en sentido horario o antihorario según la polaridad. Hay un bucle de retroalimentación que indica si la válvula está efectivamente abierta o cerrada. Para abrirla, basta aplicar voltaje positivo a la conexión superior izquierda.
El controlador
De izquierda a derecha: enchufe AC, botón de reset, LEDs de estado, conectores (a sensores, al motor), interfaz Ethernet y USB.
- LED1 (Rojo): Fijo = agua detectada localmente; Parpadeante = agua detectada remotamente; Apagado = sin fuga
- LED2 (Amarillo): Fijo = no se puede controlar la válvula; Parpadeante = no se alcanza el broker MQTT; Apagado = todo correcto
- LED3 (Azul): Fijo = todo correcto; Parpadeante = válvula cerrada; Apagado = sistema caído o sin alimentación
Sensor de agua
Interior del controlador
⚠️ ADVERTENCIA IMPORTANTE
La fuente AC/DC que uso tiene dos salidas: la primera es 12V DC para alimentar la válvula motorizada (controlada por dos relés que invierten la polaridad del motor) y la segunda es EXACTAMENTE 5V DC para alimentar el Arduino. Por eso alimento directamente el circuito de 5V, no el pin Vin que requiere al menos 6V DC. Una vez conectada la fuente AC/DC, NUNCA (repito, NUNCA) conectes ni el jack DC del Arduino ni el cable USB. Si necesitas depurar por USB, fabrica un cable casero SIN las líneas de alimentación, solo las de datos. Por cierto, la conexión entre la carcasa AC y la fuente AC/DC es de 110V. ¡No toques nunca!
Código
Detección de fugas y control de válvula motorizada (Arduino)
#include <PubSubClient.h>
#include <Network.h>
#include <Timer.h>
#include <SPI.h>
#include <Ethernet.h>
#include <Dns.h>
#include <avr/wdt.h>
/* Funcionamiento del hardware:
Tres subsistemas:
- Caja principal: Arduino Uno + Ethernet Shield
- LED rojo: fijo = agua local, parpadeo = agua remota, apagado = sin fuga
- LED amarillo: fijo = válvula fuera de servicio, parpadeo = broker MQTT inalcanzable, apagado = OK
- LED azul: fijo = válvula abierta y monitorizando, parpadeo = válvula cerrada, apagado = sistema caído
- Pulsador: al presionarlo, se inicia auto-test
- Relé dual para controlar válvula motorizada remota
- Otro relé dual para leer finales de carrera de la válvula remota
- Conjunto de detectores de agua (todos en paralelo, 3 conectores en panel frontal)
Conexiones de la válvula motorizada:
- Amarillo y Azul: DC para motor
- Negro: entrada de finales de carrera (se pone a GND en nuestro circuito)
- Rojo: se pone a GND cuando la válvula cierra totalmente (nota: por diseño interno, no hay garantía de continuidad al quitar alimentación)
- Verde: se pone a GND cuando la válvula abre totalmente (misma nota)
*/
// Configuración de red
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xCF, 0xFC, 0xEE };
IPAddress ip(192, 168, 12, 215);
IPAddress server(192, 168, 12, 130);
EthernetClient ethClient;
PubSubClient client(ethClient);
#define mqttClientPrefix "GLX"
#define mqttClientLocation "BASEMENT"
#define mqttClientUID "001"
#define mqttClientStatusTopic "Status"
#define mqttClientFaultTopic "Fault"
const int mqttInterval = 20;
int mqttIntervalCnt = 0;
int isConnectedToBroker = -1;
// Pines
const int SystemLedPin = A0;
const int FaultLedPin = A1;
const int AlarmLedPin = A2;
const int WaterDetectorPin = 2;
const int ToggleButtonPin = 3;
const int SdCardPin = 4;
const int ValveClosedPin = 5;
const int ValveOpenedPin = 6;
const int ValveControl1 = 8;
const int ValveControl2 = 7;
// Estados
int isWaterDetected = 0;
int isWaterDetectedRemotely = 0;
int isValveClosed = -1;
const int valveTimeOut = 15;
int isConnectedToValve = -1;
volatile boolean isResetRequested = 0;
const int mainLoopDelay = 500;
void(* resetFunc) (void) = 0;
void setup() {
wdt_disable();
Serial.begin(9600);
Serial.println(F("Inicio de setup"));
pinMode(SystemLedPin, OUTPUT);
pinMode(FaultLedPin, OUTPUT);
pinMode(AlarmLedPin, OUTPUT);
pinMode(WaterDetectorPin, INPUT);
pinMode(ToggleButtonPin, INPUT);
pinMode(ValveOpenedPin, INPUT);
pinMode(ValveClosedPin, INPUT);
pinMode(ValveControl1, OUTPUT); digitalWrite(ValveControl1, HIGH);
pinMode(ValveControl2, OUTPUT); digitalWrite(ValveControl2, HIGH);
pinMode(SdCardPin, OUTPUT); digitalWrite(SdCardPin, HIGH);
testLeds();
client.setServer(server, 1883);
client.setCallback(MQTTBrokerCallback);
Ethernet.begin(mac, ip);
Serial.print(F("IP actual: "));
Serial.print(Ethernet.localIP());
Serial.print(F(" - IP broker MQTT: "));
Serial.println(server);
if (openValve() == 0) {
Serial.println(F("Válvula abierta y sistema monitorizando"));
subscribeToRemoteWaterSensors();
} else {
Serial.println(F("No se pudo abrir la válvula. Sistema fuera de servicio. Use bypass de fontanería"));
}
enableInterruptOnResetButton();
delay(1500);
Serial.println(F("Fin de setup"));
}
void loop() {
configureLedsWithInitialStates();
if (isResetRequested == 1) {
Serial.println(F("Botón de reset pulsado"));
publishStatus();
wdt_enable(WDTO_1S);
delay(5000);
Serial.println(F("Este mensaje nunca debería aparecer"));
}
readLocalWaterSensor();
if (isWaterDetected == 1 || isWaterDetectedRemotely == 1) {
if (isValveClosed == 0) closeValve();
}
if (mqttIntervalCnt == 0) {
if (isWaterDetected == 1) publishFault();
publishStatus();
mqttIntervalCnt = mqttInterval;
} else {
if (isConnectedToValve == 0) {
Serial.println(F("Sistema fuera de servicio - no se puede controlar válvula. Sin monitorización"));
} else {
Serial.print(F("."));
}
mqttIntervalCnt--;
}
delay(mainLoopDelay / 2);
client.loop();
configureLedsWithFinalStates();
delay(mainLoopDelay / 2);
}
// Gestión sensor local
void readLocalWaterSensor() {
isWaterDetected = !getDebouncedValue(WaterDetectorPin, 100, 10);
Serial.print(isWaterDetected);
}
// Botón de reset
void enableInterruptOnResetButton() {
isResetRequested = 0;
attachInterrupt(1, onResetRequested, CHANGE);
}
void onResetRequested() {
detachInterrupt(1);
isResetRequested = 1;
}
// Apertura de válvula
int openValve() {
Serial.print(F("Abriendo válvula..."));
setupRelays(1);
if (waitForEndOfCycle(ValveClosedPin) == 0) {
setupRelays(2);
if (waitForEndOfCycle(ValveOpenedPin) == 0) {
isConnectedToValve = 1;
isValveClosed = 0;
setupRelays(0);
Serial.println(F(""));
return 0;
}
}
setupRelays(0);
isConnectedToValve = 0;
return -1;
}
// Cierre de válvula
int closeValve() {
Serial.print(F("Cerrando válvula..."));
setupRelays(2);
if (waitForEndOfCycle(ValveOpenedPin) == 0) {
setupRelays(1);
if (waitForEndOfCycle(ValveClosedPin) == 0) {
isConnectedToValve = 1;
isValveClosed = 1;
setupRelays(0);
Serial.println(F("Válvula cerrada. Inspeccione todas las habitaciones y limpie detectores"));
return 0;
}
}
setupRelays(0);
isConnectedToValve = 0;
return -1;
}
// Control de relés
void setupRelays(int scenario) {
switch (scenario) {
case 0: digitalWrite(ValveControl1, HIGH); digitalWrite(ValveControl2, HIGH); break;
case 1: digitalWrite(ValveControl1, HIGH); digitalWrite(ValveControl2, LOW); break;
case 2: digitalWrite(ValveControl1, LOW); digitalWrite(ValveControl2, HIGH); break;
default:
Serial.print(F("Escenario de relé inesperado: "));
Serial.println(scenario);
digitalWrite(ValveControl1, HIGH);
digitalWrite(ValveControl2, HIGH);
}
}
// Espera final de carrera
int waitForEndOfCycle(int limitSwitchPin) {
int cnt = valveTimeOut;
while (cnt > 0) {
if (getDebouncedValue(limitSwitchPin, 10, 10) == LOW) return 0;
cnt--;
Serial.print(F("."));
delay(1000);
}
Serial.println(F(" - Timeout al cerrar válvula. Verifique alimentación y cables."));
return -1;
}
// Antirrebotes
int getDebouncedValue(int inputPin, int intervalInMs, int requiredConfirmations) {
int confirmations = 1;
int currentValue = digitalRead(inputPin);
while (confirmations <= requiredConfirmations) {
delay(intervalInMs);
if (currentValue == digitalRead(inputPin)) confirmations++;
else { confirmations = 1; currentValue = digitalRead(inputPin); }
}
return currentValue;
}
// LEDs
void configureLedsWithInitialStates() {
clearLeds();
if (isWaterDetectedRemotely == 1 || isWaterDetected == 1) digitalWrite(AlarmLedPin, HIGH);
if (isConnectedToValve == 0 || isConnectedToBroker == 0) digitalWrite(FaultLedPin, HIGH);
digitalWrite(SystemLedPin, HIGH);
}
void configureLedsWithFinalStates() {
if (isWaterDetectedRemotely == 1) digitalWrite(AlarmLedPin, LOW);
if (isConnectedToBroker == 0) digitalWrite(FaultLedPin, LOW);
if (isValveClosed == 1) digitalWrite(SystemLedPin, LOW);
}
void clearLeds() {
digitalWrite(AlarmLedPin, LOW);
digitalWrite(FaultLedPin, LOW);
digitalWrite(SystemLedPin, LOW);
}
void testLeds() {
clearLeds();
digitalWrite(AlarmLedPin, HIGH); delay(500);
digitalWrite(FaultLedPin, HIGH); delay(500);
digitalWrite(SystemLedPin, HIGH); delay(500);
clearLeds();
}
// MQTT
void MQTTBrokerCallback(char* topic, byte* payload, unsigned int length) {
Serial.print(F("Nuevo mensaje MQTT. Topic="));
Serial.print(topic);
String payloadStr = (char*)payload;
String realPayload = payloadStr.substring(0, length);
Serial.print(F(", contenido="));
Serial.print(realPayload);
if (realPayload.indexOf("WaterDetected") > 0 && realPayload.indexOf(mqttClientLocation) == -1) {
isWaterDetectedRemotely = 1;
}
Serial.println();
}
String buildClientIdentifier() {
String data = mqttClientPrefix; data += "_"; data += mqttClientLocation; data += "_"; data += mqttClientUID; return data;
}
String buildDeviceStatusTopic() {
String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientStatusTopic; return data;
}
String buildFaultTopic() {
String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientFaultTopic; return data;
}
String buildAnyFaultTopic() {
String data = mqttClientPrefix; data += "/"; data += "+"; data += "/"; data += "+"; data += "/"; data += mqttClientFaultTopic; return data;
}
String buildDeviceStatusJson() {
String data = "{";
data += "\n\"ResetByOperator\": "; data += (int)isResetRequested; data += ",";
data += "\n\"WaterDetected\": "; data += (int)isWaterDetected; data += ",";
data += "\n\"ValveClosed\": "; data += (int)isValveClosed; data += ",";
data += "\n\"ValveDetected\": "; data += (int)isConnectedToValve;
data += "\n}";
return data;
}
String buildFaultJson() {
String data = "{";
data += "\n\"WaterDetected\": "; data += (int)isWaterDetected; data += ",";
data += "\n\"Location\": "; data += mqttClientLocation;
data += "\n}";
return data;
}
void publishMsg(char (&topic)[200], char (&payload)[200]) {
if (connectToBroker()) {
if (client.publish(topic, payload)) {
isConnectedToBroker = 1;
Serial.print(F("Mensaje enviado al broker MQTT, topic: "));
Serial.println(topic);
} else {
Serial.print(F("NO se envió mensaje al broker MQTT, topic: "));
Serial.println(topic);
isConnectedToBroker = 0;
}
client.loop();
}
}
void publishFault() {
char topicBuffer[200]; buildFaultTopic().toCharArray(topicBuffer, 200);
char payloadBuffer[200]; buildFaultJson().toCharArray(payloadBuffer, 200);
publishMsg(topicBuffer, payloadBuffer);
}
void publishStatus() {
char topicBuffer[200]; buildDeviceStatusTopic().toCharArray(topicBuffer, 200);
char payloadBuffer[200]; buildDeviceStatusJson().toCharArray(payloadBuffer, 200);
publishMsg(topicBuffer, payloadBuffer);
}
void subscribeToRemoteWaterSensors() {
if (connectToBroker()) {
char tempBuffer[200]; buildAnyFaultTopic().toCharArray(tempBuffer, 200);
client.subscribe(tempBuffer);
if (client.subscribe(tempBuffer)) {
isConnectedToBroker = 1;
Serial.print(F("Registrado como suscriptor en topic: "));
Serial.println(tempBuffer);
} else {
Serial.println(F("No se pudo registrar como suscriptor"));
isConnectedToBroker = 0;
}
client.loop();
} else {
isConnectedToBroker = 0;
Serial.println(F("No se puede suscribir: sin conexión al broker MQTT"));
}
}
int connectToBroker() {
Serial.println(F(""));
Serial.print(F("Conectando a red y broker MQTT... "));
char tempBuffer[200]; buildClientIdentifier().toCharArray(tempBuffer, 200);
if (client.connect(tempBuffer)) {
Serial.print(F("conectado como "));
Serial.println(tempBuffer);
} else {
switch (client.state()) {
case -4: Serial.println(F("MQTT_CONNECTION_TIMEOUT")); break;
case -3: Serial.println(F("MQTT_CONNECTION_LOST")); break;
case -2: Serial.println(F("MQTT_CONNECT_FAILED")); break;
case -1: Serial.println(F("MQTT_DISCONNECTED")); break;
case 1: Serial.println(F("MQTT_CONNECT_BAD_PROTOCOL")); break;
case 2: Serial.println(F("MQTT_CONNECT_BAD_CLIENT_ID")); break;
case 3: Serial.println(F("MQTT_CONNECT_UNAVAILABLE")); break;
case 4: Serial.println(F("MQTT_CONNECT_BAD_CREDENTIALS")); break;
case 5: Serial.println(F("MQTT_CONNECT_UNAUTHORIZED")); break;
default: Serial.print(F("falló, rc=")); Serial.println(client.state());
}
}
return client.connected();
}
Librería MQTT
Disponible para descarga en el repositorio del proyecto.
Esquemáticos
Proceso de manufactura
- Control de sensores y actuadores con Raspberry Pi: acelerómetro, servo y streaming de datos
- Sistema Automatizado de Llenado y Monitorización de Piscina
- Depuración del sistema dual-core Arduino Portenta H7 con Segger J-Link
- Mando universal Android con Arduino, 1Sheeld y control IR
- Crea Animaciones y Juegos en LCD 16×2 con Arduino UNO
- Alcancía Inteligente: Controla un Aceptador de Monedas con Arduino Nano
- Centro de Control de Potencia Arduino: N-FET, P-FET, Relé y RTC
- Detector de frecuencia de audio con Arduino – Mide los picos de sonido más intensos
- Controla un LED por Bluetooth con Arduino – Guía DIY paso a paso
- Controla un Rover Arduino con Firmata y Mando Xbox One - Guía Completa
- Válvulas de pellizco y bombas peristálticas: el estándar de oro para sistemas de lechada de cal abrasiva