Módulo 3

Proyecto práctico: sensor de temperatura + potenciómetro → salida DAC

Periféricos Analógicos

ESP32 Mecatrónica IoT UNAM

Sistema Integrado de Monitoreo y Control Térmico

Este proyecto integrador representa la culminación del Módulo 3: Señales Analógicas, combinando todos los conceptos aprendidos sobre ADC, comparadores analógicos y DAC en un sistema real de control térmico industrial.

Objetivos del Proyecto

Objetivos Técnicos
  • Implementar lectura precisa de temperatura con LM35
  • Utilizar potenciómetro como control manual de setpoint
  • Generar señal de control analógica via DAC
  • Integrar comparadores para sistemas de seguridad
  • Implementar interfaz de usuario avanzada
Objetivos Educativos
  • Consolidar conocimientos de periféricos analógicos
  • Aplicar técnicas de filtrado digital
  • Desarrollar sistemas de control en lazo cerrado
  • Implementar protocolos de comunicación
  • Diseñar interfaces humano-máquina profesionales
Aplicación Industrial Real

Este sistema puede aplicarse directamente en: control de hornos industriales, sistemas HVAC, control de procesos químicos, incubadoras médicas, y sistemas de climatización de invernaderos.

Arquitectura del Sistema

Componentes del Sistema

Sensores de Entrada (ADC)
  • Sensor LM35: Medición de temperatura ambiente (ADC1_CH0 - GPIO36)
  • Potenciómetro: Control manual de setpoint (ADC1_CH3 - GPIO39)
  • Sensor de referencia: Calibración automática (ADC1_CH6 - GPIO34)
Procesamiento de Señal
  • Filtros digitales: Anti-ruido y suavizado
  • Comparadores por software: Detección de umbrales
  • Control PID: Regulación automática de temperatura
  • Sistema de alarmas: Protección por sobre/sub temperatura
Salidas de Control (DAC)
  • Señal de calentamiento: Control de resistencia calefactora (DAC1 - GPIO25)
  • Señal de ventilación: Control de velocidad de ventilador (DAC2 - GPIO26)
Sistema de Control
Temperatura Objetivo
Control PID
Salidas DAC

Especificaciones Técnicas del Proyecto

Especificaciones de Rendimiento

ParámetroEspecificación
Rango de temperatura0°C a 100°C
Precisión±0.5°C
Tiempo de respuesta< 5 segundos
Resolución de control0.1°C
Frecuencia de muestreo10 Hz
Estabilidad±0.1°C @ 25°C

Sistemas de Seguridad

  • Protección térmica: Corte automático > 85°C
  • Falla de sensor: Modo seguro con alarma
  • Watchdog: Reset automático ante fallas
  • Límites de corriente: Protección de actuadores
  • Interfaz de emergencia: Parada manual

Lista Completa de Materiales

Componentes Principales
  • ESP32 DevKit v1
  • Sensor LM35DZ
  • Potenciómetro lineal 10kΩ
  • Display LCD 20x4 I2C
  • Módulo RTC DS3231
Actuadores y Control
  • Resistencia calefactora 12V/50W
  • Ventilador 12V DC
  • Relé estado sólido 25A
  • Driver de motor L298N
  • LEDs indicadores (RGB)
Electrónica Auxiliar
  • Fuente switching 12V/5A
  • Regulador LM7805
  • Capacitores de filtrado
  • Resistores de precisión 0.1%
  • Conectores y cableado

Implementación del Sistema Completo

C++ - Sistema Integrado de Control Térmico
/**
 * PROYECTO INTEGRADOR: Sistema de Control Térmico Industrial
 * Curso ESP32 Mecatrónica UNAM - Módulo 3
 * 
 * Descripción: Sistema completo que integra:
 * - Lectura de temperatura (LM35) via ADC
 * - Control manual con potenciómetro
 * - Salidas de control via DAC
 * - Comparadores de seguridad
 * - Interfaz de usuario avanzada
 * - Logging de datos
 * - Control PID automático
 */

#include "driver/adc.h"
#include "esp_adc_cal.h"
#include 
#include 
#include 
#include 
#include 
#include "time.h"

// ========== CONFIGURACIÓN DE HARDWARE ==========
// Entradas ADC
const adc_channel_t TEMP_SENSOR_CHANNEL = ADC_CHANNEL_0;  // GPIO36 - LM35
const adc_channel_t SETPOINT_POT_CHANNEL = ADC_CHANNEL_3; // GPIO39 - Potenciómetro
const adc_channel_t REFERENCE_CHANNEL = ADC_CHANNEL_6;    // GPIO34 - Referencia

// Salidas DAC
const int HEATER_DAC_PIN = 25;      // DAC1 - Control calefactor
const int FAN_DAC_PIN = 26;         // DAC2 - Control ventilador

