#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;} .card{background:white; padding:20px; border-radius:10px; display:inline-block; margin:10px; 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)+"'> 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><script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>";
html += "<script type='text/javascript'>google.charts.load('current', {'packages':['gauge']}); 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;'><h2>Panel de Instrumentos</h2>";
html += "<div style='display:flex; justify-content:center;'><div id='chart_t' style='width:280px; height:280px;'></div>";
html += "<div id='chart_h' style='width:280px; height:280px;'></div></div><p><a href='/' style='color:#00acee;'>VOLVER</a></p></body></html>";
return html;
}
// ==========================================
// SETUP
// ==========================================
void setup() {
Serial.begin(115200);
tiempoInicioPrograma = millis();
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 OK");
display.display();
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);
ledcAttach(PIN_FAN_PWM, LEDC_FREQ, LEDC_RES);
dht.setup(PIN_DHT, DHTesp::DHT22);
ESP32PWM::allocateTimer(0);
servoTurn.setPeriodHertz(50);
servoTurn.attach(PIN_SERVO, 500, 2400);
WiFi.begin("Wokwi-GUEST", "");
while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
digitalWrite(PIN_LED_WIFI, HIGH);
client.setServer(mqtt_server, 1883);
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();
server.sendHeader("Location", "/"); server.send(303);
});
server.begin();
Serial.println("\n[SISTEMA] Listo.");
}
// ==========================================
// LOOP PRINCIPAL
// ==========================================
void loop() {
server.handleClient();
if (!client.connected()) { client.connect("ESP32_Carry_Full_Device"); }
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
Serial.printf("[Día %d] T:%.1f (SP:%.1f) | H:%.1f | Heat:%.1f%% | Fan:%d | Válvula:%s\n",
currentSimDay(), tActual, spT, hActual, outHeat, fanPWM, valveState ? "ON" : "OFF");
// ==========================================
// PUBLICACIÓN MQTT COMPLETA (SIN RECORTES)
// ==========================================
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.");
}
}
// 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("[MOTOR] Volteo a %d grados\n", ang);
}
} else if (ultimoAngulo != 90) {
servoTurn.write(90); ultimoAngulo = 90;
Serial.println("[MOTOR] Bloqueado en centro por Fase de Nacimiento.");
}
// Pantalla OLED
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();
}
}