/*
* INCUBADORA FUSION DUAL MQTT COMPACT
* Combina HiveMQ + Adafruit IO con todas las funciones criticas
* PID completo, Servo, Buzzer, LEDs, OLED, Web Server (comentado)
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_MQTT.h>
#include <Adafruit_MQTT_Client.h>
#include <DHTesp.h>
#include <ESP32Servo.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// WiFi credentials
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// Adafruit IO credentials
#define IO_USERNAME "competitivefer50"
#define IO_KEY "aio_HjvR7438LVUoFRQ6ayMUL1bEh0kX"
#define IO_SERVER "io.adafruit.com"
#define IO_SERVERPORT 1883
// GPIO Pins
#define PIN_DHT 15
#define PIN_RELAY_HEAT 26
#define PIN_RELAY_HUM 25
#define PIN_RELAY_VALVE 33
#define PIN_SERVO 19
#define PIN_BUZZER 27
#define PIN_LED_ERR 21
#define PIN_LED_WIFI 5
#define PIN_LED_OK 2
#define PIN_FAN_PWM 32
#define OLED_SDA 14
#define OLED_SCL 12
// PID Structure with anti-windup
struct PID {
float Kp, Ki, Kd;
float integral, prevError;
PID(float p, float i, float d) : Kp(p), Ki(i), Kd(d), integral(0), prevError(0) {}
float compute(float sp, float pv, float dt) {
float err = sp - pv;
integral += err * dt;
if(integral > 50) integral = 50;
if(integral < -50) integral = -50;
float deriv = (err - prevError) / dt;
prevError = err;
return Kp*err + Ki*integral + Kd*deriv;
}
};
// Global objects
DHTesp dht;
Servo servo;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
WiFiClient espClient;
PubSubClient hivemqClient(espClient);
WiFiClient aioClient;
Adafruit_MQTT_Client mqtt(&aioClient, IO_SERVER, IO_SERVERPORT, IO_USERNAME, IO_KEY);
// Adafruit IO Feeds
Adafruit_MQTT_Publish feedTemp = Adafruit_MQTT_Publish(&mqtt, IO_USERNAME "/feeds/temperatura");
Adafruit_MQTT_Publish feedHum = Adafruit_MQTT_Publish(&mqtt, IO_USERNAME "/feeds/humedad");
Adafruit_MQTT_Publish feedDia = Adafruit_MQTT_Publish(&mqtt, IO_USERNAME "/feeds/dia");
Adafruit_MQTT_Publish feedHeat = Adafruit_MQTT_Publish(&mqtt, IO_USERNAME "/feeds/potencia-calor");
Adafruit_MQTT_Publish feedFase = Adafruit_MQTT_Publish(&mqtt, IO_USERNAME "/feeds/fase");
// PID controllers
PID pidT(3.0, 0.08, 2.0);
PID pidRH(2.0, 0.05, 1.0);
// Control variables
float temperatura = 0, humedad = 0;
float setpointTemp = 37.5, setpointRH = 55.0;
unsigned long lastControl = 0, lastHiveMQ = 0, lastAdafruit = 0, lastServo = 0, lastOLED = 0;
unsigned long windowStart = 0;
float potenciaCalor = 0, potenciaHum = 0;
int fanPWM = 0;
bool relayHeat = false, relayHum = false, valvulaOpen = false, servoPos = false;
unsigned long startTime = 0;
// Utility functions
int currentSimDay() {
return (millis() - startTime) / 180000 + 1;
}
bool isHatchingWindow() {
return currentSimDay() >= 19;
}
bool timeProportionalOn(float percent, unsigned long windowSize) {
unsigned long now = millis();
if(now - windowStart >= windowSize) windowStart = now;
return (now - windowStart) < (windowSize * percent / 100.0);
}
void MQTT_connect() {
if(mqtt.connected()) return;
Serial.print("Connecting to Adafruit IO...");
uint8_t retries = 3;
int8_t ret;
while((ret = mqtt.connect()) != 0) {
Serial.println(mqtt.connectErrorString(ret));
mqtt.disconnect();
delay(5000);
retries--;
if(retries == 0) {
Serial.println("Adafruit IO connection failed");
return;
}
}
Serial.println("Adafruit IO connected!");
}
void connectHiveMQ() {
while(!hivemqClient.connected()) {
Serial.print("Connecting to HiveMQ...");
String clientID = "ESP32Client_" + String(random(0xffff), HEX);
if(hivemqClient.connect(clientID.c_str())) {
Serial.println("HiveMQ connected!");
} else {
Serial.print("failed, rc=");
Serial.println(hivemqClient.state());
delay(5000);
}
}
}
/* WEB SERVER - COMENTADO PARA MANTENER <700 LINEAS
* Descomentar para hardware real con servidor Wokwi privado
*
* WebServer server(80);
*
* String getHTML() {
* String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
* html += "<meta name='viewport' content='width=device-width,initial-scale=1.0'>";
* html += "<title>Incubadora IoT</title>";
* html += "<style>body{font-family:Arial;margin:20px;background:#f0f0f0;}";
* html += ".container{max-width:800px;margin:auto;background:white;padding:20px;border-radius:10px;box-shadow:0 2px 5px rgba(0,0,0,0.1);}";
* html += "h1{color:#333;text-align:center;}";
* html += ".data{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin:20px 0;}";
* html += ".card{background:#f9f9f9;padding:15px;border-radius:8px;border-left:4px solid #4CAF50;}";
* html += ".label{font-size:14px;color:#666;}";
* html += ".value{font-size:24px;font-weight:bold;color:#333;margin-top:5px;}";
* html += ".controls{margin-top:20px;}";
* html += "input[type=number]{width:100%;padding:8px;margin:5px 0;border:1px solid #ddd;border-radius:4px;}";
* html += "button{background:#4CAF50;color:white;padding:10px 20px;border:none;border-radius:4px;cursor:pointer;margin:5px;}";
* html += "button:hover{background:#45a049;}</style>";
* html += "<script>setInterval(()=>location.reload(),5000);</script></head><body>";
* html += "<div class='container'><h1>🥚 Incubadora IoT Control</h1>";
* html += "<div class='data'>";
* html += "<div class='card'><div class='label'>Temperatura</div><div class='value'>" + String(temperatura, 1) + " °C</div></div>";
* html += "<div class='card'><div class='label'>Humedad</div><div class='value'>" + String(humedad, 1) + " %</div></div>";
* html += "<div class='card'><div class='label'>Día Simulado</div><div class='value'>" + String(currentSimDay()) + "</div></div>";
* html += "<div class='card'><div class='label'>Fase</div><div class='value'>" + String(isHatchingWindow() ? "Eclosión" : "Incubación") + "</div></div>";
* html += "</div>";
* html += "<div class='controls'><h3>Control Manual</h3>";
* html += "<form action='/setTemp' method='GET'>Setpoint Temp: <input type='number' name='val' step='0.1' value='" + String(setpointTemp, 1) + "'><button type='submit'>Ajustar</button></form>";
* html += "<form action='/setRH' method='GET'>Setpoint Humedad: <input type='number' name='val' step='0.1' value='" + String(setpointRH, 1) + "'><button type='submit'>Ajustar</button></form>";
* html += "</div></div></body></html>";
* return html;
* }
*
* String getChartsHTML() {
* String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
* html += "<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>";
* html += "<script type='text/javascript'>";
* html += "google.charts.load('current',{'packages':['gauge']});";
* html += "google.charts.setOnLoadCallback(drawChart);";
* html += "function drawChart(){";
* html += "var data=google.visualization.arrayToDataTable([['Label','Value'],['Temp'," + String(temperatura, 1) + "],['Hum'," + String(humedad, 1) + "]]);";
* html += "var options={width:400,height:240,redFrom:38,redTo:40,yellowFrom:36,yellowTo:38,greenFrom:35,greenTo:36,minorTicks:5,max:40};";
* html += "var chart=new google.visualization.Gauge(document.getElementById('chart'));";
* html += "chart.draw(data,options);";
* html += "}";
* html += "setInterval(()=>location.reload(),5000);";
* html += "</script></head><body><h1 style='text-align:center;'>📊 Gráficos en Tiempo Real</h1>";
* html += "<div id='chart' style='width:400px;height:240px;margin:auto;'></div></body></html>";
* return html;
* }
*
* // Web routes
* server.on("/", []() { server.send(200, "text/html", getHTML()); });
* server.on("/charts", []() { server.send(200, "text/html", getChartsHTML()); });
* server.on("/setTemp", []() {
* if(server.hasArg("val")) setpointTemp = server.arg("val").toFloat();
* server.sendHeader("Location", "/");
* server.send(303);
* });
* server.on("/setRH", []() {
* if(server.hasArg("val")) setpointRH = server.arg("val").toFloat();
* server.sendHeader("Location", "/");
* server.send(303);
* });
*
* // En setup(): server.begin();
* // En loop(): server.handleClient();
*
* FIN DE SECCION WEB SERVER COMENTADA */
void setup() {
Serial.begin(115200);
startTime = millis();
// GPIO configuration
pinMode(PIN_RELAY_HEAT, OUTPUT);
pinMode(PIN_RELAY_HUM, OUTPUT);
pinMode(PIN_RELAY_VALVE, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_LED_ERR, OUTPUT);
pinMode(PIN_LED_WIFI, OUTPUT);
pinMode(PIN_LED_OK, OUTPUT);
digitalWrite(PIN_RELAY_HEAT, LOW);
digitalWrite(PIN_RELAY_HUM, LOW);
digitalWrite(PIN_RELAY_VALVE, LOW);
digitalWrite(PIN_LED_ERR, LOW);
digitalWrite(PIN_LED_WIFI, LOW);
digitalWrite(PIN_LED_OK, LOW);
// PWM fan (ESP32 Core 3.x API)
ledcAttach(PIN_FAN_PWM, 25000, 8);
ledcWrite(PIN_FAN_PWM, 0);
// DHT sensor
dht.setup(PIN_DHT, DHTesp::DHT22);
// Servo
servo.attach(PIN_SERVO);
servo.write(90);
// OLED (I2C init BEFORE display.begin)
Wire.begin(OLED_SDA, OLED_SCL);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED init failed");
digitalWrite(PIN_LED_ERR, HIGH);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Iniciando...");
display.display();
// WiFi connection
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
digitalWrite(PIN_LED_WIFI, !digitalRead(PIN_LED_WIFI));
}
Serial.println("WiFi connected");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
digitalWrite(PIN_LED_WIFI, HIGH);
digitalWrite(PIN_LED_OK, HIGH);
// HiveMQ MQTT setup
hivemqClient.setServer("broker.hivemq.com", 1883);
connectHiveMQ();
// Adafruit IO setup
MQTT_connect();
Serial.println("Sistema iniciado - Dual MQTT activo");
display.clearDisplay();
display.setCursor(0, 0);
display.println("Sistema OK");
display.println("HiveMQ: OK");
display.println("Adafruit: OK");
display.display();
delay(2000);
}
void loop() {
unsigned long now = millis();
// Control cycle (500ms)
if(now - lastControl >= 500) {
lastControl = now;
// Read DHT22
TempAndHumidity data = dht.getTempAndHumidity();
if(dht.getStatus() == DHTesp::ERROR_NONE) {
temperatura = data.temperature;
humedad = data.humidity;
digitalWrite(PIN_LED_ERR, LOW);
} else {
digitalWrite(PIN_LED_ERR, HIGH);
tone(PIN_BUZZER, 440, 100);
Serial.println("DHT read error");
}
// PID computation
potenciaCalor = pidT.compute(setpointTemp, temperatura, 0.5);
potenciaHum = pidRH.compute(setpointRH, humedad, 0.5);
// Constrain outputs
if(potenciaCalor < 0) potenciaCalor = 0;
if(potenciaCalor > 100) potenciaCalor = 100;
if(potenciaHum < 0) potenciaHum = 0;
if(potenciaHum > 100) potenciaHum = 100;
// Fan PWM (inverse to temperature error)
float tempError = temperatura - setpointTemp;
if(tempError > 1.0) {
fanPWM = map(constrain(tempError * 50, 0, 100), 0, 100, 128, 255);
} else {
fanPWM = 64;
}
ledcWrite(PIN_FAN_PWM, fanPWM);
// Time-proportional relay control
relayHeat = timeProportionalOn(potenciaCalor, 2000);
relayHum = timeProportionalOn(potenciaHum, 2000);
digitalWrite(PIN_RELAY_HEAT, relayHeat ? HIGH : LOW);
digitalWrite(PIN_RELAY_HUM, relayHum ? HIGH : LOW);
// Valve control (open if humidity low)
valvulaOpen = (humedad < setpointRH - 3.0);
digitalWrite(PIN_RELAY_VALVE, valvulaOpen ? HIGH : LOW);
// Temperature alerts with buzzer
if(temperatura > 38.5) {
tone(PIN_BUZZER, 2000, 100);
Serial.println("ALERTA: Temperatura alta!");
}
if(temperatura < 35.5) {
tone(PIN_BUZZER, 2000, 100);
Serial.println("ALERTA: Temperatura baja!");
}
// Serial telemetry
Serial.print("Dia:");
Serial.print(currentSimDay());
Serial.print(" Fase:");
Serial.print(isHatchingWindow() ? "Eclosion" : "Incubacion");
Serial.print(" T:");
Serial.print(temperatura, 1);
Serial.print("C H:");
Serial.print(humedad, 1);
Serial.print("% Heat:");
Serial.print(potenciaCalor, 0);
Serial.print("% Fan:");
Serial.println(fanPWM);
}
// Servo turning (30s interval)
if(now - lastServo >= 30000) {
lastServo = now;
if(!isHatchingWindow()) {
servoPos = !servoPos;
int angle = servoPos ? 135 : 45;
servo.write(angle);
Serial.print("Servo volteo: ");
Serial.println(angle);
} else {
servo.write(90);
Serial.println("Servo bloqueado (eclosion)");
}
}
// HiveMQ publish (10s)
if(now - lastHiveMQ >= 10000) {
lastHiveMQ = now;
if(!hivemqClient.connected()) connectHiveMQ();
if(hivemqClient.connected()) {
String base = "incubadora/esp32/";
hivemqClient.publish((base + "temperatura").c_str(), String(temperatura, 1).c_str());
hivemqClient.publish((base + "humedad").c_str(), String(humedad, 1).c_str());
hivemqClient.publish((base + "potencia_calor").c_str(), String(potenciaCalor, 0).c_str());
hivemqClient.publish((base + "fan_pwm").c_str(), String(fanPWM).c_str());
hivemqClient.publish((base + "valvula").c_str(), valvulaOpen ? "1" : "0");
hivemqClient.publish((base + "dia").c_str(), String(currentSimDay()).c_str());
hivemqClient.publish((base + "fase").c_str(), isHatchingWindow() ? "eclosion" : "incubacion");
Serial.println("HiveMQ: 7 topics publicados");
}
}
// Adafruit IO publish (15s)
if(now - lastAdafruit >= 15000) {
lastAdafruit = now;
MQTT_connect();
if(mqtt.connected()) {
feedTemp.publish(temperatura);
feedHum.publish(humedad);
feedDia.publish((int32_t)currentSimDay());
feedHeat.publish((int32_t)potenciaCalor);
feedFase.publish(isHatchingWindow() ? "eclosion" : "incubacion");
Serial.println("Adafruit IO: 5 feeds publicados");
}
}
// OLED update (1s)
if(now - lastOLED >= 1000) {
lastOLED = now;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Dia: ");
display.print(currentSimDay());
display.print(" ");
display.println(isHatchingWindow() ? "ECLOSION" : "INCUBACION");
display.println();
display.setTextSize(2);
display.print("T:");
display.print(temperatura, 1);
display.println("C");
display.print("H:");
display.print(humedad, 1);
display.println("%");
display.display();
}
// MQTT keep-alive
hivemqClient.loop();
delay(10);
}