
Chismes del Lince 36: HALCÓN y el riesgo de incendio
Aquella tarde en Sierra Bermeja el aire quemaba. Los pinsapos estaban quietos, las jaras crujían bajo las patas y el cielo tenía ese color blanco de los días secos.
El Lince de la Noche se había sentado en una roca con la lengua fuera.
—Hoy hace un calor… peligroso —resopló.
—¿Peligroso por qué? —preguntó la Gata Montesa, que miraba el monte con los ojos entrecerrados.
—Porque lo noto. Calor, aire seco, el monte como la yesca. Huele a que algo puede pasar.
—¿Y cuánto de seco es “seco”? ¿Cuánto de caliente es “caliente”?
El Lince se quedó pensando. Luego se mojó un dedo, lo levantó en el aire y cerró los ojos con mucha solemnidad.
—Humedad… baja. Temperatura… alta. Viento… —abrió un ojo— ¿del este?
—Lince, eso es un dedo mojado.
—Es un método tradicional.
—Es un método del siglo pasado. Y con tus sentidos no puedes avisar a nadie de lo que notas.
El Lince bajó de la roca con dignidad herida y tomó el camino de la cueva, que está escondida en un sitio que no voy a decir. La Gata lo siguió. Al fondo, tras una puerta con timbre (porque el Lince es muy educado), se abría un hangar lleno de pequeñas máquinas plateadas con alas.
—Mis hidrodrones —dijo orgulloso—. Cada uno lleva un depósito de agua. Si hay un conato de incendio, salen de aquí y llegan antes de que el fuego crezca.
—¿Y se llaman?
—HALCÓN: Hidrodrones Aéreos del Lince contra CONatos. Como el halcón peregrino que vive aquí, pero con depósito.
—Muy bonito. ¿Y quién les dice cuándo salir?
El Lince abrió la boca. La cerró. La volvió a abrir.
—¿Mi dedo mojado?
—Necesitan datos —dijo la Gata—. Temperatura, humedad… algo que mida y que lo cuente de forma que lo entienda una máquina, y también las personas.
Sobre una mesa había una placa Heltec y un pequeño módulo con cuatro patillas. El Lince lo miró con cara de que acababa de encontrar el dedo mojado de la tecnología.
—Un sensor —murmuró—. El chisme de hoy.
—Un sensor —confirmó la Gata—. Y, Lince, antes de que empieces: nosotros no apagamos fuegos de verdad.
—Lo sé. Eso lo hacen los profesionales. Y se llama al 112.
Introducción
Importante: si ves fuego o humo en el monte, llama al 112. Este chisme es un modelo educativo. Ningún alumno apaga nada, ni intenta acercarse a un fuego. De eso se encargan los profesionales.
En este chisme construimos un sensor BME280 con ESP32: la placa Heltec mide la temperatura, la humedad y la presión del aire.
Con esos datos calcula un semáforo de riesgo muy sencillo: verde, amarillo o rojo. Lo escribe en su pantalla y lo manda por Bluetooth a una página web que lo muestra en grande.
Además, el programa hace algo que casi nunca se enseña: le pregunta al chip quién es. Porque dos sensores casi idénticos pueden dar o no la humedad, y la etiqueta del módulo no siempre dice la verdad.
Y si el sensor falla (o todavía no lo has soldado), la placa no se queda muda: se inventa datos creíbles y avisa de que son simulados, por la pantalla, por Bluetooth y por el puerto serie.
Por qué este chisme
Sierra Bermeja es un monte precioso y, en verano, también un monte muy seco. Cuando hace calor y la humedad del aire es baja, la vegetación se seca y un fuego tiene más fácil empezar y extenderse.
Para el Lince, la pregunta es: ¿cómo puedo saber, con datos y no con el dedo mojado, cuándo las condiciones son peores?
La respuesta empieza por medir. Un sensor pequeño y barato ya da dos de las cosas que más importan: temperatura y humedad.
No es todo lo que hay que saber (falta el viento, por ejemplo, y lo seca que esté la vegetación). Pero es un primer paso real para entender el riesgo.
Y es el primer chisme de una línea nueva: sensores que miden el entorno para después guardar datos, hacer gráficas y conectarlos entre sí con LoRa.
Cómo funciona el sensor BME280
—¿Y cómo le hablo a este módulo? —preguntó el Lince, mirando las cuatro patillas—. ¿Tiene un idioma secreto?
—Se llama I2C —dijo la Gata—. Es un bus de dos cables, SDA y SCL, en el que pueden hablar varios aparatos a la vez.
—¿Y cómo saben a quién me dirijo?
—Cada aparato tiene una dirección, como una casa en una calle. La pantalla de la placa vive en la 0x3C y el sensor en la 0x76 o la 0x77. La pantalla ya usa sus propios pines, así que el sensor tendrá su propia calle: un segundo bus I2C en los pines 13 y 23.
Qué mide realmente
El BME280 da tres datos: temperatura (en grados Celsius), humedad relativa (en %) y presión (en hectopascales, hPa).
La humedad relativa dice cuánta agua lleva el aire comparada con la máxima que podría llevar a esa temperatura. Un 20 % es un aire muy seco.
¿Qué chip tengo de verdad?
Existen dos chips casi iguales. El BME280 mide humedad. El BMP280 no. Por fuera se parecen y algunos módulos se venden con el nombre cambiado.
Por eso el programa no se fía de la etiqueta. Le pregunta al chip: todos guardan su «carnet de identidad» en el registro 0xD0.
| Valor del carnet | Chip | Mide humedad |
|---|---|---|
| 0x60 | BME280 | Sí |
| 0x58 | BMP280 | No |
El semáforo de riesgo
La placa se hace dos preguntas: ¿hace calor? y ¿el aire está seco? Cuenta cuántas veces responde «sí».
| Respuestas «sí» | Semáforo |
|---|---|
| 0 | Riesgo BAJO (verde) |
| 1 | Riesgo MEDIO (amarillo) |
| 2 | Riesgo ALTO (rojo) |
Los umbrales (30 °C y 30 % de humedad) son de ejemplo, inventados para clase. No son oficiales y no sirven para tomar decisiones reales.
Qué viaja por Bluetooth
Cada 2 segundos la placa manda una línea corta, por ejemplo 27.3,41,1013,1: temperatura, humedad, presión y riesgo. Si no encuentra el sensor, manda los mismos datos pero inventados y con una S al final (27.3,41,1013,1,S), para que la página avise de que son simulados. Si el chip es un BMP280, manda E:BMP.
Qué necesitamos
- Una placa Heltec LoRa 32 (o TTGO con pantalla OLED), con su cable USB.
- Un módulo sensor BME280 con sus 4 patillas (VCC, GND, SCL y SDA).
- Cuatro cables dupont y una breadboard.
- Un móvil con Chrome (Web Bluetooth).
- Arduino IDE con las librerías Adafruit BME280 Library (acepta instalar también Adafruit Unified Sensor y BusIO), Adafruit SSD1306 y Adafruit GFX.
- Dos archivos:
chisme-36-halcon.inoychisme-36-halcon.html.
No hace falta soldar si los cables hacen buen contacto. Si las patillas del módulo vienen sueltas, ponlas en la breadboard y añade los cables encima, apretando bien.