// Pines digitales de control
const int SAFETY_RELAY_PIN = 2;     // Relé de seguridad
const int ALARM_LED_PIN = 4;        // LED de alarma
const int STATUS_LED_PIN = 16;      // LED de estado
const int BUZZER_PIN = 17;          // Buzzer de alarma
const int EMERGENCY_STOP_PIN = 18;  // Botón de parada de emergencia

// Configuración I2C
const int I2C_SDA = 21;
const int I2C_SCL = 22;

// Display LCD
LiquidCrystal_I2C lcd(0x27, 20, 4);  // 20x4 LCD

// Calibración ADC
esp_adc_cal_characteristics_t adc_chars;

// ========== ESTRUCTURA DE DATOS DEL SISTEMA ==========
struct SystemData {
    // Lecturas de sensores
    float current_temperature;
    float setpoint_temperature;
    float reference_voltage;
    
    // Estado del control
    float control_output_heater;
    float control_output_fan;
    bool heating_active;
    bool cooling_active;
    
    // Control PID
    float pid_error;
    float pid_integral;
    float pid_derivative;
    float pid_output;
    
    // Sistema de seguridad
    bool system_alarm;
    bool emergency_stop;
    String alarm_message;
    unsigned long alarm_start_time;
    
    // Estadísticas
    float min_temp_recorded;
    float max_temp_recorded;
    unsigned long operation_time;
    unsigned long cycles_completed;
};

struct SystemConfig {
    // Parámetros PID
    float kp = 2.5;
    float ki = 0.8;
    float kd = 0.4;
    
    // Límites de seguridad
    float max_temperature_limit = 85.0;
    float min_temperature_limit = 5.0;
    
    // Configuración operacional
    float temperature_tolerance = 1.0;
    int update_frequency_hz = 10;
    bool auto_mode = true;
    
    // Comunicación
    String wifi_ssid = "ESP32_ThermalControl";
    String wifi_password = "control123";
    bool web_server_enabled = true;
};

// Variables globales del sistema
SystemData system_data;
SystemConfig system_config;
WebServer web_server(80);

// ========== CLASES ESPECIALIZADAS ==========

class ThermalSensorManager {
private:
    float calibration_offset;
    float calibration_gain;
    float temperature_buffer[10];
    int buffer_index;
    bool calibrated;
    
public:
    ThermalSensorManager() {
        calibration_offset = 0.0;
        calibration_gain = 1.0;
        buffer_index = 0;
        calibrated = false;
        memset(temperature_buffer, 0, sizeof(temperature_buffer));
    }
    
    void initialize() {
        // Configurar ADC para alta precisión
        adc1_config_width(ADC_WIDTH_BIT_12);
        adc1_config_channel_atten(TEMP_SENSOR_CHANNEL, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(SETPOINT_POT_CHANNEL, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(REFERENCE_CHANNEL, ADC_ATTEN_DB_11);
        
        // Calibrar ADC
        esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                                ADC_WIDTH_BIT_12, 1100, &adc_chars);
        
        Serial.println("Sensor manager inicializado");
    }
    
    float readTemperature() {
        // Lectura múltiple para mayor precisión
        uint32_t adc_sum = 0;
        const int samples = 16;
        
        for (int i = 0; i < samples; i++) {
            adc_sum += adc1_get_raw(TEMP_SENSOR_CHANNEL);
            delayMicroseconds(500);
        }
        
        uint32_t adc_average = adc_sum / samples;
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_average, &adc_chars);
        
        // Conversión LM35: 10mV/°C
        float temperature = voltage_mv / 10.0;
        
        // Aplicar calibración
        temperature = (temperature * calibration_gain) + calibration_offset;
        
        // Filtro de media móvil
        temperature_buffer[buffer_index] = temperature;
        buffer_index = (buffer_index + 1) % 10;
        
        float filtered_temp = 0;
        for (int i = 0; i < 10; i++) {
            filtered_temp += temperature_buffer[i];
        }
        
        return filtered_temp / 10.0;
    }
    
    float readSetpoint() {
        // Leer potenciómetro con suavizado
        uint32_t adc_sum = 0;
        const int samples = 8;
        
        for (int i = 0; i < samples; i++) {
            adc_sum += adc1_get_raw(SETPOINT_POT_CHANNEL);
        }
        
        uint32_t adc_average = adc_sum / samples;
        
        // Mapear a rango de temperatura (15-60°C)
        float percentage = (adc_average / 4095.0);
        float setpoint = 15.0 + (percentage * 45.0);
        
        return setpoint;
    }
    
    void performCalibration(float reference_temp) {
        Serial.println("Iniciando calibración automática...");
        
        // Leer temperatura actual sin calibración
        float measured_temp = readTemperature();
        
        // Calcular offset de calibración
        calibration_offset = reference_temp - measured_temp;
        calibrated = true;
        
        Serial.printf("Calibración completada. Offset: %.3f°C\n", calibration_offset);
    }
    
