
Heltec DHT22 fácil: tu móvil como termómetro (Chisme 26)
Este proyecto Heltec DHT22 es el termómetro del Lince, el Chisme 26: un sensor DHT22 mide la temperatura y la humedad, la placa Heltec LoRa 32 las enseña en su pantalla OLED y, a la vez, las manda por Bluetooth al móvil, donde aparecen en grande y con una gráfica en directo. Todo se ve desde el navegador, sin instalar ninguna app y sin emparejar nada.
Es el segundo Chisme con la Heltec. En el Chisme 25 preparamos el entorno, aprendimos a cargar programas y mandamos texto del móvil a la pantalla. Ahora damos la vuelta a la comunicación: la placa mide el mundo y el móvil lo recibe. Si ya hiciste la estación del tiempo con la micro:bit (Chisme 23), esta es su versión con una placa más potente y un sensor de verdad. Con el Heltec DHT22 aprenderás a conectar un sensor, a leerlo con una librería y a mandar sus datos por Bluetooth.
Antes de empezar: en este Chisme no repetimos la instalación del Arduino IDE, del soporte ESP32 ni de las librerías de la pantalla. Todo eso está en los pasos 1 a 4 del Chisme 25. Si la placa ya te funcionó con el letrero del Lince, tienes hecho lo más largo. Aquí solo añadimos el sensor y una librería nueva.
Heltec DHT22: qué vamos a construir
La página web del Heltec DHT22 tiene:
- Un botón para conectar la Heltec por Bluetooth.
- La temperatura en grande, con su mínimo, su máximo y una gráfica.
- La humedad en grande, con su mínimo, su máximo y otra gráfica.
- Un aviso de «último dato hace…» si la placa deja de mandar medidas, y un mensaje de ayuda si el sensor no responde.
La placa, por su lado, mide cada 2,5 segundos, lo pinta en su pantalla OLED y lo envía. Si desconectas el móvil, la placa sigue funcionando como un termómetro independiente.
Lo que necesitas para tu Heltec DHT22
- La Heltec LoRa 32 (o TTGO LoRa32) que ya usaste en el Chisme 25, con el Arduino IDE listo.
- Un sensor DHT22 (también se vende como AM2302). Mejor el módulo de 3 patillas, que ya lleva su resistencia.
- Tres cables dupont hembra-hembra o macho-hembra, según los pines de tu placa.
- El cable USB de datos de siempre.
- Un móvil Android con Chrome, con Bluetooth y ubicación encendidos. En iPhone, el navegador Bluefy.
Paso 1. Conecta el DHT22 a la Heltec
Hazlo con la placa desconectada del USB. El DHT22 tiene tres conexiones:
| Patilla del DHT22 | Se conecta a la Heltec |
|---|---|
| + (VCC) | 3V3 |
| – (GND) | GND |
| DATA (OUT) | GPIO 13 |
Tres cosas que evitan el 90 % de los problemas:
- Alimenta el sensor con 3V3, no con 5 V. Los pines del ESP32 trabajan a 3,3 voltios.
- El orden de las patillas cambia de un módulo a otro. Mira las letras impresas en tu sensor (
+,-,OUToDATA) y no te fíes del orden de las fotos. - Si tu DHT22 es el desnudo de 4 patillas, pon una resistencia de 10 kΩ entre DATA y 3V3. La cuarta patilla no se usa.
El pin 13 está libre en la TTGO LoRa32 v1.x y en la Heltec V1/V2. Si lo necesitas para otra cosa, cambia DHT_PIN en el código.
Paso 2. Instala la librería del DHT22
El sensor habla con la placa con un protocolo de un solo hilo que tiene mucha precisión de tiempos. Por suerte, otra gente ya lo programó: la librería DHT de Adafruit lo hace por nosotros.
- En el Arduino IDE, abre el Administrador de bibliotecas (el icono de los libros).
- Escribe
DHT sensor libraryy pulsa Instalar en la que se llama DHT sensor library (autora: Adafruit). - Cuando pregunte por las dependencias, pulsa Install all. Así se instala también Adafruit Unified Sensor.
- Las de la pantalla (Adafruit SSD1306 y Adafruit GFX) ya las tienes del Chisme 25.
Paso 3. Pega el código y súbelo a la placa
- Menú Archivo → Nuevo sketch y borra todo lo que haya.
- Copia el código de la placa (sección «Código 1», al final de esta página) y pégalo. Guarda el sketch como
chisme26_heltec_dht22. - Comprueba que la placa elegida es ESP32 Dev Module y que el puerto es el de tu Heltec (paso 6 del Chisme 25). No elijas «Heltec WiFi LoRa 32», aunque tu placa lo sea.
- Pulsa Subir. Si se queda en
Connecting…, mantén pulsado el botón PRG hasta que empiece a escribir. - Mira la pantalla: tras unos segundos debe aparecer algo así:
Bluetooth: esperando
24.3 C
51.8 %
Si pone «Sin datos», el sensor no está respondiendo: pasa a la sección «Si tu Heltec DHT22 falla».
Si la pantalla se queda en negro, son los pines de la OLED: cambia OLED_SDA, OLED_SCL y OLED_RST como te explicamos en el paso 5 del Chisme 25.
Truco para depurar: abre el Monitor serie (icono de la lupa, arriba a la derecha) a 115200 baudios. Verás una línea como T=24.3 H=51.8 cada 2,5 segundos. Si ahí salen datos, el sensor funciona aunque el Bluetooth aún no.
Paso 4. Abre la página en el móvil
Tienes dos opciones:
- Usar la nuestra: abre https://montesteam.org/app/chisme-26-heltec-dht22.html
- Tener la tuya: copia el código de la página (sección «Código 2»), guárdalo como
chisme-26-heltec-dht22.htmly súbelo a un sitio con https. Conhttp://o desde un archivo local, el Bluetooth del navegador no funciona.
Paso 5. Conecta y mira las medidas
- En el móvil, con Bluetooth y ubicación encendidos, abre la página en Chrome.
- Pulsa Conectar Heltec y elige Heltec-Lince.
- Tras los mensajes de «Conectando…» y «Activando notificaciones…» verás «Conectado a Heltec-Lince. Esperando la primera medida…».
- En un par de segundos aparecen la temperatura y la humedad, y empiezan a dibujarse las gráficas.