Construcción
Haz esto con la placa desenchufada.
| Patilla del sensor | Va a la placa |
|---|---|
| VCC | 3V3 (3,3 voltios, nunca 5 V) |
| GND | GND |
| SDA | GPIO 13 |
| SCL | GPIO 23 |
La pantalla de la placa ya usa los pines 21 y 22 (y en algunas placas no salen al exterior), así que el sensor usa su propio bus en el 13 y el 23. Si tu placa tiene libres otros pines, cambia SENSOR_SDA y SENSOR_SCL al principio de la PARTE 2 del código.
- Coloca el módulo en la breadboard.
- Conecta los cuatro cables de la tabla.
- Empuja bien cada cable y cada patilla hasta el fondo.
- Enchufa la placa y sube el programa.
Si la breadboard falla
La placa no se queda parada: la pantalla dice «SIMULADO: sin sensor», la página enseña una franja amarilla y el Monitor Serie lo cuenta. Los valores son inventados pero creíbles, y cambian poco a poco. El programa vuelve a buscar el sensor cada 2 segundos, así que cuando el contacto mejore, pasará solo a medir de verdad.
Si falla, cambia de agujero cada patilla y prueba otro cable. Un cable suelto suele ser el culpable, no el código.
Dónde poner el sensor
Separa el módulo unos centímetros de la placa y ponlo a la sombra. La placa se calienta al funcionar y, si el sensor está pegado a ella, medirá el calor de la placa y no el del ambiente.