    bool isCalibrated() {
        return calibrated;
    }
};

class PIDController {
private:
    float last_error;
    float integral;
    unsigned long last_update;
    float output_min, output_max;
    bool integral_reset;
    
public:
    PIDController(float min_output = -255, float max_output = 255) {
        last_error = 0;
        integral = 0;
        last_update = 0;
        output_min = min_output;
        output_max = max_output;
        integral_reset = false;
    }
    
    float compute(float setpoint, float current_value) {
        unsigned long now = millis();
        float dt = (now - last_update) / 1000.0;  // Convertir a segundos
        
        if (last_update == 0 || dt <= 0) {
            last_update = now;
            return 0;
        }
        
        // Calcular error
        float error = setpoint - current_value;
        
        // Término proporcional
        float proportional = system_config.kp * error;
        
        // Término integral con anti-windup
        integral += error * dt;
        if (integral > 100) integral = 100;
        if (integral < -100) integral = -100;
        float integral_term = system_config.ki * integral;
        
        // Término derivativo
        float derivative = (error - last_error) / dt;
        float derivative_term = system_config.kd * derivative;
        
        // Salida PID
        float output = proportional + integral_term + derivative_term;
        
        // Saturación
        if (output > output_max) output = output_max;
        if (output < output_min) output = output_min;
        
        // Actualizar variables
        last_error = error;
        last_update = now;
        
        // Guardar valores para monitoreo
        system_data.pid_error = error;
        system_data.pid_integral = integral;
        system_data.pid_derivative = derivative;
        system_data.pid_output = output;
        
        return output;
    }
    
    void reset() {
        integral = 0;
        last_error = 0;
        last_update = 0;
    }
    
    void tunePID(float kp, float ki, float kd) {
        system_config.kp = kp;
        system_config.ki = ki;
        system_config.kd = kd;
        
        Serial.printf("PID actualizado: Kp=%.3f, Ki=%.3f, Kd=%.3f\n", kp, ki, kd);
    }
};

class SafetyManager {
private:
    bool safety_triggered;
    unsigned long last_safety_check;
    String last_alarm_reason;
    
public:
    SafetyManager() {
        safety_triggered = false;
        last_safety_check = 0;
    }
    
    void initialize() {
        pinMode(SAFETY_RELAY_PIN, OUTPUT);
        pinMode(ALARM_LED_PIN, OUTPUT);
        pinMode(BUZZER_PIN, OUTPUT);
        pinMode(EMERGENCY_STOP_PIN, INPUT_PULLUP);
        
        // Estado seguro inicial
        digitalWrite(SAFETY_RELAY_PIN, LOW);
        digitalWrite(ALARM_LED_PIN, LOW);
        digitalWrite(BUZZER_PIN, LOW);
        
        Serial.println("Sistema de seguridad inicializado");
    }
    
    bool checkSafety() {
        if (millis() - last_safety_check < 100) {
            return !system_data.system_alarm;  // Check cada 100ms
        }
        
        system_data.system_alarm = false;
        system_data.alarm_message = "";
        
        // Check 1: Temperatura excesiva
        if (system_data.current_temperature > system_config.max_temperature_limit) {
            triggerAlarm("SOBRETEMPERATURA CRITICA");
            return false;
        }
        
        // Check 2: Temperatura muy baja
        if (system_data.current_temperature < system_config.min_temperature_limit) {
            triggerAlarm("TEMPERATURA EXTREMADAMENTE BAJA");
            return false;
        }
        
        // Check 3: Botón de emergencia
        if (digitalRead(EMERGENCY_STOP_PIN) == LOW) {
            triggerAlarm("PARADA DE EMERGENCIA ACTIVADA");
            system_data.emergency_stop = true;
            return false;
        }
        
        // Check 4: Falla de sensor (lectura imposible)
        if (system_data.current_temperature < -10 || system_data.current_temperature > 120) {
            triggerAlarm("FALLA EN SENSOR DE TEMPERATURA");
            return false;
        }
        
        // Check 5: Control PID fuera de rango
        if (abs(system_data.pid_output) > 300) {
            triggerAlarm("CONTROL PID INESTABLE");
            return false;
        }
        
        last_safety_check = millis();
        
        // Si llegamos aquí, todo está bien
        if (safety_triggered) {
            clearAlarm();
        }
        
        return true;
    }
    
private:
    void triggerAlarm(String reason) {
        if (!system_data.system_alarm) {
            system_data.alarm_start_time = millis();
            Serial.println("🚨 ALARMA: " + reason);
        }
        
        system_data.system_alarm = true;
        system_data.alarm_message = reason;
        safety_triggered = true;
        
        // Activar indicadores de alarma
        digitalWrite(SAFETY_RELAY_PIN, LOW);   // Cortar alimentación
        digitalWrite(ALARM_LED_PIN, HIGH);
        
        // Patrón de buzzer según tipo de alarma
        if (reason.indexOf("EMERGENCIA") >= 0 || reason.indexOf("CRITICA") >= 0) {
            // Buzzer continuo para emergencias críticas
            digitalWrite(BUZZER_PIN, (millis() % 200) < 100);
        } else {
            // Buzzer intermitente para otras alarmas
            digitalWrite(BUZZER_PIN, (millis() % 1000) < 100);
        }
    }
    
