#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_SERVO 19
#define PIN_BUZZER 27
#define PIN_LED_ERR 21 // Rojo
#define PIN_LED_WIFI 5 // Azul
#define PIN_LED_OK 2 // Verde
#define PIN_BATTERY_SENSE 34
// Pines I2C para tu OLED
#define OLED_SDA 14
#define OLED_SCL 12
// ==========================================
// VARIABLES DE CONTROL E INCUBACIÓN
// ==========================================
double Kp = 60.0; // Agresividad del calor
const double setpointT = 37.8; // Temperatura ideal (Gallina)
const float humMinima = 55.0; // Humedad mínima ideal
unsigned long windowSize = 4000;
// ==========================================
// OBJETOS Y GLOBALES
// ==========================================
Adafruit_SSD1306 display(128, 64, &Wire, -1);
DHTesp dht;
Servo servoTurn;
WiFiClient espClient;
PubSubClient client(espClient);
WebServer server(80);
float tActual = 0, hActual = 0;
unsigned long windowStartTime;
unsigned long lastMqttSend = 0;
const char* mqtt_server = "broker.hivemq.com";
// --- PÁGINA WEB LOCAL ---
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</h1>";
html += "<div class='card'><h2 style='color:#d32f2f'>" + String(tActual, 1) + "°C</h2><p>Temperatura</p></div>";
html += "<div class='card'><h2 style='color:#1976d2'>" + String(hActual, 1) + "%</h2><p>Humedad</p></div>";
html += "<p><b>Fuente:</b> " + String(analogRead(PIN_BATTERY_SENSE) > 2500 ? "Panel Solar" : "Batería") + "</p></body></html>";
return html;
}
void setup() {
Serial.begin(115200);
// 1. INICIALIZAR OLED EN PINES 14 Y 12
Wire.begin(OLED_SDA, OLED_SCL);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("Error: Pantalla no detectada");
}
display.clearDisplay();
display.setTextColor(WHITE);
display.display();
// 2. CONFIGURAR HARDWARE
pinMode(PIN_RELAY_HEAT, OUTPUT);
pinMode(PIN_RELAY_HUM, OUTPUT);
pinMode(PIN_LED_WIFI, OUTPUT);
pinMode(PIN_LED_OK, OUTPUT);
pinMode(PIN_LED_ERR, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_RELAY_HEAT, HIGH); // Relé inicia apagado
dht.setup(PIN_DHT, DHTesp::DHT22);
servoTurn.attach(PIN_SERVO);
windowStartTime = millis();
// 3. WIFI, WEB Y MQTT
WiFi.begin("Wokwi-GUEST", "");
client.setServer(mqtt_server, 1883);
server.on("/", []() { server.send(200, "text/html", getHTML()); });
server.begin();
}
void loop() {
// --- GESTIÓN DE CONECTIVIDAD ---
if (WiFi.status() == WL_CONNECTED) {
digitalWrite(PIN_LED_WIFI, HIGH);
server.handleClient();
if (!client.connected()) {
if (client.connect("ESP32_Incubadora_Carry")) {
client.publish("incubadora/carry/status", "online");
}
}
client.loop();
} else {
digitalWrite(PIN_LED_WIFI, (millis() % 1000 < 500)); // Parpadeo error WiFi
}
// --- LECTURA DE SENSORES Y CONTROL (CADA 2 SEGUNDOS) ---
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 2000) {
lastUpdate = millis();
TempAndHumidity data = dht.getTempAndHumidity();
// Sensado de Batería/Panel
int lecturaBateria = analogRead(PIN_BATTERY_SENSE);
String modoEnergia = (lecturaBateria > 2500) ? "SOLAR" : "BATERIA";
if (!isnan(data.temperature)) {
tActual = data.temperature;
hActual = data.humidity;
// 1. LÓGICA DE ALARMA (BUZZER Y LED ROJO)
if (tActual > 39.0 || tActual < 35.0 || hActual < 45.0) {
digitalWrite(PIN_LED_ERR, HIGH);
if (tActual > 39.0) tone(PIN_BUZZER, 1500, 200); // Agudo: Calor
else if (tActual < 35.0) tone(PIN_BUZZER, 800, 500); // Grave: Frío
else if (hActual < 45.0) tone(PIN_BUZZER, 400, 100); // Corto: Humedad
} else {
digitalWrite(PIN_LED_ERR, LOW);
noTone(PIN_BUZZER);
}
// 2. LÓGICA PID (Control eficiente de calor)
double error = setpointT - tActual;
double duty = (error > 0) ? error * Kp : 0;
if (duty > 100) duty = 100;
if (millis() - windowStartTime > windowSize) windowStartTime = millis();
bool encenderRelay = ((duty * windowSize / 100) > (millis() - windowStartTime));
digitalWrite(PIN_RELAY_HEAT, encenderRelay ? LOW : HIGH);
digitalWrite(PIN_LED_OK, encenderRelay ? HIGH : LOW);
digitalWrite(PIN_RELAY_HUM, (hActual < humMinima) ? LOW : HIGH);
// 3. ACTUALIZAR PANTALLA OLED
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print("PWR:"); display.print(modoEnergia);
display.setCursor(75,0);
display.print("PID:"); display.print((int)duty); display.print("%");
display.setTextSize(2);
display.setCursor(0,22);
display.print("T:"); display.print(tActual, 1); display.print("C");
display.setCursor(0,45);
display.print("H:"); display.print(hActual, 0); display.print("%");
display.display();
// 4. ENVÍO MQTT (CADA 10 SEG)
if (millis() - lastMqttSend > 10000) {
client.publish("incubadora/carry/temp", String(tActual, 1).c_str());
client.publish("incubadora/carry/hum", String(hActual, 1).c_str());
client.publish("incubadora/carry/pwr", modoEnergia.c_str());
lastMqttSend = millis();
}
Serial.printf("T: %.1f | H: %.1f | PID: %.0f%% | %s\n", tActual, hActual, duty, modoEnergia.c_str());
}
}
// --- VOLTEO AUTOMÁTICO (CADA 30 SEGUNDOS EN SIMULACIÓN) ---
int angulo = ((millis() / 30000) % 2 == 0) ? 60 : 120;
servoTurn.write(angulo);
}