Caso práctico: Termostato web con ESP32

Caso práctico: Termostato web con ESP32 — hero

case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
margin: 2.4rem 0;
padding: 1.45rem 1.55rem;
border: 1px solid rgba(148, 163, 184, 0.30);
border-radius: 14px;
background:
linear-gradient(135deg, rgba(30, 41, 59, 0.50), rgba(15, 23, 42, 0.18)),
rgba(17, 24, 39, 0.48);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.20);
}
.case-objective > h2:first-of-type,
.case-device-block-diagram > h2:first-of-type,
.prometeo-device-postcode-section > h2:first-of-type,
.prometeo-device-section-card > h2:first-of-type {
margin-top: 0;
}
.prometeo-device-section-card > :last-child,
.case-objective > :last-child,
.case-device-block-diagram > :last-child,
.prometeo-device-postcode-section > :last-child {
margin-bottom: 0;
}
.prometeo-device-section-card.prometeo-device-section-card-code {
border-left: 4px solid rgba(56, 189, 248, 0.86);
background:
linear-gradient(135deg, rgba(8, 47, 73, 0.42), rgba(15, 23, 42, 0.18)),
rgba(15, 23, 42, 0.58);
}
.prometeo-device-section-card.prometeo-device-section-card-compact {
padding: 1.2rem 1.35rem;
}
.prometeo-device-section-card pre,
.case-objective pre,
.case-device-block-diagram pre,
.prometeo-device-postcode-section pre {
max-width: 100%;
}
.prometeo-device-postcode-section .prometeo-device-flow-item {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.50), rgba(30, 41, 59, 0.28)),
rgba(15, 23, 42, 0.28);
}
@media print {
.case-objective,
.case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
background: #fff;
color: #111827;
box-shadow: none;
break-inside: avoid;
page-break-inside: avoid;
}
}

Objetivo y caso de uso

Qué construirás: Un prototipo de termostato independiente y accesible vía web usando un ESP32 que monitoriza la temperatura ambiente y escala dinámicamente la velocidad de un ventilador de refrigeración mediante Modulación por Ancho de Pulso (PWM).

Por qué es importante / Casos de uso

  • Gestión térmica de armarios AV: Enfría automáticamente receptores de audio/vídeo cerrados solo cuando se genera un exceso de calor, minimizando el ruido acústico durante escenas de películas silenciosas.
  • Cerramientos de impresoras 3D: Mantiene una temperatura ambiente estable para la impresión de ABS/PETG ventilando el exceso de calor sin causar corrientes de aire rápidas que deformen la impresión.
  • Refrigeración de racks de servidores: Proporciona refrigeración localizada y proporcional a estantes específicos de racks de laboratorios domésticos basándose en las temperaturas de escape en tiempo real.
  • Circulación de aire en invernaderos: Activa un movimiento suave de aire sobre las plántulas cuando el sol calienta una zona localizada, previniendo la humedad y el sobrecalentamiento.

Resultado esperado

  • El ESP32 transmite su propio Punto de Acceso (AP) Wi-Fi y aloja un servidor web responsivo con una latencia sub-100ms.
  • La interfaz web muestra la temperatura en tiempo real (convertida de lecturas del ADC a Celsius) y la velocidad actual del ventilador (ciclo de trabajo PWM de 0-100%).
  • El usuario puede ajustar el umbral de temperatura objetivo en tiempo real mediante un control deslizante web.
  • El ventilador escala automáticamente sus RPM de forma proporcional basándose en el diferencial entre el umbral objetivo y la temperatura ambiente actual.

Audiencia: Desarrolladores de IoT, Entusiastas del Hardware, Creadores de Domótica; Nivel: Intermedio

Arquitectura/flujo: Sensor de Temperatura Analógico (ADC) → Procesamiento ESP32 → Salida PWM al Controlador del Ventilador ↔ AP Wi-Fi del ESP32 & UI del Servidor Web Asíncrono

Diagrama de bloques conceptual

Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.

Arquitectura funcional

Botón local

ESP32 BLE

Paquete advertising

LED de estado

Móvil escáner

Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.

Ruta de validación

Código fuente

PlatformIO build

Flash

Monitor serie

Resumen conceptual de las herramientas usadas para comprobar el proyecto ESP32 publicado.