    void clearAlarm() {
        system_data.system_alarm = false;
        system_data.emergency_stop = false;
        system_data.alarm_message = "";
        safety_triggered = false;
        
        digitalWrite(ALARM_LED_PIN, LOW);
        digitalWrite(BUZZER_PIN, LOW);
        digitalWrite(SAFETY_RELAY_PIN, HIGH);  // Restablecer alimentación
        
        Serial.println("✅ Alarma eliminada - Sistema restaurado");
    }
};

class DisplayManager {
private:
    unsigned long last_update;
    int current_screen;
    int total_screens;
    
public:
    DisplayManager() {
        last_update = 0;
        current_screen = 0;
        total_screens = 4;
    }
    
    void initialize() {
        lcd.init();
        lcd.backlight();
        
        // Pantalla de inicio
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Sistema Termico UNAM");
        lcd.setCursor(0, 1);
        lcd.print("Inicializando...");
        lcd.setCursor(0, 2);
        lcd.print("ESP32 Mecatronica");
        lcd.setCursor(0, 3);
        lcd.print("Modulo 3 - Proyecto");
        
        delay(3000);
        Serial.println("Display inicializado");
    }
    
    void update() {
        if (millis() - last_update < 500) return;  // Actualizar cada 500ms
        
        lcd.clear();
        
        if (system_data.system_alarm) {
            displayAlarmScreen();
        } else {
            switch (current_screen) {
                case 0: displayMainScreen(); break;
                case 1: displayControlScreen(); break;
                case 2: displayStatisticsScreen(); break;
                case 3: displaySystemInfo(); break;
            }
        }
        
        last_update = millis();
    }
    
    void nextScreen() {
        current_screen = (current_screen + 1) % total_screens;
    }
    
private:
    void displayMainScreen() {
        lcd.setCursor(0, 0);
        lcd.printf("Temp: %.1fC (%.1fC)", 
                   system_data.current_temperature, 
                   system_data.setpoint_temperature);
        
        lcd.setCursor(0, 1);
        lcd.printf("Calef:%3.0f%% Vent:%3.0f%%", 
                   (system_data.control_output_heater / 255.0) * 100,
                   (system_data.control_output_fan / 255.0) * 100);
        
        lcd.setCursor(0, 2);
        lcd.printf("Modo: %s PID:%.1f", 
                   system_config.auto_mode ? "AUTO" : "MAN ",
                   system_data.pid_output);
        
        lcd.setCursor(0, 3);
        lcd.printf("Estado: %s", 
                   system_data.heating_active ? "CALENTANDO" :
                   system_data.cooling_active ? "ENFRIANDO " : "ESTABLE   ");
    }
    
    void displayControlScreen() {
        lcd.setCursor(0, 0);
        lcd.print("=== PARAMETROS PID ===");
        
        lcd.setCursor(0, 1);
        lcd.printf("Kp: %.2f  Ki: %.2f", system_config.kp, system_config.ki);
        
        lcd.setCursor(0, 2);
        lcd.printf("Kd: %.2f  Err: %.2f", system_config.kd, system_data.pid_error);
        
        lcd.setCursor(0, 3);
        lcd.printf("I:%.2f D:%.2f", system_data.pid_integral, system_data.pid_derivative);
    }
    
    void displayStatisticsScreen() {
        lcd.setCursor(0, 0);
        lcd.print("=== ESTADISTICAS ===");
        
        lcd.setCursor(0, 1);
        lcd.printf("Min: %.1fC Max: %.1fC", 
                   system_data.min_temp_recorded, 
                   system_data.max_temp_recorded);
        
        lcd.setCursor(0, 2);
        lcd.printf("Tiempo: %02d:%02d:%02d", 
                   (int)(system_data.operation_time / 3600),
                   (int)((system_data.operation_time % 3600) / 60),
                   (int)(system_data.operation_time % 60));
        
        lcd.setCursor(0, 3);
        lcd.printf("Ciclos: %lu", system_data.cycles_completed);
    }
    
