case-device-block-diagram, 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). 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 Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema. Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil. Resumen conceptual de las herramientas usadas para comprobar el proyecto ESP32 publicado. 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 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. 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. Para construir este prototipo, necesitas exactamente los siguientes componentes: 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. 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. El proyecto requiere dos archivos en tu espacio de trabajo de PlatformIO. El archivo Crea o sobrescribe el archivo Coloca el siguiente código en Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print. Usa los Encuentra este producto y/o libros sobre este tema en 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.
.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
Por qué es importante / Casos de uso
Resultado esperado
Diagrama de bloques conceptual
Arquitectura funcional
Ruta de validación
Nota educativa de validación
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
Nota educativa de seguridad
Requisitos previos
Materiales
* 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 puenteConfiguración/Conexión
Componente Pin / Conexión ESP32 Destino / Descripción Termistor NTC GNDUna pata del termistor NTC. Termistor NTC GPIO34 (ADC1_CH6)La otra pata del NTC. Conecta también la resistencia fija de 10k aquí. Resistencia fija de 10k 3V3Conecta 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 MOSFET GNDConecta a la tierra común (GND del ESP32 y GND de 12V). Drenador del MOSFET Negativo 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 estado GPIO21Conecta a través de la resistencia limitadora de corriente de 220 Ohm. Cátodo del LED de estado GNDConecta a la tierra común. Código validado
platformio.ini configura el entorno de compilación, y src/main.cpp contiene la lógica de la aplicación.platformio.ini
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
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.#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> °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> °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;
}
// ...#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> °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> °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
Botón local
ESP32 BLE
Paquete advertising
LED de estado
Móvil escáner
Código fuente
PlatformIO build
Flash
Monitor serie