Nota educativa de validación

Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil ESP32 DevKitC, el proyecto se comprobó como proyecto PlatformIO: el validador extrajo platformio.ini y src/main.cpp, creó un proyecto temporal y ejecutó pio run contra platform = espressif32, board = esp32dev y framework = arduino. También revisó la estructura del artículo, que los comandos sean copiables con guiones ASCII, y que no aparezcan stacks no soportados como ESP-IDF directo o placas ESP32 no acotadas.

Evidencia de validación publicada

  • Resultado automático: PASS.
  • Estructura parseada: 4 apartados, 1 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
  • Hallazgos del informe: sin hallazgos bloqueantes.

Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu placa ESP32 DevKitC exacta, tu cableado, alimentación y entorno WiFi local.

Nota educativa de seguridad

Este termostato web es un prototipo educativo de bajo voltaje, no un controlador HVAC certificado ni un sistema de protección térmica. No conmutes calefactores, compresores ni cargas de red directamente desde el ESP32; usa solo ventiladores de baja tensión o módulos de interfaz aislados, con fuente externa adecuada y GND común solo cuando el esquema lo requiera. No lo dejes controlando equipos críticos sin supervisión.

Requisitos previos

  • Conocimiento básico de divisores de tensión y convertidores analógico-digitales (ADC).
  • Familiaridad con el concepto de Modulación por Ancho de Pulso (PWM) para el control de potencia.
  • Un ordenador con Visual Studio Code y la extensión PlatformIO IDE instalada.
  • Conocimiento básico de cómo conectarse a una red Wi-Fi y navegar a una dirección IP local en un navegador web.

Materiales

Para construir este prototipo, necesitas exactamente los siguientes componentes:
* ESP32 DevKitC (Versión estándar de 38 pines o 30 pines)
* Termistor NTC de 10 kOhm (Se asume un valor B estándar de 3950)
* Resistencia de precisión de 10 kOhm (Se prefiere una tolerancia del 1%, para el divisor de tensión)
* Controlador de ventilador MOSFET de nivel lógico (p. ej., un módulo IRLZ44N o un circuito personalizado con un MOSFET de nivel lógico de canal N, una resistencia pull-down de 10k en la puerta y un diodo flyback a través de los terminales del ventilador)
* LED de estado (Cualquier color, estándar de 5mm)
* Resistencia de 220 Ohm (Para el LED de estado)
* Ventilador de PC de 12V DC (Estándar de 2 o 3 hilos, accionado mediante conmutación de lado bajo)
* Fuente de alimentación de 12V DC (Con la capacidad nominal adecuada para el consumo de corriente del ventilador)
* Placa de pruebas y cables puente

Configuración/Conexión

La configuración del hardware se divide en tres bloques funcionales: el sensor de temperatura, el controlador del ventilador y el indicador de estado.

Nota importante sobre la conexión a tierra: El GND del ESP32 debe estar conectado al GND de la fuente de alimentación de 12V para establecer un voltaje de referencia común para la puerta del MOSFET.

ComponentePin / Conexión ESP32Destino / Descripción
Termistor NTCGNDUna pata del termistor NTC.
Termistor NTCGPIO34 (ADC1_CH6)La otra pata del NTC. Conecta también la resistencia fija de 10k aquí.
Resistencia fija de 10k3V3Conecta el otro extremo de la resistencia fija de 10k al pin 3.3V del ESP32.
Puerta del MOSFET (Controlador)GPIO18Controla la conmutación PWM. (Asegúrate de que el controlador tenga una resistencia pull-down).
Fuente del MOSFETGNDConecta a la tierra común (GND del ESP32 y GND de 12V).
Drenador del MOSFETNegativo del ventilador (-)Lleva el ventilador a tierra cuando el MOSFET está activo.
Positivo del ventilador (+)Fuente de alimentación de 12VConecta directamente al terminal positivo de 12V.
Ánodo del LED de estadoGPIO21Conecta a través de la resistencia limitadora de corriente de 220 Ohm.
Cátodo del LED de estadoGNDConecta a la tierra común.

