
Chismes del Lince 35: BUITRE vigila
Sobre Sierra Bermeja volaban los buitres leonados, lentos y tranquilos, como si el cielo fuera suyo. Entre ellos, a cien metros del suelo, algo mucho más pequeño y mucho más metálico los seguía.
Abajo, en el sendero, los Centuriones ensayaban una misión. El Lince de la Noche iba el primero, con su casco nuevo y las gafas que le proyectaban datos delante de los ojos.
—¡Un foso a la izquierda! —anunció, señalando con el dedo—. ¡Y una roca grande a tres metros! ¡Y detrás de la roca hay algo que parece una cabra, pero con un 87 % de probabilidad de ser una cabra!
—Lince —dijo Justicia Ciega, que caminaba justo detrás con paso seguro—. Ninguna de esas cosas la estoy viendo.
—Claro, están en mis gafas.
—Exacto.
El Lince se paró. Justicia Ciega no ve. Con la tecnología que ella lleva, oye el mundo mejor que casi nadie, pero lo que sale en unas gafas no le llega por ningún sitio.
—Te lo voy diciendo yo —propuso el Lince, orgulloso—. Todo. Sin parar.
—Eso es agotador —murmuró la Gata Montesa, sentada en un tronco con la tableta—. Para ti y para ella. Cuando algo es importante, no se oye entre tanto ruido.
—Entonces, ¿qué hago?
De repente, una voz tranquila y seca sonó por el pinganillo de todos.
—Entorno despejado.
El Lince levantó la vista. Arriba, entre los buitres, el dron daba otra vuelta lenta.
—BUITRE —dijo la Gata—. El ojo de los Centuriones. Solo habla cuando tiene algo útil que decir.
—Y no se repite —añadió Justicia Ciega—. Eso me gusta.
El Lince se rascó la oreja bajo el casco.
—Vale. Tiene los datos. Mis gafas los enseñan. Pero ella no los ve…
—No necesito que me guíes —dijo Justicia Ciega—. Necesito oír lo mismo que tú ves.
La Gata sonrió. El Lince, por una vez, no dijo nada. Ya estaba buscando la placa Heltec en la mochila.
—Una pantalla que escribe lo que dice BUITRE —murmuró—. Y un móvil que lo diga en voz alta. La misma información por dos caminos.
—Eso —dijo la Gata— es lo más listo que has dicho en toda la mañana. BUITRE significa Bot de Ubicación e Inteligencia para Tareas de Reconocimiento del Entorno.
Introducción
En este chisme damos voz a BUITRE, el dron de los Centuriones.
Vamos a conseguir accesibilidad con ESP32. La placa Heltec hace de BUITRE: cada unos 15 segundos elige una observación corta, como «Entorno despejado» o «Obstáculo a tres metros». La escribe en su pantalla y la manda por Bluetooth al móvil. El móvil la dice en voz alta.
Así, la misma información llega por la vista y por el oído. Quien ve la pantalla la lee. Quien no la ve, la escucha.
Esta vez no hay que conectar ningún cable a la placa. BUITRE todavía no mide nada: sus observaciones son una simulación. Pero la forma de entregarlas es la de verdad.
Por qué este chisme
Mucha tecnología solo se entiende si miras una pantalla. Para una persona ciega o con poca visión, esa información simplemente no existe.
La pregunta que se hace el Lince es sencilla: si yo tuviera un alumno que no ve la pantalla, ¿cómo sabría lo que sale en ella?
La respuesta es un principio de diseño: la misma información por más de un canal. Lo que se ve, también se oye. Y lo que se oye, también se puede repetir.
Es lo que hacen los lectores de pantalla (TalkBack en Android, VoiceOver en iPhone), que ya traen los móviles. Este chisme no los sustituye: sirve para entender cómo funciona la idea y para que tus propias creaciones no dejen a nadie fuera. Así conseguimos accesibilidad con ESP32.
Además, añade algo nuevo a la serie: una voz con personalidad.
Cómo funciona BUITRE
—¿Y cómo habla BUITRE? —preguntó el Lince—. ¿Con frases largas, como yo?
—Con frases cortas —dijo la Gata—. Es tranquilo, preciso y seco. Nunca habla por hablar. Si no hay nada útil que decir, se calla.
Ese es el carácter de BUITRE, y se programa con tres reglas sencillas:
- Un mensaje cada 15 segundos, más o menos. La placa espera entre 13 y 17 segundos al azar. Así no suena a metrónomo.
- No repite. Recuerda las tres últimas frases y no las vuelve a elegir.
- A veces se calla. Una de cada seis veces no dice nada. Un vigilante que habla sin parar acaba siendo ruido.
Las frases son de dos tipos. La información es tranquila: «Entorno despejado», «Terreno irregular». El aviso es algo más serio: «Obstáculo a tres metros». Sale una de cada tres veces.
Qué viaja por Bluetooth
La placa no manda la frase entera, sino su número. Es más corto y la página tiene su propia lista con las frases bien escritas, con tildes, para que la voz las pronuncie bien.
| Quién manda | Mensaje | Significa |
|---|---|---|
| Placa → página | I3 | Información número 3 |
| Placa → página | A1 | Aviso número 1 |
| Página → placa | G | Despertar a BUITRE |
| Página → placa | S | Silencio: BUITRE deja de vigilar |
| Página → placa | N | Una observación ahora mismo (para probar) |
Los dos canales
La placa escribe la frase en su pantalla (canal de la vista). La página la dice con la voz del móvil (canal del oído). Un aviso interrumpe lo que se esté diciendo y suena algo más rápido. Y hay un botón Repite, porque quien escucha no puede volver a mirar la pantalla.
Qué necesitamos
- Una placa Heltec LoRa 32 (o TTGO con pantalla OLED), con su cable USB.
- Un móvil con Chrome (Web Bluetooth y voz). Mejor con auriculares para la prueba con los ojos cerrados.
- Arduino IDE con las librerías Adafruit SSD1306 y Adafruit GFX.
- Dos archivos:
chisme-35-buitre.inopara la placa ychisme-35-buitre.htmlpara la página.
No hace falta botón, resistencia ni protoboard. No hay imágenes que colocar junto al HTML.
Construcción
Otra vez, no hay que construir nada: es software.
- Conecta la placa al ordenador con el cable USB.
- Abre el Arduino IDE y elige la placa «ESP32 Dev Module».
- Pega el código de la placa (justo debajo) y súbelo.
- Si dice «Sketch too big»: Herramientas → Partition Scheme → «Huge APP (3MB No OTA/1MB SPIFFS)».
Cuando termine, la pantalla dirá «BUITRE dormido».

