#include <WiFi.h>
#include <PubSubClient.h>
const char* BROKER = "broker.hivemq.com";
const char* BASE = "aquacontrol/es1/b7d4916c2026"; // cambiar en AMBOS nodos para otra dupla
WiFiClient net;
PubSubClient mqtt(net);
String soilTopic = String(BASE) + "/suelo";
String cmdTopic = String(BASE) + "/orden";
String stateTopic = String(BASE) + "/estado";
uint32_t retryAt = 0;
String serialLine;
// Wokwi relays use transistor=pnp, making HIGH connect COM to NO.
// GPIO16/17 avoid GPIO5 boot strapping.
// On physical hardware verify the relay module's supply, 3.3V input compatibility,
// active polarity and safe startup before connecting a separate low-voltage load.
const int PUMP=16, VALVE=17, ALARM=22, BUZZER=21, TANK=26, FLOW=27, RESET_BUTTON=18;
volatile uint32_t pulseCounter=0;
uint32_t sessionId=0, lastSequence=0, lastCommand=0, gaps=0, lastSoil=0;
uint32_t lastReport=0, lastTick=0;
int moisture=100;
bool hasSoil=false, wasReset=false;
void IRAM_ATTR pulseISR(){ ++pulseCounter; }
// BEGIN CONTROL CORE - misma logica usada en las pruebas automatizadas
struct Controller {
bool pump=false, automatic=true, manual=false, blocked=false;
float cycle=0, total=0;
uint32_t started=0,lastFlow=0;
const char* reason="ESPERA_SUELO";
void stop(const char* why,bool latch=false) {
pump=false; manual=false; reason=why;
if(latch) blocked=true;
}
void reset(bool fresh,bool water) {
if(!fresh || !water) {stop("RESET_RECHAZADO",true);return;}
blocked=false; manual=false; reason="REARMADO";
}
void command(const char* op,bool fresh,bool water) {
if(!strcmp(op,"OFF")) {automatic=false;stop("PARO_REMOTO");}
else if(!strcmp(op,"AUTO")) {automatic=true;manual=false;reason="MODO_AUTO";}
else if(!strcmp(op,"RESET")) reset(fresh,water);
else if(!strcmp(op,"ON")) {
if(!fresh||!water||blocked) {reason="ON_RECHAZADO";return;}
automatic=false; manual=true; reason="MODO_MANUAL";
}
}
void tick(uint32_t now,int humidity,bool fresh,bool water,uint32_t pulses) {
if(pump && pulses) {
float liters=pulses/450.0f; cycle+=liters; total+=liters; lastFlow=now;
}
if(!water) {stop("NIVEL_BAJO",true);return;}
if(!fresh) {
if(pump) stop("SIN_COMUNICACION",true);
else if(!blocked) reason="ESPERA_SUELO";
return;
}
if(blocked) return;
if(pump) {
if(now-lastFlow>=3000) {stop("SIN_CAUDAL",true);return;}
if(cycle>=0.50f) {stop("LIMITE_VOLUMEN",true);return;}
if(now-started>=20000) {stop("LIMITE_TIEMPO",true);return;}
if(automatic && humidity>=55) {stop("HUMEDAD_SUFICIENTE");return;}
}
if(!pump && (manual || (automatic && humidity<35))) {
pump=true; cycle=0; started=lastFlow=now; reason="REGANDO";
}
if(!pump && automatic && !manual)
reason = humidity >= 55 ? "SUELO_HUMEDO" : "HUMEDAD_INTERMEDIA";
}
} control;
// END CONTROL CORE
bool freshSoil() {return hasSoil && mqtt.connected() && millis()-lastSoil<6000;}
bool waterOK() {return digitalRead(TANK)==LOW;}
void outputs() {
digitalWrite(PUMP,control.pump?HIGH:LOW);
digitalWrite(VALVE,control.pump?HIGH:LOW); // zone valve opens only while irrigating
digitalWrite(ALARM,control.blocked?HIGH:LOW);
if(control.blocked) tone(BUZZER,1200); else noTone(BUZZER);
}
void reportState() {
char out[200];
snprintf(out,sizeof(out),"%lu,%lu,%lu,%d,%.3f,%.3f,%lu,%s",
(unsigned long)sessionId,(unsigned long)lastSequence,(unsigned long)lastCommand,
control.pump,control.cycle,control.total,(unsigned long)gaps,control.reason);
if(mqtt.connected()) mqtt.publish(stateTopic.c_str(),out,false);
Serial.printf("B | bomba=%d humedad=%d ciclo=%.3f L total=%.3f L saltos=%lu %s\n",
control.pump,moisture,control.cycle,control.total,(unsigned long)gaps,control.reason);
}
void onMessage(char* topic,byte* bytes,unsigned int len) {
char buf[180]; if(len>=sizeof(buf)) return;
memcpy(buf,bytes,len);buf[len]=0;
unsigned long sess,seq; int hum; char op[16],extra;
if(String(topic)==soilTopic) {
if(sscanf(buf,"%lu,%lu,%d%c",&sess,&seq,&hum,&extra)!=3 || hum<0 || hum>100 || !seq) return;
if(!hasSoil || sess!=sessionId) {
if(hasSoil) control.stop("REINICIO_NODO_A",true);
sessionId=sess;lastSequence=0;lastCommand=0;gaps=0;
}
if(seq<=lastSequence) return;
if(lastSequence && seq>lastSequence+1) gaps+=seq-lastSequence-1;
lastSequence=seq;moisture=hum;lastSoil=millis();hasSoil=true;
} else if(String(topic)==cmdTopic) {
if(sscanf(buf,"%lu,%lu,%15s",&sess,&seq,op)!=3 || sess!=sessionId || !seq) return;
if(strcmp(op,"ON")&&strcmp(op,"OFF")&&strcmp(op,"AUTO")&&strcmp(op,"RESET")) return;
if(seq<=lastCommand) {reportState();return;}
lastCommand=seq;control.command(op,freshSoil(),waterOK());
// Ejecutar la decision y actualizar la salida ANTES de confirmar.
control.tick(millis(),moisture,freshSoil(),waterOK(),0);
outputs();reportState();
}
}
void connectNetwork() {
// No intentar reconexiones bloqueantes mientras la salida este activa.
if(control.pump || WiFi.status()!=WL_CONNECTED || mqtt.connected() || millis()-retryAt<3000) return;
retryAt=millis();
String id="aq-B-"+String(esp_random(),HEX);
if(mqtt.connect(id.c_str())) {
mqtt.subscribe(soilTopic.c_str());mqtt.subscribe(cmdTopic.c_str());
Serial.println("MQTT B conectado");
} else Serial.printf("MQTT B error=%d; reintento automatico\n",mqtt.state());
}
void setup() {
Serial.begin(115200);
pinMode(PUMP,OUTPUT);digitalWrite(PUMP,LOW);
pinMode(VALVE,OUTPUT);digitalWrite(VALVE,LOW);
pinMode(ALARM,OUTPUT);pinMode(BUZZER,OUTPUT);
pinMode(TANK,INPUT_PULLUP);pinMode(FLOW,INPUT_PULLDOWN);
pinMode(RESET_BUTTON,INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW),pulseISR,RISING);
mqtt.setServer(BROKER,1883);mqtt.setCallback(onMessage);mqtt.setSocketTimeout(10);mqtt.setBufferSize(384);
WiFi.begin("Wokwi-GUEST","",6);
Serial.println("AquaControl B | salidas: rele bomba GPIO16 y valvula de riego GPIO17");
Serial.println("SIMULACION: 450 pulsos/L, 15 Hz=2 L/min");
Serial.println("Tanque/flujo: izquierda NORMAL, derecha FALLA. RESET: boton azul.");
}
void loop() {
connectNetwork();mqtt.loop();
if(millis()-lastTick>=50) {
lastTick=millis();
noInterrupts();uint32_t n=pulseCounter;pulseCounter=0;interrupts();
control.tick(millis(),moisture,freshSoil(),waterOK(),n);outputs();
}
bool reset=digitalRead(RESET_BUTTON)==LOW;
if(reset&&!wasReset) {control.reset(freshSoil(),waterOK());outputs();reportState();}
wasReset=reset;
if(millis()-lastReport>=1000) {lastReport=millis();reportState();}
delay(10);
}
AQUACONTROL B RIEGO
TANQUE: izq OK / der VACIO
CAUDAL: izq OK / der SIN FLUJO
RELE BOMBA: GPIO16 / salida visible
ALARMA
REARMAR
15 Hz = 2 L/min simulados
VALVULA RIEGO: GPIO17 / salida visible