Nota sobre el Divisor de Tensión: La resistencia de 10k y el termistor NTC forman un divisor de tensión. Debido a que el NTC está conectado a GND y la resistencia fija a 3.3V, el voltaje en GPIO34 disminuirá a medida que la temperatura aumente (ya que la resistencia del NTC cae a medida que se calienta). El software tiene en cuenta esta topología específica.

Código validado

El proyecto requiere dos archivos en tu espacio de trabajo de PlatformIO. El archivo platformio.ini configura el entorno de compilación, y src/main.cpp contiene la lógica de la aplicación.

platformio.ini

Crea o sobrescribe el archivo platformio.ini en la raíz de tu proyecto con la siguiente configuración. Esto asegura que se utilicen el framework y la velocidad de baudios en serie correctos.

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

src/main.cpp

Coloca el siguiente código en src/main.cpp. Este código maneja el Punto de Acceso Wi-Fi, el servidor web de estilo asíncrono (utilizando la biblioteca WebServer integrada), los cálculos del termistor de Steinhart-Hart y la lógica PWM proporcional.

Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Pin Definitions ---
const int NTC_PIN = 34;       // ADC pin for thermistor voltage divider
const int FAN_PWM_PIN = 18;   // PWM output to MOSFET gate
const int LED_PIN = 21;       // Status LED

// --- PWM Configuration ---
const int PWM_FREQ = 5000;    // 5 kHz frequency for standard PC fans
const int PWM_CHANNEL = 0;    // LEDC channel 0
const int PWM_RES = 8;        // 8-bit resolution (0-255)

// --- Thermistor Constants (Steinhart-Hart) ---
const float SERIES_RESISTOR = 10000.0; // 10k fixed resistor
const float NOMINAL_RESISTOR = 10000.0; // 10k NTC at 25 degrees C
const float NOMINAL_TEMP = 25.0;        // 25 degrees C
const float B_COEFFICIENT = 3950.0;     // Beta value of the thermistor

// --- Global Variables ---
float currentTempC = 0.0;
float targetTempC = 25.0; // Default threshold
int currentFanSpeed = 0;  // 0 to 255

// --- Web Server on port 80 ---
WebServer server(80);

// --- HTML Dashboard (Stored in Flash) ---
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP32 Thermostat</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 20px; background-color: #f4f4f9; }
    h1 { color: #333; }
    .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 400px; margin: auto; }
    .metric { font-size: 2rem; font-weight: bold; color: #007BFF; margin: 10px 0; }
    .slider-container { margin-top: 20px; }
    input[type=range] { width: 100%; }
  </style>
</head>
<body>
  <div class="card">
    <h1>Fan Thermostat</h1>
    <p>Current Temperature:</p>
    <div class="metric"><span id="tempValue">--</span> &deg;C</div>
    <p>Fan Speed (PWM):</p>
    <div class="metric"><span id="fanValue">--</span> / 255</div>

    <div class="slider-container">
      <p>Target Threshold: <span id="targetValue">25</span> &deg;C</p>
      <input type="range" min="15" max="40" value="25" id="targetSlider" onchange="updateTarget(this.value)">
    </div>
  </div>

  <script>
    // Fetch data every 2 seconds
    setInterval(function() {
      fetch('/data')
        .then(response => response.json())
        .then(data => {
          document.getElementById('tempValue').innerText = data.temperature.toFixed(1);
          document.getElementById('fanValue').innerText = data.fan_speed;
          document.getElementById('targetValue').innerText = data.target;
          document.getElementById('targetSlider').value = data.target;
        });
    }, 2000);

    // Send new threshold to ESP32
    function updateTarget(val) {
      document.getElementById('targetValue').innerText = val;
      fetch('/set?threshold=' + val);
    }
  </script>
</body>
</html>
)rawliteral";