    void displaySystemInfo() {
        lcd.setCursor(0, 0);
        lcd.print("=== INFO SISTEMA ===");
        
        lcd.setCursor(0, 1);
        lcd.printf("WiFi: %s", WiFi.status() == WL_CONNECTED ? "Conectado" : "Desconect");
        
        lcd.setCursor(0, 2);
        lcd.printf("IP: %s", WiFi.localIP().toString().c_str());
        
        lcd.setCursor(0, 3);
        lcd.printf("Memoria: %d KB", ESP.getFreeHeap() / 1024);
    }
    
    void displayAlarmScreen() {
        // Parpadeo para llamar la atención
        bool blink = (millis() % 1000) < 500;
        
        if (blink) {
            lcd.setCursor(0, 0);
            lcd.print("!!!!! ALARMA !!!!!");
            
            lcd.setCursor(0, 1);
            lcd.print(system_data.alarm_message);
            
            lcd.setCursor(0, 2);
            unsigned long alarm_duration = (millis() - system_data.alarm_start_time) / 1000;
            lcd.printf("Duracion: %02d:%02d", 
                       (int)(alarm_duration / 60), 
                       (int)(alarm_duration % 60));
            
            lcd.setCursor(0, 3);
            lcd.print("Presione RESET");
        }
    }
};

// ========== INSTANCIAS DE CLASES ==========
ThermalSensorManager sensor_manager;
PIDController pid_controller(-255, 255);
SafetyManager safety_manager;
DisplayManager display_manager;

// ========== FUNCIONES PRINCIPALES ==========

void setup() {
    Serial.begin(115200);
    Serial.println("\n=== SISTEMA DE CONTROL TÉRMICO INDUSTRIAL ===");
    Serial.println("Curso ESP32 Mecatrónica UNAM - Módulo 3");
    Serial.println("================================================");
    
    // Inicializar I2C
    Wire.begin(I2C_SDA, I2C_SCL);
    
    // Inicializar componentes
    sensor_manager.initialize();
    safety_manager.initialize();
    display_manager.initialize();
    
    // Configurar pines DAC
    pinMode(HEATER_DAC_PIN, OUTPUT);
    pinMode(STATUS_LED_PIN, OUTPUT);
    
    // Configurar WiFi (opcional)
    if (system_config.web_server_enabled) {
        setupWiFi();
        setupWebServer();
    }
    
    // Inicializar variables del sistema
    initializeSystemData();
    
    // Calibración automática si hay referencia disponible
    delay(2000);  // Estabilizar sensores
    
    Serial.println("✅ Sistema inicializado correctamente");
    Serial.println("Comandos disponibles:");
    Serial.println("- 'SET [temp]' : Establecer setpoint");
    Serial.println("- 'PID [kp] [ki] [kd]' : Ajustar PID");
    Serial.println("- 'AUTO/MANUAL' : Cambiar modo");
    Serial.println("- 'RESET' : Reiniciar sistema");
    Serial.println("- 'STATUS' : Mostrar estado completo");
    Serial.println("================================================\n");
}

void loop() {
    // Lecturas de sensores
    system_data.current_temperature = sensor_manager.readTemperature();
    system_data.setpoint_temperature = sensor_manager.readSetpoint();
    
    // Verificar seguridad del sistema
    bool system_safe = safety_manager.checkSafety();
    
    if (system_safe && system_config.auto_mode) {
        // Control PID automático
        float pid_output = pid_controller.compute(system_data.setpoint_temperature, 
                                                 system_data.current_temperature);
        
        // Aplicar salidas de control
        applyControlOutputs(pid_output);
    } else {
        // Modo seguro o manual
        applyControlOutputs(0);  // Apagar actuadores
    }
    
    // Actualizar estadísticas
    updateSystemStatistics();
    
    // Actualizar display
    display_manager.update();
    
    // Procesar comandos serie
    processSerialCommands();
    
    // Handle web server si está habilitado
    if (system_config.web_server_enabled) {
        web_server.handleClient();
    }
    
    // Log periódico
    static unsigned long last_log = 0;
    if (millis() - last_log > 10000) {  // Cada 10 segundos
        logSystemStatus();
        last_log = millis();
    }
    
    delay(100);  // Loop principal a 10Hz
}

// ========== FUNCIONES DE CONTROL ==========

void applyControlOutputs(float pid_output) {
    if (pid_output > 0) {
        // Calentamiento necesario
        system_data.control_output_heater = constrain(pid_output, 0, 255);
        system_data.control_output_fan = 0;
        system_data.heating_active = true;
        system_data.cooling_active = false;
        
        // Aplicar señal DAC para calefactor
        dacWrite(HEATER_DAC_PIN, (uint8_t)system_data.control_output_heater);
        dacWrite(FAN_DAC_PIN, 0);
        
    } else if (pid_output < -10) {  // Zona muerta para evitar oscilaciones
        // Enfriamiento necesario
        system_data.control_output_heater = 0;
        system_data.control_output_fan = constrain(abs(pid_output), 0, 255);
        system_data.heating_active = false;
        system_data.cooling_active = true;
        
        // Aplicar señal DAC para ventilador
        dacWrite(HEATER_DAC_PIN, 0);
        dacWrite(FAN_DAC_PIN, (uint8_t)system_data.control_output_fan);
        
    } else {
        // Zona muerta - mantener estado actual
        system_data.heating_active = false;
        system_data.cooling_active = false;
        
        // Mantener salidas bajas
        dacWrite(HEATER_DAC_PIN, 0);
        dacWrite(FAN_DAC_PIN, 0);
    }
    
    // LED de estado del sistema
    digitalWrite(STATUS_LED_PIN, system_data.heating_active || system_data.cooling_active);
}