El código de la placa
Copia todo el código en un sketch nuevo del Arduino IDE. Tiene cinco partes, pero solo las PARTES 1 y 2 son el chisme: el semáforo, la comprobación del chip y la simulación que avisa cuando no hay sensor.
/*
Chisme 36 · HALCON y el riesgo de incendio
Un sensor BME280 mide temperatura, humedad y presion. La placa calcula un
SEMAFORO de riesgo muy sencillo (bajo, medio, alto), lo escribe en su pantalla
y lo manda por Bluetooth a la pagina web.
IMPORTANTE: esto es un modelo EDUCATIVO. Un sensor de temperatura y humedad NO
detecta incendios y este semaforo NO es oficial. Si ves fuego o humo en el
monte, llama al 112. Nadie de la clase apaga nada.
SI NO ENCUENTRA EL SENSOR, la placa NO se queda parada: se INVENTA unos valores
que cambian poco a poco (SIMULADOS) y lo AVISA en tres sitios: en la pantalla
("SIMULADO: sin sensor"), en la pagina web (franja amarilla) y en el puerto serie.
Asi se puede probar todo sin el sensor. Al conectarlo bien, pasa solo a medir de verdad.
PUERTO SERIE: Herramientas -> Monitor Serie, a 115200 baudios.
Este programa tiene 5 partes. Solo las PARTES 1 y 2 son el chisme:
PARTE 1 · EL RIESGO <- LEE ESTA. El semaforo y sus umbrales.
PARTE 2 · EL SENSOR <- LEE ESTA. Comprueba que chip tienes de verdad.
PARTE 3 · LA PANTALLA <- NO ES NECESARIA. Solo enseña los datos en la OLED.
PARTE 4 · EL BLUETOOTH <- Como en el chisme 32. No hace falta tocarla.
PARTE 5 · setup() y loop()
CABLEADO (con la placa DESENCHUFADA). El modulo tiene 4 patillas:
VCC ──> 3V3 (3,3 voltios, NO 5 V)
GND ──> GND
SDA ──> GPIO 13
SCL ──> GPIO 23
La pantalla de la placa ya usa los pines 21 y 22, y en algunas placas no salen
al exterior. Por eso el sensor tiene su PROPIO bus I2C en los pines 13 y 23.
Si en tu placa estan libres otros pines, cambia los dos numeros de SENSOR_SDA y
SENSOR_SCL (PARTE 2).
Librerias: Adafruit BME280 Library (te pedira instalar tambien
Adafruit Unified Sensor y Adafruit BusIO: di que si a todo),
Adafruit SSD1306 y Adafruit GFX.
Placa en el Arduino IDE: "ESP32 Dev Module".
Si al compilar dice "Sketch too big":
Herramientas -> Partition Scheme -> "Huge APP (3MB No OTA/1MB SPIFFS)".
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BME280.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
/* ============================================================
PARTE 1 · EL RIESGO (lo importante)
Dos preguntas: ¿hace calor? y ¿el aire esta seco?
0 respuestas "si" -> riesgo BAJO (verde)
1 respuesta "si" -> riesgo MEDIO (amarillo)
2 respuestas "si" -> riesgo ALTO (rojo)
OJO: estos numeros son DE EJEMPLO, inventados para clase. No son oficiales.
Cambialos y mira como cambia el semaforo.
============================================================ */
const float UMBRAL_CALOR = 30.0; // grados C: a partir de aqui "hace calor"
const float UMBRAL_SECO = 30.0; // % de humedad: por debajo de aqui "el aire esta seco"
int calcularRiesgo(float temperatura, float humedad) {
int puntos = 0;
if (temperatura >= UMBRAL_CALOR) puntos = puntos + 1; // hace calor
if (humedad <= UMBRAL_SECO) puntos = puntos + 1; // aire seco
return puntos; // 0, 1 o 2
}
const char* NOMBRE_RIESGO[] = {"BAJO", "MEDIO", "ALTO"};
/* ============================================================
PARTE 2 · EL SENSOR (lo importante)
Hay dos chips casi iguales: el BME280 mide temperatura, presion y HUMEDAD;
el BMP280 NO mide humedad. Por fuera se parecen y muchos modulos se venden
con el nombre cambiado. ¿Como saber cual tienes? Preguntandole al chip:
todos tienen un "carnet de identidad" en el registro 0xD0.
0x60 = BME280 0x58 = BMP280
============================================================ */
#define SENSOR_SDA 13 // el cable SDA del sensor va al GPIO 13
#define SENSOR_SCL 23 // el cable SCL del sensor va al GPIO 23
TwoWire I2CSensor = TwoWire(1); // un bus I2C solo para el sensor
enum Sensor { NO_HAY, ES_BME280, ES_BMP280 };
Sensor sensor = NO_HAY; // que sensor hemos encontrado
Adafruit_BME280 bme; // la libreria que lee el BME280
float temperatura = 0; // en grados Celsius
float humedad = 0; // en % de humedad relativa
float presion = 0; // en hectopascales (hPa)
int riesgo = 0; // 0, 1 o 2
bool simulando = false; // true = los valores NO son medidas: son inventados
float simT = 29.6, simH = 33.0, simP = 1013.0; // donde se queda la simulacion
void simular() { // inventa valores creibles que cambian poco a poco
float cambioT = ((int)(esp_random() % 9) - 4) / 10.0; // entre -0,4 y +0,4 grados
simT = simT + cambioT;
simH = simH - 3 * cambioT + ((int)(esp_random() % 7) - 3) / 2.0; // si sube T, baja la humedad
simP = simP + ((int)(esp_random() % 5) - 2) / 10.0; // la presion casi no cambia
simT = constrain(simT, 18.0, 38.0); // limites razonables para el monte en verano
simH = constrain(simH, 15.0, 70.0);
simP = constrain(simP, 1005.0, 1020.0);
temperatura = simT;
humedad = simH;
presion = simP;
}
void escanearBus() { // en el Monitor Serie: que responde en los cables 13 y 23
Serial.println("Buscando dispositivos en SDA=13 y SCL=23...");
int encontrados = 0;
for (byte dir = 1; dir < 127; dir++) {
I2CSensor.beginTransmission(dir);
if (I2CSensor.endTransmission() == 0) {
Serial.print(" Responde la direccion 0x");
Serial.println(dir, HEX); // el BME280 deberia ser 0x76 o 0x77
encontrados++;
}
}
if (encontrados == 0) Serial.println(" No responde NADA: revisa los 4 cables (VCC, GND, SDA, SCL).");
}
byte leerIdentificador(byte direccion) { // le pregunta al chip quien es (0 = no responde)
I2CSensor.beginTransmission(direccion);
I2CSensor.write(0xD0); // el registro del carnet de identidad
if (I2CSensor.endTransmission() != 0) return 0;
I2CSensor.requestFrom(direccion, (byte)1);
if (I2CSensor.available()) return I2CSensor.read();
return 0;
}
void buscarSensor() { // prueba las dos direcciones posibles: 0x76 y 0x77
sensor = NO_HAY;
byte direcciones[2] = {0x76, 0x77};
for (int i = 0; i < 2; i++) {
byte id = leerIdentificador(direcciones[i]);
if (id == 0x60) { // ¡es un BME280 de verdad!
if (bme.begin(direcciones[i], &I2CSensor)) sensor = ES_BME280;
return;
}
if (id == 0x58) { // es un BMP280: sin humedad
sensor = ES_BMP280;
return;
}
}
}
void medir() { // una lectura completa
simulando = false;
if (sensor == NO_HAY) buscarSensor(); // si no lo encontramos, lo volvemos a intentar
if (sensor == ES_BME280) { // (en una breadboard los cables fallan a veces)
temperatura = bme.readTemperature();
humedad = bme.readHumidity();
presion = bme.readPressure() / 100.0; // la libreria da pascales: dividimos entre 100
if (isnan(temperatura) || isnan(humedad) || temperatura < -40 || temperatura > 85)
sensor = NO_HAY; // lectura imposible: seguramente un cable suelto
}
if (sensor == ES_BMP280) return; // este chip no mide humedad: no hay nada que calcular
if (sensor == NO_HAY) { // sin sensor: valores INVENTADOS, y lo avisamos
simulando = true;
simular();
}
riesgo = calcularRiesgo(temperatura, humedad);
}
void escribirSerie() { // una linea en el Monitor Serie cada 2 segundos
if (sensor == ES_BMP280) {
Serial.println("Chip BMP280: no mide humedad. Necesitas un BME280.");
return;
}
Serial.print(simulando ? "SIMULADO (sin sensor) | " : "REAL | ");
Serial.print("T = "); Serial.print(temperatura, 1);
Serial.print(" C | H = "); Serial.print(humedad, 0);
Serial.print(" % | P = "); Serial.print(presion, 0);
Serial.print(" hPa | riesgo "); Serial.println(NOMBRE_RIESGO[riesgo]);
}
/* ============================================================
PARTE 3 · LA PANTALLA (NO ES NECESARIA: el sensor funciona sin ella)
Solo sirve para ver los datos en la OLED de la placa.
============================================================ */
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
volatile bool conectado = false; // ¿hay un movil conectado?
void pintarPantalla() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print(conectado ? "HALCON - conectado" : "HALCON - esperando");
if (sensor == ES_BMP280) {
display.setCursor(0, 20); display.print("Tu sensor es un");
display.setCursor(0, 30); display.print("BMP280: NO mide");
display.setCursor(0, 40); display.print("la humedad.");
} else {
display.setTextSize(2);
display.setCursor(0, 10); display.print(String(temperatura, 1) + " C");
display.setCursor(0, 28); display.print(String((int)round(humedad)) + " %");
display.setTextSize(1);
display.setCursor(0, 46);
if (simulando) display.print("SIMULADO: sin sensor"); // ¡avisamos de que no es real!
else display.print(String((int)round(presion)) + " hPa");
display.setCursor(0, 56);
if (riesgo == 2) display.print("ALTO. Fuego real: 112"); // ¡siempre el 112!
else display.print(String("Riesgo ") + NOMBRE_RIESGO[riesgo]);
}
display.display();
}
void arrancarPantalla() {
Wire.begin(OLED_SDA, OLED_SCL);
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
}
/* ============================================================
PARTE 4 · EL BLUETOOTH (como en el chisme 32)
La placa se anuncia como "Heltec-Halcon" y solo tiene un "buzon":
TX: por aqui la placa AVISA a la pagina cada 2 segundos:
"27.3,41,1013,1" = temperatura, humedad, presion y riesgo
"27.3,41,1013,1,S" = lo mismo, pero con una S: ¡son valores SIMULADOS!
"E:BMP" = es un BMP280, sin humedad
============================================================ */
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
BLECharacteristic *tx = nullptr;
class AlConectar : public BLEServerCallbacks {
void onConnect(BLEServer *s) { conectado = true; }
void onDisconnect(BLEServer *s) { conectado = false; BLEDevice::startAdvertising(); }
};
void avisar(String texto) { // manda un mensaje corto a la pagina
if (!conectado || !tx) return;
tx->setValue((uint8_t *)texto.c_str(), texto.length());
tx->notify();
}
void enviarEstado() { // lo que le contamos a la pagina
if (sensor == ES_BMP280) {
avisar("E:BMP");
} else {
avisar(String(temperatura, 1) + "," + String((int)round(humedad)) + "," +
String((int)round(presion)) + "," + String(riesgo) + (simulando ? ",S" : ""));
}
}
void arrancarBluetooth() {
BLEDevice::init("Heltec-Halcon");
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();
}
/* ============================================================
PARTE 5 · setup() y loop()
setup() se ejecuta UNA vez al encender. loop() se repite sin parar.
============================================================ */
unsigned long ultimaLectura = 0; // cuando fue la ultima medida (en milisegundos)
void setup() {
Serial.begin(115200); // el Monitor Serie, a 115200 baudios
arrancarPantalla();
I2CSensor.begin(SENSOR_SDA, SENSOR_SCL); // ponemos en marcha el bus del sensor
escanearBus(); // (solo para el Monitor Serie)
buscarSensor();
arrancarBluetooth();
medir();
pintarPantalla();
}
bool conectadoAntes = false;
void loop() {
if (millis() - ultimaLectura >= 2000) { // cada 2 segundos...
ultimaLectura = millis();
medir(); // ...medimos (o simulamos),
escribirSerie(); // (solo para el Monitor Serie)
pintarPantalla(); // (solo para la pantalla)
enviarEstado(); // ...y se lo contamos a la pagina
}
if (conectado != conectadoAntes) { // (solo para la pantalla) ¿cambio la conexion?
conectadoAntes = conectado;
pintarPantalla();
}
delay(5);
}
Lee y predice
- Con los umbrales de 30 y 30, ¿qué semáforo sale con 32 °C y 45 % de humedad? ¿Y con 25 °C y 20 %?
- Si cambias
UMBRAL_SECOa 50, ¿el semáforo será más o menos fácil de poner en rojo? - ¿Por qué el programa pregunta por el carnet 0xD0 en vez de intentar leer la humedad directamente?
Soluciones: 1) Con 32 °C y 45 %: medio (solo hace calor). Con 25 °C y 20 %: medio (solo está seco). 2) Más fácil: con 50 casi siempre contará como «aire seco». 3) Porque un BMP280 no tiene humedad y daría datos falsos sin avisar: así sabemos qué chip es antes de fiarnos.