// --- Function to Calculate Temperature ---
float readTemperature() {
  int adcValue = analogRead(NTC_PIN);

  // Avoid division by zero if ADC is maxed out or grounded
  if (adcValue == 0 || adcValue == 4095) return currentTempC; 

  // Convert ADC value to voltage (ESP32 ADC is 12-bit: 0-4095)
  float voltage = adcValue * (3.3 / 4095.0);

  // Calculate NTC resistance (Divider: 3.3V -> 10k -> ADC -> NTC -> GND)
  float ntcResistance = SERIES_RESISTOR * voltage / (3.3 - voltage);

  // Steinhart-Hart Equation
  float steinhart;
  steinhart = ntcResistance / NOMINAL_RESISTOR;       // (R/Ro)
  steinhart = log(steinhart);                         // ln(R/Ro)
  steinhart /= B_COEFFICIENT;                         // 1/B * ln(R/Ro)
  steinhart += 1.0 / (NOMINAL_TEMP + 273.15);         // + (1/To)
  steinhart = 1.0 / steinhart;                        // Invert
  steinhart -= 273.15;                                // Convert to Celsius

  return steinhart;
}
// ...

🔒 Este contenido es premium. Con la suscripción mensual (7 días gratis) podrás desbloquear el material didáctico completo y el pack PDF listo para imprimir.🔓 Desbloquéalo — 7 días gratis
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Pin Definitions ---
const int NTC_PIN = 34;       // ADC pin for thermistor voltage divider
const int FAN_PWM_PIN = 18;   // PWM output to MOSFET gate
const int LED_PIN = 21;       // Status LED

// --- PWM Configuration ---
const int PWM_FREQ = 5000;    // 5 kHz frequency for standard PC fans
const int PWM_CHANNEL = 0;    // LEDC channel 0
const int PWM_RES = 8;        // 8-bit resolution (0-255)

// --- Thermistor Constants (Steinhart-Hart) ---
const float SERIES_RESISTOR = 10000.0; // 10k fixed resistor
const float NOMINAL_RESISTOR = 10000.0; // 10k NTC at 25 degrees C
const float NOMINAL_TEMP = 25.0;        // 25 degrees C
const float B_COEFFICIENT = 3950.0;     // Beta value of the thermistor

// --- Global Variables ---
float currentTempC = 0.0;
float targetTempC = 25.0; // Default threshold
int currentFanSpeed = 0;  // 0 to 255

// --- Web Server on port 80 ---
WebServer server(80);

// --- HTML Dashboard (Stored in Flash) ---
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP32 Thermostat</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 20px; background-color: #f4f4f9; }
    h1 { color: #333; }
    .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 400px; margin: auto; }
    .metric { font-size: 2rem; font-weight: bold; color: #007BFF; margin: 10px 0; }
    .slider-container { margin-top: 20px; }
    input[type=range] { width: 100%; }
  </style>
</head>
<body>
  <div class="card">
    <h1>Fan Thermostat</h1>
    <p>Current Temperature:</p>
    <div class="metric"><span id="tempValue">--</span> &deg;C</div>
    <p>Fan Speed (PWM):</p>
    <div class="metric"><span id="fanValue">--</span> / 255</div>

    <div class="slider-container">
      <p>Target Threshold: <span id="targetValue">25</span> &deg;C</p>
      <input type="range" min="15" max="40" value="25" id="targetSlider" onchange="updateTarget(this.value)">
    </div>
  </div>

  <script>
    // Fetch data every 2 seconds
    setInterval(function() {
      fetch('/data')
        .then(response => response.json())
        .then(data => {
          document.getElementById('tempValue').innerText = data.temperature.toFixed(1);
          document.getElementById('fanValue').innerText = data.fan_speed;
          document.getElementById('targetValue').innerText = data.target;
          document.getElementById('targetSlider').value = data.target;
        });
    }, 2000);

    // Send new threshold to ESP32
    function updateTarget(val) {
      document.getElementById('targetValue').innerText = val;
      fetch('/set?threshold=' + val);
    }
  </script>
</body>
</html>
)rawliteral";

// --- Function to Calculate Temperature ---
float readTemperature() {
  int adcValue = analogRead(NTC_PIN);

  // Avoid division by zero if ADC is maxed out or grounded
  if (adcValue == 0 || adcValue == 4095) return currentTempC; 

  // Convert ADC value to voltage (ESP32 ADC is 12-bit: 0-4095)
  float voltage = adcValue * (3.3 / 4095.0);

  // Calculate NTC resistance (Divider: 3.3V -> 10k -> ADC -> NTC -> GND)
  float ntcResistance = SERIES_RESISTOR * voltage / (3.3 - voltage);

  // Steinhart-Hart Equation
  float steinhart;
  steinhart = ntcResistance / NOMINAL_RESISTOR;       // (R/Ro)
  steinhart = log(steinhart);                         // ln(R/Ro)
  steinhart /= B_COEFFICIENT;                         // 1/B * ln(R/Ro)
  steinhart += 1.0 / (NOMINAL_TEMP + 273.15);         // + (1/To)
  steinhart = 1.0 / steinhart;                        // Invert
  steinhart -= 273.15;                                // Convert to Celsius

  return steinhart;
}