void initializeSystemData() {
    system_data.current_temperature = 25.0;
    system_data.setpoint_temperature = 25.0;
    system_data.min_temp_recorded = 999.0;
    system_data.max_temp_recorded = -999.0;
    system_data.operation_time = 0;
    system_data.cycles_completed = 0;
    system_data.system_alarm = false;
    system_data.emergency_stop = false;
    system_data.alarm_message = "";
}

void updateSystemStatistics() {
    static unsigned long operation_start = millis();
    system_data.operation_time = (millis() - operation_start) / 1000;
    
    // Actualizar min/max
    if (system_data.current_temperature < system_data.min_temp_recorded) {
        system_data.min_temp_recorded = system_data.current_temperature;
    }
    if (system_data.current_temperature > system_data.max_temp_recorded) {
        system_data.max_temp_recorded = system_data.current_temperature;
    }
    
    // Contar ciclos de control
    static bool was_heating = false;
    if (!was_heating && system_data.heating_active) {
        system_data.cycles_completed++;
    }
    was_heating = system_data.heating_active;
}

void processSerialCommands() {
    if (!Serial.available()) return;
    
    String command = Serial.readStringUntil('\n');
    command.trim();
    command.toUpperCase();
    
    if (command.startsWith("SET ")) {
        float new_setpoint = command.substring(4).toFloat();
        if (new_setpoint >= 15.0 && new_setpoint <= 60.0) {
            // Simular ajuste manual del potenciómetro
            Serial.printf("Setpoint establecido: %.1f°C\n", new_setpoint);
        } else {
            Serial.println("Error: Setpoint fuera de rango (15-60°C)");
        }
    }
    else if (command.startsWith("PID ")) {
        // Extraer parámetros PID
        int space1 = command.indexOf(' ', 4);
        int space2 = command.indexOf(' ', space1 + 1);
        
        if (space1 > 0 && space2 > 0) {
            float kp = command.substring(4, space1).toFloat();
            float ki = command.substring(space1 + 1, space2).toFloat();
            float kd = command.substring(space2 + 1).toFloat();
            
            pid_controller.tunePID(kp, ki, kd);
        }
    }
    else if (command == "AUTO") {
        system_config.auto_mode = true;
        Serial.println("Modo automático activado");
    }
    else if (command == "MANUAL") {
        system_config.auto_mode = false;
        Serial.println("Modo manual activado");
    }
    else if (command == "RESET") {
        pid_controller.reset();
        Serial.println("Sistema PID reiniciado");
    }
    else if (command == "STATUS") {
        logSystemStatus();
    }
    else if (command == "SCREEN") {
        display_manager.nextScreen();
    }
    else if (command == "HELP") {
        Serial.println("Comandos disponibles:");
        Serial.println("SET [temp] - Establecer setpoint");
        Serial.println("PID [kp] [ki] [kd] - Ajustar parámetros PID");
        Serial.println("AUTO/MANUAL - Cambiar modo de operación");
        Serial.println("RESET - Reiniciar controlador PID");
        Serial.println("STATUS - Mostrar estado completo");
        Serial.println("SCREEN - Cambiar pantalla LCD");
    }
}

void logSystemStatus() {
    Serial.println("\n========== ESTADO DEL SISTEMA ==========");
    Serial.printf("Temperatura actual: %.2f°C\n", system_data.current_temperature);
    Serial.printf("Setpoint: %.2f°C\n", system_data.setpoint_temperature);
    Serial.printf("Error PID: %.2f°C\n", system_data.pid_error);
    Serial.printf("Salida PID: %.1f\n", system_data.pid_output);
    Serial.printf("Calefactor: %.0f%% (DAC: %d)\n", 
                  (system_data.control_output_heater / 255.0) * 100,
                  (int)system_data.control_output_heater);
    Serial.printf("Ventilador: %.0f%% (DAC: %d)\n", 
                  (system_data.control_output_fan / 255.0) * 100,
                  (int)system_data.control_output_fan);
    Serial.printf("Modo: %s\n", system_config.auto_mode ? "AUTOMÁTICO" : "MANUAL");
    Serial.printf("Estado: %s\n", 
                  system_data.heating_active ? "CALENTANDO" :
                  system_data.cooling_active ? "ENFRIANDO" : "ESTABLE");
    
    if (system_data.system_alarm) {
        Serial.printf("🚨 ALARMA: %s\n", system_data.alarm_message.c_str());
    }
    
    Serial.printf("Tiempo operación: %02d:%02d:%02d\n",
                  (int)(system_data.operation_time / 3600),
                  (int)((system_data.operation_time % 3600) / 60),
                  (int)(system_data.operation_time % 60));
    Serial.printf("Min/Max registrado: %.1f°C / %.1f°C\n",
                  system_data.min_temp_recorded, system_data.max_temp_recorded);
    Serial.printf("Memoria libre: %d KB\n", ESP.getFreeHeap() / 1024);
    Serial.println("=====================================\n");
}

// ========== CONFIGURACIÓN WIFI Y WEB SERVER ==========

void setupWiFi() {
    WiFi.softAP(system_config.wifi_ssid.c_str(), system_config.wifi_password.c_str());
    Serial.printf("WiFi AP iniciado: %s\n", system_config.wifi_ssid.c_str());
    Serial.printf("IP del servidor: %s\n", WiFi.softAPIP().toString().c_str());
}

void setupWebServer() {
    // Endpoint para obtener datos del sistema
    web_server.on("/api/data", HTTP_GET, []() {
        DynamicJsonDocument doc(1024);
        doc["temperature"] = system_data.current_temperature;
        doc["setpoint"] = system_data.setpoint_temperature;
        doc["pid_output"] = system_data.pid_output;
        doc["heater_output"] = (system_data.control_output_heater / 255.0) * 100;
        doc["fan_output"] = (system_data.control_output_fan / 255.0) * 100;
        doc["auto_mode"] = system_config.auto_mode;
        doc["alarm"] = system_data.system_alarm;
        doc["alarm_message"] = system_data.alarm_message;
        doc["operation_time"] = system_data.operation_time;
        doc["min_temp"] = system_data.min_temp_recorded;
        doc["max_temp"] = system_data.max_temp_recorded;
        
        String response;
        serializeJson(doc, response);
        web_server.send(200, "application/json", response);
    });
    
    // Endpoint para control remoto
    web_server.on("/api/control", HTTP_POST, []() {
        if (web_server.hasArg("mode")) {
            String mode = web_server.arg("mode");
            system_config.auto_mode = (mode == "auto");
        }
        
        if (web_server.hasArg("setpoint")) {
            float setpoint = web_server.arg("setpoint").toFloat();
            if (setpoint >= 15.0 && setpoint <= 60.0) {
                // En implementación real, esto controlaría un DAC para el potenciómetro
                Serial.printf("Setpoint remoto establecido: %.1f°C\n", setpoint);
            }
        }
        
        web_server.send(200, "application/json", "{\"status\":\"ok\"}");
    });
    
    // Página web básica de interfaz
    web_server.on("/", HTTP_GET, []() {
        String html = R"(
        
        
        
            Control Térmico ESP32
            
            
            
        
        
            

Sistema de Control Térmico

Cargando...
Conectando...

Control

)"; web_server.send(200, "text/html", html); }); web_server.begin(); Serial.println("Servidor web iniciado en puerto 80"); }

Guía Completa de Montaje y Configuración

Procedimiento de Montaje Paso a Paso

Fase 1: Preparación del Hardware
  1. Verificación de componentes: Inspeccionar todos los materiales listados
  2. Preparación del ESP32: Instalar en protoboard o PCB personalizada
  3. Configuración de alimentación: Conectar fuente 12V y regulador 5V
  4. Instalación de componentes pasivos: Resistores y capacitores de filtrado
Fase 2: Conexiones Analógicas
  1. Sensor LM35: Conectar con filtrado RC (100nF + 10kΩ)
  2. Potenciómetro: Configurar divisor de voltaje con buffer
  3. Referencias de voltaje: Crear referencias estables para calibración
  4. Salidas DAC: Conectar amplificadores operacionales para interfaz
Fase 3: Actuadores y Control
  1. Relé de seguridad: Instalar con aislamiento óptico
  2. Control de calefactor: Conexión via relé estado sólido
  3. Control de ventilador: Driver PWM con transistor MOSFET
  4. Indicadores LED: Con resistores limitadores apropiados

Diagrama de Conexiones

Conexiones Críticas del Sistema
Entradas ADC:
  • GPIO36 ← LM35 (Vout)
  • GPIO39 ← Potenciómetro (Wiper)
  • GPIO34 ← Referencia 2.5V
Salidas DAC:
  • GPIO25 → Amplificador calefactor
  • GPIO26 → Control PWM ventilador
⚠️ Precauciones de Seguridad
  • Desconectar alimentación durante montaje
  • Verificar polaridad de componentes
  • Usar aislamiento para conexiones de AC
  • Implementar fusibles de protección
  • Verificar todas las conexiones antes del primer encendido
  • Mantener ventilación adecuada
✅ Verificaciones Pre-Operación
  • Continuidad en todas las conexiones
  • Niveles de voltaje correctos
  • Funcionamiento de sensores
  • Respuesta de actuadores
  • Sistema de seguridad operativo

Resultados Esperados y Validación

Control de Temperatura

Precisión: ±0.5°C

Tiempo de establecimiento: < 30 segundos

Sobrepaso máximo: < 2°C

Estabilidad: ±0.1°C en estado estable

Sistemas de Seguridad

Tiempo de respuesta: < 100ms

Corte por sobretemperatura: 85°C

Detección de fallas: Automática

Parada de emergencia: < 1 segundo

Conectividad

Interfaz web: Tiempo real

Logging de datos: Cada 10 segundos

Control remoto: API REST

Monitoreo: 24/7 disponible

Protocolos de Prueba

Prueba 1: Calibración y Precisión
  1. Verificar lectura de temperatura con termómetro de referencia
  2. Validar linealidad en rango 20-60°C
  3. Comprobar repetibilidad (10 mediciones)
  4. Evaluar deriva térmica durante 1 hora
Prueba 2: Control Automático
  1. Establecer setpoint de 40°C desde temperatura ambiente
  2. Medir tiempo de establecimiento y sobrepaso
  3. Aplicar perturbaciones (cambios de carga)
  4. Verificar estabilidad en estado estable
Prueba 3: Sistemas de Seguridad
  1. Simular sobretemperatura (sensor externo)
  2. Verificar activación de alarmas
  3. Probar parada de emergencia
  4. Comprobar restablecimiento automático

Troubleshooting y Optimización

Problemas Comunes

Problema: Lecturas de temperatura inestables

Causas posibles:

  • Ruido en alimentación del ADC
  • Conexiones flojas o corroídas
  • Interferencia electromagnética
  • Autocalentamiento del sensor
Problema: Control PID oscilante

Soluciones:

  • Reducir ganancia proporcional (Kp)
  • Aumentar tiempo derivativo (Kd)
  • Implementar filtro anti-windup
  • Ajustar frecuencia de muestreo
Problema: Salidas DAC no lineales

Diagnóstico:

  • Verificar impedancia de carga
  • Comprobar filtrado de salida
  • Calibrar curva de transferencia
  • Evaluar deriva térmica

Optimizaciones Avanzadas

Mejora de Precisión
  • Implementar calibración de 3 puntos
  • Usar referencias de voltaje de precisión
  • Agregar compensación térmica
  • Filtrado adaptativo según condiciones
Optimización de Control
  • Implementar control predictivo (MPC)
  • Agregar feedforward para perturbaciones
  • Auto-tuning de parámetros PID
  • Control robusto ante incertidumbre
Extensiones del Sistema
  • Múltiples zonas de temperatura
  • Interfaz gráfica avanzada
  • Base de datos histórica
  • Integración con sistemas SCADA

Evaluación del Proyecto

Rúbrica de Evaluación

Criterio Excelente (4) Bueno (3) Satisfactorio (2) Puntos
Implementación ADC Precisión <0.5°C, filtrado óptimo Precisión <1°C, filtrado básico Funcional con ruido moderado __/4
Control PID Estable, sin sobrepaso Estable con sobrepaso mínimo Funcional con oscilaciones __/4
Salidas DAC Lineal, calibrada, precisa Funcional con desviaciones menores Operativa básica __/4
Sistema Seguridad Completo, robusto, probado Funcional, algunas protecciones Básico, protección mínima __/4
Interfaz Usuario Profesional, intuitiva, completa Funcional, información clara Básica, información esencial __/4
Criterios de Certificación
  • Aprobado: 10-12 puntos (Sistema funcional básico)
  • Competente: 13-16 puntos (Sistema robusto y confiable)
  • Experto: 17-20 puntos (Sistema de calidad industrial)
Entregables del Proyecto
  • Código fuente completo documentado
  • Esquema eléctrico profesional
  • Video demostrativo del funcionamiento
  • Reporte de pruebas y calibración
  • Manual de usuario
  • Análisis de mejoras futuras
Presentación Oral
  • Demostración en vivo (15 min)
  • Explicación técnica detallada
  • Manejo de preguntas técnicas
  • Propuestas de mejora

Referencias Técnicas y Extensiones

Proyectos Relacionados y Extensiones
  • Módulo 4: Protocolos I2C/SPI para sensores avanzados
  • Módulo 5: Conectividad WiFi para monitoreo remoto
  • Módulo 6: Integración con actuadores complejos
  • Proyecto Final: Sistema multi-zona completo