La página web
Guarda este código como chisme-36-halcon.html y ábrelo con Chrome en el móvil o en el ordenador.
La PARTE 1 es lo importante: lee la línea que manda la placa y la convierte en datos y semáforo. La PARTE 2 es el Bluetooth, casi igual que en el chisme 32. Si la línea acaba en S, la página enseña una franja amarilla: los datos son simulados.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chisme 36 · HALCÓN y el riesgo de incendio</title>
<style>
/* Los colores, en un solo sitio: cámbialos y cambia toda la página */
:root { --fondo:#eef2ef; --texto:#10201c; --suave:#4d625b; --verde:#1f5b4a;
--bajo:#2e9e5b; --medio:#f2b01e; --alto:#d63a2f; }
@media (prefers-color-scheme: dark) {
:root { --fondo:#0f1a17; --texto:#e8f1ed; --suave:#9db3aa; --verde:#5fc4a2; }
}
body { margin:0; padding:1.5rem 1rem; background:var(--fondo); color:var(--texto);
font:18px/1.45 system-ui, sans-serif; text-align:center; }
h1 { font-size:1.5rem; margin:0 0 .3rem; }
p { color:var(--suave); margin:.3rem 0; }
button { font:inherit; font-weight:700; padding:.8rem 1.4rem; margin:.6rem .3rem;
border:2px solid var(--verde); background:var(--verde); color:var(--fondo);
border-radius:.9rem; cursor:pointer; }
/* El aviso del 112: siempre visible, arriba del todo */
#aviso112 { background:var(--alto); color:#fff; font-weight:700; padding:.8rem 1rem;
border-radius:.9rem; max-width:34rem; margin:0 auto 1.2rem; }
#aviso112 span { font-size:1.6rem; display:block; }
/* Los tres datos */
#datos { display:flex; justify-content:center; gap:1rem; flex-wrap:wrap; margin:1rem 0; }
.dato { min-width:7rem; }
.dato b { display:block; font-size:2.2rem; color:var(--texto); }
.dato small { color:var(--suave); }
/* El semáforo: un círculo de color Y una palabra (no dependemos solo del color) */
#semaforo { width:8rem; height:8rem; border-radius:50%; margin:1rem auto .4rem;
background:var(--suave); display:flex; align-items:center; justify-content:center;
font-weight:700; font-size:1.3rem; color:#fff; transition:background .4s; }
#semaforo.r0 { background:var(--bajo); }
#semaforo.r1 { background:var(--medio); color:#10201c; }
#semaforo.r2 { background:var(--alto); }
/* La franja amarilla: solo se ve cuando los datos son SIMULADOS */
#simulado { display:none; background:#ffd23f; color:#10201c; font-weight:700; padding:.6rem 1rem;
border-radius:.9rem; max-width:34rem; margin:.8rem auto; }
#simulado.visible { display:block; }
#historia { min-height:2.5rem; }
#historial { text-align:left; max-width:30rem; margin:1rem auto; list-style:none; padding:0; }
#historial li { padding:.2rem 0; border-bottom:1px solid var(--suave); }
</style>
</head>
<body>
<div id="aviso112">
<span>Si ves fuego o humo en el monte: llama al 112</span>
Esto es un modelo educativo. Nadie de clase apaga nada.
</div>
<h1>HALCÓN y el riesgo de incendio</h1>
<p id="estado">Sin conectar.</p>
<button id="botonConectar">Conectar la placa</button>
<div id="simulado" aria-live="polite">DATOS SIMULADOS: la placa no encuentra el sensor. No son medidas reales.</div>
<div id="datos" aria-live="polite">
<div class="dato"><b id="temperatura">--</b><small>temperatura (°C)</small></div>
<div class="dato"><b id="humedad">--</b><small>humedad (%)</small></div>
<div class="dato"><b id="presion">--</b><small>presión (hPa)</small></div>
</div>
<div id="semaforo" aria-live="polite">--</div>
<p id="historia"></p>
<p id="extremos"></p>
<ul id="historial"></ul>
<script>
/* ============================================================
PARTE 1 · LO IMPORTANTE (léela)
La placa nos manda cada 2 segundos una línea como: 27.3,41,1013,1
(temperatura, humedad, presión y riesgo). Aquí la leemos y la enseñamos.
Si la placa no encuentra el sensor, se inventa los valores y añade una S
al final (27.3,41,1013,1,S): entonces enseñamos la franja amarilla.
============================================================ */
const NOMBRES = ["Riesgo BAJO", "Riesgo MEDIO", "Riesgo ALTO"];
const HISTORIAS = [
"HALCÓN descansa en la cueva.",
"HALCÓN se prepara en la cueva (en la historia, claro).",
"En la historia, HALCÓN saldría ahora. En la vida real: 112 y los profesionales."
];
let temperaturaMaxima = null; // la temperatura más alta que hemos visto
let humedadMinima = null; // la humedad más baja que hemos visto
let eraSimulado = null; // ¿los datos anteriores eran simulados?
// Esta función se ejecuta CADA VEZ que la placa nos avisa.
function alRecibirAviso(texto) {
if (texto === "E:BMP") { // el chip es un BMP280
mostrarProblema("Tu sensor es un BMP280: mide temperatura y presión, pero no humedad.");
return;
}
const partes = texto.split(","); // "27.3,41,1013,1" -> 4 trozos (o 5 con la S)
if (partes.length !== 4 && partes.length !== 5) return; // algo raro: lo ignoramos
const temperatura = Number(partes[0]);
const humedad = Number(partes[1]);
const presion = Number(partes[2]);
const riesgo = Number(partes[3]); // 0, 1 o 2
const simulado = partes[4] === "S"; // ¿son datos inventados?
if (riesgo < 0 || riesgo > 2 || isNaN(temperatura)) return;
mostrarDatos(temperatura, humedad, presion, riesgo, simulado);
}
function mostrarDatos(temperatura, humedad, presion, riesgo, simulado) {
document.getElementById("simulado").classList.toggle("visible", simulado); // la franja amarilla
if (simulado !== eraSimulado) { // si cambiamos de simulado a real (o al revés)...
temperaturaMaxima = null; // ...empezamos los extremos de cero
humedadMinima = null;
eraSimulado = simulado;
}
document.getElementById("estado").textContent =
simulado ? "Simulando: sin sensor, cada 2 segundos." : "Midiendo cada 2 segundos.";
document.getElementById("temperatura").textContent = temperatura.toFixed(1);
document.getElementById("humedad").textContent = humedad;
document.getElementById("presion").textContent = presion;
const semaforo = document.getElementById("semaforo");
semaforo.className = "r" + riesgo; // cambia el color
semaforo.textContent = NOMBRES[riesgo]; // y escribe la palabra
document.getElementById("historia").textContent = HISTORIAS[riesgo];
// Los extremos de esta sesión (los más calurosos y secos)
if (temperaturaMaxima === null || temperatura > temperaturaMaxima) temperaturaMaxima = temperatura;
if (humedadMinima === null || humedad < humedadMinima) humedadMinima = humedad;
document.getElementById("extremos").textContent =
"Máxima: " + temperaturaMaxima.toFixed(1) + " °C · Humedad mínima: " + humedadMinima + " %";
// Apuntamos la lectura en el historial (las 8 últimas)
const linea = document.createElement("li");
linea.textContent = new Date().toLocaleTimeString("es-ES") + " · " + temperatura.toFixed(1) +
" °C · " + humedad + " % · " + NOMBRES[riesgo] + (simulado ? " (simulado)" : "");
const lista = document.getElementById("historial");
lista.insertBefore(linea, lista.firstChild);
while (lista.children.length > 8) lista.removeChild(lista.lastChild);
}
function mostrarProblema(mensaje) {
document.getElementById("simulado").classList.remove("visible");
document.getElementById("estado").textContent = mensaje;
document.getElementById("semaforo").className = "";
document.getElementById("semaforo").textContent = "--";
}
/* ============================================================
PARTE 2 · EL BLUETOOTH (casi igual que en el chisme 32; no hace falta tocarlo)
============================================================ */
const SERVICIO = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UUID_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // por aquí RECIBIMOS los avisos
let dispositivo;
const estado = document.getElementById("estado");
const espera = ms => new Promise(r => setTimeout(r, ms));
async function abrirCanales() {
for (let intento = 1; intento <= 3; intento++) { // a veces falla la primera vez: reintentamos
try {
const servidor = await dispositivo.gatt.connect();
await espera(600);
const servicio = await servidor.getPrimaryService(SERVICIO);
const canalAvisos = await servicio.getCharacteristic(UUID_TX);
canalAvisos.addEventListener("characteristicvaluechanged", e => alRecibirAviso(new TextDecoder().decode(e.target.value)));
await canalAvisos.startNotifications();
estado.textContent = "Conectada a " + dispositivo.name + ". Esperando datos...";
return;
} catch (e) {
if (intento === 3) throw e;
await espera(800);
}
}
}
document.getElementById("botonConectar").onclick = async function () {
if (!navigator.bluetooth) { estado.textContent = "Este navegador no permite Bluetooth. Usa Chrome."; return; }
try {
estado.textContent = "Buscando la placa...";
dispositivo = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [SERVICIO] });
dispositivo.addEventListener("gattserverdisconnected", () => {
estado.textContent = "Conexión perdida. Pulsa «Conectar la placa».";
});
await abrirCanales();
} catch (e) {
estado.textContent = "Error: " + e.message + " (elige «Heltec-Halcon» en la lista).";
}
};
</script>
</body>
</html>
Lee y predice
- Si la placa manda
35.0,18,1008,2, ¿qué aparece en los tres números y en el semáforo? - ¿Por qué el semáforo lleva escrita la palabra «Riesgo ALTO» además de ser rojo?
- Si la placa manda
hola, ¿qué hace la página?
Soluciones: 1) 35.0 °C, 18 %, 1008 hPa y un semáforo rojo con «Riesgo ALTO». 2) Porque no todo el mundo distingue los colores (y quien usa un lector de pantalla no los ve): la información debe estar también en palabras. 3) Nada: no tiene 4 trozos y lo ignora.
Cómo probar el sensor BME280
- Enciende la placa. Si el sensor está bien conectado, la pantalla mostrará temperatura, humedad y presión. Si no lo encuentra, dirá «SIMULADO: sin sensor» y verás valores inventados que cambian: sirven para probar la página sin sensor.
- Abre
chisme-36-halcon.htmlen Chrome y pulsa «Conectar la placa». Elige Heltec-Halcon. - A los 2 segundos aparecerán los datos y el semáforo.
- Compara la pantalla de la placa con la de la página: deben decir lo mismo.
Tres pruebas seguras
- Sopla sobre el sensor. Tu aliento lleva mucha agua: verás subir la humedad en pocos segundos y bajar después.
- Pon la mano cerca (sin tocar). La temperatura subirá un poco. Nada de secadores ni fuentes de calor.
- Cambia los umbrales. Para ver los tres colores sin calentar nada, baja
UMBRAL_CALORa 20 o subeUMBRAL_SECOa 70, sube el programa y mira el semáforo.
La prueba del chip
Con la placa desenchufada, suelta el cable SDA y vuelve a enchufarla. La pantalla debe decir «SIMULADO: sin sensor», la página debe enseñar la franja amarilla y el Monitor Serie debe avisar de que no encuentra nada. Vuelve a conectar el cable y, a los pocos segundos, empezará a medir de verdad.
Si tu sensor es un BMP280, el mensaje será otro: «Tu sensor es un BMP280: NO mide la humedad». No pasa nada: has descubierto qué chip tienes de verdad.
Si algo no va
- Siempre dice «SIMULADO: sin sensor»: revisa SDA (13) y SCL (23). Pueden estar cambiados. Abre el Monitor Serie (115200 baudios): la placa lista las direcciones que responden y debe salir 0x76 o 0x77. Si no sale ninguna, el fallo está en los cables.
- Los valores saltan o son raros: un cable flojo. Aprétalo o cámbialo de agujero.
- No aparece la placa en la lista: reinicia la placa y pulsa de nuevo «Conectar la placa».
Qué estamos midiendo realmente
La pantalla dice «27,3 °C y 41 %». Pero medir no significa medir bien. Estas son las trampas.
El sensor tiene incertidumbre
Ningún sensor es exacto. Según sus especificaciones, el BME280 tiene un margen de error de alrededor de ±1 °C en temperatura y de ±3 puntos de humedad relativa. Una lectura de 41 % puede ser 38 % o 44 %.
Lo importante no es el valor de una lectura, sino ver cómo cambia a lo largo del tiempo.
¿Dónde está el sensor?
El sensor mide el aire que lo rodea. Si está pegado a la placa, medirá el calor de la placa. Si le da el sol, medirá el sol sobre él, no el aire. Por eso los servicios meteorológicos ponen sus sensores a la sombra y ventilados.
Humedad relativa: depende de la temperatura
La humedad que mide es relativa: cuánta agua lleva el aire comparada con la máxima que podría llevar. Si el aire se calienta, puede llevar más agua y la humedad relativa baja aunque no haya perdido nada.
Por eso temperatura y humedad van siempre juntas en el semáforo.
La simulación respeta esa regla: cuando sube la temperatura inventada, baja la humedad inventada, para que los datos falsos se parezcan a los reales. Pero siguen siendo falsos: sirven para comprobar que el programa funciona, nunca para sacar conclusiones.
La presión no entra en el semáforo
La presión baja al subir de altura y cambia con el tiempo atmosférico. La medimos porque el sensor la da, pero no la usamos para el riesgo.
El semáforo es un modelo, no la realidad
Tiene solo dos preguntas. Un cálculo real de riesgo de incendio tiene en cuenta mucho más: el viento, lo seca que esté la vegetación, el terreno, los días sin llover. Nuestro semáforo no los conoce.
Y los umbrales (30 °C y 30 %) son inventados. Si los bajas, el semáforo se pondrá en rojo más a menudo: más falsas alarmas. Si los subes, se avisará menos veces: se te pueden escapar situaciones de riesgo. No existe el umbral perfecto: es un compromiso.
Riesgo no es incendio
Un riesgo ALTO significa que las condiciones son malas. No significa que haya fuego ni que vaya a haberlo. Y un riesgo BAJO no garantiza que no pase nada.
Por eso este sensor no detecta incendios ni sustituye a nada profesional. Si ves humo o fuego, 112.
Qué sale del dispositivo
Nada. Los datos van de la placa al móvil por Bluetooth y solo viven en la memoria de la página. Al cerrarla, desaparecen.
Para el aula
Antes de empezar, que quede claro: si alguien ve fuego o humo en el monte, llama al 112. En clase se miden temperatura y humedad. Nadie enciende fuego, nadie se acerca a uno y nadie intenta apagar nada. El semáforo es un modelo para aprender, no una alarma.
Este chisme sirve para algo muy científico: desconfiar de una medida, entender de qué depende y decidir qué conclusiones se pueden sacar.
Preguntas para empezar
- ¿Qué tres variables medimos? ¿En qué unidades?
- ¿Qué error puede tener cada medida?
- ¿Por qué un semáforo con dos preguntas no es un detector de incendios?
- ¿Qué datos faltarían para estimar mejor el riesgo?
Cinco actividades
- ¿Qué chip tengo? Cada grupo apunta lo que pone en la etiqueta del módulo y lo que dice el programa. ¿Coinciden siempre? ¿Qué aprendemos de fiarnos solo de la etiqueta?
- Dónde ponerlo. Mide el mismo aula en tres sitios: junto a la ventana, junto a la placa y a la sombra, y cerca de la puerta. Apunta temperatura y humedad a los 2 minutos. ¿Cuál es la mejor posición? ¿Por qué?
- Dos sensores, dos valores. Si hay dos módulos, ponlos uno junto al otro. ¿Dan lo mismo? ¿Cuánto difieren? Esa diferencia es una pista de la incertidumbre.
- Un día de datos. Apunta temperatura y humedad cada hora durante la jornada. ¿Qué pasa con la humedad cuando sube la temperatura? ¿Es casualidad o hay una razón?
- Los umbrales. Cada grupo propone sus propios valores de calor y sequedad y los defiende. Después se discute: ¿prefieres más falsas alarmas o que se escape algún caso de riesgo?
Para pensar
Si el semáforo se pone en rojo, ¿qué debería hacer una persona? ¿Y una máquina? ¿Por qué la decisión de actuar en un fuego real debe tomarla siempre un profesional?
Retos
- Haz que funcione. Cablea el sensor, sube el código y consigue ver tu primera temperatura.
- Modifica algo. Cambia los umbrales (
UMBRAL_CALORyUMBRAL_SECO) y mira cómo cambia el semáforo. - Añade el viento. No tenemos sensor de viento, pero sí puedes meterlo a mano: añade a la página un control de tres niveles (sin viento, moderado, fuerte) y haz que sume un punto al riesgo. ¿Cómo cambia la historia?
- Guarda los datos. Añade un botón que descargue las lecturas en un archivo CSV.
- Representa los datos. Dibuja una gráfica con temperatura y humedad a lo largo del tiempo. ¿Se ve la relación entre ambas?
- Automatiza el proceso. Que la página guarde solo una lectura cada minuto durante una hora, sin pulsar nada.
- Haz que otro dispositivo participe. Dos placas con dos sensores en distintos sitios del instituto. Es el paso previo a conectarlas con LoRa en el próximo chisme.
- Conviértelo en investigación. Elige cinco puntos del centro (patio, aula, pasillo…) y mide en cada uno a la misma hora. ¿Dónde hace más calor? ¿Dónde está el aire más seco? ¿Por qué?
Qué hemos aprendido
Hemos construido un sensor BME280 con ESP32 que mide temperatura, humedad y presión y calcula un semáforo de riesgo educativo.
Hemos aprendido que dos chips casi iguales pueden ser distintos y que hay que preguntarle al chip, no fiarse de la etiqueta. Que un sensor mide su entorno inmediato, con un error, y que su posición importa. Y que un semáforo de dos preguntas es un modelo, no la realidad.
También hemos aprendido lo más importante: riesgo no es incendio. Y que, si hay fuego, el teléfono que se marca es el 112.
En Sierra Bermeja, el Lince colgó el sensor a la sombra de un pino, a un par de metros de la cueva. El semáforo marcaba verde.
—Riesgo bajo —anunció orgulloso—. Mis HALCÓN pueden descansar.
—Tu sensor mide el aire de este pino —dijo la Gata—, a la sombra y con brisa. A tres kilómetros, en una ladera al sol, puede ser otra historia.
—Entonces necesitaré más sensores.
—Y unirlos entre sí.
El Lince miró la placa Heltec. Y la placa Heltec, que tiene LoRa, le devolvió una mirada llena de ideas.
Para seguir investigando
- Todos los chismes de la serie están en la colección de Chismes del Lince.
- Otro chisme que mide el ambiente con la Heltec: el termómetro Bluetooth con Heltec y DHT22 (chisme 26). ¿Qué diferencias ves con el BME280?
- Para recordar cómo se conecta la placa por Bluetooth, empieza por El letrero del Lince (chisme 25).
- Las especificaciones del sensor están en la página del BME280 de Bosch Sensortec (en inglés).
- Para ver otras placas y documentación del fabricante, visita la página oficial de Heltec Automation.
Si ves fuego o humo en el monte, llama al 112. Este es un prototipo educativo: no es un detector de incendios ni un dispositivo certificado.
Etiqueta:Bluetooth, bme280, esp32, HALCÓN, Heltec LoRa 32, Humedad, I2C, sensores, temperatura, Web Bluetooth



