/*
TP Cisterna - ESP32 + HC-SR04 + Potenciómetro + Servo + MQTT + PID
Publica:
/grupo11/setpoint (SP %)
/grupo11/nivel (PV %)
/grupo11/salida (OUT %)
Pines (según tu armado):
POT (wiper) -> GPIO34
HC-SR04 TRIG -> GPIO5
HC-SR04 ECHO -> GPIO18
SERVO signal -> GPIO13
VCC: POT a 3V3, HC-SR04 y SERVO a 5V, GND común
Nota real: ECHO del HC-SR04 es 5V, en físico se baja a 3.3V con divisor.
*/
#include "WiFi.h"
#include <PubSubClient.h>
#include <ESP32Servo.h> // En Wokwi suele funcionar mejor que Servo.h
// ===== BROKER EMQX =====
#define AIO_SERVER "163.10.3.73"
#define AIO_SERVERPORT 1883
#define AIO_USERNAME "embebidos"
#define AIO_KEY "Digitales1"
// ===== TOPICS =====
const char* TOPIC_SP = "/grupo11/setpoint";
const char* TOPIC_PV = "/grupo11/nivel";
const char* TOPIC_OUT = "/grupo11/salida";
// (Opcional) tópico para mandar "ON/OFF" al control
const char* TOPIC_ENABLE = "/grupo11/onoff";
// ===== PINOUT =====
static const int PIN_TRIG = 5;
static const int PIN_ECHO = 18;
static const int PIN_POT = 34; // ADC
static const int PIN_SERVO = 13;
// ===== TANQUE (escala para % nivel) =====
const float TANK_HEIGHT_CM = 30.0f; // altura “virtual” tanque
const float DIST_MIN_CM = 2.0f; // límite típico HC-SR04
const float DIST_MAX_CM = TANK_HEIGHT_CM;
// ===== PID =====
float Kp = 2.0f;
float Ki = 0.05f;
float Kd = 0.8f;
const float Ts = 1.0f; // 200 ms
float integral = 0.0f;
float prevError = 0.0f;
const float OUT_MIN = 0.0f;
const float OUT_MAX = 100.0f;
// ===== MQTT =====
WiFiClient client;
PubSubClient mqtt(client);
// ===== SERVO =====
Servo valveServo;
// ===== CONTROL ENABLE =====
bool controlEnabled = true;
// ----------------- helpers -----------------
float clampf(float x, float lo, float hi) {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
void connectWiFi() {
Serial.print("Connecting to ");
Serial.println("Wokwi-GUEST");
WiFi.begin("Wokwi-GUEST", "");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
}
void connectMQTT() {
mqtt.setServer(AIO_SERVER, AIO_SERVERPORT);
while (!mqtt.connected()) {
Serial.print("Connecting to MQTT...");
if (mqtt.connect("esp32-cisterna-grupo11", AIO_USERNAME, AIO_KEY)) {
Serial.println("connected!");
} else {
Serial.print("failed, rc=");
Serial.print(mqtt.state());
Serial.println(" retrying in 2 seconds");
delay(2000);
}
}
}
// HC-SR04: distancia en cm
float readDistanceCm() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH, 30000); // timeout 30ms
float dist = (duration * 0.0343f) / 2.0f;
if (dist <= 0) dist = DIST_MAX_CM;
return clampf(dist, DIST_MIN_CM, DIST_MAX_CM);
}
// Convertir distancia a % de nivel (cerca = lleno)
float distanceToLevelPercent(float distCm) {
float level = (DIST_MAX_CM - distCm) / (DIST_MAX_CM - DIST_MIN_CM) * 100.0f;
return clampf(level, 0.0f, 100.0f);
}
// Potenciómetro a setpoint %
float readSetpointPercent() {
int adc = analogRead(PIN_POT); // 0..4095
return (adc / 4095.0f) * 100.0f;
}
// PID: devuelve apertura 0..100 %
float pidCompute(float sp, float pv) {
float error = sp - pv;
integral += error * Ts;
float derivative = (error - prevError) / Ts;
prevError = error;
float u = Kp * error + Ki * integral + Kd * derivative;
float uSat = clampf(u, OUT_MIN, OUT_MAX);
// anti-windup simple
if (u != uSat) integral *= 0.98f;
return uSat;
}
// OUT% a ángulo servo
void setValveFromPercent(float outPercent) {
int angle = (int)round(outPercent * 180.0 / 100.0);
angle = (int)clampf((float)angle, 0.0f, 180.0f);
valveServo.write(angle);
}
void mqttPublishFloat(const char* topic, float value) {
char payload[32];
dtostrf(value, 0, 2, payload);
mqtt.publish(topic, payload);
}
// ----------------- setup / loop -----------------
unsigned long lastControlMs = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
analogReadResolution(12); // 0..4095
valveServo.attach(PIN_SERVO);
connectWiFi();
connectMQTT();
Serial.println("Setup done!");
}
void loop() {
// reconexión robusta
if (WiFi.status() != WL_CONNECTED) connectWiFi();
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
unsigned long now = millis();
if (now - lastControlMs >= (unsigned long)(Ts * 1000)) {
lastControlMs = now;
float sp = readSetpointPercent();
float dist = readDistanceCm();
float pv = distanceToLevelPercent(dist);
float out;
if (controlEnabled) {
out = pidCompute(sp, pv);
} else {
out = 0.0f; // si está OFF, cerramos válvula
integral = 0.0f; // reseteo integral para que no “explote” al volver
prevError = 0.0f;
}
setValveFromPercent(out);
// Serial (demostración)
Serial.print("SP(%): "); Serial.print(sp, 2);
Serial.print(" | PV(%): "); Serial.print(pv, 2);
Serial.print(" | OUT(%): "); Serial.print(out, 2);
Serial.print(" | CTRL: "); Serial.println(controlEnabled ? "ON" : "OFF");
// MQTT
mqttPublishFloat(TOPIC_SP, sp);
mqttPublishFloat(TOPIC_PV, pv);
mqttPublishFloat(TOPIC_OUT, out);
}
}