// --- Web Server Route Handlers ---
void handleRoot() {
  server.send(200, "text/html", index_html);
}

void handleData() {
  String json = "{";
  json += "\"temperature\":" + String(currentTempC) + ",";
  json += "\"fan_speed\":" + String(currentFanSpeed) + ",";
  json += "\"target\":" + String(targetTempC);
  json += "}";
  server.send(200, "application/json", json);
}

void handleSet() {
  if (server.hasArg("threshold")) {
    targetTempC = server.arg("threshold").toFloat();
    Serial.print("New target threshold set to: ");
    Serial.println(targetTempC);
  }
  server.send(200, "text/plain", "OK");
}

void setup() {
  Serial.begin(115200);
  Serial.println("\nInitializing ESP32 Thermostat...");

  // Setup Pins
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  // Setup PWM for Fan (Using ESP32 LEDC API)
  ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES);
  ledcAttachPin(FAN_PWM_PIN, PWM_CHANNEL);
  ledcWrite(PWM_CHANNEL, 0);

  // Setup Wi-Fi Access Point
  WiFi.softAP("ESP32-Thermostat", "admin1234");
  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);

  // Setup Web Server Routes
  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.on("/set", handleSet);
  server.begin();
  Serial.println("HTTP server started.");
}

void loop() {
  server.handleClient();

  // Read temperature periodically
  static unsigned long lastUpdate = 0;
  if (millis() - lastUpdate > 1000) {
    lastUpdate = millis();

    currentTempC = readTemperature();

    // Proportional Fan Control Logic
    // If temp is below threshold, fan is off (0).
    // If temp is at threshold, fan starts at minimum effective PWM (e.g., 100).
    // If temp is 5 degrees above threshold, fan runs at max (255).
    if (currentTempC < targetTempC) {
      currentFanSpeed = 0;
    } else {
      // Map temperature range [target, target+5] to PWM range [100, 255]
      currentFanSpeed = map(currentTempC * 10, targetTempC * 10, (targetTempC + 5) * 10, 100, 255);
      // Constrain to ensure we don't exceed 8-bit limits
      currentFanSpeed = constrain(currentFanSpeed, 100, 255); 
    }

    // Apply PWM to fan
    ledcWrite(PWM_CHANNEL, currentFanSpeed);

    // Update Status LED
    if (currentFanSpeed > 0) {
      digitalWrite(LED_PIN, HIGH);
    } else {
      digitalWrite(LED_PIN, LOW);
    }

    // Log to Serial Monitor
    Serial.printf("Temp: %.1f C | Target: %.1f C | Fan PWM: %d\n", currentTempC, targetTempC, currentFanSpeed);
  }
}

Comandos de compilación/flasheo/ejecución

Usa los

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué microcontrolador se utiliza para construir el prototipo de termostato?




Pregunta 2: ¿Qué técnica se utiliza para escalar dinámicamente la velocidad del ventilador de refrigeración?




Pregunta 3: ¿Cómo se puede acceder a este prototipo de termostato?




Pregunta 4: ¿Qué variable monitoriza principalmente el ESP32 en este proyecto?




Pregunta 5: ¿Cuál es una de las ventajas de usar este termostato en armarios AV (audio/vídeo)?




Pregunta 6: ¿Por qué es útil este sistema en cerramientos de impresoras 3D?




Pregunta 7: ¿Qué materiales de impresión 3D se mencionan específicamente como beneficiados por este sistema?




Pregunta 8: En el contexto de racks de servidores, ¿cómo proporciona refrigeración este sistema?




Pregunta 9: ¿Qué problema previene este termostato en la circulación de aire en invernaderos?




Pregunta 10: ¿Qué condición activa el movimiento suave de aire sobre las plántulas en el caso de uso del invernadero?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio