/* PROYECTO DE TESIS ASLINGNEY,LUISALVARADO,JUSTINVALLEJOS.
INCUBADORA Carry FUSION - DUAL MQTT COMPACT
Configuracion dual MQTT: HiveMQ + Adafruit IO
Control PID temperatura y humedad
Servo volteo con bloqueo en eclosion
Buzzer, LEDs, OLED, Reles
Servidor web comentado (descomentar para hardware)
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Servo.h>
// #include <WebServer.h> // Descomentar para hardware
// Pines
#define DHT_PIN 15
#define DHT_TYPE DHT22
#define OLED_SDA 14
#define OLED_SCL 12
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_ADDR 0x3C
#define PIN_RELE_CALOR 26
#define PIN_RELE_HUMEDAD 25
#define PIN_BUZZER 27
#define PIN_LED_TEMP 21
#define PIN_LED_HUM 5
#define PIN_SERVO 19
#define PIN_LED_WIFI 2
// WiFi
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// HiveMQ
const char* hivemq_server = "broker.hivemq.com";
const int hivemq_port = 1883;
const char* hivemq_client = "ESP32_Incubadora_Carry_001";
// Adafruit IO
const char* aio_server = "io.adafruit.com";
const int aio_port = 1883;
const char* aio_username = "competitivefer50";
const char* aio_key = "aio_HjvR7438LVUoFRQ6ayMUL1bEh0kX";
const char* aio_client = "ESP32_Incubadora_AIO";
// Objetos
WiFiClient espClient1, espClient2;
PubSubClient hivemq(espClient1);
PubSubClient aio(espClient2);
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
Servo servo;
// WebServer server(80); // Descomentar para hardware
// Variables PID
struct PIDController {
float Kp, Ki, Kd;
float integral, lastError;
float outMin, outMax;
PIDController(float p, float i, float d, float min, float max)
: Kp(p), Ki(i), Kd(d), integral(0), lastError(0), outMin(min), outMax(max) {}
float compute(float setpoint, float input, float dt) {
float error = setpoint - input;
integral += error * dt;
integral = constrain(integral, outMin/Ki, outMax/Ki);
float derivative = (error - lastError) / dt;
lastError = error;
float output = Kp*error + Ki*integral + Kd*derivative;
return constrain(output, outMin, outMax);
}
};
PIDController pidTemp(3.0, 0.08, 2.0, 0, 100);
PIDController pidHum(2.0, 0.05, 1.0, 0, 100);
// Variables globales
DHTesp dht;
float temp = 0, hum = 0;
float setTemp = 37.5, setHum = 60.0;
int dayIncubation = 1;
bool manualControl = false;
bool alarmActive = false;
unsigned long lastPID = 0, lastServo = 0, lastHiveMQ = 0, lastAIO = 0;
unsigned long lastBuzzer = 0, lastOLED = 0, lastTelemetry = 0, windowStartTime = 0;
bool servoPos = false;
int fanPWM = 0; // PWM del ventilador
float potenciaCalor = 0; // Potencia del calentador (%)
const unsigned long windowSize = 2000;
// Funciones auxiliares
bool isHatchingWindow() {
return (dayIncubation >= 19 && dayIncubation <= 21);
}
void checkAlarms() {
alarmActive = (temp < setTemp - 2 || temp > setTemp + 2 ||
hum < setHum - 10 || hum > setHum + 10);
}
void updateBuzzer() {
unsigned long now = millis();
if(alarmActive) {
if(now - lastBuzzer >= 500) {
lastBuzzer = now;
static bool buzzerState = false;
buzzerState = !buzzerState;
ledcWrite(PIN_BUZZER, buzzerState ? 128 : 0);
}
} else {
ledcWrite(PIN_BUZZER, 0);
}
}
void updateLEDs() {
digitalWrite(PIN_LED_TEMP, (temp < setTemp - 1 || temp > setTemp + 1) ? HIGH : LOW);
digitalWrite(PIN_LED_HUM, (hum < setHum - 5 || hum > setHum + 5) ? HIGH : LOW);
}
void updateOLED() {
unsigned long now = millis();
if(now - lastOLED >= 1000) {
lastOLED = now;
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
// Linea 1: Titulo (0 pixels from top)
oled.setCursor(0, 0);
oled.print("INCUBADORA CARRY");
// Linea 2: Dia y Fase (8 pixels from top)
oled.setCursor(0, 8);
oled.print("Dia:");
oled.print(dayIncubation);
oled.print(" ");
oled.print(isHatchingWindow() ? "ECLOSION" : "INCUBA");
// Linea 3: Temperatura con status (16 pixels from top)
oled.setCursor(0, 16);
oled.print("T:");
oled.print(temp, 1);
oled.print("(");
oled.print(setTemp, 1);
oled.print(")[");
oled.print(getTemperatureStatus());
oled.print("]");
// Linea 4: Humedad con status (24 pixels from top)
oled.setCursor(0, 24);
oled.print("H:");
oled.print(hum, 1);
oled.print("%(");
oled.print(setHum, 1);
oled.print(")[");
oled.print(getHumidityStatus());
oled.print("]");
// Linea 5: Heat, Fan, Valvula (32 pixels from top)
oled.setCursor(0, 32);
float heatPower = potenciaCalor;
oled.print("H:");
oled.print((int)heatPower);
oled.print("% F:");
oled.print(fanPWM);
oled.print(" V:");
oled.print(digitalRead(PIN_RELE_HUMEDAD) ? "ON" : "OFF");
// Linea 6: Servo y countdown (40 pixels from top)
oled.setCursor(0, 40);
oled.print("Servo:");
oled.print(getCurrentServoAngle());
oled.print(" Volteo:");
oled.print(getNextServoTime());
oled.print("s");
// Linea 7: MQTT status (48 pixels from top)
oled.setCursor(0, 48);
oled.print("HM:");
oled.print(hivemq.connected() ? "OK" : "ERR");
oled.print(" AIO:");
oled.print(aio.connected() ? "OK" : "ERR");
// Linea 8: Estado general (56 pixels from top)
oled.setCursor(0, 56);
oled.print(alarmActive ? "ALARMA!" : "Sistema OK");
oled.display();
}
}
// TELEMETRIA COMPLETA - FUNCIONES
// Retorna estado de temperatura: ALTA, BAJA, OK
String getTemperatureStatus() {
float diff = temp - setTemp;
if (diff > 1.0) return "ALTA";
if (diff < -1.0) return "BAJA";
return "OK";
}
// Retorna estado de humedad: ALTA, BAJA, OK
String getHumidityStatus() {
float diff = hum - setHum;
if (diff > 3.0) return "ALTA";
if (diff < -3.0) return "BAJA";
return "OK";
}
// Retorna segundos hasta proximo volteo del servo
int getNextServoTime() {
unsigned long elapsed = millis() - lastServo;
unsigned long remaining = (elapsed < 30000) ? (30000 - elapsed) : 0;
return remaining / 1000;
}
// Retorna angulo actual del servo
int getCurrentServoAngle() {
if (isHatchingWindow()) return 90;
return servoPos ? 135 : 45;
}
// ==FUNCION DE TELEMETRIA COMPLETA PARA MONITOR SERIAL ===
// Imprime todos los parametros del sistema con formato mejorado
void imprimirTelemetriaCompleta() {
// Obtener tiempo actual en formato HH:MM:SS
unsigned long totalSeconds = millis() / 1000;
int hours = (totalSeconds / 3600) % 24;
int minutes = (totalSeconds / 60) % 60;
int seconds = totalSeconds % 60;
Serial.println();
Serial.println("===============================================================");
Serial.print("[");
if (hours < 10) Serial.print("0");
Serial.print(hours);
Serial.print(":");
if (minutes < 10) Serial.print("0");
Serial.print(minutes);
Serial.print(":");
if (seconds < 10) Serial.print("0");
Serial.print(seconds);
Serial.println("] TELEMETRIA SISTEMA INCUBADORA CARRY");
Serial.println("===============================================================");
// DIA Y FASE
Serial.print("DIA: ");
Serial.print(dayIncubation);
Serial.print(" | FASE: ");
Serial.println(isHatchingWindow() ? "Eclosion (Dias 19-21)" : "Incubacion (Dias 1-18)");
// TEMPERATURA
Serial.print("TEMPERATURA: ");
Serial.print(temp, 1);
Serial.print("C (SP:");
Serial.print(setTemp, 1);
Serial.print("C) [");
Serial.print(getTemperatureStatus());
float tempDiff = temp - setTemp;
if (tempDiff != 0) {
Serial.print(" ");
if (tempDiff > 0) Serial.print("+");
Serial.print(tempDiff, 1);
Serial.print("C");
}
Serial.println("]");
// HUMEDAD
Serial.print("HUMEDAD: ");
Serial.print(hum, 1);
Serial.print("% (SP:");
Serial.print(setHum, 1);
Serial.print("%) [");
Serial.print(getHumidityStatus());
float humDiff = hum - setHum;
if (humDiff != 0) {
Serial.print(" ");
if (humDiff > 0) Serial.print("+");
Serial.print(humDiff, 1);
Serial.print("%");
}
Serial.println("]");
// POTENCIA CALOR Y RELE
unsigned long windowTime = millis() - windowStartTime;
float heatPower = (potenciaCalor / 100.0) * 100.0;
bool heatRelayOn = digitalRead(PIN_RELE_CALOR);
Serial.print("POTENCIA CALOR: ");
Serial.print((int)heatPower);
Serial.print("% | RELE: ");
Serial.println(heatRelayOn ? "ON" : "OFF");
// VENTILADOR Y PWM
Serial.print("VENTILADOR PWM: ");
Serial.print(fanPWM);
Serial.print(" (");
Serial.print((fanPWM * 100) / 255);
Serial.println("%)");
// VALVULA DE HUMEDAD
bool humidityValveOn = digitalRead(PIN_RELE_HUMEDAD);
Serial.print("VALVULA: ");
Serial.println(humidityValveOn ? "ON" : "OFF");
// SERVO
Serial.print("SERVO: ");
Serial.print(getCurrentServoAngle());
Serial.print(" grados | Proximo volteo: ");
int nextTurnSeconds = getNextServoTime();
Serial.print(nextTurnSeconds);
Serial.println("s");
// MQTT HIVEMQ
Serial.print("MQTT HiveMQ: ");
if (hivemq.connected()) {
unsigned long lastHiveMQSeconds = (millis() - lastHiveMQ) / 1000;
Serial.print("Conectado (ultimo: hace ");
Serial.print(lastHiveMQSeconds);
Serial.println("s)");
} else {
Serial.println("Desconectado");
}
// MQTT ADAFRUIT IO
Serial.print("MQTT Adafruit IO: ");
if (aio.connected()) {
unsigned long lastAIOSeconds = (millis() - lastAIO) / 1000;
Serial.print("Conectado (ultimo: hace ");
Serial.print(lastAIOSeconds);
Serial.println("s)");
} else {
Serial.println("Desconectado");
}
// ALARMAS
Serial.print("ALARMAS: ");
bool hasAlarms = false;
if (temp > setTemp + 2.0) {
Serial.print("Temperatura alta detectada ");
hasAlarms = true;
}
if (temp < setTemp - 2.0) {
Serial.print("Temperatura baja detectada ");
hasAlarms = true;
}
if (hum < setHum - 5.0) {
Serial.print("Humedad baja detectada ");
hasAlarms = true;
}
if (!hasAlarms) {
Serial.print("Sistema OK");
}
Serial.println();
Serial.println("===============================================================");
Serial.println();
}
void reconnectHiveMQ() {
if(!hivemq.connected()) {
Serial.print("Conectando HiveMQ...");
if(hivemq.connect(hivemq_client)) {
Serial.println("OK");
hivemq.subscribe("incubadora/carry/settemp");
hivemq.subscribe("incubadora/carry/sethum");
hivemq.subscribe("incubadora/carry/setday");
} else {
Serial.print("Fallo, rc=");
Serial.println(hivemq.state());
}
}
}
void reconnectAIO() {
if(!aio.connected()) {
Serial.print("Conectando Adafruit IO...");
if(aio.connect(aio_client, aio_username, aio_key)) {
Serial.println("OK");
aio.subscribe("competitivefer50/feeds/incubadora.settemp");
aio.subscribe("competitivefer50/feeds/incubadora.sethum");
aio.subscribe("competitivefer50/feeds/incubadora.setday");
} else {
Serial.print("Fallo, rc=");
Serial.println(aio.state());
}
}
}
void callbackHiveMQ(char* topic, byte* payload, unsigned int length) {
String msg = "";
for(unsigned int i = 0; i < length; i++) msg += (char)payload[i];
Serial.print("HiveMQ [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(msg);
String topicStr = String(topic);
if(topicStr.endsWith("settemp")) {
setTemp = msg.toFloat();
Serial.print("Temp ajustada a: ");
Serial.println(setTemp);
} else if(topicStr.endsWith("sethum")) {
setHum = msg.toFloat();
Serial.print("Humedad ajustada a: ");
Serial.println(setHum);
} else if(topicStr.endsWith("setday")) {
dayIncubation = msg.toInt();
Serial.print("Dia ajustado a: ");
Serial.println(dayIncubation);
}
}
void callbackAIO(char* topic, byte* payload, unsigned int length) {
String msg = "";
for(unsigned int i = 0; i < length; i++) msg += (char)payload[i];
Serial.print("AIO [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(msg);
String topicStr = String(topic);
if(topicStr.endsWith("settemp")) {
setTemp = msg.toFloat();
Serial.print("Temp ajustada (AIO) a: ");
Serial.println(setTemp);
} else if(topicStr.endsWith("sethum")) {
setHum = msg.toFloat();
Serial.print("Humedad ajustada (AIO) a: ");
Serial.println(setHum);
} else if(topicStr.endsWith("setday")) {
dayIncubation = msg.toInt();
Serial.print("Dia ajustado (AIO) a: ");
Serial.println(dayIncubation);
}
}
void publishHiveMQ() {
unsigned long now = millis();
if(now - lastHiveMQ >= 10000) {
lastHiveMQ = now;
hivemq.publish("incubadora/carry/temp", String(temp, 2).c_str());
hivemq.publish("incubadora/carry/hum", String(hum, 2).c_str());
hivemq.publish("incubadora/carry/settemp", String(setTemp, 2).c_str());
hivemq.publish("incubadora/carry/sethum", String(setHum, 2).c_str());
hivemq.publish("incubadora/carry/day", String(dayIncubation).c_str());
hivemq.publish("incubadora/carry/alarm", alarmActive ? "1" : "0");
hivemq.publish("incubadora/carry/status", "online");
Serial.println("Publicado en HiveMQ");
}
}
void publishAIO() {
unsigned long now = millis();
if(now - lastAIO >= 15000) {
lastAIO = now;
aio.publish("competitivefer50/feeds/incubadora.temp", String(temp, 2).c_str());
aio.publish("competitivefer50/feeds/incubadora.hum", String(hum, 2).c_str());
aio.publish("competitivefer50/feeds/incubadora.day", String(dayIncubation).c_str());
aio.publish("competitivefer50/feeds/incubadora.alarm", alarmActive ? "1" : "0");
aio.publish("competitivefer50/feeds/incubadora.status", "online");
aio.publish("competitivefer50/feeds/potencia-calor", String((int)potenciaCalor).c_str());
Serial.println("Publicado en Adafruit IO");
}
}
// ====== FUNCIONES AUXILIARES DE TELEMETRIA ======
void setup() {
Serial.begin(115200);
Serial.println("Iniciando Incubadora Carry - Fusion Dual MQTT");
// Pines
pinMode(PIN_LED_WIFI, OUTPUT); // Configurar LED WiFi como salida
pinMode(PIN_RELE_CALOR, OUTPUT);
pinMode(PIN_RELE_HUMEDAD, OUTPUT);
pinMode(PIN_LED_TEMP, OUTPUT);
pinMode(PIN_LED_HUM, OUTPUT);
digitalWrite(PIN_RELE_CALOR, LOW);
digitalWrite(PIN_RELE_HUMEDAD, LOW);
digitalWrite(PIN_LED_TEMP, LOW);
digitalWrite(PIN_LED_HUM, LOW);
digitalWrite(PIN_LED_WIFI, LOW);
// Buzzer PWM
ledcAttach(PIN_BUZZER, 2000, 8);
ledcWrite(PIN_BUZZER, 0);
// DHT
dht.setup(DHT_PIN, DHTesp::DHT22);
// OLED
Wire.begin(OLED_SDA, OLED_SCL);
if(!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("Error OLED");
} else {
Serial.println("OLED OK");
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
oled.print("Iniciando...");
oled.display();
}
// Servo (Advanced configuration for MG995 - Carry incubator)
// servo.setPeriodHertz(50); // 50 Hz PWM frequency (20ms period - servo standard)
// servo.attach(PIN_SERVO, 500, 2400); // Pulse range: 500-2400 µs for MG995 servo Descomentar en simulacion fisica
servo.attach(PIN_SERVO);
servo.write(90); // Center position (90° - neutral)
Serial.println("Servo OK (Advanced API - MG995)");
// WiFi
WiFi.begin(ssid, password);
Serial.print("Conectando WiFi");
int attempts = 0;
// AÑADIR: Parpadeo del LED mientras se conecta
while(WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
// Parpadear LED verde mientras conecta
digitalWrite(PIN_LED_WIFI, !digitalRead(PIN_LED_WIFI));
attempts++;
}
if(WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi conectado");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
// AÑADIR: Encender LED verde cuando conecta
digitalWrite(PIN_LED_WIFI, HIGH); // LED verde ON = WiFi conectado
} else {
Serial.println("WiFi fallido");
// AÑADIR: Apagar LED verde si falla
digitalWrite(PIN_LED_WIFI, LOW); // LED verde OFF = WiFi fallido
}
// MQTT
hivemq.setServer(hivemq_server, hivemq_port);
hivemq.setCallback(callbackHiveMQ);
aio.setServer(aio_server, aio_port);
aio.setCallback(callbackAIO);
// Servidor web (comentado para Wokwi)
/*
server.on("/", []() {
String html = "<html><body><h1>Incubadora Carry</h1>";
html += "<p>Temp: " + String(temp, 1) + "C (SP: " + String(setTemp, 1) + ")</p>";
html += "<p>Hum: " + String(hum, 1) + "% (SP: " + String(setHum, 1) + ")</p>";
html += "<p>Dia: " + String(dayIncubation) + "</p>";
html += "<p>Alarma: " + String(alarmActive ? "ACTIVA" : "OK") + "</p>";
html += "</body></html>";
server.send(200, "text/html", html);
});
server.begin();
Serial.println("Servidor web iniciado");
*/
Serial.println("Setup completo");
windowStartTime = millis();
}
void loop() {
unsigned long now = millis();
// Reconexion MQTT
if(WiFi.status() == WL_CONNECTED) {
if(!hivemq.connected()) reconnectHiveMQ();
if(!aio.connected()) reconnectAIO();
hivemq.loop();
aio.loop();
}
// Lectura DHT
TempAndHumidity data = dht.getTempAndHumidity();
if(dht.getStatus() == DHTesp::ERROR_NONE) {
temp = data.temperature;
hum = data.humidity;
}
// PID
if(now - lastPID >= 1000) {
float dt = (now - lastPID) / 1000.0;
lastPID = now;
float outTemp = pidTemp.compute(setTemp, temp, dt);
potenciaCalor = outTemp; // Guardar para telemetria
// Control de ventilador
float tempError = temp - setTemp;
if(tempError > 1.0) {
// Temperatura alta: aumentar ventilador proporcionalmente
fanPWM = map(constrain(tempError * 50, 0, 100), 0, 100, 128, 255);
} else {
// Temperatura OK o baja: ventilador mínimo
fanPWM = 64; // Circulación mínima constante
}
float outHum = pidHum.compute(setHum, hum, dt);
unsigned long windowTime = now - windowStartTime;
if(windowTime >= windowSize) windowStartTime = now;
digitalWrite(PIN_RELE_CALOR, (outTemp > (windowTime * 100.0 / windowSize)) ? HIGH : LOW);
digitalWrite(PIN_RELE_HUMEDAD, (outHum > (windowTime * 100.0 / windowSize)) ? HIGH : LOW);
}
// Servo
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)");
}
}
// Alarmas y actualizacion
checkAlarms();
updateBuzzer();
updateLEDs();
updateOLED();
// Telemetria completa cada 5 segundos
if(now - lastTelemetry >= 5000) {
lastTelemetry = now;
imprimirTelemetriaCompleta();
}
publishHiveMQ();
publishAIO();
// Servidor web
// server.handleClient(); // Descomentar para hardware
}