El código de la placa
Copia todo el código en un sketch nuevo del Arduino IDE. Tiene cuatro partes, pero solo la PARTE 1 es el chisme. La PARTE 2 (la pantalla) es el canal de la vista: la voz funciona aunque la pantalla no se vea.
/*
Chisme 35 · BUITRE vigila
BUITRE es el dron de los Centuriones. Cada unos 15 segundos hace una
observacion corta. La placa la escribe en su pantalla y la manda por
Bluetooth al movil, que la dice en voz alta (para quien no puede ver la pantalla).
OJO: BUITRE todavia NO mide nada. Las observaciones se eligen al azar de una
lista. Es una simulacion para aprender a dar la misma informacion por dos
canales: la vista (pantalla) y el oido (voz).
Este programa tiene 4 partes. Solo la PARTE 1 es el chisme:
PARTE 1 · BUITRE <- LEE ESTA. Es lo unico importante.
PARTE 2 · LA PANTALLA <- Es el canal de la VISTA. Escribe lo que dice BUITRE.
PARTE 3 · EL BLUETOOTH <- Como en el chisme 34. No hace falta tocarla.
PARTE 4 · setup() y loop()
Esta vez NO hay que conectar nada a la placa: solo la placa y el movil.
Librerias: 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 <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
/* ============================================================
PARTE 1 · BUITRE (lo importante)
BUITRE tiene dos listas de frases: INFORMACION (tranquila) y AVISO (algo mas serio).
La pantalla no tiene tildes, asi que aqui van sin ellas.
OJO: el ORDEN de estas listas debe ser el mismo que en la pagina web,
porque a la pagina solo le mandamos el numero de la frase.
============================================================ */
const char* INFORMACION[] = {
"Entorno despejado", // numero 0
"Ruta despejada", // numero 1
"Sin incidencias", // numero 2
"Terreno irregular", // numero 3
"Actividad detectada al norte", // numero 4
"Objeto a veinte metros" // numero 5
};
const char* AVISO[] = {
"Movimiento a la derecha", // numero 0
"Obstaculo a tres metros", // numero 1
"Movimiento a la izquierda", // numero 2
"Foso a la izquierda", // numero 3
"Escalon delante", // numero 4
"Animal a diez metros" // numero 5
};
const int HAY_INFORMACION = sizeof(INFORMACION) / sizeof(INFORMACION[0]);
const int HAY_AVISOS = sizeof(AVISO) / sizeof(AVISO[0]);
bool vigilando = false; // ¿esta BUITRE vigilando?
unsigned long proximaObservacion = 0; // cuando toca la siguiente (en milisegundos)
int recientes[3] = {-1, -1, -1}; // las 3 ultimas frases, para no repetirlas
String textoPantalla = "BUITRE dormido"; // lo que se ve en la pantalla
volatile char ordenPendiente = 0; // orden llegada del movil: 'G', 'S' o 'N'
void programarSiguiente() {
proximaObservacion = millis() + 13000 + esp_random() % 4000; // entre 13 y 17 segundos
}
void observar(bool obligatoria) {
// A veces BUITRE no tiene nada que decir y se calla (1 de cada 6 veces)
if (!obligatoria && esp_random() % 6 == 0) return;
bool esAviso = (esp_random() % 3 == 0); // 1 de cada 3 es un AVISO
int numero = 0;
int codigo = 0;
for (int intento = 0; intento < 10; intento++) { // buscamos una que no sea de las ultimas 3
int total = esAviso ? HAY_AVISOS : HAY_INFORMACION;
numero = esp_random() % total;
codigo = (esAviso ? 100 : 0) + numero;
if (codigo != recientes[0] && codigo != recientes[1] && codigo != recientes[2]) break;
}
recientes[2] = recientes[1];
recientes[1] = recientes[0];
recientes[0] = codigo;
textoPantalla = esAviso ? AVISO[numero] : INFORMACION[numero];
pintarPantalla(); // canal de la VISTA
avisar(String(esAviso ? "A" : "I") + String(numero)); // canal del OIDO (lo dice el movil)
}
void atenderOrden(char orden) {
if (orden == 'G') { // G = despertar a BUITRE
vigilando = true;
textoPantalla = "BUITRE vigila";
proximaObservacion = millis() + 3000; // la primera observacion, a los 3 segundos
pintarPantalla();
}
if (orden == 'S') { // S = silencio: BUITRE deja de vigilar
vigilando = false;
textoPantalla = "BUITRE dormido";
pintarPantalla();
}
if (orden == 'N' && vigilando) { // N = una observacion ahora mismo (para probar)
observar(true);
programarSiguiente();
}
}
void vigilar() { // se llama sin parar desde loop()
if (vigilando && millis() >= proximaObservacion) {
observar(false);
programarSiguiente();
}
}
/* ============================================================
PARTE 2 · LA PANTALLA (el canal de la VISTA)
Escribe la frase partida en lineas de 10 letras, con letra grande.
La voz funciona aunque la pantalla no se vea: la pantalla es para los que SI ven.
============================================================ */
#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 escribirPartido(String texto) { // parte el texto en lineas de maximo 10 letras
texto += " ";
String linea = "";
String palabra = "";
int y = 9;
for (unsigned int i = 0; i < texto.length(); i++) {
char c = texto[i];
if (c != ' ') { palabra += c; continue; } // seguimos formando la palabra
if (linea.length() > 0 && linea.length() + 1 + palabra.length() > 10) {
display.setCursor(0, y); display.print(linea); // la linea esta llena: la escribimos
y += 16;
linea = palabra;
} else {
linea = (linea.length() > 0) ? linea + " " + palabra : palabra;
}
palabra = "";
}
display.setCursor(0, y); display.print(linea);
}
void pintarPantalla() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print(conectado ? "BUITRE - conectado" : "BUITRE - esperando");
display.setTextSize(2);
escribirPartido(textoPantalla);
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 3 · EL BLUETOOTH (como en el chisme 34)
La placa se anuncia como "Heltec-Buitre". Tiene dos "buzones":
RX: por aqui LLEGAN las ordenes de la pagina:
"G" = despertar a BUITRE "S" = silencio "N" = observacion ya
TX: por aqui la placa AVISA a la pagina:
"I3" = informacion numero 3 "A1" = aviso numero 1
============================================================ */
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_RX "6e400002-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(); }
};
class AlEscribir : public BLECharacteristicCallbacks { // llega algo desde la pagina
void onWrite(BLECharacteristic *c) {
auto v = c->getValue();
if (v.length() == 0) return;
ordenPendiente = v[0]; // la guardamos; loop() la atendera enseguida
}
};
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 arrancarBluetooth() {
BLEDevice::init("Heltec-Buitre");
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());
BLECharacteristic *rx = svc->createCharacteristic(UUID_RX,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
rx->setCallbacks(new AlEscribir());
svc->start();
BLEAdvertising *pub = BLEDevice::getAdvertising();
pub->addServiceUUID(SERVICE_UUID);
pub->setScanResponse(true);
BLEDevice::startAdvertising();
}
/* ============================================================
PARTE 4 · setup() y loop()
setup() se ejecuta UNA vez al encender. loop() se repite sin parar.
============================================================ */
void setup() {
arrancarPantalla();
arrancarBluetooth();
pintarPantalla();
}
bool conectadoAntes = false;
void loop() {
if (ordenPendiente != 0) { // ¿ha llegado una orden del movil?
char orden = ordenPendiente;
ordenPendiente = 0;
atenderOrden(orden);
}
vigilar(); // ¿toca una observacion?
if (conectado != conectadoAntes) { // ¿cambio la conexion?
conectadoAntes = conectado;
if (!conectado) { vigilando = false; textoPantalla = "BUITRE dormido"; }
pintarPantalla();
}
delay(5);
}
Lee y predice
- Si cambias
13000 + esp_random() % 4000por5000 + esp_random() % 2000, ¿cada cuántos segundos hablará BUITRE como mínimo y como máximo? - Si cambias
esp_random() % 6 == 0poresp_random() % 2 == 0, ¿qué le pasará a BUITRE? - ¿Por qué la placa manda
I3y no la frase completa?
Soluciones: 1) Entre 5 y 7 segundos. 2) Se callará la mitad de las veces: parecerá mucho más tímido. 3) Es más corto, y así la página puede tener la frase con tildes, que la pantalla de la placa no sabe escribir.
La página web con voz
Guarda este código como chisme-35-buitre.html y ábrelo con Chrome en el móvil o en el ordenador.
La PARTE 1 es lo importante: la lista de frases y la función que las dice. La PARTE 2 es el Bluetooth, casi igual que en el chisme 34.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chisme 35 · BUITRE vigila</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; --aviso:#ffd23f; }
@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.2rem; margin:.4rem .3rem;
border:2px solid var(--verde); background:var(--verde); color:var(--fondo);
border-radius:.9rem; cursor:pointer; touch-action:manipulation; }
button:disabled { opacity:.45; cursor:not-allowed; }
/* Texto grande para quien ve poco; el color amarillo marca un AVISO */
#mensaje { font-size:2rem; font-weight:700; color:var(--texto); min-height:3.5rem;
margin:1.2rem auto; padding:.6rem; max-width:30rem; border-radius:1rem; }
#mensaje.aviso { background:var(--aviso); color:#10201c; }
#repetir { font-size:1.4rem; padding:1.2rem 2rem; }
#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>
<h1>BUITRE vigila</h1>
<p>El dron de los Centuriones: lo que dice la placa, también por la voz.</p>
<p id="estado">Sin conectar.</p>
<button id="botonConectar">Conectar la placa</button>
<button id="botonDespertar" disabled>Despertar a BUITRE</button>
<button id="botonSilencio" disabled>Silencio</button>
<!-- aria-live: los lectores de pantalla (TalkBack, VoiceOver) anuncian solos cada cambio de este texto -->
<div id="mensaje" aria-live="polite">BUITRE dormido.</div>
<button id="repetir" disabled>Repite</button>
<button id="botonAhora" disabled>Observación ya</button>
<button id="botonVoz">Voz: sí</button>
<ul id="historial"></ul>
<script>
/* ============================================================
PARTE 1 · LO IMPORTANTE (léela)
La placa solo nos manda el NÚMERO de la frase ("I3" o "A1").
Aquí está la lista completa, con tildes, para que la voz las pronuncie bien.
OJO: el orden debe ser el mismo que en la placa.
============================================================ */
const INFORMACION = [
"Entorno despejado.", // número 0
"Ruta despejada.", // número 1
"Sin incidencias.", // número 2
"Terreno irregular.", // número 3
"Actividad detectada al norte.", // número 4
"Objeto a veinte metros." // número 5
];
const AVISO = [
"Movimiento detectado a la derecha.", // número 0
"Obstáculo a tres metros.", // número 1
"Movimiento detectado a la izquierda.", // número 2
"Foso a la izquierda.", // número 3
"Escalón delante.", // número 4
"Animal a diez metros." // número 5
];
let ultimoTexto = ""; // la última frase, para poder repetirla
let vozActiva = true; // ¿hablamos o no?
// La placa nos AVISA: "I3" = información 3, "A1" = aviso 1.
function alRecibirAviso(codigo) {
if (codigo[0] !== "I" && codigo[0] !== "A") return; // solo entendemos "I" y "A"
const esAviso = codigo[0] === "A";
const numero = Number(codigo.slice(1));
const texto = esAviso ? AVISO[numero] : INFORMACION[numero];
if (!texto) return; // un número que no está en la lista: lo ignoramos
ultimoTexto = texto;
mostrar(texto, esAviso); // canal de la VISTA
hablar(texto, esAviso); // canal del OÍDO
}
// Escribe la frase en grande y la apunta en el historial.
function mostrar(texto, esAviso) {
const caja = document.getElementById("mensaje");
caja.textContent = texto;
caja.classList.toggle("aviso", esAviso); // un aviso se ve en amarillo
const hora = new Date().toLocaleTimeString("es-ES");
const linea = document.createElement("li");
linea.textContent = hora + " · " + (esAviso ? "AVISO: " : "") + texto;
const lista = document.getElementById("historial");
lista.insertBefore(linea, lista.firstChild); // la más nueva, arriba
while (lista.children.length > 8) lista.removeChild(lista.lastChild);
}
// Dice la frase en voz alta con la voz del móvil.
function hablar(texto, esAviso) {
if (!vozActiva || !window.speechSynthesis) return;
if (esAviso) speechSynthesis.cancel(); // un aviso corta lo que se esté diciendo
const voz = new SpeechSynthesisUtterance(texto);
voz.lang = "es-ES"; // en español
voz.rate = esAviso ? 1.1 : 0.95; // el aviso, un poco más rápido
voz.pitch = esAviso ? 0.9 : 0.75; // BUITRE: voz grave, tranquila y seca
speechSynthesis.speak(voz);
}
// Los botones
document.getElementById("botonDespertar").onclick = function () {
document.getElementById("mensaje").textContent = "BUITRE vigila.";
document.getElementById("mensaje").classList.remove("aviso");
enviarOrden("G"); // "G" = despertar a BUITRE
};
document.getElementById("botonSilencio").onclick = function () {
speechSynthesis.cancel();
document.getElementById("mensaje").textContent = "BUITRE dormido.";
document.getElementById("mensaje").classList.remove("aviso");
enviarOrden("S"); // "S" = silencio
};
document.getElementById("botonAhora").onclick = function () {
enviarOrden("N"); // "N" = una observación ahora mismo (para probar)
};
document.getElementById("repetir").onclick = function () {
// Quien escucha no puede volver a mirar la pantalla: por eso existe este botón.
if (ultimoTexto) hablar(ultimoTexto, true);
};
document.getElementById("botonVoz").onclick = function () {
vozActiva = !vozActiva;
this.textContent = vozActiva ? "Voz: sí" : "Voz: no";
if (!vozActiva) speechSynthesis.cancel();
};
/* ============================================================
PARTE 2 · EL BLUETOOTH (casi igual que en el chisme 34; no hace falta tocarlo)
============================================================ */
const SERVICIO = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UUID_RX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; // aquí ESCRIBIMOS las órdenes
const UUID_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // por aquí RECIBIMOS los avisos
let dispositivo, canalOrdenes;
const estado = document.getElementById("estado");
const espera = ms => new Promise(r => setTimeout(r, ms));
async function enviarOrden(orden) {
const datos = new TextEncoder().encode(orden);
try {
if (canalOrdenes.writeValueWithoutResponse) await canalOrdenes.writeValueWithoutResponse(datos);
else await canalOrdenes.writeValue(datos);
} catch (e) { estado.textContent = "No se pudo enviar: " + e.message; }
}
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);
canalOrdenes = await servicio.getCharacteristic(UUID_RX);
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 + ".";
for (const id of ["botonDespertar", "botonSilencio", "botonAhora", "repetir"])
document.getElementById(id).disabled = false;
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; }
// Al pulsar este botón el navegador nos deja hablar: dejamos la voz preparada.
if (window.speechSynthesis) speechSynthesis.speak(new SpeechSynthesisUtterance(""));
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».";
for (const id of ["botonDespertar", "botonSilencio", "botonAhora", "repetir"])
document.getElementById(id).disabled = true;
});
await abrirCanales();
} catch (e) {
estado.textContent = "Error: " + e.message + " (elige «Heltec-Buitre» en la lista).";
}
};
</script>
</body>
</html>
Lee y predice
- En
hablar(), ¿qué pasa si cambiasvoz.pitch = esAviso ? 0.9 : 0.75porvoz.pitch = 2? - ¿Por qué un aviso llama a
speechSynthesis.cancel()y una información no? - Si la placa manda
A9, ¿qué hace la página?
Soluciones: 1) La voz sonará aguda y simpática: BUITRE perderá el aire serio. 2) Un aviso es más importante y debe oírse ya; una información puede esperar su turno. 3) Nada: no hay aviso número 9 y la página lo ignora.
Cómo probar a BUITRE
- Enciende la placa. La pantalla debe decir «BUITRE dormido».
- Abre
chisme-35-buitre.htmlen Chrome y sube el volumen del móvil. - Pulsa «Conectar la placa» y elige Heltec-Buitre en la lista.
- Pulsa «Despertar a BUITRE». A los 3 segundos dirá su primera observación.
- Mira la pantalla de la placa y escucha el móvil a la vez. Deben decir lo mismo.
Si no quieres esperar 15 segundos, pulsa «Observación ya». Si se te escapa una frase, pulsa «Repite».

