#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>
#include <ESP32Servo.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
#include <WebServer.h>
// ==========================================
// CONFIGURACIÓN DE PINES
// ==========================================
#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_BATTERY_SENSE 34
#define OLED_SDA 14
#define OLED_SCL 12
#define PIN_FAN_PWM 32
// ==========================================
// ESTRUCTURA PID COMPLETA
// ==========================================
struct PID {
double Kp, Ki, Kd;
double integ = 0, prevErr = 0;
double outMin = 0, outMax = 100;
double compute(double sp, double pv, double dt) {
if (dt <= 0) return 0;
double err = sp - pv;
double P = Kp * err;
integ += err * dt;
double I = Ki * integ;
double D = Kd * (err - prevErr) / dt;
prevErr = err;
double u = P + I + D;
return constrain(u, outMin, outMax);
}
};
PID pidT{3.0, 0.08, 2.0};
PID pidRH{2.0, 0.05, 1.0};
// ==========================================
// PARÁMETROS DE PROCESO
// ==========================================
float setT_Incub = 37.8f;
float setRH_Incub = 53.0f;
float setRH_Hatch = 70.0f;
const int LEDC_FREQ = 25000;
const int LEDC_RES = 8;
const float TURN_INTERVAL_MS = 30000;
const int TURN_ANGLE_MIN = 45;
const int TURN_ANGLE_MAX = 135;
const unsigned long DURACION_DIA_SIM = 180000;
const double TP_CYCLE_SEC = 2.0;
// ==========================================
// VARIABLES GLOBALES
// ==========================================
unsigned long tiempoInicioPrograma;
unsigned long lastUpdate = 0;
unsigned long lastMqttSend = 0;
unsigned long lastOLED = 0;
unsigned long lastTurn = 0;
float tActual = 0, hActual = 0;
double outHeat = 0, outHum = 0;
int fanPWM = 0;
bool turnDir = false;
int ultimoAngulo = -1;
bool valveState = false;
const char* mqtt_server = "broker.hivemq.com";
Adafruit_SSD1306 display(128, 64, &Wire, -1);
DHTesp dht;
Servo servoTurn;
WiFiClient espClient;
PubSubClient client(espClient);
WebServer server(80);
// ==========================================
// UTILIDADES
// ==========================================
int currentSimDay() {
return ((millis() - tiempoInicioPrograma) / DURACION_DIA_SIM) + 1;
}
bool isHatchingWindow() {
return (currentSimDay() >= 19);
}
bool timeProportionalOn(double percent) {
double onWindow = TP_CYCLE_SEC * (percent / 100.0);
double phase = fmod(millis() / 1000.0, TP_CYCLE_SEC);
return (phase < onWindow);
}
// ==========================================
// INTERFAZ WEB COMPLETA
// ==========================================
String getHTML() {
String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><meta http-equiv='refresh' content='5'>";
html += "<style>body{font-family:Arial; text-align:center; background:#f4f4f4;} ";
html += ".card{background:white; padding:20px; border-radius:10px; display:inline-block; margin:10px; ";
html += "box-shadow:0 4px 8px rgba(0,0,0,0.1); min-width:150px;}</style></head><body>";
html += "<h1>🐣 Incubadora Carry IoT - Día " + String(currentSimDay()) + "</h1>";
html += "<div class='card'><h2>" + String(tActual, 1) + "°C</h2><p>Temp Actual</p></div>";
html += "<div class='card'><h2>" + String(hActual, 1) + "%</h2><p>Humedad Actual</p></div><br>";
html += "<div class='card'><h3>" + String(outHeat, 1) + "%</h3><p>Potencia Calor</p></div>";
html += "<div class='card'><h3>" + String(fanPWM) + "</h3><p>Ventilador (PWM)</p></div>";
html += "<p>Fase: <b>" + String(isHatchingWindow() ? "NACIMIENTO (LOCKDOWN)" : "INCUBACIÓN ACTIVA") + "</b></p>";
html += "<hr><form action='/set'>Set T: <input name='t' value='"+String(setT_Incub,1)+"'> ";
html += "Set RH: <input name='r' value='"+String(setRH_Incub,1)+"'> <input type='submit' value='Guardar'></form>";
html += "<p><a href='/graficas'>📊 Ver Monitores Gauge</a></p></body></html>";
return html;
}
String getChartsHTML() {
String html = "<!DOCTYPE html><html><head>";
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 dataT = google.visualization.arrayToDataTable([['Label', 'Value'],['Temp', " + String(tActual) + "]]);";
html += "var chartT = new google.visualization.Gauge(document.getElementById('chart_t'));";
html += "chartT.draw(dataT, {min:30, max:45, yellowFrom:38, yellowTo:39, redFrom:39, redTo:45});";
html += "var dataH = google.visualization.arrayToDataTable([['Label', 'Value'],['Hum', " + String(hActual) + "]]);";
html += "var chartH = new google.visualization.Gauge(document.getElementById('chart_h'));";
html += "chartH.draw(dataH, {min:30, max:95, yellowFrom:75, yellowTo:85, redFrom:85, redTo:95});";
html += "} setTimeout(function(){ location.reload(); }, 3000);</script></head>";
html += "<body style='background:#222; color:white; text-align:center;'>";
html += "<h2>📊 Panel de Instrumentos</h2>";
html += "<div style='display:flex; justify-content:center;'>";
html += "<div id='chart_t' style='width:280px; height:280px;'></div>";
html += "<div id='chart_h' style='width:280px; height:280px;'></div></div>";
html += "<p><a href='/' style='color:#00acee;'>⬅ VOLVER</a></p></body></html>";
return html;
}
// ==========================================
// FUNCIÓN DE IMPRESIÓN DE BANNER ASCII
// ==========================================
void printBanner() {
Serial.println("\n╔═══════════════════════════════════════════════════════╗");
Serial.println("║ INCUBADORA AVÍCOLA IoT - SISTEMA CARRY ║");
Serial.println("║ Proyecto de Tesis - Ingeniería Electrónica ║");
Serial.println("╚═══════════════════════════════════════════════════════╝");
Serial.println();
}
void printWiFiInfo() {
Serial.println("┌─────────────────────────────────────────────────────┐");
Serial.println("│ INFORMACIÓN DE CONECTIVIDAD WiFi │");
Serial.println("├─────────────────────────────────────────────────────┤");
Serial.print("│ ✓ Red WiFi: ");
Serial.println("Wokwi-GUEST");
Serial.print("│ ✓ IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("│ ✓ MAC Address: ");
Serial.println(WiFi.macAddress());
Serial.print("│ ✓ Signal Strength: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
Serial.println("└─────────────────────────────────────────────────────┘");
Serial.println();
}
void printWebServerInfo() {
Serial.println("┌─────────────────────────────────────────────────────┐");
Serial.println("│ SERVIDOR WEB HTTP - RUTAS DISPONIBLES │");
Serial.println("├─────────────────────────────────────────────────────┤");
Serial.println("│ 🌐 Para acceder al servidor web en Wokwi: │");
Serial.println("│ 1. Haz clic derecho en el ESP32 │");
Serial.println("│ 2. Selecciona 'Open Web Browser' │");
Serial.println("│ 3. Navega a las siguientes URLs: │");
Serial.println("│ │");
Serial.print("│ 📄 Página Principal: http://");
Serial.println(WiFi.localIP());
Serial.print("│ 📊 Monitores Gauge: http://");
Serial.print(WiFi.localIP());
Serial.println("/graficas");
Serial.print("│ ⚙️ Configuración: http://");
Serial.print(WiFi.localIP());
Serial.println("/set");
Serial.println("└─────────────────────────────────────────────────────┘");
Serial.println();
}
void printMQTTInfo() {
Serial.println("┌─────────────────────────────────────────────────────┐");
Serial.println("│ TELEMETRÍA MQTT - BROKER HiveMQ │");
Serial.println("├─────────────────────────────────────────────────────┤");
Serial.print("│ 📡 Broker: ");
Serial.println(mqtt_server);
Serial.println("│ 📤 Topics publicados cada 10s: │");
Serial.println("│ - carry/telemetria/temp │");
Serial.println("│ - carry/telemetria/hum │");
Serial.println("│ - carry/telemetria/potencia_calor │");
Serial.println("│ - carry/telemetria/fan_pwm │");
Serial.println("│ - carry/telemetria/valvula │");
Serial.println("│ - carry/telemetria/dia │");
Serial.println("│ - carry/telemetria/fase │");
Serial.println("└─────────────────────────────────────────────────────┘");
Serial.println();
}
// ==========================================
// SETUP
// ==========================================
void setup() {
Serial.begin(115200);
delay(1000); // Esperar a que el monitor serial esté listo
// Banner de inicio
printBanner();
tiempoInicioPrograma = millis();
Serial.println("[INIT] Inicializando componentes del sistema...");
// Inicializar OLED
Wire.begin(OLED_SDA, OLED_SCL);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0,0);
display.println("SISTEMA CARRY");
display.println("Iniciando...");
display.display();
Serial.println("[INIT] ✓ Display OLED inicializado");
// Configurar pines
pinMode(PIN_RELAY_HEAT, OUTPUT);
pinMode(PIN_RELAY_HUM, OUTPUT);
pinMode(PIN_RELAY_VALVE, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_LED_WIFI, OUTPUT);
pinMode(PIN_LED_OK, OUTPUT);
pinMode(PIN_LED_ERR, OUTPUT);
Serial.println("[INIT] ✓ Pines GPIO configurados");
// Configurar PWM del ventilador
ledcAttach(PIN_FAN_PWM, LEDC_FREQ, LEDC_RES);
Serial.println("[INIT] ✓ PWM del ventilador configurado");
// Inicializar DHT22
dht.setup(PIN_DHT, DHTesp::DHT22);
Serial.println("[INIT] ✓ Sensor DHT22 inicializado (GPIO 15)");
// Inicializar Servo
ESP32PWM::allocateTimer(0);
servoTurn.setPeriodHertz(50);
servoTurn.attach(PIN_SERVO, 500, 2400);
Serial.println("[INIT] ✓ Servomotor MG995 inicializado (GPIO 19)");
// Conectar WiFi
Serial.println("\n[WiFi] Conectando a Wokwi-GUEST...");
WiFi.begin("Wokwi-GUEST", "");
int intentos = 0;
while (WiFi.status() != WL_CONNECTED && intentos < 20) {
delay(500);
Serial.print(".");
intentos++;
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
digitalWrite(PIN_LED_WIFI, HIGH);
Serial.println("[WiFi] ✓ Conexión WiFi establecida");
// Mostrar información detallada de WiFi
printWiFiInfo();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.println("WiFi: CONECTADO");
display.print("IP: ");
display.println(WiFi.localIP());
display.display();
delay(2000);
} else {
Serial.println("[WiFi] ✗ Error: No se pudo conectar a WiFi");
digitalWrite(PIN_LED_ERR, HIGH);
}
// Configurar MQTT
client.setServer(mqtt_server, 1883);
Serial.println("[MQTT] Configurado broker HiveMQ");
// Configurar servidor web
server.on("/", []() {
server.send(200, "text/html", getHTML());
});
server.on("/graficas", []() {
server.send(200, "text/html", getChartsHTML());
});
server.on("/set", []() {
if(server.hasArg("t")) setT_Incub = server.arg("t").toFloat();
if(server.hasArg("r")) setRH_Incub = server.arg("r").toFloat();
Serial.printf("[WEB] Setpoints actualizados: T=%.1f°C, RH=%.1f%%\n", setT_Incub, setRH_Incub);
server.sendHeader("Location", "/");
server.send(303);
});
server.begin();
Serial.println("[WEB] ✓ Servidor HTTP iniciado en puerto 80");
// Mostrar información del servidor web
printWebServerInfo();
// Mostrar información de MQTT
printMQTTInfo();
Serial.println("╔═══════════════════════════════════════════════════════╗");
Serial.println("║ 🚀 SISTEMA LISTO Y OPERACIONAL 🚀 ║");
Serial.println("╚═══════════════════════════════════════════════════════╝");
Serial.println("\n[MONITOR] Iniciando ciclo de control...\n");
}
// ==========================================
// LOOP PRINCIPAL
// ==========================================
void loop() {
server.handleClient();
// Reconectar MQTT si es necesario
if (!client.connected()) {
if (client.connect("ESP32_Carry_Full_Device")) {
Serial.println("[MQTT] ✓ Reconexión exitosa");
}
}
client.loop();
unsigned long now = millis();
// Bloque de Control y Lectura (500ms)
if (now - lastUpdate >= 500) {
double dt = (now - lastUpdate) / 1000.0;
lastUpdate = now;
TempAndHumidity data = dht.getTempAndHumidity();
bool dhtOk = !isnan(data.temperature);
if (dhtOk) {
tActual = data.temperature;
hActual = data.humidity;
digitalWrite(PIN_LED_ERR, LOW);
} else {
digitalWrite(PIN_LED_ERR, HIGH);
tone(PIN_BUZZER, 440, 100);
}
float spT = setT_Incub;
float spRH = isHatchingWindow() ? setRH_Hatch : setRH_Incub;
outHeat = pidT.compute(spT, tActual, dt);
outHum = pidRH.compute(spRH, hActual, dt);
double eT = tActual - spT;
fanPWM = (eT > 0) ? (int)constrain(30.0 * eT + 60, 0, 255) : 0;
ledcWrite(PIN_FAN_PWM, fanPWM);
bool hOn = timeProportionalOn(outHeat);
bool rOn = timeProportionalOn(outHum);
digitalWrite(PIN_RELAY_HEAT, hOn ? LOW : HIGH);
digitalWrite(PIN_RELAY_HUM, rOn ? LOW : HIGH);
digitalWrite(PIN_LED_OK, hOn);
valveState = (hActual < (spRH - 10.0)) && rOn;
digitalWrite(PIN_RELAY_VALVE, valveState ? LOW : HIGH);
if (tActual > 38.5 || tActual < 35.5) {
tone(PIN_BUZZER, 2000, 100);
}
// IMPRESIÓN SERIAL DETALLADA CON FORMATO
Serial.printf("┃ Día %02d │ T:%.1f°C (SP:%.1f) │ H:%.1f%% │ Heat:%.1f%% │ Fan:%03d │ Válv:%s ┃\n",
currentSimDay(), tActual, spT, hActual, outHeat, fanPWM, valveState ? "ON " : "OFF");
// PUBLICACIÓN MQTT COMPLETA (cada 10s)
if (now - lastMqttSend > 10000) {
lastMqttSend = now;
client.publish("carry/telemetria/temp", String(tActual).c_str());
client.publish("carry/telemetria/hum", String(hActual).c_str());
client.publish("carry/telemetria/potencia_calor", String(outHeat).c_str());
client.publish("carry/telemetria/fan_pwm", String(fanPWM).c_str());
client.publish("carry/telemetria/valvula", valveState ? "1" : "0");
client.publish("carry/telemetria/dia", String(currentSimDay()).c_str());
client.publish("carry/telemetria/fase", isHatchingWindow() ? "HATCHING" : "INCUBATION");
Serial.println("┃ [MQTT] 📤 Telemetría completa enviada a HiveMQ ┃");
}
}
// Control de Volteo
if (!isHatchingWindow()) {
if (now - lastTurn >= 30000) {
lastTurn = now;
turnDir = !turnDir;
int ang = turnDir ? TURN_ANGLE_MIN : TURN_ANGLE_MAX;
servoTurn.write(ang);
Serial.printf("┃ [SERVO] 🔄 Volteo a %d° (dirección: %s) ┃\n",
ang, turnDir ? "MIN" : "MAX");
}
} else if (ultimoAngulo != 90) {
servoTurn.write(90);
ultimoAngulo = 90;
Serial.println("┃ [SERVO] 🔒 Bloqueado en 90° (Fase de Nacimiento) ┃");
}
// Actualización OLED (cada 1s)
if (now - lastOLED >= 1000) {
lastOLED = now;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.printf("DIA: %d %s", currentSimDay(), isHatchingWindow() ? "NACIMIENTO" : "INCUBANDO");
display.drawFastHLine(0, 10, 128, WHITE);
display.setTextSize(2);
display.setCursor(0,20);
display.printf("%.1f C", tActual);
display.setCursor(0,45);
display.printf("%.0f%% RH", hActual);
display.display();
}
}