Paso 6. Haz que el Heltec DHT22 se mueva
Un termómetro que no cambia es aburrido. Prueba estas tres cosas y mira las gráficas:
- Sopla suavemente sobre el sensor: la humedad sube enseguida, porque tu aliento es muy húmedo.
- Coge el sensor con los dedos unos segundos: la temperatura sube despacio.
- Acércalo a una ventana o sácalo a la sombra: compara.
La página guarda los últimos 4 minutos aproximadamente y va anotando el mínimo y el máximo desde que conectaste.
Cómo funciona el Heltec DHT22
El sensor mide, la librería traduce
El DHT22 tiene dentro un sensor de humedad y un termistor (una resistencia que cambia con la temperatura). Un pequeño chip los convierte en números y los manda por el cable DATA. La librería de Adafruit se encarga del protocolo, y nosotros solo llamamos a dht.readTemperature() y dht.readHumidity().
Un máximo de una medida cada 2 segundos
El DHT22 es lento: necesita al menos 2 segundos entre una medida y la siguiente. Por eso el código usa PERIODO 2500 (2,5 segundos). Si lo pones más bajo, el sensor empezará a dar errores.
Si la lectura falla, la librería devuelve «NaN»
NaN significa «no es un número». Cuando el sensor no responde, el código lo detecta con isnan(), lo apunta en el monitor serie, enseña «Sin datos» en la pantalla y manda ERR al móvil. La página lo traduce en un aviso para revisar el cableado.
Un mensaje corto cada vez
Cada medida viaja como una línea de texto que termina en salto de línea:
T:24.3,H:51.8
Bluetooth solo permite avisos de 20 bytes. Esta línea ocupa unos 14, así que cabe en un solo aviso: no hace falta trocearla como en el Chisme 25. La página la recibe en recibir(), la reconoce con una expresión regular y saca los dos números.
La gráfica es una línea SVG
La página guarda los últimos 90 valores de cada medida y los convierte en una línea (polyline) dentro de un dibujo SVG. Cada vez que llega una medida nueva, se calcula el mínimo y el máximo de los valores guardados y se redibuja. No usa ninguna librería de gráficas.
Solo en un sentido
En el Chisme 25 la página escribía y la placa contestaba. Aquí la placa solo habla, así que hemos quitado la característica RX del servicio Bluetooth y el botón PRG. La conexión de la página (con sus reintentos y la reconexión automática) es la misma de los Chismes 21 a 25.
Los números del DHT22
| Dato | Valor aproximado (según el fabricante) |
|---|---|
| Temperatura | de -40 a 80 °C, con ±0,5 °C de error |
| Humedad | de 0 a 100 %, con ±2 a ±5 % de error |
| Tiempo entre medidas | 2 segundos como mínimo |
Los valores son aproximados
Aunque la pantalla enseñe un decimal, no te fíes ciegamente de él. El Heltec DHT22 es un sensor barato, pensado para aprender, y hay varios motivos para que se desvíe:
- La placa se calienta (el ESP32, la pantalla y el Bluetooth). Si el sensor está pegado a ella, medirá más grados de los reales. Alárgalo con cables de unos 10 cm.
- El sol directo o un radiador falsean la medida. Mejor a la sombra y con aire.
- El sensor tarda en estabilizarse al cambiar de sitio: espera un par de minutos.
- Compáralo con un termómetro de casa: si siempre marca 1 grado de más, ya sabes cuánto restar.
Retos para clase con el Heltec DHT22
- Grados Fahrenheit: en la página, antes de mostrar la temperatura, aplica
t * 9 / 5 + 32. ¿Qué pasa con la gráfica? - Alarma de calor: haz que el número se ponga rojo y el móvil vibre si la temperatura pasa de 30 °C.
- Más despacio: cambia
PERIODOa 5000. ¿Se nota en la gráfica? ¿Y a 1000, que es menos de lo que permite el sensor? - El aula en directo: deja el Heltec DHT22 en clase durante una hora. ¿Cuánto sube la temperatura cuando entran los alumnos? ¿Y la humedad?
- Sol y sombra: pon el sensor una vez al sol y otra a la sombra, durante el mismo tiempo, y compara las gráficas.
- Reto avanzado: guarda las medidas y descárgalas en un archivo CSV para analizarlas en una hoja de cálculo, como hicimos en el Chisme 23.
Si tu Heltec DHT22 falla
- La pantalla de la placa pone «Sin datos»: el sensor no responde. Revisa que DATA esté en el pin 13, que el sensor tenga 3V3 (no 5 V) y GND, y que no hayas cruzado las patillas. Si es de 4 patillas, falta la resistencia de 10 kΩ.
- La página dice «El sensor no responde»: es el mismo problema, avisado desde el móvil.
- Conecta, pero no salen números: comprueba que has subido el programa del Chisme 26 y no el del 25. La placa del 25 se conecta, pero no manda medidas.
- Los valores saltan mucho: cables largos o flojos. Acórtalos y apriétalos.
- Marca más grados de los reales: mira la sección «Los valores son aproximados».
- Problemas de Bluetooth (no aparece «Heltec-Lince»,
NotSupportedError,NetworkError…): son los mismos del Chisme 25. Mira su sección «Si tu Heltec Bluetooth falla». - En iPhone no conecta: abre la página con Bluefy.
- La página debe abrirse con https://.
Preguntas frecuentes
¿Qué es el DHT22 y qué mide?
El DHT22 es un sensor digital que mide la temperatura y la humedad relativa del aire. Se conecta con tres cables y se lee con una librería, por eso es de los primeros sensores que se usan con Arduino y ESP32.
¿Puedo usar un DHT11 en lugar del DHT22?
Sí. En el código cambia DHT dht(DHT_PIN, DHT22); por DHT dht(DHT_PIN, DHT11);. El DHT11 es más barato, pero mide con menos precisión (±2 °C y ±5 %) y no baja de 0 °C.
¿Hace falta una resistencia con el Heltec DHT22?
Con el módulo de 3 patillas no, porque ya la lleva. Con el sensor desnudo de 4 patillas sí: una resistencia de 10 kΩ entre DATA y 3V3.
¿Puedo usar otro pin distinto del 13?
Sí, cualquier pin libre de la placa. Cambia DHT_PIN en el código. Evita los pines de la pantalla OLED y los de la radio LoRa.
¿Por qué mi Heltec DHT22 mide más temperatura de la real?
Casi siempre porque el sensor está muy cerca de la placa, que se calienta. Aleja el sensor con cables y espera a que se estabilice.
Qué hemos aprendido
Con el Heltec DHT22 hemos visto que un sensor digital se conecta con tres cables y se lee con una librería que esconde el protocolo. Hemos aprendido que un sensor tiene sus límites (no se le puede preguntar más de una vez cada 2 segundos y no es exacto) y que el código debe prever qué pasa cuando la lectura falla. Hemos comprobado que un mensaje pequeño cabe en un solo aviso de Bluetooth y que la página puede convertir ese texto en números y gráficas sin librerías externas. Y, sobre todo, que la misma conexión del Chisme 25 sirve para dos usos opuestos: mandar texto a la placa o recibir datos de ella.
¿Quieres seguir con la Heltec? Mira el curso gratuito de micro:bit de MonteSTEAM y el resto de los Chismes del Lince. Si quieres saber más del sensor, la guía del DHT de Adafruit es muy completa y la API Web Bluetooth explica la parte del navegador.
Datos para WordPress
- Título de la entrada (H1): Chismes del Lince 26: Heltec DHT22, el termómetro del Lince
- Slug (URL):
chismes-del-lince-26-heltec-dht22-termometro-bluetooth - Categorías: AIDARAC – Ciencia Ciudadana, Los Chismes del Lince, Proyecto AIDARAC (quita «Uncategorized»)
- Etiquetas: Heltec DHT22, DHT22, Heltec LoRa 32, ESP32, Web Bluetooth, sensor de temperatura, Arduino IDE, Chismes del Lince, AIDARAC, montesteam, ies monterroso (en el Chisme 25 quedó una etiqueta con errata, «heltec bluetooh»; conviene corregirla)
- Imagen destacada:
heltec-dht22-termometro.png
Rank Math (panel SEO de la entrada)
- Palabra clave principal:
Heltec DHT22 - Palabras clave secundarias:
termómetro Bluetooth,sensor de temperatura ESP32,Heltec LoRa 32 - Título SEO (56 caracteres):
Heltec DHT22 fácil: tu móvil como termómetro (Chisme 26) - Meta descripción (unos 146 caracteres):
Heltec DHT22 paso a paso: conecta el sensor, carga el código y mira la temperatura y la humedad en directo en el móvil por Bluetooth, con gráfica. - Permalink / URL:
chismes-del-lince-26-heltec-dht22-termometro-bluetooth - Texto alternativo de la imagen destacada:
Heltec DHT22: placa Heltec LoRa 32 con un sensor DHT22 midiendo la temperatura y la humedad, que se ven en el móvil - Pestaña Social (Facebook y X): título y descripción iguales que los SEO, con la imagen destacada
- Tipo de Schema: Article
Lista de comprobación para que Rank Math dé luz verde
- Título SEO: la palabra clave va al principio, lleva un número (26) y una power word («fácil»). Si Rank Math no la reconoce, cambia «fácil» por «gratis» o «increíble».
- Descripción SEO y URL: pega los textos de arriba. Los dos contienen «Heltec DHT22» (en la URL,
heltec-dht22). - Primer 10 % del contenido: el primer párrafo empieza con «Este proyecto Heltec DHT22».
- Subtítulos: la palabra clave aparece en varios H2 («Heltec DHT22: qué vamos a construir», «Lo que necesitas para tu Heltec DHT22», «Cómo funciona el Heltec DHT22», «Retos…», «Si tu Heltec DHT22 falla»).
- Densidad: «Heltec DHT22» aparece unas 33 veces: alrededor del 1 % del texto (0,66 % si Rank Math cuenta también el código de abajo). Rank Math pide entre 0,5 y 2,5 %. Si al pegarlo sigue marcando «baja», añade un par de menciones en el primer y el último párrafo.
- Imágenes con la palabra clave en el alt: el texto alternativo de las cinco imágenes ya empieza por «Heltec DHT22». Al subirlas a WordPress, copia ese texto en el campo «Texto alternativo» (a veces no se hereda), y sube también la imagen destacada con el alt de arriba.
- Enlaces externos: hay tres (librería DHT de Adafruit, guía de Adafruit y API Web Bluetooth). Al insertarlos en WordPress, deja desmarcado «nofollow» en al menos uno para que Rank Math los cuente como enlaces salientes normales.
- Enlaces internos: ya hay (Chismes 23 y 25, el curso de micro:bit y la categoría).
- Página web: sube el HTML a
https://montesteam.org/app/chisme-26-heltec-dht22.html(es la dirección que cita el paso 4). - FAQ: añade el bloque FAQ de Rank Math y pega las cinco preguntas, para que Google pueda mostrarlas como resultado enriquecido.
- Imágenes a subir:
26-resultado.jpg,26-cableado.png,26-biblioteca.png,26-pantalla.jpegy26-pagina.png, y sustituye los marcadoresSUBIR-IMAGEN-…del texto.
Código 1: programa de la placa (Arduino IDE)
/*
Chisme 26 · El termómetro del Lince (Heltec LoRa con ESP32 + DHT22)
Mide temperatura y humedad con un DHT22, las enseña en la pantalla OLED
y las envía por Bluetooth (Nordic UART) a la página del móvil.
Librerías (Gestor de bibliotecas): "DHT sensor library" (Adafruit),
"Adafruit Unified Sensor", Adafruit SSD1306 y Adafruit GFX.
Placa en el Arduino IDE: "ESP32 Dev Module" (aunque tu placa sea una Heltec).
CABLEADO DEL DHT22 (los tres pines, con la placa apagada):
patilla + (VCC) -> 3V3 (no uses 5 V)
patilla - (GND) -> GND
patilla DATA -> GPIO 13
Si tu sensor es de 4 patillas sin placa, pon una resistencia de 10 kΩ
entre DATA y 3V3. Los módulos de 3 patillas ya la llevan.
Mensaje por Bluetooth (una línea): T:23.4,H:45.2 o ERR
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16 // pon -1 si tu placa no tiene reset de pantalla
#define OLED_ADDR 0x3C
#define DHT_PIN 13 // pin DATA del DHT22
#define PERIODO 2500 // ms entre medidas (el DHT22 necesita 2 s como mínimo)
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e" // la placa envía por aquí
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
DHT dht(DHT_PIN, DHT22);
BLECharacteristic *tx = nullptr;
volatile bool conectado = false;
unsigned long ultima = 0;
float temp = NAN, hum = NAN;
class AlConectar : public BLEServerCallbacks {
void onConnect(BLEServer *s) { conectado = true; }
void onDisconnect(BLEServer *s) {
conectado = false;
BLEDevice::startAdvertising(); // vuelve a ser visible para reconectar
}
};
void enviar(const String &m) { // máximo 20 bytes por aviso
if (conectado && tx) {
String s = m + "\n";
tx->setValue((uint8_t *)s.c_str(), s.length() > 20 ? 20 : s.length());
tx->notify();
}
}
void pintar() {
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.print(conectado ? "Bluetooth: conectado" : "Bluetooth: esperando");
if (isnan(temp) || isnan(hum)) {
display.setCursor(0, 28);
display.setTextSize(2);
display.print("Sin datos");
} else {
display.setTextSize(2);
display.setCursor(0, 16);
display.print(temp, 1);
display.print(" C");
display.setCursor(0, 42);
display.print(hum, 1);
display.print(" %");
}
display.display();
}
void medir() {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) {
temp = NAN; hum = NAN;
Serial.println("Error leyendo el DHT22");
enviar("ERR");
} else {
temp = t; hum = h;
Serial.printf("T=%.1f H=%.1f\n", t, h);
enviar("T:" + String(t, 1) + ",H:" + String(h, 1));
}
pintar();
}
void setup() {
Serial.begin(115200);
dht.begin();
Wire.begin(OLED_SDA, OLED_SCL);
if (OLED_RST >= 0) {
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
}
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) Serial.println("No se encuentra la OLED");
display.setTextColor(SSD1306_WHITE);
pintar();
BLEDevice::init("Heltec-Lince");
BLEServer *servidor = BLEDevice::createServer();
servidor->setCallbacks(new AlConectar());
BLEService *svc = servidor->createService(SERVICE_UUID);
tx = svc->createCharacteristic(UUID_TX, BLECharacteristic::PROPERTY_NOTIFY);
tx->addDescriptor(new BLE2902());
svc->start();
BLEAdvertising *pub = BLEDevice::getAdvertising();
pub->addServiceUUID(SERVICE_UUID);
pub->setScanResponse(true);
BLEDevice::startAdvertising();
}
void loop() {
static bool antes = false;
if (conectado != antes) { // cambio de estado: refrescar y medir ya
antes = conectado;
delay(800); // deja que el móvil active las notificaciones
ultima = 0;
}
if (millis() - ultima >= PERIODO || ultima == 0) {
ultima = millis();
medir();
}
delay(20);
}
Código 2: la página web (HTML)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Chisme 26 · El termómetro del Lince (Heltec + DHT22)</title>
<style>
:root{
--bg:#eef2ef; --ink:#10201c; --soft:#4d625b; --card:#ffffff; --line:#cfdad5;
--pine:#1f5b4a; --slow:#2563a8; --ok:#1f7a4d; --norte:#c8321f; --oro:#b8892b;
box-sizing:border-box;
padding-top:env(safe-area-inset-top,0px); padding-bottom:env(safe-area-inset-bottom,0px);
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg:#0f1a17; --ink:#e8f1ed; --soft:#9db3aa; --card:#172622; --line:#2a3d37;
--pine:#5fc4a2; --slow:#7db3ee; --ok:#6fd89f; --norte:#ff6a55; --oro:#e0b45a;}}
:root[data-theme="dark"]{--bg:#0f1a17; --ink:#e8f1ed; --soft:#9db3aa; --card:#172622; --line:#2a3d37;
--pine:#5fc4a2; --slow:#7db3ee; --ok:#6fd89f; --norte:#ff6a55; --oro:#e0b45a;}
*{box-sizing:border-box}
html{scroll-padding-top:env(safe-area-inset-top,0px)}
body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.45 "Trebuchet MS","Segoe UI",system-ui,sans-serif}
.tira{height:.55rem;background:linear-gradient(90deg,var(--oro),var(--norte),var(--pine))}
main{max-width:30rem;margin:0 auto;padding:1rem 1rem 2rem;text-align:center}
h1{font-size:1.6rem;margin:.2rem 0 .1rem}
.sub{color:var(--soft);margin:0 0 .9rem;font-size:.95rem}
button{font:inherit;font-weight:700;border:2px solid var(--pine);background:var(--pine);color:var(--bg);
border-radius:.9rem;padding:.7rem 1.1rem;min-height:3.1rem;cursor:pointer}
button:disabled{opacity:.5;cursor:not-allowed}
button:focus-visible{outline:3px solid var(--slow);outline-offset:2px}
#estado{margin:.7rem 0 .1rem;color:var(--soft);min-height:1.5em}
#ayuda{margin:0 0 .4rem;color:var(--soft);font-size:.88rem;min-height:1.1em}
.medida{background:var(--card);border:1px solid var(--line);border-radius:1rem;padding:.7rem 1rem .5rem;margin:.8rem 0;text-align:left}
.medida h2{margin:0;font-size:1rem;color:var(--soft);font-weight:700}
.valor{font-size:3.2rem;font-weight:700;line-height:1.1}
.valor small{font-size:1.4rem;color:var(--soft);font-weight:400}
#vt{color:var(--norte)} #vh{color:var(--slow)}
.minmax{color:var(--soft);font-size:.9rem}
svg{width:100%;height:4.2rem;display:block;margin-top:.3rem}
svg polyline{fill:none;stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
#gt{stroke:var(--norte)} #gh{stroke:var(--slow)}
#edad{color:var(--soft);font-size:.9rem;min-height:1.3em}
.nota{color:var(--soft);font-size:.85rem;margin:.8rem 0 0}
.guia{text-align:left;background:var(--card);border:1px solid var(--line);border-radius:.8rem;padding:.2rem .9rem;margin-top:1.2rem}
.guia summary{font-weight:700;cursor:pointer;padding:.6rem 0}
.guia ol{padding-left:1.3rem;margin:.2rem 0 .6rem}
.guia li{margin:.4rem 0}
</style>
</head>
<body>
<div class="tira"></div>
<main>
<h1>El termómetro del Lince</h1>
<p class="sub">Temperatura y humedad del DHT22, en directo en tu móvil.</p>
<button id="conectar">Conectar Heltec</button>
<div id="estado" role="status">Sin conectar.</div>
<div id="ayuda"></div>
<section class="medida" aria-label="Temperatura">
<h2>Temperatura</h2>
<div class="valor" id="vt">--<small> °C</small></div>
<div class="minmax" id="mt">mín -- · máx --</div>
<svg viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true"><polyline id="gt" points=""/></svg>
</section>
<section class="medida" aria-label="Humedad">
<h2>Humedad</h2>
<div class="valor" id="vh">--<small> %</small></div>
<div class="minmax" id="mh">mín -- · máx --</div>
<svg viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true"><polyline id="gh" points=""/></svg>
</section>
<div id="edad" aria-live="polite"></div>
<p class="nota">Valores aproximados: el DHT22 se equivoca hasta ±0,5 °C en temperatura y entre ±2 y ±5 % en humedad. Aleja el sensor de la placa, que se calienta, y espera un par de minutos a que se estabilice.</p>
<details class="guia">
<summary>Guía de uso</summary>
<ol>
<li>Conecta el DHT22: <b>+</b> a 3V3, <b>-</b> a GND y <b>DATA</b> al pin 13 de la Heltec.</li>
<li>Carga <b>chisme-26-heltec-dht22.ino</b> con el Arduino IDE. En la pantalla verás los valores, que se actualizan cada 2,5 segundos.</li>
<li>Abre esta página con <b>Chrome</b> (Android u ordenador) y pulsa <b>Conectar Heltec</b>. Elige <b>Heltec-Lince</b>.</li>
<li>Sopla sobre el sensor o acércalo a algo caliente: verás subir la gráfica.</li>
</ol>
</details>
</main>
<script>
const UART_SERVICE = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UART_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // la Heltec envía por aquí
const $ = id => document.getElementById(id);
const espera = ms => new Promise(r => setTimeout(r, ms));
let dispositivo = null, conectando = false, reconectando = false, buffer = "", pasoCx = "", diag = "";
let ultimoDato = 0, errSensor = false;
const MAX = 90; // puntos de la gráfica (unos 4 minutos)
const serie = { t: [], h: [] };
/* Medidas */
function pintaGrafica(id, datos){
if (datos.length < 2){ $(id).setAttribute("points", ""); return; }
const mn = Math.min(...datos), mx = Math.max(...datos), r = (mx - mn) || 1;
$(id).setAttribute("points", datos.map((v, i) =>
(i * 300 / (MAX - 1)).toFixed(1) + "," + (55 - (v - mn) / r * 50).toFixed(1)).join(" "));
}
function actualiza(clave, vId, mId, gId, v, unidad){
const s = serie[clave]; s.push(v); if (s.length > MAX) s.shift();
$(vId).innerHTML = v.toFixed(1) + "<small> " + unidad + "</small>";
$(mId).textContent = "mín " + Math.min(...s).toFixed(1) + " · máx " + Math.max(...s).toFixed(1);
pintaGrafica(gId, s);
}
function linea(t){
const m = /^T:(-?\d+(?:\.\d+)?),H:(\d+(?:\.\d+)?)$/.exec(t);
if (m){
ultimoDato = Date.now(); errSensor = false;
actualiza("t", "vt", "mt", "gt", parseFloat(m[1]), "°C");
actualiza("h", "vh", "mh", "gh", parseFloat(m[2]), "%");
} else if (t === "ERR"){
ultimoDato = Date.now(); errSensor = true;
$("edad").textContent = "El sensor no responde. Revisa el cableado (DATA al pin 13).";
}
}
function recibir(ev){
buffer += new TextDecoder().decode(ev.target.value);
let i;
while ((i = buffer.indexOf("\n")) >= 0){ linea(buffer.slice(0, i).trim()); buffer = buffer.slice(i + 1); }
}
setInterval(() => {
if (!ultimoDato) return;
const s = Math.round((Date.now() - ultimoDato) / 1000);
if (errSensor && s < 8) $("edad").textContent = "El sensor no responde. Revisa el cableado (DATA al pin 13).";
else if (s >= 8) $("edad").textContent = "Último dato hace " + s + " s";
else $("edad").textContent = "";
}, 1000);
/* Conexión Bluetooth (mismo esquema que las páginas 21 a 25) */
const propsDe = c => ["read", "write", "writeWithoutResponse", "notify", "indicate"].filter(k => c.properties[k]).join(", ");
function estado(t, ayuda){ if (!/^Error/.test(t)) pasoCx = t; $("estado").textContent = t; $("ayuda").textContent = ayuda || ""; }
function explicar(e){
const nombre = e && e.name ? e.name : "Error", m = e && e.message ? e.message : String(e);
let a = "";
if (nombre === "NotFoundError" && /cancel/i.test(m)) a = "No has elegido ningún dispositivo. Pulsa Conectar y elige Heltec-Lince.";
else if (nombre === "NotFoundError") a = "La placa no tiene el servicio UART. Comprueba que has cargado el programa del Chisme 26.";
else if (nombre === "NotSupportedError") a = "No se pudo activar el envío de datos. Olvida la Heltec en Ajustes → Bluetooth y reinicia el móvil y la placa.";
else if (nombre === "NetworkError") a = "No se pudo abrir la conexión. Comprueba que no haya otra app conectada y reinicia la placa (botón RST).";
else if (nombre === "SecurityError") a = "La página debe abrirse con https://.";
else if (nombre === "NotAllowedError") a = "Permiso denegado. Permite el Bluetooth y los dispositivos cercanos para el navegador.";
estado("Error (" + nombre + "): " + m, "Falló en el paso: «" + pasoCx + "». " + a + (diag ? " Características: " + diag : ""));
}
async function conectarGatt(dev){
let ultimoError;
for (let k = 1; k <= 4; k++){
try {
estado("Conectando… (intento " + k + " de 4)");
const srv = dev.gatt.connected ? dev.gatt : await dev.gatt.connect();
await espera(600);
estado("Buscando el servicio UART…");
return await srv.getPrimaryService(UART_SERVICE);
} catch (e){
ultimoError = e;
if (e.name === "NotFoundError" || e.name === "SecurityError") break;
try { if (dev.gatt.connected) dev.gatt.disconnect(); } catch (_){}
await espera(800);
}
}
throw ultimoError;
}
async function enlazar(dev){
const svc = await conectarGatt(dev);
buffer = ""; diag = "";
estado("Leyendo las características del servicio…");
const chars = await svc.getCharacteristics();
diag = chars.map(c => c.uuid.slice(0, 8) + " [" + propsDe(c) + "]").join(" · ");
const cand = chars.filter(c => c.properties.notify || c.properties.indicate)
.sort((a, b) => (b.uuid === UART_TX) - (a.uuid === UART_TX));
if (!cand.length) throw new Error("El servicio UART no tiene ninguna característica que envíe datos.");
let ch = null, ultimoErr;
for (const c of cand){
c.addEventListener("characteristicvaluechanged", recibir);
for (let i = 1; i <= 2 && !ch; i++){
try { estado("Activando notificaciones en " + c.uuid.slice(0, 8) + "… (intento " + i + ")"); await c.startNotifications(); ch = c; }
catch (e){ ultimoErr = e; await espera(1000); }
}
if (ch) break;
c.removeEventListener("characteristicvaluechanged", recibir);
}
if (!ch) throw ultimoErr;
estado("Conectado a " + (dev.name || "Heltec") + ". Esperando la primera medida…");
}
async function reconectar(){
if (reconectando) return;
reconectando = true;
for (let i = 1; i <= 20; i++){
estado("Conexión perdida. Reintentando… (" + i + " de 20)", "Mantén la Heltec cerca y encendida.");
await espera(3000);
try { await enlazar(dispositivo); reconectando = false; return; } catch (_) {}
}
reconectando = false;
estado("No se pudo reconectar.", "Pulsa Conectar para volver a empezar.");
}
$("conectar").onclick = async () => {
if (conectando || reconectando) return;
if (!navigator.bluetooth){ estado("Este navegador no permite Bluetooth.", "Usa Chrome o Edge (en ordenador o en Android)."); return; }
conectando = true; $("conectar").disabled = true;
try {
estado("Buscando la Heltec…");
const dev = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [UART_SERVICE] });
dispositivo = dev;
dev.addEventListener("gattserverdisconnected", reconectar);
await enlazar(dev);
} catch (e){ explicar(e); }
finally { conectando = false; $("conectar").disabled = false; }
};
</script>
</body>
</html>