La prueba con los ojos cerrados
Es la prueba que importa. Pón el móvil con auriculares, tapa la pantalla de la placa con la mano y cierra los ojos.
Sigue la «misión» durante dos minutos solo escuchando. Después, otra persona te pregunta qué avisos has oído. ¿Te has enterado de todo? ¿Qué te ha faltado?
Si algo no va
- No se oye nada: sube el volumen del móvil y comprueba que el botón dice «Voz: sí».
- La voz no es en español: tu móvil no tiene una voz en español instalada. Se añade en los ajustes de voz del sistema.
- Habla dos veces: si usas un lector de pantalla (TalkBack, VoiceOver), pulsa «Voz: no». El lector ya lee el texto solo.
- No aparece la placa en la lista: reinicia la placa y pulsa de nuevo «Conectar la placa».
Qué hace BUITRE realmente
BUITRE suena muy convincente. Pero conviene tener claro lo que no es.
No mide nada
La placa elige la frase al azar. Si dice «Obstáculo a tres metros», no hay ningún obstáculo ni ningún sensor que haya medido tres metros.
Es una simulación. Sirve para ensayar cómo se entrega la información, no para fiarse de ella. Un aviso falso en una situación real sería peor que ninguno.
Todavía no es una IA
BUITRE es una lista de frases, un generador de números aleatorios y tres reglas. No hay modelo ni aprendizaje. Eso es bueno para entenderlo: no hay magia, solo una tabla y un sorteo.
Más adelante, esas frases podrán salir de datos reales y de un modelo. Cuando eso pase, habrá que preguntar de dónde salen los datos, qué puede equivocarse y qué sale del dispositivo.
Un canal no basta
La pantalla sirve a quien ve. La voz sirve a quien oye. Ninguna sirve a todos.
Por eso la información va por dos canales y hay un botón para repetirla. Una persona que escucha no puede «volver a mirar»: depende de que el mensaje se repita.
Hablar demasiado también es un problema
Si BUITRE dijera algo cada segundo, habría tanta información que la importante se perdería. Por eso hay pausas, silencios y avisos que cortan a las informaciones.
Cuánto hablar es una decisión de diseño. Se prueba con personas, no se adivina.
Qué sale del dispositivo
Nada. Las frases están en la página y en la placa, y la voz la genera el propio móvil. El historial solo vive en la memoria de la página y desaparece al cerrarla.
Para el aula
Este chisme da pie a hablar de algo que casi nunca se trabaja en clase: cómo diseñar tecnología que pueda usar todo el mundo.
Preguntas para empezar
- Si un compañero no pudiera ver la pantalla, ¿cómo sabría lo que pone?
- ¿Qué información de tu móvil solo llega por la vista? ¿Y cuál solo por el oído?
- ¿Qué ganamos con dos canales? ¿Y si falla uno?
- ¿Por qué las frases de BUITRE son cortas?
Cuatro actividades
- La prueba a ciegas. Por parejas: una persona sigue la misión con los ojos cerrados y auriculares, y la otra anota qué se le escapa. Después se cambian los papeles. ¿Qué frases cuestan más de entender?
- Cuánto hablar. Cambia el intervalo (de 15 s a 5 s y a 30 s). Con cada valor, 2 minutos de prueba. ¿Con cuál se entiende mejor? ¿Con cuál se molesta menos?
- Escribe las frases. Cada alumno propone 3 frases nuevas para BUITRE. Se votan: ¿son cortas?, ¿son claras?, ¿se entienden sin ver nada?
- El lector de pantalla. Activa TalkBack o VoiceOver en un móvil y usa la página con la pantalla apagada. ¿Qué botones se entienden por su nombre? ¿Cuál cambiarías?
Para pensar
BUITRE inventa lo que dice. ¿Qué pasaría si alguien se fiara de él en una situación real? ¿Qué necesitaría BUITRE para merecer confianza?
Las pruebas deben hacerse con compañeros que quieran participar y sin presionar a nadie. Simular no ver es solo una forma de empatizar: no equivale a vivir sin vista.
Retos
- Haz que funcione. Sube el código, conecta y escucha la primera frase.
- Modifica algo. Cambia la voz (
pitchyrate) hasta encontrar la que te parezca la de BUITRE. - Añade frases. Escribe cuatro frases nuevas en la placa y en la página, en el mismo orden. ¿Qué pasa si las pones en distinto orden?
- Guarda los datos. Haz que la página cuente cuántas informaciones y cuántos avisos ha dicho BUITRE y qué frase se repite más.
- Una tercera vía. Añade un canal más: que un LED parpadee con cada aviso (como en el chisme 31) o que suene un tono distinto para información y para aviso.
- Automatiza. Que BUITRE se despierte solo al conectar, sin pulsar «Despertar».
- Haz que otro dispositivo participe. Dos placas con dos BUITRES: uno vigila el norte y otro el sur. ¿Cómo evitas que hablen a la vez?
- Conviértelo en investigación. Haz la prueba a ciegas con 10 compañeros y calcula qué porcentaje de frases se entiende a la primera. Compara frases cortas y largas.
Qué hemos aprendido
Hemos hecho un ejercicio de accesibilidad con ESP32: una placa que da su información por la vista y por el oído.
Hemos aprendido que la misma información debe llegar por más de un canal, que una persona que escucha necesita poder repetirla, y que cuándo y cuánto se habla es una decisión de diseño.
También hemos visto que BUITRE, por ahora, solo simula: no mide nada y no es una IA. Y eso está bien. Así se entiende primero la forma y después se le pone contenido real.
En Sierra Bermeja, el ensayo terminó con la placa en el bolsillo del Lince y el móvil colgado del pinganillo de Justicia Ciega.
—Obstáculo a tres metros —dijo el móvil con voz tranquila.
Justicia Ciega paró en seco. Delante había una roca. De las de verdad.
—Eso no estaba en la lista —murmuró la Gata.
—Estaba —dijo el Lince, pálido—. Es un aviso al azar. Ha acertado por pura casualidad.
—Pues a BUITRE le queda mucho por aprender —dijo Justicia Ciega, rodeando la roca con una sonrisa—. Pero me ha gustado oírlo.
Para seguir investigando
- Todos los chismes de la serie están en la colección de Chismes del Lince.
- La Gata Montesa ya hablaba en el chisme 7: La Gata Montesa te habla.
- Si quieres otro chisme donde la placa avisa al móvil, mira el semáforo Bluetooth con Heltec (chisme 27).
- La voz de la página usa la API SpeechSynthesis, documentada por MDN (en inglés).
- Para ver otras placas y documentación del fabricante, visita la página oficial de Heltec Automation.
Este es un prototipo educativo: no es una ayuda técnica certificada ni sustituye a los lectores de pantalla del móvil.



